容器的核心技术是 Cgroup + Namespace。
容器 = cgroup + namespace + rootfs + 容器引擎
- Cgroup: 资源控制
- namespace: 访问隔离
- rootfs:文件系统隔离。镜像的本质就是一个rootfs文件
- 容器引擎:生命周期控制
一、 Cgroup
Cgroup 是 Control group 的简称,是 Linux 内核提供的一个特性,用于限制和隔离一组进程对系统资源的使用。对不同资源的具体管理是由各个子系统分工完成的。
子系统 | 作用 |
---|---|
devices | 设备权限控制 |
cpuset | 分配指定的CPU和内存节点 |
CPU | 控制CPU使用率 |
cpuacct | 统计CPU使用情况 |
memory | 限制内存的使用上限 |
freezer | 暂停Cgroup 中的进程 |
net_cls | 配合流控限制网络带宽 |
net_prio | 设置进程的网络流量优先级 |
perf_event | 允许 Perf 工具基于 Cgroup 分组做性能检测 |
huge_tlb | 限制 HugeTLB 的使用 |
在 Cgroup 出现之前,只能对一个进程做资源限制,如 ulimit
限制一个进程的打开文件上限、栈大小。而 Cgroup 可以对进程进行任意分组,如何分组由用户自定义。
二、 Namespace
Namespace 是将内核的全局资源做封装,使得每个namespace 都有一份独立的资源,因此不同的进程在各自的namespace内对同一种资源的使用互不干扰。
目前,Linux 内核实现了6种 Namespace。
Namespace | 作用 |
---|---|
IPC | 隔离 System V IPC 和 POSIX 消息队列 |
Network | 隔离网络资源 |
Mount | 隔离文件系统挂载点 |
PID | 隔离进程ID |
UTS | 隔离主机名和域名 |
User | 隔离用户和用户组 |
6种命名空间
- UTS namespace
UTS namespace 对主机名和域名进行隔离。为什么要隔离主机名?因为主机名可以代替IP来访问。如果不隔离,同名访问会出冲突。 - IPC namespace
Linux 提供很多种进程通信机制,IPC namespace 针对 System V 和 POSIX 消息队列,这些 IPC 机制会使用标识符来区别不同的消息队列,然后两个进程通过标识符找到对应的消息队列。
IPC namespace 使得 相同的标识符在两个 namespace 代表不同的消息队列,因此两个namespace 中的进程不能通过 IPC 来通信。 - PID namespace
隔离进程号,不同namespace 的进程可以使用相同的进程号。
当创建一个 PID namespace 时,第一个进程的PID 是1,即 init 进程。它负责回收所有孤儿进程的资源,所有发给 init 进程的信号都会被屏蔽。 - Mount namespace
隔离文件挂载点,每个进程能看到的文件系统都记录在/proc/$$/mounts
里。在一个 namespace 里挂载、卸载的动作不会影响到其他 namespace。 - Network namespace
隔离网络资源。每个 namespace 都有自己的网络设备、IP、路由表、/proc/net 目录、端口号等。网络隔离可以保证独立使用网络资源,比如开发两个web 应用可以使用80端口。
新创建的 Network namespace 只有 loopback 一个网络设备,需要手动添加网络设备。 - User namespace
隔离用户和用户组。它的厉害之处在于,可以让宿主机上的一个普通用户在 namespace 里成为 0 号用户,也就是 root 用户。这样普通用户可以在容器内“随心所欲”,但是影响也仅限在容器内。
三、rootfs
rootfs 代表一个 Docker 容器在启动时(而非运行后)其内部进程可见的文件系统视角,或者叫 Docker 容器的根目录。
先来看一下,Linux 操作系统内核启动时,内核会先挂载一个只读的 rootfs,当系统检测其完整性之后,决定是否将其切换到读写模式。
Docker 沿用这种思想,不同的是,挂载rootfs 完毕之后,没有像 Linux 那样将容器的文件系统切换到读写模式,而是利用联合挂载技术,在这个只读的 rootfs 上挂载一个读写的文件系统,挂载后该读写文件系统空空如也。Docker 文件系统简单理解为:只读的 rootfs + 可读写的文件系统。