docker 设计及源码分析
1、dockerd
是一个长期运行的守护进程(docker daemon)。负责管理 docker 容器的生命周期、镜像和存储等。实际还是通过grpc 的协议调用 containerd 的 api 接口,来完成容器管理。
代码所在路径:cmd/dockerd/docker.go
1、newDaemonCommand()
创建一个 config 的初始化对象

2、config.New()
创建配置的实例化对象

他配置了 dockerd 服务默认的运行配置,
3、config_linux.setPlatformDefaults()
设置默认值,其中 linux 的默认值较 Windows 比较特殊。
- 用户资源限制(ulimits):用于限制用户可以使用的系统资源,如文件描述符数量、进程数等。当前为初始化状态
 - 共享内存大小(shmSize):用于设置容器共享内存大小,是一种进程间通信(IPC)机制,允许多个进程共享统一内存,默认 64M
 - 安全计算配置文件(seccompProfile):用于限制容器可以使用的系统调用,从而降低容器的攻击面
 - 进程间通信模式(IPC mode):决定了容器之间如何共享IPC资源,如共享内存、信号量等,默认为 private,表示为每个容器创建一个新的 IPC 命名空间。
 - 容器运行时(runtimes):设置 docker 启动时支持的容器运行时,当前为初始化状态
 
然后判断系统的 cgroup 模式,其中 cgroups.Unified 表示 cgroup v2,那么使用 private 的容器 cgroup 模式,如果为 cgroup v1,那么使用 host 的容器 cgroup 模式。

rootless.RunningWithRootlessKit(),判断当前 docker 守护进程是否在 rootless 模式(允许在不需要root权限的情况下运行Docker守护进程)下运行,如果是的话,则需要按Rootless管理的模式去获取对应可执行文件路径,否则,为默认路径。

4、daemonCli.start()
构建守护进程启动脚本


loadDaemonCliConfig 先加载配置文件,如证书、日志等级等,如果启动命令中指定了配置文件,就合并下,如果有冲突就直接报错。
然后新建一个加密和保护 docker api 通信的,TLS 配置。

检查一下 root 用户,设置默认 umask 值为 0022,然后创建docker daemon 的根目录,默认是 /var/lib/docker,也就是 setPlatformDefaults() 函数中获取到的路径的配置。

如果指定了 pidfile 的路径,检查是否存在,不存在就创建。然后把当前进程的 pid 写入到 pid 文件,在启动成功后,使用 defer在最后删除这个文件。
然后如果配置开启了 rootless 模式的话,需要设置当前这个 pid 文件的粘滞位,来确保运行时目录只能被文件的所有者修改或删除。,防止未授权的访问和修改。
5、loadListeners()
加载 docker daemon 的监听器,处理包括客户端请求、镜像拉取等。
解析命令行启动时入参指定的 host

检查是否绑定到 tcp 地址以及 TLS 配置中客户端身份验证模式是否为正常开启,没开的话就告警一下。
checkTLSAuthOK,检查 TLS 验证是否明确禁用,未来设计会默认要求开启身份认证。如果没开启,检查 ip 是 localhost 或者 回环地址,IPv4 就是 127.0.0.1,都不是的话就打印对应错误日志。

如果协议是 tcp 的,先确保端口没有被容器占用。然后初始化监听器,其中入参包括协议格式,地址,socket 和 tls 相关的配置。

6、listeners.Init()
监听器初始化,分别对应了三种协议格式的处理。
首先是 fd 的协议格式,-H fd://将告诉 docker 该服务正在由 Systemd 启动,并将使用套接字激活。然后,systemd 将创建目标套接字并将其传递给 Docker 守护进程以供使用。相关配置:https://github.com/moby/moby/tree/master/contrib/init/systemd。具体分析可以看下:https://stackoverflow.com/questions/43303507/what-does-fd-mean-exactly-in-dockerd-h-fd
/usr/bin/dockerd -H fd://

第二个是 tcp 的协议格式,通过指定协议地址、端口的形式来构建服务,可能会导致任何有权访问该端口的人都具有完全的 docker 访问权限。

第三个是 unix,先是获取了默认 docker 组的组标识符 gid,创建一个 unix 域套接字监听器。然后尝试给 addr 这个套接字文件设置粘滞位(保证目录所有者才能删除或更改文件)。


到此监听器 listeners 和 hosts 都已经构造完成。
7、initContainerd()

