containerd 源码分析:启动注册流程


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: %sn", 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.Newcontainerd 运行的主逻辑。

首先,将注册的插件加载到 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 注册的插件,ctrcontainerd 官方提供的命令行工具。如下:

# ctr plugins ls TYPE                                   ID                       PLATFORMS      STATUS io.containerd.image-verifier.v1        bindir                   -              ok io.containerd.internal.v1              opt                      -              ok ... 

2. 小结

本文主要介绍了 containerd 的启动注册插件流程。当然,插件的类型众多,插件是如何工作的,插件之间如何交互,kubernetes 又是怎么和 containerd 交互的,这些会在下文中继续介绍。


发表评论

评论已关闭。

相关文章