源码导读系列之 containerd

源码导读系列之 kubelet
containerd之前是藏匿在docker之后的容器运行时,kubernetes宣称后续不再维护docker-shim后,containerd获得了越来越多的关注,后来containerd补充了CRI和镜像存储相关的代码,就可以作为kubelet的一种运行时。相比于之前docker的方式,调用链缩短了很多。

image.png

个人感觉containerd的代码比kubernetes的代码可读性差很多,原因是containerd的代码中同名的方法有点多,甚至goland都无法识别究竟这个接口的实现对象都有哪些,而且grpc代码的消息调用链不明显,接口众多。

通过阅读containerd的代码,想到了以下几个问题,找到这几个问题的答案,基本就可以无障碍的阅读containerd的代码。

  • kubelet与containerd交互的方式是什么?
  • containerd的各个组件的作用是什么?
  • cri是如何融合到containerd的代码中的?
  • containerd的grpc方法是如何注册的?
  • containerd与container-shim是如何交互的?
  • containerd-shim与runc是如何交互的?

kubelet与containerd交互概览

下图展示了kubelet如何与containerd进行交互,containred通过cri插件实现了CRI的接口。

  1. kubelet通过grpc调用containerd的RunSanbox方法,创建了Pod运行的基本环境,包括pause容器和containerd-shim。
  2. kubelet通过grpc调用containerd的CreateContainer和StartContainer创建Pod中的容器,包括initContainer和normalContainer。
image.png

创建pod

创建一个使用busybox镜像且cmd设置sleep 3000000的pod。下图是使用bcc工具exec-snoop抓取了containerd创建Pod的具体流程,其中省略了部分runc调用的细节。

systemd(1)─┬
           ├─containerd(1694)─┬─{containerd}(1696)
           ├─containerd-shim(20799)─┬─pause(20821)
           │                        ├─sleep(20852)
           │                        ├─{containerd-shim}(20802)
           │                        ├─{containerd-shim}(20803)

containerd-shim  20792  1694     0 /usr/bin/containerd-shim-runc-v2 -namespace k8s.io -address /run/containerd/containerd.sock -publish-binary /usr/bin/containerd -id 193727d35be73014e4adfe4ba93aa20b3834654a65990b9de9fba123592044aa -debug start
containerd-shim  20799  20792    0 /usr/bin/containerd-shim-runc-v2 -namespace k8s.io -id 193727d35be73014e4adfe4ba93aa20b3834654a65990b9de9fba123592044aa -address /run/containerd/containerd.sock
runc             20811  20799    0 /usr/bin/runc --root /run/containerd/runc/k8s.io --log /run/containerd/io.containerd.runtime.v2.task/k8s.io/193727d35be73014e4adfe4ba93aa20b3834654a65990b9de9fba123592044aa/log.json --log-format json --systemd-cgroup create --bundle /run/containerd/io.containerd.runtime.v2.task/k8s.io/193727d35be73014e4adfe4ba93aa20b3834654a65990b9de9fba123592044aa --pid-file /run/containerd/io.containerd.runtime.v2.task/k8s.io/193727d35be73014e4adfe4ba93aa20b3834654a65990b9de9fba123592044aa/init.pid 193727d35be73014e4adfe4ba93aa20b3834654a65990b9de9fba123592044aa/run/containerd/io.containerd.runtime.v2.task/k8s.io/193727d35be73014e4adfe4ba93aa20b3834654a65990b9de9fba123592044aa --pid-file /run/containerd/io.containerd.runtime.v2.task/k8s.io/193727d35be73014e4adfe4ba93aa20b3834654a65990b9de9fba123592044aa/init.pid 193727d35be73014e4adfe4ba93aa20b3834654a65990b9de9fba123592044aa
runc             20827  20799    0 /usr/bin/runc --root /run/containerd/runc/k8s.io --log /run/containerd/io.containerd.runtime.v2.task/k8s.io/193727d35be73014e4adfe4ba93aa20b3834654a65990b9de9fba123592044aa/log.json --log-format json --systemd-cgroup start 193727d35be73014e4adfe4ba93aa20b3834654a65990b9de9fba123592044aa
pause            20821  20799    0 /pause
containerd-shim  20834  1694     0 /usr/bin/containerd-shim-runc-v2 -namespace k8s.io -address /run/containerd/containerd.sock -publish-binary /usr/bin/containerd -id f2ba685582ea81fb6568da6cec0ee3086dd8e15db11214ddfed19f4ded41725b -debug start
runc             20841  20799    0 /usr/bin/runc --root /run/containerd/runc/k8s.io --log /run/containerd/io.containerd.runtime.v2.task/k8s.io/f2ba685582ea81fb6568da6cec0ee3086dd8e15db11214ddfed19f4ded41725b/log.json --log-format json --systemd-cgroup create --bundle /run/containerd/io.containerd.runtime.v2.task/k8s.io/f2ba685582ea81fb6568da6cec0ee3086dd8e15db11214ddfed19f4ded41725b --pid-file /run/containerd/io.containerd.runtime.v2.task/k8s.io/f2ba685582ea81fb6568da6cec0ee3086dd8e15db11214ddfed19f4ded41725b/init.pid f2ba685582ea81fb6568da6cec0ee3086dd8e15db11214ddfed19f4ded41725b
runc             20859  20799    0 /usr/bin/runc --root /run/containerd/runc/k8s.io --log /run/containerd/io.containerd.runtime.v2.task/k8s.io/f2ba685582ea81fb6568da6cec0ee3086dd8e15db11214ddfed19f4ded41725b/log.json --log-format json --systemd-cgroup start f2ba685582ea81fb6568da6cec0ee3086dd8e15db11214ddfed19f4ded41725b
sleep            20852  20799    0 /bin/sleep 30000000