先检查是否有正在运行的 containerd 服务,这里默认的地址就是 /var/run/containerd/containerd.sock 。判断 honorXDG( 即开启 rootlessKit 模式),那么就通过用户指定的环境变量来获取当前 runtime 的目录并构建 Containerd 的默认地址路径,然后检查该路径下是否存在 containerd.sock 文件。


获取 containerd 守护进程的相关配置,先获取平台相关配置,然后判断日志等级。如果容器运行时与容器运行时接口(CRI)的集成被禁用,则通过 supervisor.WithCRIDisabled() 方法禁用 CRI 支持

通过 supervisor 的形式,来启动 containerd 的服务,其中套接字文件就是 containerd.sock

8、start() 启动 containerd 服务
到这里成功启动了 containerd 服务,也就是下面 9962 这个进程。

在函数入口处先拼接了要启动的命令行,其中
rootRir 是 docker daemon 的状态目录,存储各种状态、持久化数据等,默认是 /var/lib/docker/containerd/
statDir是 docker daemon 的根目录,存储二进制文件、配置文件等,默认是 /var/run/docker/containerd/
configFile 是相关配置文件存储,路径默认为 /var/run/docker/containerd/containerd.toml

然后通过 goroutine 实现了用于监控和启动服务的功能。使用定时器来定时执行健康检查。如果守护进程没在运行(daemonPid 为 -1),就通过 startContainerd 启动服务,然后建立grpc 协议监控的client,客户端建立成功,进行连接健康检查,通过 client.IsServing 判断它健康检查是否通过。
在下面判断 pid 状态如果是活跃的,那么说明 pid 不等于 -1,但是 client 是空,是有问题的。所以杀掉当前进程,在下次循环时重启。


通过 startContainerd() 函数,拼接启动命令,即 containerd --config /var/run/docker/containerd/containerd.toml --log-level info然后使用 exec.Command 启动 containerd 服务。通过代码的日志打印也可以对照查看。



创建一个用于监控 containerd 服务的客户端,其中 address 就是 containerd.tmol 中的配置,然后进行连接健康检查。


9、apiShutdown
实现了一个 http 服务的优雅关闭,保证在接收到关闭信号时,http 服务器能够优雅地处理已有链接,并在关闭后通知主流程。并且在函数最后,根据 apiShutdown 通道是否关闭,来决定是预留一点额外处理时间,还是直接关闭服务器。

10、preNotifyReady()
在设置守护进程之前,通过 api 处于活跃状态,这里只针对 Windows,暂时不关注。

11、middlewares()
加载用于 api 服务的中间件,包括用于处理实验性功能的,检查请求的 api 版本是否在支持范围内、跨域处理以及授权相关的。


12、NewDaemon()
检查系统是否满足运行要求,然后创建要注册的 service 服务对象,检查根密钥大小是否满足限制,以及是否开启了默认网关配置。

这里 verifyDaemonSettings 需要重点关注下,他校验配置文件中的配置是否通过格式化,并且 configureRuntimes 配置了默认的 runtime 。其中,DefaultRuntime 就是 runc,然后生成了一个默认Runtimes的 map,具体内容为:
{
    "io.containerd.runc.v2": {  // types.Runtime
        "Path": "runc",
        "ShimConfig": {  // types.ShimConfig
            "Binary": "io.containerd.runc.v2",
            "Opts": {  // v2runcoptions.Options
                "BinaryName": "runc",
                "Root": "/var/run/docker/runtime-runc",
                "SystemdCgroup": false, // 如果没有通过 exec-opts 参数指定为 systemd,那么默认就是 cgroupfs
                "NoPivotRoot": false // 默认和宿主机不共享文件系统,而是隔离
            }
        }
    }
}

设置 dns 文件,通过判断文件中是否只包含 127.0.0.53 作为唯一 dns ,存在则为使用 systemd-resolved 的系统,那么就是用他生成他生成的文件路径,/run/systemd/resolve/resolv.conf,否则就使用默认的/etc/resolv.conf 。


配置用户命名空间的根目录重映射,并加载用户身份信息,确保容器内的用户和组,跟宿主机有正确的映射关系,用来进行用户隔离和命名空间隔离。通过读取启动参数 --userns-remap 指定,如--userns-remap=username:groupname
获取进程在明湖命名空间中的 root 用户和组的标识符。然后设置进程的包括 oom 分数、进程卸载文件系统时的行为等配置。

