containerd 原始碼分析:建立 container(一)

lubanseven發表於2024-06-04

0. 前言

Kubernetes:kubelet 原始碼分析之 pod 建立流程 介紹了 kubelet 建立 pod 的流程,containerd 原始碼分析:kubelet 和 containerd 互動
介紹了 kubelet 透過 cri 介面和 containerd 互動的過程,containerd 原始碼分析:啟動註冊流程 介紹了 containerd 作為高階容器執行時的啟動流程。透過這三篇文章熟悉了 kubeletcontainerd 的行為,對於 containerd 如何透過 OCI 介面建立容器 container 並沒有涉及。

image

本文將繼續介紹 containerd 是如何建立容器 container 的。

1. ctr

在介紹建立容器前,首先簡單介紹下 ctrctrcontainerd 的命令列客戶端,本文會透過 ctr 進行除錯和分析。

1.1 ctr CLI

作為命令列工具 ctr 包括一系列和 containerd 互動的命令。主要命令如下:

COMMANDS:
   plugins, plugin            provides information about containerd plugins
   containers, c, container   manage containers
   images, image, i           manage images
   run                        run a container
   snapshots, snapshot        manage snapshots
   tasks, t, task             manage tasks
   install                    install a new package
   oci                        OCI tools
   shim                       interact with a shim directly

containers|c|container

不同與 Kubernetes 層面的 container,這裡 ctr 命令管理的 containers 實際是管理儲存在 boltDB 中的 container metadata。

建立 container

# ctr c create docker.io/library/nginx:alpine nginx1
# ctr c ls
CONTAINER    IMAGE                             RUNTIME
nginx1       docker.io/library/nginx:alpine    io.containerd.runc.v2

透過 boltbrowser 檢視 boltDB 儲存的 container metadata,container metadata 儲存在目錄 /var/lib/containerd/io.containerd.metadata.v1.bolt

image

tasks|t|task

task 是實際啟動容器程序的命令,ctr task start 根據建立的 container 啟動容器:

# ctr t start nginx1
/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
...

run

ctr 的 run 命令,實際是 ctr c createctr t start 命令的組合。

接下來,使用 ctr run 命令做為除錯引數分析完整的建立 container 容器的流程。

1.2 ctr 除錯

ctr 程式碼集中在 containerd 專案中,配置 ctr 的除錯引數:

{
   "version": "0.2.0",
   "configurations": [
      {
         "name": "ctr",
         "type": "go",
         "request": "launch",
         "mode": "auto",
         "program": "${fileDirname}",
         "args": ["run", "docker.io/library/nginx:alpine", "nginx1"]
      }
   ]
}

除錯 ctr

image

進入 run.Command 看其中做了什麼。

// containerd/cmd/ctr/commands/run/run.go
// Command runs a container
var Command = &cli.Command{
	Name:      "run",
	Usage:     "Run a container",
   ...
   Action: func(context *cli.Context) error {
      ...
      // step1: 建立訪問 containerd 的 client
      client, ctx, cancel, err := commands.NewClient(context)
		if err != nil {
			return err
		}
		defer cancel()

      // step2: 建立 container
      container, err := NewContainer(ctx, client, context)
		if err != nil {
			return err
		}
      ...

      opts := tasks.GetNewTaskOpts(context)
		ioOpts := []cio.Opt{cio.WithFIFODir(context.String("fifo-dir"))}
      // step3: 建立 task
		task, err := tasks.NewTask(ctx, client, container, context.String("checkpoint"), con, context.Bool("null-io"), context.String("log-uri"), ioOpts, opts...)
		if err != nil {
			return err
		}

      ...
      // step4: 啟動 task
      if err := task.Start(ctx); err != nil {
			return err
		}
      ...
   }
}

NewContainer 中根據 client 建立 container。接著根據 container 建立 task,然後啟動該 task 來啟動容器。

1.2.1 建立 container

進入 NewContainer

// containerd/cmd/ctr/commands/run/run_unix.go
func NewContainer(ctx gocontext.Context, client *containerd.Client, context *cli.Context) (containerd.Container, error) {
   ...
   return client.NewContainer(ctx, id, cOpts...)
}

// containerd/client/client.go
func (c *Client) NewContainer(ctx context.Context, id string, opts ...NewContainerOpts) (Container, error) {
   ...
   container := containers.Container{
		ID: id,
		Runtime: containers.RuntimeInfo{
			Name: c.runtime,
		},
	}
   ...
   // 呼叫 containerd 介面建立 container
   r, err := c.ContainerService().Create(ctx, container)
	if err != nil {
		return nil, err
	}
	return containerFromRecord(c, r), nil
}

