1、前情回顾
上次《浅入浅出docker run命令源码》代码看到调用了grpc去让containerd启动容器就没有继续看了.
连一刻都没有为dockerd
的无疾而终而哀悼,立刻来到战场的是containerd
…
这次,我们先解决下面的问题
1、dockerd
是怎么启动的containerd
2、怎么调试containerd
的源码
3、containerd
架构
4、containerd
接口对应的源码在哪里
2. dockerd 是怎么启动 containerd的?
dockerd 作为 Docker 服务的核心程序,在启动时会依赖列表和配置进行流程初始化,其中就包括 containerd 的启动。代码的位置很明显,一进来就可以看到
dockerd
会先去看containerd
是否已经启动,判断逻辑就是看/run/containerd/containerd.sock
是否存在。如果不存在,继续往里面走,就会看到下面的代码
这里通过goroutine
去启动containerd
,然后创建一个定时器, 如果在15秒内没有启动成功就会会停止掉dockerd
进程。这里会导致你debug
不了后续的启动代码,这里需要修改以下这个超时时间,方便我们慢慢看参数。
修改为15分钟,重新编译dockerd
代码, 如何编译请看我另一篇文章《docker源码阅读环境搭建》。继续debug
, 可见调用了$PATH
环境变量中的containerd
…, 这是我以前安装docker时候docker安装的。
看到启动的命令了,接下来就是该怎么debug源码了。
3. 怎样调试 containerd 源码?
前面看到,如果containerd
进程存在了,则不会再启动,而是直接使用已经启动的containerd
进程,那么我们只需要编译源码后,用debug模式启动,等待dockerd
进程访问即可。
因此我们得自己编译源代码,替换掉dockerd默认启动的二进制文件。
containerd的源码在另一个项目,所以得把源码拉下来先 , 拉取源码前需要 先看containerd的版本号,这个要看moby项目的vendor.mod
文件,这里我的版本是
所以我们得clone对应的版本,在远程服务器上以及本地上都clone
git clone --branch v1.7.24 --single-branch https://github.com/containerd/containerd.git
官方的安装教程
https://github.com/containerd/containerd/blob/main/BUILDING.md
从安装文档得知,理论上我们还得编译containerd
默认依赖的runc,但是由于我们之前通过apt已经安装过docker
,所以这次先搞定containerd
,等研究到runc
再去折腾.
执行构建命令前需要先启动docker的守护进程, 我启动的是我编译好的
./bundles/binary-daemon/dockerd
安装文档里面写了可以用docker
镜像来编译containerd
开始编译,我本地go的版本是1.23.3,找了个相近的版本,在containerd
源码目录下
docker run -it \
-v ${PWD}:/src/containerd \
-w /src/containerd golang:1.23.4
然后在容器内输入命令
GODEBUG=1 make
等待编译完成即可。感觉这容器只是提供了go环境,并没有什么多大卵用,理论上直接在本地执行
GODEBUG=1 make
也是可以的。编译好后,二进制文件在 {源码目录}/bin
目录下。GoLand测试一下是否能debug
在远程服务器的源码根目录下执行下面的命令,需要加上前面从dockerd
得到的参数
但是又不能直接用原来的配置文件,需要将配置文件复制到containerd源码根目目录下后,修改配置文件中xx.socket
文件的路径如下
debug命令
dlv --listen=:8006 --headless=true --api-version=2 --accept-multiclient exec ./bin/containerd -- --config containerd.toml
containerd 源码的主函数位于 cmd/containerd/main.go
,
GoLand
打上断点运行,可以看到进来了
注意,需要containerd启动完成后,再启动dockerd
4. containerd 架构是怎样的
这是containerd官方的图,看起来应该就是它的架构图了
没怎么研究过,先简单看看就行,流程先走通,等需要再去研究。
5. containerd 接口代码在哪里?
containerd 源码的主函数位于 cmd/containerd/main.go
:
func main() {
app := command.App()
if err := app.Run(os.Args); err != nil {
fmt.Fprintf(os.Stderr, "containerd: %s\n", err)
os.Exit(1)
}
}
跟踪进入 app.Run
,发现最终调用了app.Action 函数。app.Action是在command.App()里面赋值的,继续看Action的逻辑
会看到调用了server方法去创建一个goroutine
去启动监听服务。这个server
就是我们要盯防的gRPC server。
众所周知,gprc
需要使用 Protobuf
定义服务和消息格式,然后在用 protoc
编译器生成客户端和服务器的代码,最后在服务器端实现接口中定义的方法。启动流程代码如下:
server := grpc.NewServer()
service.Register(server)
server.Serve(listener)
那么接下来就是要找.proto
文件以及service
注册的逻辑在哪里了。
.proto文件
easy job,暴力ctrl + shift + f全局搜索后缀,发现在api包里面
service注册逻辑
在项目启动的时候,go的运行时会自动执行每个脚本的init()函数加载插件,如下图,
然后在创建 grpc server
的时候进行初始化, 逻辑在创建server
的函数里,会先把所有注册了的插件形成一个有序的集合,然后for循环处理,
plugins, err := LoadPlugins(ctx, config)
for _, p := range plugins {
... 注册service
}
注册代码截图如下
这里的p
就是在项目启动时由go
运行时执行init函数注册的插件对象,p.Init
就是调用对应其结构体中的函数变量InitFn
,用来初始化插件的,service.Register
里面就是要找的api.RegisterXXXXXServer(server, s)
注册代码
最后值得一提的是,在 containerd 的代码结构中,gRPC
的实现通常被分为多个文件,比如 local.go
和 service.go
。据说这种分工是为了将代码逻辑模块化,更清晰地划分功能和职责。具体如何还得看看代码才知道,简而言之就是
local.go
通常用于处理服务的本地实现逻辑,直接与底层模块交互。
service.go
用于定义 gRPC 服务的接口,实现与客户端的通信。
请求过来的时候,执行service.go中方法,其中会调用local.go中的方法来执行相应的处理逻辑。
他喵的,所以我要看的代码在哪里?
假设你再moby项目中,得到了请求路径如下:
/containerd.services.containers.v1.Containers/Create
1、直接搜/containerd.services.containers.v1.Containers/Create
就能找到生成的xx_grpc.pb.go
,.proto
文件就放在一起的。
2、接着在.proto
文件中获取包名,
以截图例子,
package
是消息的命名空间,是protobuf
用来防止命名冲突的冲突的,如果没有go_package
,也是生成代码的包路径,但如果有go_package
,则以go_package
为准, 包路径为
github.com/containerd/containerd/api/services/containers/v1
,全局搜索包路径,看到有这两个组合的,一般处理逻辑代码就在这了
结论
啥都还没开始看呢。。。
标签:run,启动,浅入,containerd,代码,源码,dockerd,go From: https://blog.csdn.net/wy53111/article/details/144469658