设置通用资源,将CPU、内存等资源解析转为 grpc 的类型。
并捕捉 SIGUSR1 信号时,触发导出当前进程中所有 go 的栈信息。

设置 seccomp (secure computing mode)配置文件,用于限制进程系统调用的安全机制,包括builtin模式等。
设置默认隔离模式,只针对 win。
配置 go 的线程限制,读取/proc/sys/kernel/threads-max内核允许最大线程数,设置为他的 90%。
确保默认的 appArmor 的安全模式存在,该模式允许管理员为每个程序指定安全策略。

创建 containers 和 runtimes 的文件夹,并设置对应的所属用户和组。
containers,存储容器的相关数据,每个容器都有个单独的子目录,包括元数据、文件系统、文件状态等。
runtimes,存储不同的 OCI 来允许 docker 启动不同的运行时。

其中加载运行时,会有一个初始化的操作,首先通过配置文件(默认 /etc/docker/daemon.json)作为入参,先删除老的 runtimes 文件夹,在读取这个配置中的。配置一般为:
{
  "runtimes": {
    "myruntime": {
      "path": "/path/to/myruntime",
      "runtimeArgs": ["arg1", "arg2"]
    }
  }
}
会遍历这个 runtimes,校验各种参数格式,之后存储为新的 runtimes 文件。

遍历配置文件中的 runtimes 。如果配置中的 path 不为空,那么通过路径配置及额外的参数,来生成一个对应的脚本文件。

V2 Shim 是 Containerd 中的一个组件,用于在容器运行时创建和管理容器进程获取默认配置文件,默认值就是 io.containerd.runc.v2,默认的 runtimeName 就是 runc。通过配置文件判断是否使用了 systemd 作为 cgroup 的驱动程序。

否则,path 为空,就使用 type ,则不需要 args 参数,创建一个新的 shimconfig,并设置 binary 字段为运行时。

然后创建了一个用于暴露 metrics 的 metrics.sock,并提供相关的 api。在回调函数中,将创建的 sock 文件挂载到容器的中,这样可以通过接口获取到容器内监控的数据,来达到监控容器数据的目的。

如果配置文件中 containerd的参数,也就是 containerd grpc 的地址不为空,创建一个与 containerd 实例连接的客户端。

创建 docker 镜像数据的根目录,如 /var/lib/docker/image/overlay2 ,然后创建一个基于文件系统的镜像存储后端,用于存储 docker 镜像各层(layers)的文件。

创建用于存储镜像引用信息的文件,如 /var/lib/docker/image/overlay2/repositories.json 。
返回一个 store 的接口类型,这个接口是进行过存储的通用接口,包括创建、删除、查找等操作。也就是创建了一个新的 docker 镜像存储对象,该对象提供了一组用于管理镜像的方法。
创建用于存储镜像分发元数据的文件 distribution ,其中包括镜像的标签、大小、创建时间等。

判断 docker daemon 是否使用了 dockerd,如果是,就从里面获取 leases 服务(用于管理资源的租约,允许一个实体,比如容器运行时,在一段时间独占某个资源,防止其他实体对该资源并发访问),其中包括文件系统层的写入、镜像拉取和推送。以及获取内容存储。
然后构造一个镜像管理的服务对象。

创建一个用于每 5 分钟执行,检查已经完成的执行命令并清理不再被容器引用的执行命令。也就是 exec 时指定的命令,如 /bin/bash 。
然后初始化 docker daemon 的与 containerd 的链接,并创建一个用于通信的客户端。

13、restore()
负责在 daemon 启动时先恢复已经存在的容器状态。
1、遍历容器存储目录:默认为 /var/lib/docker/containers ,获取之前已经存在的容器列表。然后并发加载多个容器状态。
2、初始化网络控制器:通过 initNetworkController 初始化一个 libnetwork 控制器,并设置连接方式,默认为 bridge 桥接,然后获取网络信息中的 ipv4 等信息,将该网关 ip 设置为 HostGatewayIP 字段。
3、注册容器和连接:将容器注册到 docker daemon 中,并注册容器之间的链接关系,便于容器间访问。
4、重启容器:对检测到需要重启的容器进行重启操作。


