containerd 是一种容器运行时,且是一个CNCF 毕业项目。
从 Kubernetes1.24 开始,dockershim已从 Kubernetes 项目移出,标志着Kubernetes统一按照 CRI 支持容器运行时。而 containerd 作为广泛使用的容器运行时,流行必定有其过人之处。
containerd 的特点之一:插件式拓展
containerd采用插件式架构,方便拓展。从架构图和配置文件可看出,Content、Snapshot 组件都是 以 plugin 方式接入。
/etc/containerd/cofnig.toml
[plugins]
[plugins."io.containerd.metadata.v1.bolt"]
content_sharing_policy = "shared"
[plugins."io.containerd.monitor.v1.cgroups"]
no_prometheus = false
[plugins."io.containerd.runtime.v1.linux"]
no_shim = false
runtime = "runc"
runtime_root = ""
shim = "containerd-shim"
shim_debug = false
[plugins."io.containerd.runtime.v2.task"]
platforms = ["linux/amd64"]
sched_core = false
[plugins."io.containerd.snapshotter.v1.native"]
root_path = ""
[plugins."io.containerd.snapshotter.v1.overlayfs"]
root_path = ""
upperdir_label = false
所有的插件通过 containerd/plugin.Reginster()
注册,并在 containerd 启动时进行初始化。
下面演示如何通过对 containerd 二次开发加入定制插件demo,提供 gRPC 访问和containerd 内部 cri 插件使用。
定义接口
containerd 所有对外暴露的 gRPC 接口定义在 api 目录下。在 api/services 新建 demo/v1 目录,并新建 doc.go 文件,声明 package,并新建文件夹 v1,新建文件 demo.proto。目录如下:
❯ tree demo
demo
└── v1
├── demo.pb.go
├── demo.proto
└── doc.go
2 directories, 3 files
demo.proto
syntax = "proto3";
package containerd.services.demo.v1;
option go_package = "github.com/containerd/containerd/api/services/demo/v1;demo";
service Demo {
rpc Ping(PingRequest) returns (PongResponse);
}
message PingRequest {
string msg = 1;
}
message PongResponse {
string msg = 2;
}
在 containerd 项目根目录,执行 make protos 即可生成 demo.pb.go 文件。 make protos 使用了protobuild 项目,深入研究可参考 protobuild 项目说明。
实现接口
containerd 的所有 services 放在 services 目录下,进入目录可看到content、snapshots 等目录,对应是 containerd 的插件。
新建 demo 目录,并新建 local.go 和 service.go 文件。代码如下。local.go 注册 ServicePlugin,对外提供 demo 的所有逻辑接口。service.go 注册 GRPCPlugin,对外提供 grpc 访问。
local.go
package demo
import (
"context"
demoapi "github.com/containerd/containerd/api/services/demo/v1"
"github.com/containerd/containerd/plugin"
"github.com/containerd/containerd/services"
"google.golang.org/grpc"
)
func init() {
plugin.Register(&plugin.Registration{
Type: plugin.ServicePlugin,
ID: services.DemoService,
InitFn: func(ic *plugin.InitContext) (interface{}, error) {
return &local{}, nil
},
})
}
type local struct{}
var _ demoapi.DemoClient = &local{}
func (l *local) Ping(ctx context.Context, req *demoapi.PingRequest, _ ...grpc.CallOption) (*demoapi.PongResponse, error) {
resp := &demoapi.PongResponse{Msg: "hello " + req.Msg}
return resp, nil
}
service.go
package demo
import (
demoapi "github.com/containerd/containerd/api/services/demo/v1"
"github.com/containerd/containerd/plugin"
"github.com/containerd/containerd/services"
"golang.org/x/net/context"
"google.golang.org/grpc"
)
func init() {
plugin.Register(&plugin.Registration{
Type: plugin.GRPCPlugin,
ID: "demo",
Requires: []plugin.Type{
plugin.ServicePlugin,
},
InitFn: func(ic *plugin.InitContext) (interface{}, error) {
p, err := ic.GetByID(plugin.ServicePlugin, services.DemoService)
if err != nil {
return nil, err
}
return &service{local: p.(demoapi.DemoClient)}, nil
},
})
}
type service struct {
local demoapi.DemoClient
}
var _ demoapi.DemoServer = &service{}
func (s *service) Register(server *grpc.Server) error {
demoapi.RegisterDemoServer(server, s)
return nil
}
func (s *service) Ping(ctx context.Context, req *demoapi.PingRequest) (*demoapi.PongResponse, error) {
return s.local.Ping(ctx, req)
}
简单的 demo plugin 已经实现,接下来部署运行 containerd。
通过 make bin/containerd
重新编译 containerd可执行文件,重新部署后,通过 journalctl 查看日志,可发现以下 log。说明 demo-service 和 demo GRPC plugin 已经在 containerd 启动时成功注册了。
Aug 15 15:06:02 0000000g-1skzsvv7ro containerd[4125]: time="2023-08-15T15:06:02.899951562+08:00" level=info msg="loading plugin \"io.containerd.service.v1.demo-service\"..." type=io.containerd.service.v1
Aug 15 15:06:02 0000000g-1skzsvv7ro containerd[4125]: time="2023-08-15T15:06:02.902695763+08:00" level=info msg="loading plugin \"io.containerd.grpc.v1.demo\"..." type=io.containerd.grpc.v1
再进一步探索,查看Registration定义,发现 Requires字段,此字段声明当前 plugin 依赖哪些 plugin,contaienrd 启动时会分析依赖关系调整 plugin 启动顺序。如 demo GRPC plugin 声明依赖 ServicePlugin,通过 log 可确认 demo GRPCPlugin 在 service 启动后启动。实现逻辑在 LoadPlugins 函数。
验证
编写一个简单的 client,验证 containerd 是否已经对外暴露 demo GRPC 接口。如果对 Golang GRPC 实现不了解,可参考官方 helloword 例子,了解 server 和 client 端使用。
在 cmd 新建 demo 目录,新建 client.go 文件。demo如下,通过socket 连接 containerd,再访问 Ping API。
package main
import (
"context"
demoapi "github.com/containerd/containerd/api/services/demo/v1"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"log"
"time"
)
func main() {
opts := []grpc.DialOption{
grpc.WithBlock(),
grpc.WithTransportCredentials(insecure.NewCredentials()),
}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
defer cancel()
conn, err := grpc.DialContext(ctx, "unix:///run/containerd/containerd.sock", opts...)
if err != nil {
log.Fatal("grpc dial error", err)
}
demoClient := demoapi.NewDemoClient(conn)
resp, err := demoClient.Ping(ctx, &demoapi.PingRequest{Msg: "container"})
if err != nil {
log.Fatal("failed to ping", err)
}
log.Println("ping pong", resp.Msg)
}
通过 make bin/demo
编译 demo 执行文件,执行测试。
❯ sudo ./bin/demo
2023/08/15 15:06:18 ping pong hello container
正常打印出log,说明正常访问。
CRI访问
部署在 k8s 集群节点的 containerd,kebelet 通过 cri接口访问 containerd,官方对此有说明。
访问方式如下:
那怎样能让 CRI-plugin 能访问自定义 plugin 呢?不妨也查看源代码寻找答案。
其实 cri 也是通过 GRPC plugin 方式注册的,且 containerd 在 CRI-pulgin 里是一个 client。 通过 contaienrd.New()
实例化的一个 client,提供 WithServices
ClientOpt注入更多依赖 service。
这样只需要额外提供实例化 DemoService 的 ServiceOpt 即可。
// pkg/cri/cri.go
// Register CRI service plugin
func init() {
config := criconfig.DefaultConfig()
plugin.Register(&plugin.Registration{
Type: plugin.GRPCPlugin,
ID: "cri",
Config: &config,
Requires: []plugin.Type{
plugin.EventPlugin,
plugin.ServicePlugin,
},
InitFn: initCRIService,
})
}
func initCRIService(ic *plugin.InitContext) (interface{}, error) {
...
serviceOpts, err := getServicesOpts(ic)
...
client, err := containerd.New("", containerd.WithServices(serviceOpts...))
...
}
func getServicesOpts(ic *plugin.InitContext) ([]containerd.ServicesOpt, error) {
...
for s, fn := range map[string]func(interface{}) containerd.ServicesOpt{
...
services.DemoService: func(s interface{}) containerd.ServicesOpt {
return containerd.WithDemoService(s.(demoapi.DemoClient))
},
...
}
WithDemoService
可自行探索实现,思路是观察 plugins 类型和 ServiceOpt 定义注入 DemoService。
至此,一个可供 gRPC 和 CRI-Plugin 访问的插件已完成。