【Docker入门系列】从 LXC 到 containerd:一文梳理 Docker 容器运行时架构演进与轻量化原理

🔥 本文专栏: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 整个体系的哪一层,又是在解决哪一个具体问题。

相关推荐
海上小飞龙1 小时前
分布式和微服务,一次讲清
分布式·微服务·架构
天远API2 小时前
零信任架构实战:基于天远公安三要素即时版构建自动化理赔合规网关
人工智能·python·架构·自动化
乱码三千2 小时前
开发了一套AI智能体协作知识体系
人工智能·架构·代码规范
吴佳浩 Alben3 小时前
走向 Memory OS:企业私有化 Agent 设计与实现
人工智能·深度学习·神经网络·语言模型·架构·自动化·ai编程
隔窗听雨眠3 小时前
Dify基于TiDB的数据架构重构实践:从数十万容器到统一存储的系统性演进
重构·架构·tidb
CAE虚拟与现实3 小时前
docker desktop中的build功能是要build什么
java·docker·容器
fb_123453 小时前
Docker入门-第1-2章-容器生态系统与架构详解
docker·容器·架构
allnlei5 小时前
s6-overlay - 装在 Docker 容器里的轻量级管家
java·docker·容器
zhou lily11 小时前
高可用(HA)架构:在混合云环境下,如何通过多活容灾保障核心业务流的稳定性
架构