14、containerStart()
上面提到有一个重启容器的操作,也就是 containerStart() 。这里文件所在路径一般在:/var/lib/docker/容器 id/
大概分为这几个步骤:
- conditionalMountOnStart:进行文件挂载操作
 - initializeNetworking:初始化网络,包括分配 ip 等
 - createSpec:创建容器运行规范,包括设置 cgroups、资源限制、namespace 等
 - saveAppArmorConfig:保存 AppArmor 配置到容器对象。用于限制进程权限,提高安全性。
 - getCheckpointDir:获取检查点路径,用于恢复容器状态,包括容器的内存、文件系统和进程状态等。
 

获取创建容器的二进制文件路径及创建参数,其中在 initRuntimes 通过遍历 runtimes,构造除了ShimConfig 的相关配置。

在 NewDaemon() 入口处,有一个 verifyDaemonSettings() 的操作,加载了默认的 runtime 和 shimConfig。所以 getLivecontainerdCreateOptions 获取到对应的 shim 和 opts。默认是 io.containerd.runc.v2。
这里通过 docker info 也能看到。

在 ReplaceContainer 返回了一个 container 的 interface,实现了对 containerd 的访问。包括启动、创建任务、检索任务、删除。

但是接口的具体实现是由具体的 runtime 来实现的。所以这里就没有具体代码了。
最后,回到第一个函数,通过 execute 执行 /usr/bin/dockerd -H unix://var/run/docker.sock命令。


15、startMetricsServer()
开启一个用于监控的 api 接口服务,这里默认是为空,即不开启。

16、createAndStartCluster()
默认读取 exec-data ,即/var/run/docker 中 swarm文件夹中配置,来开启 swarm 集群,因为默认文件夹内是空的,所以是不开启的。
然后将带有 swarm 端点的容器进行重新启动。

至此,daemon 已经初始化完成。
17、CreateMux()
创建一个10s 超时的新建路由器,内部包含了接口路由、服务注册、ImageService的接口(包括拉取、推送、创建等)。其中路由、接口的相关具体实现在 moby/api中。
然后开启一个 goroutine,用来监听上面 swarm 集群是否创建成功。
通过 setupConfigReloadTrap() 监听 SIGHUP 信号,可以触发配置的重新加载。
使用 notifyReady(),通知系统 init 进程(systemd),发送一个 READY=1 消息给 systemd,表示服务准备完成。

18、httpServer.Serve(ls)
在第 5 步的时候,加载了 Listeners,这个时候遍历所有的监听器,并创建对应的 http 服务。

这里默认就是 docker 的 socket

19、clean()
上面httpServer.Server()是一个阻塞操作,当他返回错误的时候,即需要关闭服务。
然后确保如果 daemon 在关闭时,需要进行一些清理,如果有错误信息的话,进行记录。

20、小结
到这里,dockerd 相关的源码已经结束,其中主体流程就是
- 加载配置文件,初始化一些默认配置,防止配置文件中不存在
 - 初始化监听器
 - 启动 containerd 服务
 - 通过 shim 重启历史容器
 - 加载中间件,鉴权相关等
 - 开启 http 服务
 - defer 完成优雅关闭
 
docker 设计及源码分析的更多相关文章
- Redis5设计与源码分析读后感(一)认识Redis
		
一.初识redis 定义 Redis是一个开源的Key-Value数据库,通常被称为数据结构服务器,其值可以是多种常见的数据格式,且读写性能极高,且所有操作都是原子性的. 高性能的主要原因 1.基于内 ...
 - Redis5设计与源码分析读后感(四)压缩列表
		
一.引言 上一节我们总结了跳跃表的知识,我们知道了有序数组可以用跳跃表实现,也可以用压缩列表来实现,这一篇文章我们来总结一下压缩列表相关的知识. 二.压缩列表简介 定义:压缩列表 ziplist 本质 ...
 - Redis5设计与源码分析读后感(三)跳跃表
		
一.引言 有序集合在日常开发中相当常见,比如做排名等相关的功能,肯定要用到排序的功能,那么常见底层实现有很多种: 数组 :不便于元素的插入和删除 链表 :查询效率低,需要遍历所有元素 平衡树OR红黑树 ...
 - 《redis 5设计与源码分析》:第二章 简单动态字符串
		
介绍 简单动态字符串(Simple Dynamic Strings, SDS)是Redis的基本数据结构之一,用于存储字符串和整型数据.它的特点是:方便扩容.二进制安全. 二进制安全 在C语言中,用& ...
 - Redis5设计与源码分析读后感(二)简单动态字符串SDS
		
