containerd 原始碼分析:啟動註冊流程

lubanseven發表於2024-05-21

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.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 互動的,這些會在下文中繼續介紹。


相關文章