Docker 源码解析:(三)Docker Daemon 处理请求与执行任务的方式

大家好,我是费益洲。本文将详细解析 Docker Daemon 接收 Docker Client 请求的方式以及执行后续各类任务的过程。
前置条件
Docker Daemon 源码版本:v26.1.4
Docker Daemon 项目地址:Docker Daemon
Docker Client 执行命令:docker run --name nginx -d -p 80:80 -p 443:443 nginx:latest
关键目录结构如下:
moby/
├── api # REST API 路由与处理
├── cmd/dockerd/ # dockerd 主程序入口
├── container # 容器数据结构定义
├── daemon # 守护进程核心逻辑(容器、镜像、网络)
├── distribution # 镜像拉取与推送逻辑
├── layer # 镜像分层管理
├── libcontainerd # 与 containerd 的 gRPC 客户端
├── libnetwork # 网络相关的核心逻辑
├── runconfig # 容器运行配置解析
└── volume # 卷管理
处理请求
Docker Daemon 依然采用了 cobra 框架构建 dockerd 二进制,通过执行带参二进制文件的方式启动 Docker Daemon,然后通过系统进程服务管理 dockerd 的生命周期。
命令行构建
Docker Daemon 的命令行构建非常简单,只有一层 dockerd 命令,主要代码如下所示:
cmd/dockerd/docker.go
// line 75
funcmain() {
...
// line 101 初始化Docker Daemon Command
cmd, err := newDaemonCommand()
if err != nil {
onError(err)
}
cmd.SetOut(stdout)
// line 106 执行命令
if err := cmd.Execute(); err != nil {
onError(err)
}
}
newDaemonCommand 函数中,定义了 dockerd 命令及其参数,并设置了 dockerd 启动时需要加载的默认配置文件 config.json,主要逻辑如下所示:
cmd/dockerd/docker.go
// line 21
funcnewDaemonCommand()(*cobra.Command, error) {
...
// line 29 定义Docker Daemon Command
cmd := &cobra.Command{
Use: "dockerd [OPTIONS]",
...
RunE: func(cmd *cobra.Command, args []string)error {
opts.flags = cmd.Flags()
// line 37
return runDaemon(opts)
},
...
}
SetupRootCommand(cmd)
flags := cmd.Flags()
flags.BoolP("version", "v", false, "Print version information and quit")
// 加载默认配置:/etc/docker/daemon.json
defaultDaemonConfigFile, err := getDefaultDaemonConfigFile()
if err != nil {
returnnil, err
}
// 将默认配置作为config-file选项的默认值
flags.StringVar(&opts.configFile, "config-file", defaultDaemonConfigFile, "Daemon configuration file")
configureCertsDir()
opts.installFlags(flags)
if err := installConfigFlags(opts.daemonConfig, flags); err != nil {
returnnil, err
}
installServiceFlags(flags)
return cmd, nil
}
Docker Daemon 还是借助了 cobra 框架构建的命令行,所以选项及参数的解析由 cobra 框架负责,至此,Docker Daemon 的命令行已经构建完成,接下来我们看启动 Docker Daemon 服务的逻辑。
服务启动
在执行了 dockerd 命令后,会启动一个后台接口服务,用来接收 Docker Client 发送的请求,主要逻辑如下所示:
cmd/dockerd/docker_unix.go
// line 11
funcrunDaemon(opts *daemonOptions)error {
daemonCli := NewDaemonCli()
// line 13 服务启动入口
return daemonCli.start(opts)
}
cmd/dockerd/daemon.go
// line 90
func(cli *DaemonCli)start(opts *daemonOptions)(err error) {
...
// line 93 加载合并后的后台服务配置
if cli.Config, err = loadDaemonCliConfig(opts); err != nil {
return err
}
...
// line 178 根据配置创建Net Listener
lss, hosts, err := loadListeners(cli.Config, tlsConfig)
if err != nil {
return errors.Wrap(err, "failed to load listeners")
}
...
// line 318 创建并注册接口路由
httpServer.Handler = apiServer.CreateMux(routerOpts.Build()...)
...
// line 329 启动服务
var (
apiWG sync.WaitGroup
errAPI = make(chan error, 1)
)
for _, ls := range lss {
apiWG.Add(1)
gofunc(ls net.Listener) {
defer apiWG.Done()
log.G(ctx).Infof("API listen on %s", ls.Addr())
if err := httpServer.Serve(ls); err != http.ErrServerClosed {
log.G(ctx).WithFields(log.Fields{
"error": err,
"listener": ls.Addr(),
}).Error("ServeAPI error")
select {
case errAPI <- err:
default:
}
}
}(ls)
}
apiWG.Wait()
close(errAPI)
...
}
就上就是和服务启动相关的主要逻辑,这部分的代码的功能就是根据配置,启动一个后台接口服务,处理 Docker Client 发出的各类请求。具体的接口路由注册被封装在 routerOpts.Build() 函数中,和容器创建与启动的源码如下所示,其余代码感兴趣的同志可以自行查看源码。
注册路由
// line 674
func(opts routerOptions)Build() []router.Router {
...
// line 681
routers := []router.Router{
...
// line 684
container.NewRouter(opts.daemon, decoder, opts.daemon.RawSysInfo().CgroupUnified),
...
}
...
return routers
}
api/server/router/container/container.go
// line 16
funcNewRouter(b Backend, decoder httputils.ContainerDecoder, cgroup2 bool)router.Router {
r := &containerRouter{
backend: b,
decoder: decoder,
cgroup2: cgroup2,
}
// line 23
r.initRoutes()
return r
}
...
// line 32
func(r *containerRouter)initRoutes() {
r.routes = []router.Route{
...
// line 49
router.NewPostRoute("/containers/create", r.postContainersCreate),
...
// line 54
router.NewPostRoute("/containers/{name:.*}/start", r.postContainersStart),
...
}
}
创建容器
由于容器进程在创建容器的时候并未启动,所以创建容器的主要逻辑就是将容器的所有配置信息,包括 host、network、layer 等信息全部创建并记录下来,用户可以通过 docker ps -a 查看到创建好的容器,也可以通过 docker container inspect {容器名称或ID} 来查看创建好的容器的详细配置信息。接下来我们从容器创建接口路由的绑定函数开始梳理容器创建的主要流程:
api/server/router/container/container_routes.go
// line 444
func(s *containerRouter)postContainersCreate(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string)error {
...
// line 639
ccr, err := s.backend.ContainerCreate(ctx, backend.ContainerCreateConfig{
Name: name,
Config: config,
HostConfig: hostConfig,
NetworkingConfig: networkingConfig,
Platform: platform,
DefaultReadOnlyNonRecursive: defaultReadOnlyNonRecursive,
})
...
}
daemon/create.go
// line 44
func(daemon *Daemon)ContainerCreate(ctx context.Context, params backend.ContainerCreateConfig)(containertypes.CreateResponse, error) {
return daemon.containerCreate(ctx, daemon.config(), createOpts{
params: params,
})
}
...
// line 59
func(daemon *Daemon)containerCreate(ctx context.Context, daemonCfg *configStore, opts createOpts)(containertypes.CreateResponse, error) {
...
// line 79 对主机配置和配置结构进行验证
warnings, err := daemon.verifyContainerSettings(daemonCfg, opts.params.HostConfig, opts.params.Config, false)
...
// line 84 获取镜像元数据并校验镜像支持的架构与当前宿主机是否一致
if opts.params.Platform == nil && opts.params.Config.Image != "" {
img, err := daemon.imageService.GetImage(ctx, opts.params.Config.Image, backend.GetImageOpts{Platform: opts.params.Platform})
...
if img != null {
...
// line 97
if !images.OnlyPlatformWithFallback(p).Match(imgPlat) {
warnings = append(warnings, fmt.Sprintf("The requested image's platform (%s) does not match the detected host platform (%s) and no specific platform was requested", platforms.Format(imgPlat), platforms.Format(p)))
}
}
// line 103 校验容器网络配置是否合法
err = daemon.validateNetworkingConfig(opts.params.NetworkingConfig)
...
// line 111 调整容器必要参数
err = daemon.adaptContainerSettings(&daemonCfg.Config, opts.params.HostConfig)
...
// line 116 创建容器
ctr, err := daemon.create(ctx, &daemonCfg.Config, opts)
...
}
// line 130
func(daemon *Daemon)create(ctx context.Context, daemonCfg *config.Config, opts createOpts)(retC *container.Container, retErr error) {
...
// line 177 初始化容器ID、name、hostname等信息
if ctr, err = daemon.newContainer(opts.params.Name, os, opts.params.Config, opts.params.HostConfig, imgID, opts.managed); err != nil {
returnnil, err
}
// line 180 如果容器创建异常,则清除容器文件
deferfunc() {
if retErr != nil {
err = daemon.cleanupContainer(ctr, backend.ContainerRmConfig{
ForceRemove: true,
RemoveVolume: true,
})
if err != nil {
log.G(ctx).WithError(err).Error("failed to cleanup container on create error")
}
}
}()
...
// line 237 将容器信息注册成容器对象,供Docker Damon使用
if err := daemon.Register(ctr); err != nil {
returnnil, err
}
...
}
daemon/container.go
// line 106
func(daemon *Daemon)Register(c *container.Container)error {
...
// line 119 在Docker Daemon中维护一份容器ID和容器信息的Map映射
daemon.containers.Add(c.ID, c)
// line 120 将容器信息写入文件及内存数据库
return c.CheckpointTo(daemon.containersReplica)
}
此处需要格外注意的是 c.CheckpointTo() 函数。Docker Daemon 为了维护容器的状态及信息,且为了防止信息丢失,会将容器信息存入内存数据库并写入容器文件。c.CheckpointTo() 函数的主体逻辑如下所示:
container/container.go
// line 203
func(container *Container)CheckpointTo(store *ViewDB)error {
deepCopy, err := container.toDisk()
if err != nil {
return err
}
return store.Save(deepCopy)
}
其中,container.toDisk() 会将容器信息写入容器信息所在目录中,默认路径为/var/lib/docker/containers/{容器ID}/,会写入config.v2.json和hostconfig.json两个文件,分别记录容器的状态、基本信息、Host 信息、网络信息、镜像信息、端口绑定信息等。而 store.Save() 函数则是 Docker Daemon 将容器信息存储内存数据库,方便数据管理,使用的库是 go-memdb,感兴趣的同志请自行查看。至此,容器创建逻辑完成,接下来我们继续查看容器启动的逻辑。
启动容器
api/server/router/container/container_routes.go
// line 180
func(s *containerRouter)postContainersStart(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string)error {
...
// line 197 容器启动逻辑入口
if err := s.backend.ContainerStart(ctx, vars["name"], r.Form.Get("checkpoint"), r.Form.Get("checkpoint-dir")); err != nil {
return err
}
...
}
daemon/start.go
// line 42
func(daemon *Daemon)ContainerStart(ctx context.Context, name string, checkpoint string, checkpointDir string)error {
...
// line 48 根据容器ID前缀或容器名称获取容器信息
ctr, err := daemon.GetContainer(name)
if err != nil {
return err
}
// line 52 校验容器状态
if err := validateState(ctr); err != nil {
return err
}
// line 58 对主机配置和配置结构进行验证
if _, err = daemon.verifyContainerSettings(daemonCfg, ctr.HostConfig, nil, false); err != nil {
return errdefs.InvalidParameter(err)
}
// line 62 启动容器
return daemon.containerStart(ctx, daemonCfg, ctr, checkpoint, checkpointDir, true)
}
// line 69
func(daemon *Daemon)containerStart(ctx context.Context, daemonCfg *configStore, container *container.Container, checkpoint string, checkpointDir string, resetRestartManager bool)(retErr error) {
...
// line 113 镜像文件联合挂载到系统中,挂载目录作为容器的BaseDir
if err := daemon.conditionalMountOnStart(container); err != nil {
return err
}
// line 117 初始化容器网络
if err := daemon.initializeNetworking(&daemonCfg.Config, container); err != nil {
return err
}
...
// line 183 创建启动容器任务
tsk, err := ctr.NewTask(context.TODO(),
checkpointDir, container.StreamConfig.Stdin() != nil || container.Config.Tty,
container.InitializeStdio)
...
// line 202 执行容器启动任务
if err := tsk.Start(context.TODO()); err != nil {
return setExitCodeFromError(container.SetExitCode, err)
}
...
}
挂载镜像文件
启动容器的前提是准备好容器需要的系统文件,以下给出挂载镜像文件的具体的函数调用顺序:
daemon/daemon_unix.go
// line 1388
func(daemon *Daemon)conditionalMountOnStart(container *container.Container)error {
return daemon.Mount(container)
}
daemon/daemon.go
// line 1389
func(daemon *Daemon)Mount(container *container.Container)error {
return daemon.imageService.Mount(context.Background(), container)
}
daemon/images/mount.go
// line 15
func(i *ImageService)Mount(ctx context.Context, container *container.Container)error {
...
// line 19
dir, err := container.RWLayer.Mount(container.GetMountLabel())
...
}
layer/mounted_layer.go
// line 102
func(rl *referencedRWLayer)Mount(mountLabel string)(string, error) {
return rl.layerStore.driver.Get(rl.mountedLayer.mountID, mountLabel)
}
daemon/graphdriver/overlay2/overlay.go
// line 508
func(d *Driver)Get(id, mountLabel string)(_ string, retErr error) {
...
// line 565 初始化内核函数,通过调用内核函数挂载文件
mount := unix.Mount
...
// line 596 联合文件挂载
if err := mount("overlay", mountTarget, "overlay", 0, mountData); err != nil {
return"", fmt.Errorf("error creating overlay mount to %s: %v", mergedDir, err)
}
...
}
镜像文件最终会通过调用内核函数通过联合文件挂载系统(OverlayFS)将镜像文件挂载到系统中,并返回挂载后的文件目录,容器会将这个目录作为 BaseDir。
分配 veth-pair
在启动容器之前,Docker Daemon 会调用 libnetwork 中的函数为容器分配 veth-pair 虚拟网卡,并会为 veth-pair 分配名称、Mac 以及 IP 等信息,以下给出具体的函数调用顺序:
daemon/container_operation.go
// line 924
func(daemon *Daemon)initializeNetworking(cfg *config.Config, container *container.Container)error {
...
// line 950
if err := daemon.allocateNetwork(cfg, container); err != nil {
return err
}
...
}
为容器配置网络的逻辑较为复杂,后续会单独开一章进行这部分逻辑的解读,主要包括为容器分配并配置 veth-pair,以及对于端口映射部分路由表的操作过程,后续会尽快推出该文章,感兴趣的同志们可以关注一波。
调用 Containerd 启动容器
tsk.Start() 会调用到 libcontainerd 包中的函数,具体调用逻辑如下:
libcontainerd/remote/client.go
// line 242
func(t *task)Start(ctx context.Context)error {
return wrapError(t.Task.Start(ctx))
}
vendor/github.com/containerd/containerd/task.go
// line 215
func(t *task)Start(ctx context.Context)error {
r, err := t.client.TaskService().Start(ctx, &tasks.StartRequest{
ContainerID: t.id,
})
if err != nil {
if t.io != nil {
t.io.Cancel()
t.io.Close()
}
return errdefs.FromGRPC(err)
}
t.pid = r.Pid
returnnil
}
vendor/github.com/containerd/containerd/api/services/tasks/v1/tasks_grpc.pb.go
// line 66
func(c *tasksClient)Start(ctx context.Context, in *StartRequest, opts ...grpc.CallOption)(*StartResponse, error) {
out := new(StartResponse)
err := c.cc.Invoke(ctx, "/containerd.services.tasks.v1.Tasks/Start", in, out, opts...)
if err != nil {
returnnil, err
}
return out, nil
}
最终会调用 containerd 中的 grpc 接口,将启动容器的请求发送给 containerd,然后等待 containerd 的响应,如果成功,则返回容器 ID 给 Docker Client。至此,容器的创建和启动过程在 Docker Daemon 中的逻辑部分就讲述结束了,关于容器网络的那部分逻辑后续会尽快出一个单章进行讲述,感兴趣的同志们可以关注一下~
夜雨聆风