重點在 Client.ContainerService().Create

// containerd/client/containerstore.go
func (r *remoteContainers) Create(ctx context.Context, container containers.Container) (containers.Container, error) {
	created, err := r.client.Create(ctx, &containersapi.CreateContainerRequest{
		Container: containerToProto(&container),
	})
	if err != nil {
		return containers.Container{}, errdefs.FromGRPC(err)
	}

	return containerFromProto(created.Container), nil
}

// containerd/api/services/containers/v1/containers_grpc.pb.go
func (c *containersClient) Create(ctx context.Context, in *CreateContainerRequest, opts ...grpc.CallOption) (*CreateContainerResponse, error) {
	out := new(CreateContainerResponse)
	err := c.cc.Invoke(ctx, "/containerd.services.containers.v1.Containers/Create", in, out, opts...)
	if err != nil {
		return nil, err
	}
	return out, nil
}

呼叫 /containerd.services.containers.v1.Containers/Create grpc 介面建立 container。container 並不是容器程序,而是儲存在資料庫中的 container metadata。

/containerd.services.containers.v1.Containers/Create 是由 containerdio.containerd.grpc.v1.containers 外掛提供的服務:

// containerd/plugins/services/service.go
func (s *service) Create(ctx context.Context, req *api.CreateContainerRequest) (*api.CreateContainerResponse, error) {
	return s.local.Create(ctx, req)
}

外掛例項呼叫 local 物件的 Create 方法建立 container。檢視 local 物件具體指的什麼。

// containerd/plugins/services/service.go
func init() {
	registry.Register(&plugin.Registration{
		Type: plugins.GRPCPlugin,
		ID:   "containers",
		Requires: []plugin.Type{
			plugins.ServicePlugin,
		},
		InitFn: func(ic *plugin.InitContext) (interface{}, error) {
         // plugins.ServicePlugin:io.containerd.service.v1
         // services.ContainersService:containers-service
			i, err := ic.GetByID(plugins.ServicePlugin, services.ContainersService)
			if err != nil {
				return nil, err
			}
			return &service{local: i.(api.ContainersClient)}, nil
		},
	})
}

local 物件是 containerdio.containerd.service.v1.containers-service 外掛的例項。檢視該例項的 Create 方法。

// containerd/plugins/services/containers/local.go
func (l *local) Create(ctx context.Context, req *api.CreateContainerRequest, _ ...grpc.CallOption) (*api.CreateContainerResponse, error) {
	var resp api.CreateContainerResponse

	if err := l.withStoreUpdate(ctx, func(ctx context.Context) error {
		container := containerFromProto(req.Container)

		created, err := l.Store.Create(ctx, container)
		if err != nil {
			return err
		}

		resp.Container = containerToProto(&created)

		return nil
	}); err != nil {
		return &resp, errdefs.ToGRPC(err)
	}
	...

	return &resp, nil
}

local.Create 呼叫 local.withStoreUpdate 方法建立 container。

// containerd/plugins/services/containers/local.go
func (l *local) withStoreUpdate(ctx context.Context, fn func(ctx context.Context) error) error {
	return l.db.Update(l.withStore(ctx, fn))
}

local.withStoreUpdate 呼叫 db 物件的 Update 方法建立 container。

// containerd/plugins/services/containers/local.go
func init() {
	registry.Register(&plugin.Registration{
		...
		InitFn: func(ic *plugin.InitContext) (interface{}, error) {
			m, err := ic.GetSingle(plugins.MetadataPlugin)
			if err != nil {
				return nil, err
			}
			ep, err := ic.GetSingle(plugins.EventPlugin)
			if err != nil {
				return nil, err
			}

			db := m.(*metadata.DB)
			return &local{
				Store:     metadata.NewContainerStore(db),
				db:        db,
				publisher: ep.(events.Publisher),
			}, nil
		},
	})
}

db 物件是 io.containerd.metadata.v1 外掛的例項,該外掛透過 boltDB 提供 metadata 儲存服務。

metadata 外掛實際呼叫的是匿名函式 fn 的內容,在 fn 中透過 l.Store.Create(ctx, container) 將 container 的 metadata 資訊註冊到 boltDB 資料庫中。

建立 container 的過程實際是將 container 資訊註冊到 boltDB 的過程。


相關文章