docker组件
docker
docker既是整套技术和产品的名称,又是其中一个组件的名称。到今天,docker组件只是一个最外围的入口,为使用者提供一种命令行形式的客户端(CLI)来执行容器的各种操作,使用golang实现。docker客户端将用户输入的命令和参数转换为后端服务的调用参数,通过调用后端服务来实现各类容器操作。
这个组件其实是可替代性最强的组件,有很多的替代性实现,例如各种其他语言的docker客户端和库。这个客户端组件使用docker这个名称是为了和最早期的docker使用习惯保持一致。在docker的早期实现中,所有功能都实现在一个二进制程序docker中,docker既能作为客户端,又能作为服务端,所有操作都是基于docker程序完成的。
dockerd
dockerd是运行于服务器上的后台守护进程(daemon),负责实现容器镜像的拉取和管理以及容器创建、运行等各类操作。dockerd向外提供RESTful API,其他程序(例如docker客户端)可以通过API来调用dockerd的各种功能,实现对容器的操作。但时至今日,在dockerd中实现的容器管理功能也已经不多,主要是镜像下载和管理相关的功能,其他的容器操作能力已经分离到containerd组件中,通过grpc接口来调用。又被称为docker engine、docker daemon。
containerd
containerd是另一个后台守护进程,是真正实现容器创建、运行、销毁等各类操作的组件,它也包含了独立于dockerd的镜像下载、上传和管理功能。containerd向外暴露grpc形式的接口来提供容器操作能力。dockerd在启动时会自动启动containerd作为其容器管理工具,当然containerd也可以独立运行。containerd是从docker中分离出来的容器管理相关的核心能力组件
runc
runc实现了容器的底层功能,例如创建、运行等。runc通过调用内核接口为容器创建和管理cgroup、namespace等Linux内核功能,来实现容器的核心特性。runc是一个可以直接运行的二进制程序,对外提供的接口就是程序运行时提供的子命令和命令参数。runc内通过调用内置的libcontainer库功能来操作cgroup、namespace等内核特性。
containerd-shim
除了这些主要组件外,还有containerd-shim这个组件。containerd-shim位于containerd和runc之间,当containerd需要创建运行容器时,它没有直接运行runc,而是运行了shim,再由shim间接的运行runc。
shim主要有3个用途:
- 让runc进程可以退出,不需要一直运行。这里有个疑问,为了让runc可以退出所以再启动一个shim,听起来似乎没什么意义。我理解这样设计的原因还是想让runc的功能集中在容器核心功能本身,同时也便于runc的后续升级。shim作为一个简单的中间进程,不太需要升级,其他组件升级时它可以保持运行,从而不影响已运行的容器。
- 作为容器中进程的父进程,为容器进程维护stdin等管道fd。如果containerd直接作为容器进程的父进程,那么一旦containerd需要升级重启,就会导致管道和tty master fd被关闭,容器进程也会执行异常而退出。
- 运行容器的退出状态被上报到docker等上层组件,又避免上层组件进程作为容器进程的直接父进程来执行wait4等待。这一条没太理解,可能与shim实现相关,或许是shim有什么别的方式可以上报容器的退出状态从而不需要直接等待它?需要阅读shim的实现代码来确认。
docker架构
Client
Docker客户端,最常用的Docker客户端是docker命令。通过docker命令我们可以方便地在Host上构建和运行容器。docker支持很多操作(docker命令行工具),用户也可以通过REST API与服务器通信。
Docker-Host
- Docker daemon:Docker daemon是服务器组件,即Docker守护进程服务器,以Linux后台服务的方式运行。
Docker daemon运行在Docker host上,负责创建、运行、监控容器,构建、存储镜像。默认配置下, Docker daemon只能响应来自本地Host的客户端请求。如果要允许远程客户端请求,需要在配置文件中打开TCP监听(支持IPV4和IPV6)。 - Containers:
Docker容器,用于加载Docker镜像。换句话说,Docker容器就是Docker镜像的运行实例。我们知道镜像(Image)是只读的,在启动一个Container时,其实就是基于Image来新建一个专用的可写仓供用户使用。 - Image:
可将Docker镜像看成只读模板(它类似于虚拟机使用的ISO镜像文件),通过它可以创建Docker容器。例如某个镜像可能包含一个Ubuntu操作系统、一个Apache HTTP Server以及用户开发的Web应用。
Registry
我们去构建镜像时,镜像做好之后应该有一个统一存放位置,我们称之为Docker仓库,Registry是存放Docker镜像的仓库,Registry分私有和公有两种。
容器运行过程详解
当我们使用docker run运行一个命令在容器中时,在容器运行时层面会发生什么?
- 如果本地没有镜像,则从镜像仓库(registry)拉取镜像
- 镜像被提取到一个写时复制(COW)的文件系统上,所有的容器层相互堆叠以形成一个合并的文件系统
- 为容器准备一个挂载点
- 从容器镜像中设置元数据,包括诸如覆盖 CMD、来自用户输入的 ENTRYPOINT、设置 SECCOMP 规则等设置,以确保容器按预期运行
- 提醒内核为该容器分配某种隔离,如进程、网络和文件系统( 命名空间(namespace))
- 提醒内核为该容器分配一些资源限制,如 CPU 或内存限制( 控制组(cgroup))
- 传递一个系统调用(syscall)给内核用于启动容器
- 设置 SELinux/AppArmor
以上,就是容器运行时负责的所有的工作。当我们提及容器运行时,想到的可能是 runc、lxc、containerd、rkt、cri-o 等等。这些都是容器引擎和容器运行时,每一种都是为不同的情况建立的。