CRI
CRI是Container Runtime Interface(容器运行时接口)的缩写。它是k8s团队提出的容器操作接口标准,符合CRI标准的容器模块才能集成到k8s体系中与kubelet交互。符合CRI的容器技术模块包括dockershim(用于兼容dockerd)、rktlet(用于兼容rkt)、containerd(with CRI plugin)、CRI-O等。
OCI
OCI是Open Container Initiative(开放容器倡议)的缩写。OCI是以docker为首的容器技术公司创建的组织,也是这个组织制定的容器相关标准的统称。OCI标准主要包括两部分:镜像标准和运行时标准。符合OCI运行时标准的容器底层实现模块能够被containerd、CRI-O等容器操作模块集成调用。runc就是从docker中拆分出来捐献给OCI组织的底层实现模块,也是第一个支持OCI标准的模块。除了runc外,还有gVisor(runsc)、kata等其他符合OCI标准的实现。
namespace(资源隔离)
2007年前后,Linux内核支持Cgroup和NameSpace技术,这两种技术在增加对Linux的整体控制的同时,也成为了保持环境隔离的重要框架。
NameSpace(命名空间)主要包含以下六种技术:
(1)MNT Namespace(提供磁盘挂载点和文件系统的隔离能力):
每个容器都要有独立的根文件系统用户空间,以实现在容器里面启动服务并且使用容器的运行环境。换句话说,就是在容器里面不能访问宿主机的资源,宿主机是使用了chroot技术把容器锁定到一个指的运行目录里面。
(2)IPC Namespace(提供进程间通信的隔离能力):
一个容器内的进程间通信,允许一个容器内的不同进程的(内存,缓存等)数据访问,但是不能跨容器访问其他容器的数据 。
(3)UTS Namespace(提供主机名隔离能力):
用于系统标识,其中包含了hostname和域名domainname,它使得一个容器拥有属于自己hostname标识,这个主机名标识独立于宿主机系统和其上的他容器 。
(4)PID Namespace(提供进程隔离能力):
CentOS Linux系统中,有一个PID为1的进程(init/systemd)是其他所有进程的父进程。在每个容器内也要有一个父进程来管理其下属的子进程,多个容器进程的PID namespace进程隔离(比如PID编号重复、容器内的主进程与回收子进程等)。
(5)Net Namespace(提供网络隔离能力):
每一个容器都类似于虚拟机一样有自己的网卡,监听端口,TCP/IP协议栈等。以Docker为例,使用network namespace启动一个vethX接口,这样你的容器将拥有它自己的桥接ip地址,通常是docker0。上面提到的docker0本质上是Linux的虚拟网桥(Virtual Bridge),网桥是在OSI七层模型的数据链路网络设备,通过mac地址对网络进行划分,并且在不同网络直接传递数据。
(6)User Namespace(提供用户隔离能力):
各个容器内可能会出现重名的用户和用户组名称,或重复的用户UID或者GID,那么怎隔离各个容器内的用户空间呢?User Namespace允许在各个宿主机的各个容器空间内创建相同的用户名以及相同的用户UID和GID,只是会用户的作用范围限制在每个容器内。即A容器和B容器可以有相同的用户名称和ID的账户,但是此用户的有效范围仅是当前容器内,不能访问另外一个容器内的文件系统,即相互隔离,互不影响,永不相见。
Cgroups(资源限制)
一个容器如果不对其做任何资源限制,则宿主机(也称为物理机)会允许其占用无限大的内存空间,有时候会因为代码bug程序会一直申请内存,直到把宿主机内存占完。综上所述,为了避免此类的问题出现,宿主机有必要对容器进行资源分配限制,比如CPU,内存,磁盘等。
Linux Cgroups的全称是Linux Control Groups,它最主要的作用就是限制一个进程组能够使用的资源上限,包括CPU,内存,磁盘,网络带宽等等。
此外,Linux Cgroups还能够对进程优先级设置,以及将进程挂起和恢复等操作。
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个用途:
(1)让runc进程可以退出,不需要一直运行。这里有个疑问,为了让runc可以退出所以再启动一个shim,听起来似乎没什么意义。我理解这样设计的原因还是想让runc的功能集中在容器核心功能本身,同时也便于runc的后续升级。shim作为一个简单的中间进程,不太需要升级,其他组件升级时它可以保持运行,从而不影响已运行的容器。
(2)作为容器中进程的父进程,为容器进程维护stdin等管道fd。如果containerd直接作为容器进程的父进程,那么一旦containerd需要升级重启,就会导致管道和tty master fd被关闭,容器进程也会执行异常而退出。
(3)运行容器的退出状态被上报到docker等上层组件,又避免上层组件进程作为容器进程的直接父进程来执行wait4等待。这一条没太理解,可能与shim实现相关,或许是shim有什么别的方式可以上报容器的退出状态从而不需要直接等待它?需要阅读shim的实现代码来确认。
docker架构和运行过程详解
docker架构
Client
Docker客户端,最常用的Docker客户端是docker命令。通过docker命令我们可以方便地在Host上构建和运行容器。docker支持很多操作(docker命令行工具),用户也可以通过REST API与服务器通信。
Docker-Host
(1)Docker daemon:
Docker daemon是服务器组件,即Docker守护进程服务器,以Linux后台服务的方式运行。
Docker daemon运行在Docker host上,负责创建、运行、监控容器,构建、存储镜像。默认配置下,Docker daemon只能响应来自本地Host的客户端请求。如果要允许远程客户端请求,需要在配置文件中打开TCP监听(支持IPV4和IPV6)。
(2)Containers:
Docker容器,用于加载Docker镜像。换句话说,Docker容器就是Docker镜像的运行实例。我们知道镜像(Image)是只读的,在启动一个Container时,其实就是基于Image来新建一个专用的可写仓供用户使用。
(3)Image:
可将Docker镜像看成只读模板(它类似于虚拟机使用的ISO镜像文件),通过它可以创建Docker容器。例如某个镜像可能包含一个Ubuntu操作系统、一个Apache HTTP Server以及用户开发的Web应用。
Registry
我们去构建镜像时,镜像做好之后应该有一个统一存放位置,我们称之为Docker仓库,Registry是存放Docker镜像的仓库,Registry分私有和公有两种。
容器运行过程详解
当我们使用docker run运行一个命令在容器中时,在容器运行时层面会发生什么?
1、如果本地没有镜像,则从镜像 登记仓库(registry)拉取镜像
2、镜像被提取到一个写时复制(COW)的文件系统上,所有的容器层相互堆叠以形成一个合并的文件系统
3、为容器准备一个挂载点
4、从容器镜像中设置元数据,包括诸如覆盖 CMD、来自用户输入的 ENTRYPOINT、设置 SECCOMP 规则等设置,以确保容器按预期运行
5、提醒内核为该容器分配某种隔离,如进程、网络和文件系统( 命名空间(namespace))
6、提醒内核为该容器分配一些资源限制,如 CPU 或内存限制( 控制组(cgroup))
7、传递一个系统调用(syscall)给内核用于启动容器
8、设置 SELinux/AppArmor
以上,就是容器运行时负责的所有的工作。当我们提及容器运行时,想到的可能是 runc、lxc、containerd、rkt、cri-o 等等。这些都是容器引擎和容器运行时,每一种都是为不同的情况建立的。