1.前情回顾
上一篇我们已经知道如何找到对应的gRPC请求接口的逻辑代码了,但是还没有看具体的代码。
在最初的《浅入浅出docker run命令源码》中
已知,启动容器还需要启动shim进程以及runc进程。但是具体是如何启动的,还不清楚。这篇文章中,主要解决问题是containerd是如何启动shim、runc进程的。
2.前置知识
接着《浅入浅出docker run命令源码》代码腰斩处继续看dockerd的源码,可知,docker run
命令执行的过程中,dockerd
与containerd
交互时,主要的三个gRPC
请求按顺序排列如下:
/containerd.services.containers.v1.Containers/Create
创建容器的元数据/containerd.services.tasks.v1.Tasks/Create
启动shim进程,调用runc创建容器环境/containerd.services.tasks.v1.Tasks/Start
启动任务对应的容器主进程(我们在容器中跑的程序)
很明显我们要关注的是,第2、3个请求。
该篇直接从接口处代码开始,在前面《浅入浅出docker run命令源码2-containerd篇》中已经说明了知道了gRPC
请求路径后,如何找到对应的源码,这里不再赘述。
3.Tasks/Create请求源码阅读
在该请求中,containerd
会启动一个shim
进程,在shim
进程中又调用runc
去准备好容器启动所需要的namespace环境。
前面有个概念没有说,在containerd
中,插件是有类型的,例如service
是gRPC
插件,local
是service
插件。在创建gRPC
插件的时候,会查找它所依赖的service
插件。下面就是负责处理该请求的Tasks
插件
回到正题,该请求是由containerd
的tasks
插件接收请求并处理,正如前面《浅入浅出docker run命令源码2-containerd篇》中说的,service.go
中注册的是gRPC插件,负责对外,真正的处理逻辑在其引用的loacl.go
中。
local.Create
一眼扫过去,大概就能猜到rtime.Create()
就是启动shim进程逻辑的代码入口,左下角是到真正启动shim进程时的调用栈
3.1部分结构体说明
整个调用的过程,其实就是准备各种参数,为启动shim
进程准备。刚看这部分的代码,会有种怎么老是在create
或者start
什么的,如调用栈中方法名所示。其实这里如果了理解TaskManage
、ShimManager
、ShimTask
、shim
这些抽象的意义,代码看起来就很清晰了。
3.1.1 TaskManager
taskManager负责所有容器任务的调度和状态管理。它是上层系统(如 Kubernetes 或 Docker)与 containerd
交互的主要入口,是逻辑层面的抽象。
TaskManager只提供了Create、Delete、Get、Tasks这几个方法,对应了容器的创建、删除、查询单个容器、查询所有容器。
以Create方法为例,代码折叠后,可以看出TaskManager的逻辑就是
- 1、先让
ShimManager
搞个shim
- 2、创建一个task
- 3、执行task
没有shim是如何工作的,也没有task是如何工作的逻辑在里面,只负责流程逻辑的串联。只要流程不变,不会影响到TaskManager,我可不管你shim是怎么保存、task是如何实现的。
3.1.2 ShimManager
从前面的TaskManager
的实现逻辑来看,ShimManager
的定位就是管理容器的。由于containerd
为了解耦,通过shim
进程来启动容器,而TaskManager又不管事,所以只能搞个 ShimManager
来背锅了。ShimManager职责是 shim
进程管理 的抽象层,负责启动、监控和销毁 shim
,以及与 shim
之间的通信。
如下图所示,管理shim,其实就是对shim这个抽象对象进行CRUD。其他的小写方法是服务于TaskManager的初始化以及关闭时候清理环境等操作的。
以Start
方法为例,可以看到启动前需要先创建一个Bundle对象,然后调用startShim方法返回一个shim
对象,最后将shim
对象加入到集合中。再看Delete
方法中也差不多,具体的实现都是在shim
对象中,ShimManager
仅仅只是shim
对象的维护。
3.1.3 shimTask与shim
shim
记录了shim进程的相关信息,例如runc需要的bundle信息,以及与shim进程通讯用的rpc连接
type shim struct {
bundle *Bundle
client any
}
ShimTask
是具体任务的实现类,负责单个容器任务的生命周期操作。
其本质是一个rpc
客户端,通过shim
的client
去完成容器生命周期操作。之所以要弄个shimTask
出来,我的理解是containerd
中通过shim
进程启动容器这逻辑是基本不会变的,因此shim
也是基本不变的,其管理功能也是基本不会变的,但容器生命周期操作这些逻辑代码后续可能会变化,为了修改起来更清晰,才搞了shimTask
这个东西出来。要是我自己写,可能就直接在shim
里面实现草草了事了。。。
shim
用ttrpc
通信,ttrpc
是gRPC
为本地通信的优化版本,各个操作对应的请求路径还要再看shimTask
的task
对象的代码。task
是一个 taskClient
结构体实例,是ttrpc
代码生成的
3.2 containerd启动shim进程
这里只讲述关于containerd相关的代码,shim进程的代码,请看后续的《浅入浅出docker run命令源码3-shim篇》。
从Tasks/Create
入口处跟着代码走,会看到下面代码。这里就是真正去启动一个进程的代码位置
图中的代码是我为了debug shim
进程修改过的,会有些细微不一样,但是圈中的代码是没有改动的。
首先cmd.CombineOutput()
中会执行启动shim进程的命令并返回了out
对象获取命令的输出。
然后parseStartResponse()
会处理输出的内容返回下面的结构体
type BootstrapParams struct {
Version int `json:"version"`
Address string `json:"address"`
Protocol string `json:"protocol"`}
makeConnection()
使用结构体中的Address去连接shim进程得到一个ttrpc
连接对象
然后返回一个shim
对象给shimManger
保存起来,然后再返回给TaskManager。
TaskManager
得到shim
后会创建一个ShimTask
,并用ShimTask
向shim
进程发出请求
shimTask, err := newShimTask(shim)
if err != nil {
return nil, err
}
t, err := shimTask.Create(ctx, opts)
在shimTask.Create中会通过ttrpc,让shim进程去干活了
请求的路径如下,执行服务containerd.task.v2.Task
的Create
方法
该请求就是让shim
进程去启动runc
进程,剩下的逻辑,需要关注的在shim
进程的代码里面了。
4.Tasks/Start请求源码阅读
Start
方法中,其实就是获取Create
方法中创建的shimTask
,然后调用它的start
方法,最终触发ttrpc
请求,让shim
进程启动容器,最终返回启动结果给dockerd
,涉及的知识点已经在Create
方法中写了,这里就不再赘述了。
5.总结
本来想叫shim篇的,结果发现要把shim进程的逻辑也放这写,感觉还得费不少时间,还是拆开写吧。擦,本来我只是想随便看看docker run
的代码,意思一下得了,怎么给我干到这来了。。