🔥 本文专栏:Docker
🌸作者主页:努力努力再努力wz



💪 今日博客励志语录:
真正拉开人与人差距的,从来不是某一天突然拼命,而是在看不到结果的时候,依然没有停止向前。
思维导图
text
单体应用
↓
模块资源需求不同
↓
垂直扩容粒度过粗
↓
水平扩容只能复制完整应用
↓
微服务
↓
将模块拆成独立服务
↓
服务数量增加,部署环境管理复杂
↓
Docker
↓
程序 + 用户态依赖一起交付
↓
但 Docker 并不自己实现资源隔离
↓
Linux Kernel
├── Namespace:隔离"能看到什么"
└── CGroup:限制"能使用多少"
↓
容器进程本质仍然是 Linux 普通进程
↓
问题:谁来组织这些 Linux 内核能力?
↓
早期 Docker
↓
LXC
↓
Linux Kernel
LXC 的问题:
├── Docker 的底层能力受 LXC 抽象和接口约束
└── LXC 版本 / 实现差异会影响 Docker 的行为一致性
↓
Docker 自己实现 libcontainer
↓
Docker
↓
libcontainer
↓
Linux Kernel
↓
进一步拆分
↓
runc
↓
将 libcontainer 的容器运行能力做成独立可执行程序
↓
Docker CLI
↓
dockerd
↓
containerd
↓
runc
↓
Linux Kernel
最终:
容器不是一台"迷你虚拟机"
而是:
普通 Linux 进程
+
Namespace 隔离视图
+
CGroup 资源限制
+
独立 rootfs / 网络等运行环境
一、先从 Docker 为什么会出现说起
在理解 Docker 的底层架构之前,如果直接去记:
text
LXC
libcontainer
runc
containerd
dockerd
很容易出现一个问题:
每一个名词单独看似乎都能理解,但是却不知道它为什么会出现,也不知道这些组件之间到底是什么关系。
所以这里仍然沿用之前学习 Docker 时建立的思路:
不从名词出发,而是从"前一层到底出现了什么问题"出发。
二、从单体应用到微服务:为什么部署环境会逐渐成为问题
1. 单体应用的扩容粒度太粗
最开始,一个应用可能包含多个模块:
text
Application
├── 登录模块
├── 消息模块
├── 用户模块
└── 后台管理模块
虽然这些模块属于同一个应用,但是它们对于底层硬件资源的需求可能完全不同。
例如:
text
消息模块
→ 网络 I/O 高
→ CPU 消耗高
后台管理模块
→ 请求量很低
→ 资源消耗较低
当高负载模块逐渐成为整个应用的性能瓶颈以后,一个最直接的办法就是:
text
提升 CPU
提升内存
提升磁盘性能
也就是:
text
垂直扩容(Scale Up)
但是问题在于:
真正需要更多资源的可能只有其中一个模块,但是由于所有模块被打包在同一个应用中,我们只能整体升级整个节点。
因此:
text
单体应用
↓
扩容粒度太粗
2. 水平扩容提高了系统吞吐量,但没有解决模块粒度问题
既然单台机器存在性能上限,那么可以复制多个完整的应用实例:
text
Load Balancer
│
┌───────────┼───────────┐
↓ ↓ ↓
Node-1 Node-2 Node-3
│ │ │
Application Application Application
这样整个系统的处理能力确实提高了。
但是需要注意:
text
Node-1
└── A + B + C + D
Node-2
└── A + B + C + D
Node-3
└── A + B + C + D
即使真正高负载的只有 B 模块,我们复制的仍然是:
text
整个 Application
而不是:
text
A × 2
B × 10
C × 1
D × 2
所以:
水平扩容解决了整个系统吞吐量的问题,但是没有解决"按模块独立扩容"的问题。
并且,如果某一个模块发生修改:
text
修改模块 B
↓
重新编译整个应用
↓
重新部署相关应用实例
模块之间仍然存在较强的部署耦合。
3. 微服务把模块变成独立部署单元
于是进一步演化:
text
单体应用:
[A B C D]
↓
微服务:
[A Service]
[B Service]
[C Service]
[D Service]
需要注意:
微服务不是"每一个模块只能对应一个运行实例"。
而是:
text
一个服务
↓
可以拥有多个服务实例
例如:
text
A Service × 2
B Service × 10
C Service × 1
D Service × 3
这样哪个服务压力高,就单独扩哪个服务。
同时,一个服务发生修改以后,也可以只重新构建和部署该服务。
至此:
text
整个应用作为扩容单位
↓
单个服务作为扩容单位
扩容和部署的粒度变细了。
三、微服务又自然引出了运行环境管理问题
虽然微服务解决了部署和扩容粒度的问题,但是服务最终仍然是:
text
Linux 进程
例如一个 C++ 服务:
text
./server
它运行时可能还依赖:
text
glibc
libstdc++
OpenSSL
protobuf
MySQL client
Redis client
配置文件
证书
其他动态库
当服务只有几个、机器只有几台时,我们可以手动:
text
安装依赖
↓
复制配置文件
↓
配置环境变量
↓
启动进程
但是当系统发展成:
text
几十台 / 几百台机器
+
大量服务实例
问题就出现了:
text
Node-A 安装的是 OpenSSL 3.x
Node-B 安装的是 OpenSSL 1.x
Node-A 有某个动态库
Node-B 没有
Node-A 配置正确
Node-B 配置错误
因此我们真正需要解决的是:
如何把"应用程序 + 它所需要的用户态运行环境"作为一个整体进行交付?
Docker 的价值就开始体现出来。
四、Docker 的核心不是"重新造一台计算机"
这里需要先建立一个非常重要的认知:
Docker 和虚拟机虽然都能够提供相对独立的运行环境,但是它们的思路完全不同。
1. 虚拟机:重新构造一台计算机
一台计算机大致可以抽象为:
text
硬件
↓
操作系统
↓
应用进程
而虚拟机的思路就是:
text
物理硬件
↓
Hypervisor
↓
虚拟 CPU / 虚拟内存 / 虚拟设备
↓
Guest OS Kernel
↓
应用进程
也就是说:
虚拟机首先虚拟出一套硬件环境,然后在这套虚拟硬件之上运行完整的 Guest OS。
2. Docker:不重新构造内核,而是利用 Linux 已经提供的能力
Docker 的思路并不是:
text
重新虚拟一套 CPU
重新虚拟一套内存
重新启动一个 Linux Kernel
而是:
text
物理硬件
↓
Host Linux Kernel
↓
Container Process
多个容器中的进程:
text
Container A ┐
Container B ├── 共享同一个 Host Linux Kernel
Container C ┘
因此:
Docker 并没有给每个容器重新运行一套 Linux 内核。
那么问题来了:
如果大家共享同一个内核,容器之间为什么看起来又像是彼此独立的环境?
答案就是:
text
Namespace
+
CGroup
五、Namespace 与 CGroup:容器隔离的底层基础
1. Namespace:决定进程"能看到什么"
Namespace 的核心作用可以简单理解成:
给进程提供不同的资源视图。
例如:
text
PID Namespace
→ 进程看到独立的 PID 空间
Mount Namespace
→ 进程看到独立的挂载视图
Network Namespace
→ 进程看到独立的网络环境
UTS Namespace
→ 进程看到独立的 hostname 等信息
因此所谓:
text
容器内部只看到几个进程
并不是宿主机真的只剩下几个进程。
而是:
这个进程处在特定的 PID Namespace 中,因此内核只向它暴露这个 Namespace 下对应的进程视图。
2. CGroup:决定进程"能使用多少"
Namespace 主要解决:
text
你能看到什么
而 CGroup 主要解决:
text
你能使用多少
例如:
text
memory.max
cpu.max
pids.max
都可以用于限制一组进程能够消耗的资源。
因此我们可以先建立:
text
Namespace
↓
隔离资源视图
CGroup
↓
限制资源使用
六、容器进程本质上仍然是 Linux 普通进程
认识了 Namespace 和 CGroup 之后,一个非常重要的认知就形成了:
"容器进程"并不是 Linux 内核中新出现的一种特殊进程类型。
更准确地说:
text
task A
├── 属于 PID Namespace A
├── 属于 Mount Namespace A
├── 属于 Network Namespace A
└── 属于 CGroup A
另外一个进程:
text
task B
├── 属于 PID Namespace B
├── 属于 Mount Namespace B
├── 属于 Network Namespace B
└── 属于 CGroup B
对于 Linux 内核来说:
它们本质上仍然都是普通 task,只不过拥有不同的 Namespace 上下文以及不同的 CGroup 归属。
例如同一个进程:
text
宿主机视角:
PID = 27431
容器内部视角:
PID = 1
并不是存在两个不同的进程。
而是:
同一个 Linux task,在不同 PID Namespace 中拥有不同的可见标识。
因此以后再看到:
text
容器进程
脑子里不要把它理解成:
text
一种完全独立于 Linux 普通进程之外的新进程
而应该理解成:
text
普通 Linux 进程
+
特定 Namespace 资源视图
+
特定 CGroup 资源限制
七、Docker 到底负责什么?
这里又能够建立一个非常核心的认知:
Docker 自己并不实现 Namespace 和 CGroup。
真正创建、维护这些资源隔离关系的仍然是:
text
Linux Kernel
Docker 所做的事情更接近:
text
描述
+
组织
+
配置
+
调用
也就是说,Docker 告诉内核:
text
这个进程需要:
PID Namespace
Mount Namespace
Network Namespace
并且:
最多使用多少内存
最多使用多少 CPU
...
最终真正完成:
text
Namespace 的建立
CGroup 的限制
进程调度
内存分配
资源访问控制
的仍然是 Linux 内核。
因此 Docker 能够如此轻量,本质原因之一就在这里:
Docker 没有重新实现一套进程、CPU、内存、网络和文件系统机制,而是复用了 Linux 内核已经提供的能力。
八、既然 Linux 已经提供了能力,谁负责把这些能力组织成"容器"?
理论上,我们完全可以自己:
text
clone / unshare
↓
创建 Namespace
mount
↓
构造文件系统视图
配置 veth
↓
构造网络环境
配置 cgroup
↓
限制 CPU / 内存
execve
↓
启动目标进程
但是这里会发现:
"创建一个容器"并不是调用一个系统调用就结束,而是一整套复杂流程。
所以自然需要一个更高层的封装。
早期 Docker 使用的就是:
text
LXC
九、LXC:把 Linux 底层能力封装成"容器"语义
1. LXC 并不是一层薄薄的系统调用封装
如果只是普通的薄封装,可能类似:
cpp
Socket()
{
fd = socket(...);
}
一个成员函数内部基本就是:
text
接收参数
↓
调用一个系统调用
↓
检查返回值
但是 LXC 不一样。
例如:
text
启动容器
这个高层动作背后可能包含:
text
读取容器配置
↓
准备 rootfs
↓
创建 Namespace
↓
准备 Mount
↓
配置 CGroup
↓
配置网络
↓
创建进程
↓
准备进程执行环境
↓
exec 目标程序
因此:
LXC 封装的不是单独某一个 syscall,而是"如何利用一系列 Linux 内核能力构造和管理容器"的完整过程。
2. liblxc 与 LXC 命令行工具
可以先把 LXC 理解成:
text
LXC CLI
↓
liblxc
↓
Linux Kernel
例如执行:
bash
lxc-start -n ubuntu
大致经历:
text
用户输入命令
↓
Bash 解析
↓
发现它是外部程序
↓
创建子进程
↓
execve
↓
进程替换成 lxc-start
↓
lxc-start 解析参数
↓
调用 liblxc
↓
liblxc 组织底层 Linux 能力
↓
Linux Kernel 真正执行
这里需要注意:
像
lxc-start这样的外部命令,在执行时本质上就是一个进程。
它完成特定操作之后可以结束。
而真正的资源隔离关系:
text
Namespace
CGroup
一旦已经建立,就由 Linux 内核负责继续维护。
十、早期 Docker 为什么依赖 LXC?
早期可以把 Docker 的底层关系理解成:
text
Docker
↓
LXC
↓
Linux Kernel
此时 Docker 并不需要自己关心:
text
clone 怎么调用
unshare 怎么调用
Namespace 怎么组织
CGroup 怎么配置
Mount 怎么准备
...
这些底层过程都已经由 LXC 封装。
Docker 只需要站在更高层:
text
我要创建一个容器
我要启动一个容器
我要停止一个容器
然后利用 LXC 已经提供的容器语义即可。
这显然是一个非常合理的早期工程选择:
已经存在成熟工具,就没有必要一开始重新实现一整套 Linux 容器底层逻辑。
十一、既然 LXC 已经能用,Docker 为什么后来还要摆脱 LXC?
这里是整个架构演进中非常关键的一步。
并不是:
text
LXC 做不到容器
也不是:
text
Docker 原来完全不能使用 Linux 内核能力
真正的问题在于:
Docker 越来越不希望自己的核心容器运行行为被另外一层独立项目所约束。
1. Docker 的控制能力需要经过 LXC 这一层抽象
使用 LXC 时:
text
Docker 的需求
↓
转换成 LXC 能理解的配置 / API
↓
LXC
↓
Linux Kernel
假设 LXC 对外提供:
text
A
B
C
这些能力。
Docker 现在希望进一步精确控制:
text
D
但是 LXC 并没有提供对应的高层接口,那么 Docker 就不能单纯按照自己的设计直接组织这一能力,而需要:
text
修改 / 扩展 / 适配 LXC
这就意味着:
Docker 底层容器行为的定义权并不完全掌握在自己手里。
可以类比客户端库:
text
客户端库提供:
create(A, B, C)
那么使用方能够控制什么,很大程度上取决于:
text
客户端库到底暴露了什么接口
2. LXC 自己也存在版本和实现差异
另外一个问题是:
text
Docker
↓
依赖 LXC
而不同宿主机可能存在:
text
机器 A:
LXC Version X
机器 B:
LXC Version Y
机器 C:
LXC Version Z
不同版本可能存在:
text
接口差异
行为差异
实现差异
于是 Docker 就需要:
text
适配不同版本
或者要求:
text
宿主机安装兼容版本
这会降低 Docker 底层运行行为的一致性和可控性。
这里和 Docker 最开始解决的:
text
应用程序运行环境依赖
不是完全同一个层面的问题。
前者是:
text
应用
↓
依赖用户态运行环境
而现在的问题是:
text
Docker 自己
↓
依赖宿主机上的 LXC
但是它们体现了一个类似的工程问题:
额外的环境依赖,会增加行为差异和部署复杂度。
十二、libcontainer:Docker 把底层容器运行能力拿回自己手里
于是 Docker 开始维护自己的底层容器运行库:
text
libcontainer
于是架构从:
text
Docker
↓
LXC
↓
Linux Kernel
逐渐变成:
text
Docker
↓
libcontainer
↓
Linux Kernel
1. libcontainer 是什么?
可以先直接理解成:
libcontainer 是 Docker 自己实现和维护的一套底层容器运行库,用来组织 Linux 提供的 Namespace、CGroup、Mount、Capability 等能力。
因此:
text
Docker
↓
libcontainer
↓
clone / unshare / setns / mount / cgroup ...
↓
Linux Kernel
2. libcontainer 也不是一层 syscall 薄封装
这里和 LXC 的理解是一致的。
它并不是:
text
libcontainer.clone()
↓
clone()
这么简单。
而更接近:
text
创建容器
↓
准备 Namespace
↓
配置 CGroup
↓
准备 rootfs / mount
↓
设置 Capability
↓
准备网络
↓
创建并启动目标进程
因此:
libcontainer 封装的仍然是"如何利用一系列 Linux 内核能力构造 Docker 所需要的容器环境"这一整套过程。
所以从 LXC 到 libcontainer,Linux 底层能力本身并没有发生根本变化。
真正发生变化的是:
text
以前:
底层容器运行逻辑由 LXC 负责
后来:
底层容器运行逻辑由 Docker 自己维护的 libcontainer 负责
也就是说:
Docker 将底层容器运行逻辑的定义权、控制权和实现维护权拿回了自己这一侧。
十三、runc:把 libcontainer 的能力变成独立运行时程序
认识了 libcontainer 之后,接下来又会看到:
text
runc
这里先抓住一个核心区别:
text
libcontainer
= 库
runc
= 可执行程序
也就是说,原来:
text
Docker
↓
直接调用 libcontainer
↓
Linux Kernel
后来进一步拆分:
text
Docker
↓
runc
↓
libcontainer
↓
Linux Kernel
1. runc 和 LXC CLI 很像
之前 LXC 中:
text
lxc-start
lxc-create
lxc-stop
...
这些命令行工具:
text
启动成进程
↓
调用 liblxc
↓
完成特定容器操作
↓
进程结束
而 runc 也可以按照类似方式理解:
text
runc create
runc start
runc delete
执行后:
text
Shell
↓
启动 runc 进程
↓
runc 执行容器运行逻辑
↓
Linux Kernel
↓
完成动作
↓
runc 进程退出
所以:
runc 可以理解成 libcontainer 容器运行能力上面的独立命令行入口。
2. runc 和 libcontainer 不是运行时动态链接关系
这里特别需要注意。
不能把它简单理解成:
text
runc 进程
↓
运行时加载 libcontainer.so
↓
调用函数
更准确的心智模型是:
text
runc 源码
+
libcontainer 源码
↓
一起构建
↓
runc 可执行程序
因此:
libcontainer 的代码和能力已经位于 runc 的实现体系内部,而不是要求 runc 运行时再去寻找一个独立动态库。
所以从架构理解上可以继续画:
text
runc
↓
libcontainer
↓
Linux Kernel
但是要知道:
这里表达的是代码职责层级,而不是说 runc 运行时一定会动态链接一个独立的 libcontainer 动态库。
十四、一次性执行程序与长期管理进程
认识 runc 之后,还需要区分两类进程。
第一类:
text
runc create
runc start
lxc-start
...
这类更接近:
text
执行一次命令
↓
启动一个进程
↓
完成一个具体动作
↓
进程结束
而另外一类:
text
daemon
则是:
text
启动
↓
长期驻留
↓
持续等待和处理请求
例如:
text
containerd
dockerd
就属于长期运行的后台服务。
这就自然引出了 Docker 现代架构中的另外两个核心组件。
十五、containerd:专门负责容器运行管理的后台组件
如果只有 runc,那么我们已经拥有:
text
真正创建 / 启动容器
的底层能力。
但是实际 Docker 系统中还需要长期处理:
text
有哪些容器
哪些容器正在运行
启动某个容器
停止某个容器
删除某个容器
查询状态
管理相关运行资源
因此需要一个长期存在的管理组件:
text
containerd
可以先建立一个最简单的心智模型:
text
containerd
= 专门负责容器运行与生命周期管理的后台服务
它不是普通意义上的:
text
一个代码模块
而是一个:
text
独立 daemon 进程
所以:
text
containerd
↓
长期运行
↓
接收容器管理请求
↓
真正需要执行底层容器动作时
↓
调用底层 runtime
↓
runc
在我们当前讨论的抽象层次下,可以先记:
text
containerd
= 管理者
runc
= 底层执行者
十六、dockerd 又是什么?
这里最容易混淆的地方,就是:
text
Docker
这个词在很多资料里会被作为一个比较宽泛的统称。
但真正拆开以后,可以得到:
text
docker CLI
↓
dockerd
↓
containerd
↓
runc
↓
Linux Kernel
1. docker CLI:用户输入命令时启动的客户端
例如:
bash
docker run nginx
docker ps
docker stop xxx
这里的:
text
docker
是命令行客户端程序。
执行:
bash
docker run nginx
以后:
text
Shell
↓
启动 docker CLI 进程
↓
解析参数
↓
向 dockerd 发送请求
↓
等待结果
↓
CLI 进程结束
所以:
text
docker CLI
= 客户端
可以类比:
text
redis-cli
2. dockerd:Docker 的后台服务端
dockerd 则是:
text
Docker daemon
它是长期运行的后台服务。
因此:
text
docker CLI
↓
dockerd
就有点类似:
text
redis-cli
↓
redis-server
docker CLI 自己不会真正去创建 Namespace、配置 CGroup。
它只是在告诉后台:
text
我要运行 nginx
然后由 dockerd 继续处理。
3. dockerd 和 containerd 的职责不同
这里又会出现一个问题:
既然 dockerd 已经是长期运行的后台管理进程,为什么还需要 containerd?
当前阶段可以先进行职责划分:
text
dockerd
= Docker 产品更上层的管理者
containerd
= 更靠近容器运行时的管理者
runc
= 真正执行底层容器创建动作的执行者
例如:
text
docker run nginx
↓
docker CLI
↓
dockerd
dockerd 需要考虑 Docker 更上层的逻辑,例如:
text
镜像
网络
Volume
容器配置
Docker API
...
当最终需要:
text
真正启动这个容器
时,再向下交给 containerd。
于是可以建立:
text
用户
↓
docker CLI
↓
dockerd
↓
containerd
↓
runc
↓
libcontainer 相关容器运行逻辑
↓
Linux Kernel
需要说明的是:
这张图是我们当前为了理解职责所建立的抽象模型,真实实现中还有更细的中间组件与运行时细节,后续再继续展开即可。
十七、从 LXC 到 containerd:整个演进到底改变了什么?
现在回过头看整个过程:
第一阶段:Docker 依赖 LXC
text
Docker
↓
LXC
↓
Linux Kernel
Docker 复用 LXC 已经实现好的:
text
Namespace
CGroup
Mount
Network
Process
等容器运行逻辑。
第二阶段:Docker 引入 libcontainer
text
Docker
↓
libcontainer
↓
Linux Kernel
Docker 不再把核心容器运行逻辑交给 LXC,而是自己掌握这部分实现。
第三阶段:独立出 runc
text
Docker
↓
runc
↓
libcontainer
↓
Linux Kernel
原本作为库存在的底层容器运行能力,被进一步组织成独立的运行时程序。
第四阶段:引入 containerd 作为长期运行的容器管理层
text
docker CLI
↓
dockerd
↓
containerd
↓
runc
↓
Linux Kernel
于是整个体系的职责逐渐拆开:
text
docker CLI
→ 接收用户命令
dockerd
→ Docker 上层服务管理
containerd
→ 容器运行管理
runc
→ 执行一次具体底层容器运行操作
Linux Kernel
→ 真正完成 Namespace、CGroup、进程调度等行为
这里最重要的不是死记这些名字。
而是理解:
Docker 的架构一直在把不同职责逐步拆开,让每一层只负责自己应该负责的事情。
十八、为什么容器比虚拟机资源利用率更高?
最后回到一个非常经典的面试问题:
为什么 Docker 容器比虚拟机更加轻量、资源利用率更高、启动速度更快?
答案其实已经包含在前面的整个心智模型中了。
1. 虚拟机需要完整 Guest OS
虚拟机:
text
物理硬件
↓
Hypervisor
↓
虚拟 CPU / 内存 / 设备
↓
Guest OS Kernel
↓
应用进程
每一个虚拟机都需要维护:
text
完整 Guest Kernel
+
大量基础系统服务
+
自己的用户态环境
即使只是为了运行一个简单服务,也需要维持这一整套操作系统环境。
因此:
text
每个 VM 的额外资源开销
通常比较大。
2. 容器共享 Host Kernel
Docker 容器:
text
物理硬件
↓
Host Linux Kernel
↓
Container Process
多个容器:
text
Container A ┐
Container B ├── 共享 Host Linux Kernel
Container C ┘
它们并没有分别启动:
text
Kernel A
Kernel B
Kernel C
而是:
text
所有容器进程
↓
仍然由同一个 Host Linux Kernel 管理
因此自然省掉了大量:
text
Guest OS
虚拟硬件
独立 Kernel
所带来的额外资源消耗。
所以:
在相同硬件上,通常可以部署更多容器实例,从而获得更高的部署密度和资源利用率。
3. 容器启动更快
虚拟机启动大致需要:
text
创建 / 初始化虚拟硬件
↓
启动 Guest Kernel
↓
初始化 Guest OS
↓
启动系统服务
↓
启动应用
而容器更接近:
text
准备 Namespace
↓
准备 CGroup
↓
准备 rootfs / mount
↓
创建 Linux 进程
↓
exec 应用
所以:
容器更像"启动一个经过隔离配置的 Linux 进程",而不是"重新启动一台计算机"。
这也是为什么容器通常能够获得更快的启动速度。
4. CPU 执行路径也更加直接,但不要理解成"虚拟机每条指令都很慢"
这里需要避免一个常见误区。
不能简单说:
text
虚拟机中的所有 CPU 指令
↓
都必须经过 Hypervisor 软件模拟
↓
所以非常慢
现代虚拟化通常拥有硬件虚拟化支持。
普通 CPU 指令的执行开销可能并没有想象中那么夸张。
真正更加准确的理解是:
虚拟机额外存在虚拟硬件、Guest Kernel、设备虚拟化等抽象层,在内存管理、I/O、设备访问等路径上可能产生额外开销。
而容器中的进程:
text
Container Process
↓
Host Linux Kernel
↓
Physical CPU
仍然直接作为宿主机 Linux 的普通进程接受内核调度。
所以容器不存在:
text
每个实例再维护一整套 Guest Kernel
这样的额外成本。
十九、最终心智模型
学完这一部分以后,不应该把 Docker 理解成:
text
一个更加轻量的虚拟机
更准确的理解应该是:
text
Docker Container
=
普通 Linux 进程
+
Namespace 隔离视图
+
CGroup 资源限制
+
rootfs / 网络 / 配置等运行环境
而真正负责创建和维护底层资源隔离能力的是:
text
Linux Kernel
Docker 体系负责的是:
text
描述
组织
管理
调用
整个容器运行时架构的演进可以最终压缩成:
text
早期:
Docker
↓
LXC
↓
Linux Kernel
后来:
Docker
↓
libcontainer
↓
Linux Kernel
进一步拆分:
Docker
↓
runc
↓
libcontainer
↓
Linux Kernel
现代职责模型:
docker CLI
↓
dockerd
↓
containerd
↓
runc
↓
Linux Kernel
其中:
text
docker CLI
→ 一次性的用户命令入口
dockerd
→ Docker 上层后台管理服务
containerd
→ 容器运行管理后台服务
runc
→ 底层容器运行执行程序
libcontainer
→ runc 内部的底层 Linux 容器实现能力
Linux Kernel
→ 真正执行 Namespace、CGroup、进程、Mount 等底层机制
因此,这一整套知识真正应该记住的不是:
text
LXC
libcontainer
runc
containerd
dockerd
这些孤立名词。
而是下面这条演进逻辑:
text
Linux 已经提供 Namespace / CGroup 等底层能力
↓
但是直接组织这些能力过于复杂
↓
LXC 对"容器"进行高层封装
↓
早期 Docker 直接复用 LXC
↓
Docker 希望获得更强的底层控制权和一致性
↓
自己实现 libcontainer
↓
进一步把底层容器运行能力独立成 runc
↓
再把长期容器运行管理职责交给 containerd
↓
Docker 上层由 dockerd 负责
↓
用户通过 docker CLI 发起请求
只要这条链条真正建立起来,后续再继续学习:
text
OCI
containerd-shim
Docker Image
OverlayFS
rootfs
Docker Network
这些内容时,就不会再变成单独记忆一个又一个名词,而是能够明确知道:
它处在 Docker 整个体系的哪一层,又是在解决哪一个具体问题。