1、 调用container-shim start 启动用于创建runc的containerd-shim,第一个containerd-shim退出后,第二个containerd-shim的父进程就变成了1,这样containerd-shim就与containerd脱离了关系,重启containerd也不会影响containerd-shim进程。
2、通过ttrpc调用第二个containerd-shim的Newtask方法,之后调用runc create。
3、再通过ttrpc调用第二个containerd-shim的Start方法,之后调用runc start启动pause容器。
4、以同样的方式启动Pod中定义的container,注意第二次的containerd-shim只有start这一次调用,原因是1中创建的第二个containerd-shim及是可以与runc交互的进程,这里不用重复创建

containerd的CRI插件

containerd的CRI的实现部分通过插件的形式集成到了containerd中,这种插件化的方式使得扩展containerd变得相对容易,并且核心代码几乎不需要变动。CRI注册了自己特有的grpc方法到containerd中,这样外部就可以通过调用统一的containerd的grpc方法进而调用到CRI的grpc方法。


image.png
// 注册CRI插件
func init() {
    config := criconfig.DefaultConfig()
    plugin.Register(&plugin.Registration{
        Type:   plugin.GRPCPlugin,
        ID:     "cri",
        Config: &config,
        Requires: []plugin.Type{
            plugin.ServicePlugin,
        },
        InitFn: initCRIService,
    })
}

// 注册grpc方法
func (c *criService) Register(s *grpc.Server) error {
    return c.register(s)
}

containerd-shim

containerd启动containerd-shim,containerd-shim再启动一个containerd-shim,此时先前containerd-shim退出。新的container-shim进程的父进程就变成了1,此containerd-shim就是用户init程序的父进程。

创建sandbox流程图.png
func (m *TaskManager) Create(ctx context.Context, id string, opts runtime.CreateOpts) (_ runtime.Task, retErr error) {
    bundle, err := NewBundle(ctx, m.root, m.state, id, opts.Spec.Value)
     //  启动containerd-shim进程
    shim, err := m.startShim(ctx, bundle, id, opts)
    //  向第二个containerd-shim发送create指令
    t, err := shim.Create(ctx, opts)
    if err := m.tasks.Add(ctx, t); err != nil 
    return t, nil
}

func (b *binary) Start(ctx context.Context, opts *types.Any, onClose func()) (_ *shim, err error) {
    args := []string{"-id", b.bundle.ID}
    args = append(args, "start")
    cmd, err := client.Command(
        ctx,
        b.runtime,
        b.containerdAddress,
        b.containerdTTRPCAddress,
        b.bundle.Path,
        opts,
        args...,
    )
    out, err := cmd.CombinedOutput()
    // 返回一个unix socket
    address := strings.TrimSpace(string(out))
    conn, err := client.Connect(address, client.AnonDialer)
   // 新建与unix socket的连接
    client := ttrpc.NewClient(conn, ttrpc.WithOnClose(onCloseWithShimLog))
    return &shim{
        bundle:  b.bundle,
        client:  client,
        task:    task.NewTaskClient(client),
        events:  b.events,
        rtTasks: b.rtTasks,
    }
}

