0. 前言
Kubernetes:kubelet 原始碼分析之 pod 建立流程 介紹了 kubelet
建立 pod 的流程,containerd 原始碼分析:kubelet 和 containerd 互動
介紹了 kubelet
透過 cri 介面和 containerd
互動的過程,containerd 原始碼分析:啟動註冊流程 介紹了 containerd
作為高階容器執行時的啟動流程。透過這三篇文章熟悉了 kubelet
和 containerd
的行為,對於 containerd
如何透過 OCI 介面建立容器 container
並沒有涉及。
本文將繼續介紹 containerd
是如何建立容器 container
的。
1. ctr
在介紹建立容器前,首先簡單介紹下 ctr
。ctr
是 containerd
的命令列客戶端,本文會透過 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
。
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 create
和 ctr 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
:
進入 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
是由 containerd
的 io.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
物件是 containerd
的 io.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 的過程。