一.引言 学习之前先了解几个概念: SDS定义:简单动态字符串,Redis的基本数据结构之一,用于储存字符串和整型数据. 二进制安全:C语言中用"\0"表示字符串结束,如果字符串本 ...
 - ABP源码分析一:整体项目结构及目录
		
ABP是一套非常优秀的web应用程序架构,适合用来搭建集中式架构的web应用程序. 整个Abp的Infrastructure是以Abp这个package为核心模块(core)+15个模块(module ...
 - jQuery源码分析系列
		
声明:本文为原创文章,如需转载,请注明来源并保留原文链接Aaron,谢谢! 版本截止到2013.8.24 jQuery官方发布最新的的2.0.3为准 附上每一章的源码注释分析 :https://git ...
 - jquery2源码分析系列
		
学习jquery的源码对于提高前端的能力很有帮助,下面的系列是我在网上看到的对jquery2的源码的分析.等有时间了好好研究下.我们知道jquery2开始就不支持IE6-8了,从jquery2的源码中 ...
 - [转]jQuery源码分析系列
		
文章转自:jQuery源码分析系列-Aaron 版本截止到2013.8.24 jQuery官方发布最新的的2.0.3为准 附上每一章的源码注释分析 :https://github.com/JsAaro ...
 - SOFABolt 源码分析
		
SOFABolt 是一个轻量级.高性能.易用的远程通信框架,基于netty4.1,由蚂蚁金服开源. 本系列博客会分析 SOFABolt 的使用姿势,设计方案及详细的源码解析.后续还会分析 SOFABo ...
 
随机推荐
- C++的编译链接与在vs中build提速
			
通过gcc或msvc,clang等编译器编译出来的C++源文件是.o文件.在windows上也就是PE文件,linux为ELF文件,在这一步中,调用其它代码文件中的函数的函数地址是未知的(00000) ...
 - Solution Set -「CF 1490」
			
「CF 1490A」Dense Array Link. 显然不满足的 adjacent elements 之间一直加 \(\min\times2,\min\times4,\cdots,\min\tim ...
 - Teamcenter RAC 开发之《AbstractRendering》
			
背景 关于Teamcenter RAC 客制化渲染表单,做一两个有时间做还是可以的,问题是大批量做的时候就会存在很多重复的代码 例如: 1.定义很多 TCProperty,JTextFiled,ite ...
 - IDEA2019 Debug傻瓜式上手教程
			
Step Into (F7):步入,如果当前行有方法,可以进入方法内部,一般用于进入自定义方法内,不会进入官方类库的方法. Force Step Into (Alt + Shift + F7) ...
 - Destoon模板存放及调用规则
			
一.模板存放及调用规则 模板存放于系统 template 目录,template 目录下的一个目录例如 template/default/ 即为一套模板模板文件以 .htm 为扩展名,可直接存放于模板 ...
 - 使用Blazor WASM实现可取消的多文件带校验并发分片上传
			
前言 上传大文件时,原始HTTP文件上传功能可能会影响使用体验,此时使用分片上传功能可以有效避免原始上传的弊端.由于分片上传不是HTTP标准的一部分,所以只能自行开发相互配合的服务端和客户端.文件分片 ...
 - 如何提高redux开发效率?当然是redux-tookit啦!
			
前言 使用react-redux的朋友都经历过这种痛苦吧? 定义一个store仓库,首先创建各种文件,比如reducer.action.store...,然后 将redux和react连接使用.整个流 ...
 - buffer busy waits等待事件案例-vage
			
转自vage 讨厌香草冰激凌的汽车与Buffer busy wiats的故事 记得好几年前看到过一个故事,通用公司曾收到一客户的邮件,邮件中客户描述了一个非常奇怪的问题.他们家有晚饭后去 ...
 - Qt信号槽与事件循环学习笔记
			
事件与事件循环 信号槽机制 事件与事件循环 在Qt中,事件(event)被封装为QEvent类/子类对象,用来表示应用内部或外部发生的各种事情.事件可以被任何QObject子类的对象接收并处理. 根据 ...
 - MySQL的index merge(索引合并)导致数据库死锁分析与解决方案
			
背景 在DBS-集群列表-更多-连接查询-死锁中,看到9月22日有数据库死锁日志,后排查发现是因为mysql的优化-index merge(索引合并)导致数据库死锁. 定义 index merge(索 ...