// 第二个containerd-shim启动grpc服务
func (s *Client) Serve() {
    shimapi.RegisterTaskService(server, s.service)
}

containerd-shim 向 containerd转发事件

转发事件.png

cri服务处理事件,进程退出,oom等,并更新container或者sandbox的状态。之后kubelet就可以调用CRI接口获取container的状态信息,包括oom状态码等。
后续再写文章描述kubelet如何获取到pod的oom信息。

// containerd-shim
func run(id string, initFunc Init, config Config) error {
    ttrpcAddress := os.Getenv(ttrpcAddressEnv)
    // 建立到containerd的连接 
    publisher, err := NewPublisher(ttrpcAddress)
}
// 开始处理shim的事件并转发到containerd
func (l *RemoteEventsPublisher) processQueue() {
  forwardRequest()
}

func (l *RemoteEventsPublisher) forwardRequest(ctx context.Context, req *v1.ForwardRequest) error {
    // 到containerd的events的grpc接口
    service, err := l.client.EventsService()
    service.Forward(fCtx, req)
}

// containerd :
// 发布消息
func (e *Exchange) Publish(ctx context.Context, topic string, event events.Event) (err error) {
    validateTopic(topic)
    return e.broadcaster.Write(&envelope)
}
// 向订阅者发布消息
func (b *Broadcaster) run() {
    for {
        select {
        case event := <-b.events:
            for _, sink := range b.sinks {
                sink.Write(event)
                }
            }
        }
}

// cri订阅事件
func (em *eventMonitor) subscribe(subscriber events.Subscriber) {
    // note: filters are any match, if you want any match but not in namespace foo
    // then you have to manually filter namespace foo
    filters := []string{
        `topic=="/tasks/oom"`,
        `topic~="/images/"`,
    }
    em.ch, em.errCh = subscriber.Subscribe(em.ctx, filters...)
}

// cri处理容器事件,并更新容器状态,
func (em *eventMonitor) start() <-chan error {
    go func() {
        for {
            select {
            case e := <-em.ch:
                id, evt, err := convertEvent(e.Event)
                em.handleEvent(evt)
                       }
        }
    }()
}

containerd-shim与runc的交互

containerd-shim 通过二进制方式调用runc create和start真正的去启动容器。

func newInit(ctx context.Context, path, workDir, namespace string, platform stdio.Platform,
    r *process.CreateConfig, options *options.Options, rootfs string) (*process.Init, error) {
    runtime := process.NewRunc(options.Root, path, namespace, options.BinaryName, options.CriuPath, options.SystemdCgroup)
    p := process.New(r.ID, runtime, stdio.Stdio{
        Stdin:    r.Stdin,
        Stdout:   r.Stdout,
        Stderr:   r.Stderr,
        Terminal: r.Terminal,
    })
    p.Bundle = r.Bundle
    p.Platform = platform
    p.Rootfs = rootfs
    p.WorkDir = workDir
    p.IoUID = int(options.IoUid)
    p.IoGID = int(options.IoGid)
    p.NoPivotRoot = options.NoPivotRoot
    p.NoNewKeyring = options.NoNewKeyring
    p.CriuWorkPath = options.CriuWorkPath
    if p.CriuWorkPath == "" {
        // if criu work path not set, use container WorkDir
        p.CriuWorkPath = p.WorkDir
    }
    return p, nil
}

// runc create 
func (r *Runc) Create(context context.Context, id, bundle string, opts *CreateOpts) error {
    args := []string{"create", "--bundle", bundle}
    cmd := r.command(context, append(args, id)...)
    ec, err := Monitor.Start(cmd)
}

// runc start
func (r *Runc) Start(context context.Context, id string) error {
    return r.runOrError(r.command(context, "start", id))
}

ctr,runc命令行

image.png

参考文档

https://github.com/containerd/containerd/tree/main/runtime/v2

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 207,248评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,681评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,443评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,475评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,458评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,185评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,451评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,112评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,609评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,083评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,163评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,803评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,357评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,357评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,590评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,636评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,925评论 2 344

推荐阅读更多精彩内容