0. 前言
containerd
是一個行業標準的容器執行時,其強調簡單性、健壯性和可移植性。本文將從 containerd
的程式碼結構入手,檢視 containerd
的啟動註冊流程。
1. 啟動註冊流程
1.1 containerd
首先以除錯模式執行 containerd
:
// containerd/cmd/containerd/main.go
package main
import (
...
_ "github.com/containerd/containerd/v2/cmd/containerd/builtins"
)
...
func main() {
app := command.App()
if err := app.Run(os.Args); err != nil {
fmt.Fprintf(os.Stderr, "containerd: %s\n", err)
os.Exit(1)
}
}
在啟動 containerd
時,匯入匿名包 github.com/containerd/containerd/v2/cmd/containerd/builtins
註冊外掛。
接著,進入 command.App()
:
// containerd/cmd/containerd/server/server.go
func App() *cli.App {
app := cli.NewApp()
app.Name = "containerd"
...
app.Action = func(context *cli.Context) error {
...
go func() {
defer close(chsrv)
server, err := server.New(ctx, config)
if err != nil {
select {
case chsrv <- srvResp{err: err}:
case <-ctx.Done():
}
return
}
...
}()
...
}
}
這裡省略了一系列初始化過程,重點在 server.New(ctx, config)
。
// containerd/cmd/containerd/server/server.go
func New(ctx context.Context, config *srvconfig.Config) (*Server, error) {
...
// 將外掛載入到 loaded 中
loaded, err := LoadPlugins(ctx, config)
if err != nil {
return nil, err
}
...
serverOpts := []grpc.ServerOption{
grpc.StatsHandler(otelgrpc.NewServerHandler()),
grpc.ChainStreamInterceptor(
streamNamespaceInterceptor,
prometheusServerMetrics.StreamServerInterceptor(),
),
grpc.ChainUnaryInterceptor(
unaryNamespaceInterceptor,
prometheusServerMetrics.UnaryServerInterceptor(),
),
}
...
var (
grpcServer = grpc.NewServer(serverOpts...)
tcpServer = grpc.NewServer(tcpServerOpts...)
grpcServices []grpcService
tcpServices []tcpService
ttrpcServices []ttrpcService
s = &Server{
prometheusServerMetrics: prometheusServerMetrics,
grpcServer: grpcServer,
tcpServer: tcpServer,
ttrpcServer: ttrpcServer,
config: config,
}
...
)
...
// 遍歷外掛
for _, p := range loaded {
...
result := p.Init(initContext)
if err := initialized.Add(result); err != nil {
return nil, fmt.Errorf("could not add plugin result to plugin set: %w", err)
}
instance, err := result.Instance()
...
if src, ok := instance.(grpcService); ok {
grpcServices = append(grpcServices, src)
}
if src, ok := instance.(ttrpcService); ok {
ttrpcServices = append(ttrpcServices, src)
}
if service, ok := instance.(tcpService); ok {
tcpServices = append(tcpServices, service)
}
...
}
// 註冊外掛服務
for _, service := range grpcServices {
if err := service.Register(grpcServer); err != nil {
return nil, err
}
}
for _, service := range ttrpcServices {
if err := service.RegisterTTRPC(ttrpcServer); err != nil {
return nil, err
}
}
for _, service := range tcpServices {
if err := service.RegisterTCP(tcpServer); err != nil {
return nil, err
}
}
...
}
server.New
是 containerd
執行的主邏輯。
首先,將註冊的外掛載入到 loaded
,接著遍歷 loaded
。透過 result := p.Init(initContext)
獲取外掛的例項。
以 io.containerd.grpc.v1.containers
外掛為例,檢視 p.Init
是如何獲取外掛物件的。
// containerd/vendor/github.com/containerd/plugin/plugin.go
func (r Registration) Init(ic *InitContext) *Plugin {
// 呼叫註冊外掛的 InitFn 函式
p, err := r.InitFn(ic)
return &Plugin{
Registration: r,
Config: ic.Config,
Meta: *ic.Meta,
instance: p,
err: err,
}
}
// containerd/plugins/services/containers/service.go
func init() {
registry.Register(&plugin.Registration{
Type: plugins.GRPCPlugin,
ID: "containers",
Requires: []plugin.Type{
plugins.ServicePlugin,
},
// 執行 InitFn 返回 service 物件
InitFn: func(ic *plugin.InitContext) (interface{}, error) {
i, err := ic.GetByID(plugins.ServicePlugin, services.ContainersService)
if err != nil {
return nil, err
}
return &service{local: i.(api.ContainersClient)}, nil
},
})
}
獲取到外掛例項後,根據外掛型別註冊外掛例項以提供對應的(grpc/ttrpc/tcp)服務。
1.2 註冊外掛
註冊外掛是透過 init
機制實現的。在 main
中匯入 github.com/containerd/containerd/v2/cmd/containerd/builtins
包。
builtins
包匯入包含 init
的外掛包實現外掛註冊。以 cri
外掛為例:
// containerd/cmd/containerd/builtins/cri.go
package builtins
import (
_ "github.com/containerd/containerd/v2/plugins/cri"
...
)
// containerd/plugins/cri/cri.go
package cri
...
// Register CRI service plugin
func init() {
defaultConfig := criconfig.DefaultServerConfig()
registry.Register(&plugin.Registration{
Type: plugins.GRPCPlugin,
ID: "cri",
Requires: []plugin.Type{
...
},
Config: &defaultConfig,
ConfigMigration: func(ctx context.Context, configVersion int, pluginConfigs map[string]interface{}) error {
...
},
InitFn: initCRIService,
})
}
在 init
中透過 registry.Register
註冊外掛:
package registry
...
var register = struct {
sync.RWMutex
r plugin.Registry
}{}
// Register allows plugins to register
func Register(r *plugin.Registration) {
register.Lock()
defer register.Unlock()
register.r = register.r.Register(r)
}
可以看到外掛註冊的過程實際是將外掛結構體 plugin.Registration
註冊到 register.plugin.Registry
的過程。
register.plugin.Registry
實際是一個包含 Registration
的切片。
package plugin
type Registry []*Registration
1.3 檢視外掛
使用 ctr
檢視 containerd
註冊的外掛,ctr
是 containerd
官方提供的命令列工具。如下:
# ctr plugins ls
TYPE ID PLATFORMS STATUS
io.containerd.image-verifier.v1 bindir - ok
io.containerd.internal.v1 opt - ok
...
2. 小結
本文主要介紹了 containerd
的啟動註冊外掛流程。當然,外掛的型別眾多,外掛是如何工作的,外掛之間如何互動,kubernetes 又是怎麼和 containerd
互動的,這些會在下文中繼續介紹。