


💪 今日博客励志语录:
有时候坚持并不是相信自己一定会赢,而是不愿意让现在的自己,替未来的自己认输
思维导图
Docker 为什么能够创建容器?
↓
Docker 本身并没有重新实现一套操作系统
↓
真正的隔离与资源控制能力来自 Linux Kernel
↓
Namespace:隔离“能看到什么”
Cgroup:限制“能使用多少”
Mount / rootfs:构造容器文件系统视图
↓
早期:Docker 借助 LXC 组织这些 Linux 能力
↓
LXC 对外提供 create / start / stop / destroy 等容器语义
↓
Docker 逐渐希望自己掌握底层容器实现
↓
libcontainer
↓
直接封装 Linux namespace / cgroup / mount 等能力
↓
runc
↓
将底层容器能力做成独立、一次性的动作执行器
↓
但是 runc 干完动作就退出,不能承担长期管理
↓
containerd + shim
↓
containerd:长期管理容器生命周期
shim:长期承接具体容器/task 的运行状态
runc:需要动作时临时启动,执行完成后退出
↓
Docker 整体进一步形成 Client / Server 架构
↓
Docker CLI
↓
Docker Engine API + HTTP
↓
Unix Domain Socket / TCP
↓
dockerd
↓
containerd → shim → runc → libcontainer → Linux Kernel
↓
真正的容器进程运行在 Docker Host
↓
与此同时:
应用程序 + 依赖库 + 配置 + 用户态 rootfs
↓
Docker Image
↓
Registry 负责镜像存储与分发
↓
目标 Docker Host 拉取镜像并创建容器
↓
多服务 / 多节点 / 云计算 / 大数据 / 微服务场景
↓
Docker 的价值被进一步放大
引入:不要先把 Docker 理解成“一个命令工具”
在刚开始接触 Docker 时,很容易直接看到:
docker run
docker ps
docker pull
Dockerfile
Image
Container
然后把 Docker 理解成一个能够“启动容器”的命令行工具。
但是如果只从命令开始学,会出现一个比较明显的问题:
命令会用了,但是不知道 Docker 为什么要设计出 runc、containerd、shim、Image、Registry 这些东西,更不知道这些组件之间究竟是什么关系。
因此,这里仍然沿用此前学习 Docker 时建立的心智模型:
不从“它有什么”开始,而从“它为什么会一步一步变成现在这样”开始。
首先要明确一个最底层的认知:
Docker 并没有凭空创造容器。容器真正依赖的是 Linux 内核已经提供的 namespace、cgroup、mount 等能力。Docker 做的事情,是把这些底层能力进一步组织、封装和产品化。
一、先回到底层:容器真正依赖的是 Linux Kernel
1. Docker 并没有重新实现一套操作系统
此前已经学习过,所谓容器本质上仍然是 Linux 中的普通进程。
假设宿主机中运行:
Host Linux
├── process A
├── process B
└── container process
容器进程并没有自己的 CPU,也没有自己的物理内存,更没有自己的 Linux 内核。
它和其他进程一样,最终都需要经过宿主机 Linux Kernel:
容器进程
↓
Linux Kernel
↓
CPU / Memory / Disk / Network
只不过 Linux 内核能够给这个进程构造一个被隔离后的资源视图。
例如:
PID Namespace
→ 让进程看到独立的进程编号空间
Mount Namespace
→ 让进程看到独立的挂载视图
Network Namespace
→ 让进程看到独立的网络协议栈视图
UTS Namespace
→ 让进程看到独立的 hostname 等信息
所以 Namespace 解决的是:
这个进程“能够看到什么”。
而 Cgroup 解决的是另外一个问题:
这个进程组最多使用多少 CPU?
最多使用多少内存?
能够产生多少 I/O?
也就是说:
Cgroup 解决的是这个进程“能够使用多少资源”。
因此,最底层的容器模型其实一直没有变化:
Linux Kernel
├── Namespace:资源视图隔离
├── Cgroup:资源使用限制
├── Mount:文件系统视图
├── Capability:权限拆分
└── Seccomp:系统调用限制
Docker、LXC、runc 等都位于这些 Linux 能力的上层。
二、从 Linux 原始能力到 LXC:把“内核能力”封装成“容器语义”
1. Linux 提供的是能力,而不是一个 create_container 系统调用
Linux Kernel 并不存在:
create_container();
这样一个高级接口。
真正创建一个容器环境,需要组合大量操作,例如:
创建 Namespace
↓
创建 / 启动目标进程
↓
配置 Cgroup
↓
准备 rootfs
↓
配置 Mount Namespace
↓
配置 UID / GID
↓
配置 Capability
↓
配置 Seccomp
↓
最终 exec 用户程序
因此,Linux 提供的是一组比较底层的“积木”。
而真正的容器工具需要完成的是:
将这些积木组合起来,形成“创建容器、启动容器、停止容器、销毁容器”这样的高级语义。
2. LXC 做的事情就是封装这一组 Linux 能力
LXC 可以先简单理解成:
Linux Kernel
↑
LXC
↑
create / start / stop / destroy ...
也就是说,LXC 将:
namespace
cgroup
mount
进程创建
权限配置
文件系统准备
这些底层动作组织起来,然后向上层暴露更容易使用的容器生命周期接口。
这和我们学习 C++ 时的“封装”思想是一致的。
不过这里需要注意:LXC 的封装并不是此前我们实现 Socket 类时那种特别薄的一层封装。
例如我们以前可能写:
int Socket::Bind(...) {
int ret = bind(...);
if (ret < 0) {
// error
}
return ret;
}
这种封装基本是:
一个成员函数
≈
一个系统调用
但是容器运行时的封装要厚得多。
一个逻辑上的:
CreateContainer
内部可能对应:
namespace 创建
+
cgroup 配置
+
mount rootfs
+
权限设置
+
父子进程同步
+
exec
+
一系列错误处理
所以更准确地说:
LXC 把 Linux 提供的一系列原子能力组合成了一套真正具有“容器语义”的高层接口。
三、Docker 为什么逐渐从 LXC 转向 libcontainer
1. Docker 早期可以借助 LXC 创建容器
早期可以先抽象成:
Docker
↓
LXC
↓
Linux Kernel
Docker 不需要自己直接面对底层复杂的 namespace、cgroup、mount 等细节。
它只需要告诉 LXC:
创建一个容器
启动一个容器
停止一个容器
然后由 LXC 完成真正的底层工作。
这个设计在早期非常合理,因为 Docker 可以快速利用 Linux 已经成熟的容器技术。
2. 但是长期依赖 LXC 会带来控制权问题
随着 Docker 自身不断发展,它会逐渐希望更加精确地控制容器创建流程。
例如 Docker 可能希望自己决定:
Namespace 应该怎样组合
Cgroup 应该如何组织
rootfs 应该怎样准备
Capability 应该如何配置
容器生命周期应该怎样控制
如果所有事情都必须经过 LXC:
Docker
↓
LXC 对外提供什么能力
↓
Docker 才能使用什么能力
那么 Docker 的底层实现会受到 LXC 抽象边界的限制。
如果 Docker 想增加某些更深层的控制能力,就可能需要:
修改 LXC
或者
等待 LXC 提供新的能力
同时,依赖外部 LXC 还意味着运行环境中需要保证:
LXC 已经安装
+
LXC 版本正确
+
不同版本行为能够兼容
这和 Docker 自己希望解决的“减少环境依赖”在一定程度上也是冲突的。
因此 Docker 后来逐渐把这部分能力掌握到自己手中。
3. libcontainer:Docker 自己控制底层容器实现
于是出现了:
libcontainer
可以先理解成:
Docker 自己实现的一套 Linux 容器底层库。
它同样不会创造新的内核机制,而是直接组织:
namespace
cgroup
mount
capability
seccomp
进程管理
这些 Linux Kernel 已经提供的能力。
于是架构逐渐变成:
原来:
Docker
↓
LXC
↓
Linux Kernel
后来:
Docker
↓
libcontainer
↓
Linux Kernel
这里最重要的变化并不是“底层容器原理发生了变化”,而是:
Docker 将底层容器创建逻辑的控制权从 LXC 转移到了自己手中。
四、从 libcontainer 到 runc:把“库”变成真正的动作执行器
1. libcontainer 只是一个库
libcontainer 本质上仍然是程序内部调用的代码库。
它不是一个长期运行的容器管理进程。
也就是说,不能简单理解成:
libcontainer start xxx
真正需要有一个可执行程序,将这些底层能力组织成可以被上层调用的容器动作。
于是引出了:
runc
2. runc 可以理解成低级容器动作执行器
runc 内部集成了 libcontainer 的能力。
当上层需要完成某个具体动作时,可以启动一个 runc 进程,例如概念上:
runc create
runc start
runc exec
runc kill
runc delete
然后:
runc
↓
libcontainer
↓
Linux Kernel 接口
↓
namespace / cgroup / mount ...
最终完成真正的容器操作。
这里需要建立一个非常重要的认知:
runc 不是一个一直运行在后台的容器管理守护进程,而是一个“需要做动作时启动,动作完成后退出”的一次性执行器。
可以抽象成:
上层发出一个动作
↓
启动 runc 进程
↓
runc 调用 libcontainer
↓
完成 Linux 底层操作
↓
runc 退出
所以runc 则是:
为了完成某一次具体任务而临时拉起的执行程序。
五、既然 runc 会退出,谁负责长期管理容器?
这就继续引出了:
containerd
+
containerd-shim
1. containerd:长期存在的容器生命周期管理者
runc 做完动作就退出,因此它并不适合长期维护:
有哪些容器
容器现在是什么状态
什么时候需要 exec
什么时候需要 stop
什么时候需要 delete
于是需要一个长期存在的后台进程来完成管理工作。
这里就是:
containerd
可以先形成一个非常简单的认知:
containerd
= 长期运行的容器生命周期管理进程
而:
runc
= 某一次具体容器动作的执行器
所以此前最初建立的简化模型:
containerd
↓
runc
↓
容器进程
在学习早期并没有问题。
只是随着继续深入,会发现中间还存在 shim。
2. shim:随着容器/task 生命周期长期存在的运行时代理
进一步展开以后:
containerd
↓
shim
↓
runc
↓
容器进程
这里可以先使用一个简化模型:
每一个容器旁边有一个长期存在的 shim,负责承接这个容器运行期间的具体事务。
严格来说,现代 containerd 的 Runtime V2 可以让一个 shim 管理一组 task / container,所以“一容器一个 shim”不是绝对规则。
但是在 Docker 入门阶段,用:
一个容器
↔
一个长期存在的 shim
来建立心智模型非常直观。
3. containerd、shim、runc 的关系
假设现在已经存在容器 A,需要执行一次 exec。
可以抽象成:
containerd
:“我要操作容器 A”
↓
定位容器 A 对应的 shim
↓
shim 接收这次操作
↓
启动一个新的 runc 进程
↓
runc exec ...
↓
libcontainer
↓
Linux Kernel
↓
动作完成
↓
runc 退出
如果以后又执行另外一个动作:
kill / delete / exec ...
则可以再次临时启动 runc。
所以真正应该形成的生命周期模型是:
containerd
→ 长期运行
shim
→ 容器 / task 运行期间长期存在
runc
→ 只有真正执行某个底层动作时才短暂存在
容器进程
→ 业务真正运行的进程
因此可以把职责压缩成:
containerd
= 总管理者
shim
= 具体容器运行期间的长期代理
runc
= 临时动作执行器
libcontainer
= Linux 容器底层能力封装库
Linux Kernel
= 真正提供 Namespace / Cgroup / Mount 等能力
六、Docker 版本与历史旁支:几个容易混淆的点
这一部分不属于 Docker 核心运行原理,但学习时经常会遇到,因此简单建立认知即可。
1. Docker 本身主要使用 Go 实现
一个容易记混的地方是:
Docker 并不是 Google 后来用 Go 重写出来的。
Docker 自身从早期就是 Go 技术栈中的重要项目。
Google 当时同样有自己的容器技术积累,也曾开源:
lmctfy
Let Me Contain That For You
它可以理解成 Google 自己的一套 Linux 容器管理方案。
但是它并不是“Google 用 Go 重写的 Docker”,其核心实现主要是 C++。
这里真正应该建立的是:
LXC
Docker / libcontainer
Google lmctfy
虽然实现方案不同,但最终仍然都需要建立在:
Linux Kernel
↓
namespace / cgroup / mount ...
这些能力之上。
也就是说:
大家不是各自发明了一套新的“容器内核”,而是在用户态用不同方式组织 Linux 已经提供的容器能力。
2. Docker 曾经存在 CE / EE 的版本划分
Docker 后续逐渐商业化以后,历史上曾经使用:
Docker CE
Community Edition
社区版
Docker EE
Enterprise Edition
企业版
来区分社区方向和企业商业方向。
对于当前学习 Docker 原理来说,没有必要过度纠结商业版本历史。
真正需要关心的是:
我们学习的核心仍然是 Docker Engine、容器运行时、镜像、网络、存储等技术本身。
七、Docker 整体为什么采用 Client / Server 架构
在学习 Redis、MySQL 时,我们已经非常熟悉 Client / Server 模型。
例如 Redis:
redis-cli
↓
redis-server
MySQL:
mysql client
↓
mysqld
客户端负责:
接收用户输入
构造请求
发送请求
接收响应
而真正的数据处理、资源管理都发生在服务端。
Docker 也是同样的思想。
可以先抽象成:
Docker CLI
↓
dockerd
其中:
Docker CLI
= 客户端
dockerd
= Docker 服务端守护进程
这里需要建立一个非常关键的认知:
Docker CLI 只是控制入口,真正创建容器、管理镜像、配置网络、调用 containerd / runc 的能力都位于 Docker Host,也就是服务端一侧。
这一点和 MySQL、Redis 完全一致。
例如 MySQL 客户端发送:
SELECT * FROM user;
真正查询数据的是:
mysqld
因为:
数据在服务端
+
真正处理 SQL 的能力也在服务端
Docker 同理。
客户端发送:
“我要启动一个容器”
真正的:
namespace 创建
cgroup 配置
rootfs 挂载
容器进程启动
全部发生在 Docker Host 上。
八、Docker CLI 到 dockerd:请求到底怎么发送?
1. Docker 并不是直接把 docker run nginx 原字符串发送过去
假设用户在 Shell 中输入:
docker run nginx
从用户视角来看这是一条字符串命令。
但是程序真正执行时,一般会先由 Shell 完成命令行解析,然后 Docker CLI 获得对应的参数,例如概念上:
argv[0] = docker
argv[1] = run
argv[2] = nginx
Docker CLI 再根据这些参数理解用户真正想做的事情。
所以这里不是:
"docker run nginx"
↓
原样编码
↓
服务端还原出 "docker run nginx"
而是:
Docker CLI 解析命令参数
↓
理解业务语义
↓
按照 Docker Engine API 构造 HTTP 请求
这里需要额外注意一点:Docker CLI 本身并不是一个长期运行、持续等待用户输入的客户端进程。它和我们之前接触到的 LXC 命令行工具比较类似,通常是“执行一次命令,就启动一次进程”。
例如当我们在 Shell 中执行:
docker run nginx
此时 Shell 会启动一个 docker CLI 进程,并将 run、nginx 等参数传递给它。Docker CLI 再解析这些参数,理解当前需要执行的 Docker 操作,并按照 Docker Engine API 的规范构造相应的 HTTP 请求,通过 Unix Domain Socket 或 TCP 发送给后台长期运行的 dockerd。当本次请求处理完成、结果输出之后,这个 Docker CLI 进程也就随之退出。
整个过程可以简单理解为:
用户输入 docker run nginx
↓
Shell 启动 Docker CLI 进程
↓
Docker CLI 解析本次命令参数
↓
构造并发送 HTTP 请求
↓
dockerd 执行对应操作
↓
Docker CLI 获取结果并输出
↓
Docker CLI 进程退出
下一次再执行:
docker ps
则会重新启动一个新的 Docker CLI 进程,完成这一次请求后再次退出。因此,Docker 架构中真正长期运行的是服务端的 dockerd、containerd 等后台进程,而 Docker CLI 只是一次性完成某次控制请求的命令行客户端。
这一点和 redis-cli 的交互模式有所区别。redis-cli 可以进入一个持续运行的交互环境,不断接收 SET、GET 等命令;而 Docker CLI 常见的使用方式则更接近:
一次 docker 命令
→ 一个 Docker CLI 进程
→ 一次或一组 API 请求
→ 执行完成
→ 进程退出
因此,后面分析 Docker 整体架构时,可以把 Docker CLI 看成一个短生命周期的控制端工具,而把真正长期存在、负责容器管理与运行的主体放在 Docker 服务端这一侧。
2. 和 Redis RESP 的区别
此前学习 Redis 时,我们知道客户端和服务端之间使用 RESP。
例如:
SET name wang
redis-cli 会将:
SET
name
wang
这些命令参数按照 RESP 格式组织,然后发送给 redis-server。
redis-server 解析 RESP 后,得到 Redis 命令和参数,再执行对应操作。
Docker 的思想类似,但它没有为 CLI 和 dockerd 再设计一套类似 RESP 的命令协议,而是直接大量利用 HTTP。
因此可以对比:
Redis:
用户命令
↓
redis-cli 解析
↓
RESP 序列化
↓
TCP
↓
redis-server 解析 RESP
↓
执行命令
Docker:
用户命令
↓
Docker CLI 解析
↓
按照 Docker Engine API 构造 HTTP 请求
↓
HTTP 序列化
↓
Unix Domain Socket / TCP
↓
dockerd 解析 HTTP
↓
执行对应业务逻辑
3. Docker Engine API 到底是什么
这里最容易误解成:
先构造一种 Docker 自己的 API 报文
↓
再转换成 HTTP 报文
实际上没有必要这样理解。
Docker Engine API 更像一套:
接口规范。
它规定:
哪个操作应该使用什么 HTTP Method
访问哪个 URL
参数放在哪里
Body 使用什么格式
服务端应该返回什么结果
例如概念上:
POST /containers/create
表示创建容器。
Body 中可能携带:
{
"Image": "nginx"
}
因此最直接的模型就是:
Docker CLI 解析参数
↓
按照 Docker Engine API 的规则
直接构造 HTTP 请求
↓
序列化为 HTTP 字节流
↓
发送
这里没有必要额外插入一层“Docker 私有报文格式”。
4. REST API 又是什么
REST API 不是一种新的传输协议。
可以先简单理解成:
利用 HTTP 已经提供的 Method、URL、Header、Body 等机制来设计程序接口的一种风格。
例如:
GET /users/100
→ 获取用户
POST /users
→ 创建用户
DELETE /users/100
→ 删除用户
REST 并不是完全忽略 HTTP 原本的语义,只借一个 HTTP 外壳。
相反,它通常会尽量利用 HTTP 已经存在的语义:
GET
→ 获取
DELETE
→ 删除
POST
→ 提交 / 创建 / 触发操作
然后再由具体业务定义:
/users
/containers
/images
这些资源到底代表什么。
Docker Engine API 可以简单理解成:
一组基于 HTTP 暴露的 Docker 控制接口,其中很多设计带有 REST 风格。
没有必要在入门阶段死抠它是否完全满足“纯 REST”。
九、Unix Domain Socket 与 TCP:HTTP 请求最终从哪里发送?
HTTP 只是应用层请求格式。
真正把请求字节从 Docker CLI 送到 dockerd,还需要底层通信通道。
1. 本机通信:Unix Domain Socket
如果 Docker CLI 和 dockerd 位于同一台 Linux 主机,默认常见方式是:
Docker CLI
↓
/var/run/docker.sock
↓
dockerd
这里使用的是:
Unix Domain Socket
本质上属于本机进程间通信,不需要经过完整的 IP + TCP 网络路径。
2. 远程通信:TCP
如果:
Docker CLI 在机器 A
dockerd 在机器 B
那么就可以通过网络使用 TCP:
机器 A
Docker CLI
↓
HTTP
↓
TCP
↓
机器 B
dockerd
所以这里可以总结为:
Docker Engine API / HTTP
= 请求表达方式
Unix Domain Socket / TCP
= 请求字节的传输通道
本机常用 Unix Domain Socket,跨主机则需要 TCP 等网络通信方式。
十、最关键的 C/S 认知:容器一定运行在 Docker Host 上
这一点非常重要,因为后面讨论 Docker 部署时,很容易不自觉地把客户端和服务端混在一起。
假设:
机器 A
→ Docker CLI
机器 B
→ dockerd
此时 A 发送:
启动容器
真正执行:
containerd
shim
runc
libcontainer
namespace
cgroup
的是机器 B。
因为:
runc 在服务端
libcontainer 在服务端
Linux Kernel 也在服务端
所以真正的容器进程自然也运行在机器 B。
完整链条是:
机器 A:Docker CLI
↓
HTTP / TCP
↓
机器 B:dockerd
↓
containerd
↓
shim
↓
runc
↓
libcontainer
↓
机器 B 的 Linux Kernel
↓
机器 B 上的容器进程
因此后续学习 Docker 原理时,客户端这一侧其实可以逐渐折叠掉。
真正需要重点关心的是:
Docker Host
│
├── dockerd
├── containerd
├── shim
├── runc
├── image
└── Linux Kernel
也就是说:
客户端只是控制端;容器真正的运行、资源隔离、镜像使用以及生命周期管理,全都发生在 Docker 服务端所在的节点。
十一、在理解 Registry 之前,先弄清楚 Image 到底是什么
Docker 最开始想解决的问题之一,就是:
程序换一台机器以后还能不能直接运行?
一个 C++ 服务并不是只有一个可执行文件。
它可能还依赖:
server
├── libstdc++.so
├── glibc
├── libssl.so
├── protobuf
├── libmysqlclient.so
├── 配置文件
└── 其他用户态工具与文件
如果每部署到一台新机器都重新:
安装依赖
检查版本
配置环境
准备目录
准备配置文件
那么节点越多,部署成本越高,也越容易出现环境不一致。
因此 Docker 希望把:
可执行程序
+
依赖库
+
配置
+
工具
+
用户态文件系统环境
提前准备好。
这就引出了:
Docker Image
1. 镜像不是完整操作系统
这里需要非常注意:
镜像不会包含一套独立的 Linux Kernel。
但是它可以包含一套看起来很像某个 Linux 发行版的用户态文件系统,例如:
/
├── bin
├── lib
├── usr
├── etc
└── app
比如一个 Ubuntu 镜像中可能包含:
/bin/bash
/bin/ls
/lib/...
/usr/...
/etc/...
这些属于:
用户态 rootfs
但是不会包含一个独立内核。
真正运行时仍然是:
镜像提供:
程序 + 库 + rootfs
↓
容器进程
↓
宿主机 Linux Kernel
2. rootfs 和此前学习的 mount namespace 正好接起来
此前已经学习过:
容器进程需要看到一套独立的文件系统视图
那么这套文件从哪里来?
其中很重要的一部分就是镜像提供的 rootfs。
可以抽象成:
Docker Image
↓
提供用户态 rootfs
↓
准备为容器进程使用的根文件系统
↓
结合 Mount Namespace
↓
容器进程看到:
/
├── bin
├── lib
├── usr
├── etc
└── app
因此:
Image
→ 解决“容器里面有哪些文件”
Namespace
→ 解决“容器进程看到什么资源视图”
Cgroup
→ 解决“容器进程能够使用多少资源”
三者是不同层次的问题。
3. Image 与 Container 不是一个东西
镜像本身只是静态数据。
例如:
nginx image
放在磁盘中,它并不会自己执行。
只有 Docker 根据这个镜像创建真正的进程以后,才形成运行中的容器。
所以可以先类比:
Image
≈ 模板
Container
≈ 根据模板创建出来的运行实例
一个镜像完全可以创建多个容器:
nginx image
│
┌────────┼────────┐
↓ ↓ ↓
container1 container2 container3
这些容器:
进程不同
Namespace 不同
Cgroup 不同
网络环境不同
运行状态不同
但是都可以共享同一个镜像作为基础。
因此,当前阶段最重要的镜像认知可以压缩成一句:
Docker Image 是一份静态、只读的容器运行模板,包含应用程序、依赖库、配置以及用户态 rootfs;真正创建容器时,再结合宿主机 Linux Kernel 提供的 Namespace、Cgroup 等能力启动业务进程。
十二、Registry:镜像已经有了,下一步就是“镜像放在哪里”
理解镜像以后,Registry 就非常自然了。
假设开发机 A 已经制作出了:
myapp:v1
如果需要将它部署到:
Node-B
Node-C
Node-D
那么这些节点都需要先获得同一份镜像。
当然可以手动复制,但是随着节点数量增加,这种方式很快就会变得不可维护。
因此需要一个专门负责:
存储镜像
+
分发镜像
的远程服务。
这就是:
Registry
可以先类比:
GitHub
→ 存储和分发代码仓库
百度网盘
→ 存储和分发普通文件
Docker Registry
→ 存储和分发 Docker 镜像
1. push 与 pull
Registry 当前阶段最核心的两个动作就是:
push
→ 将镜像上传到 Registry
pull
→ 从 Registry 获取镜像
于是部署流程变成:
开发 / 构建节点
↓
制作 Image
↓
push
↓
Registry
↓
pull
↓
目标 Docker Host
↓
本地获得 Image
↓
创建 Container
所以:
Registry 不负责运行容器。它只是镜像的存储和分发中心。
真正的容器仍然由目标 Docker Host 自己创建。
Registry 不参与:
namespace
cgroup
shim
runc
这些运行时动作。
2. 不要死记成“客户端上传、服务端下载”
学习初期可以看到典型场景:
开发机
→ push
Registry
→ pull
生产服务器
但是从本质上说,只要拥有权限,任何 Docker Host 都可以:
push
或者
pull
所以更准确的关系是:
Docker Host ↔ Registry
而不是 Registry 天然绑定“客户端”或者“服务端”角色。
当前阶段认识到这里就足够了,后面再继续深入:
Repository
Tag
Layer
Manifest
这些镜像体系中的概念。
十三、把目前所有组件放回一张 Docker 整体架构图
经过前面的铺垫,现在再看 Docker 架构就不会只看到一堆名词。

说明: 上图用于表达整体心智模型,重点是“控制链路、运行时链路和镜像分发链路”的职责划分。实际实现中,Docker CLI 的
push / pull仍然首先是对 Docker Engine 发起控制请求,真正和 Registry 进行镜像数据交互的是 Docker Engine 一侧;因此图中的 Registry 箭头可以理解为“镜像从构建侧进入 Registry,再由目标 Docker Host 获取”的简化数据流,而不是强调精确的进程调用路径。
整张图可以拆成三条链路。
1. 控制链路
用户
↓
Shell
↓
Docker CLI
↓
Docker Engine API
↓
HTTP
↓
Unix Domain Socket / TCP
↓
dockerd
这一条链路解决:
用户如何告诉 Docker 服务端“我要做什么”。
2. 容器运行时链路
dockerd
↓
containerd
↓
shim
↓
runc
↓
libcontainer
↓
Linux Kernel
↓
Container Process
这一条链路解决:
服务端真正怎样把“创建 / 启动 / exec / stop”等操作落实到 Linux Kernel。
3. 镜像分发链路
Docker Host / 构建节点
↓ push
Registry
↓ pull
目标 Docker Host
↓
Image
↓
Container
这一条链路解决:
程序及其运行环境怎样在不同节点之间标准化地分发。
因此整个 Docker 架构可以进一步压缩成四块:
Docker CLI
= 控制客户端
dockerd + containerd + shim + runc
= 服务端容器管理与运行时体系
Linux Kernel
= 真正提供容器隔离和资源控制能力
Registry
= 镜像的存储与分发中心
十四、从 Docker 核心能力继续过渡到 Docker 生态
理解前面的架构以后,再看所谓“Docker 生态”就不会觉得它是一些完全无关的名词堆积。
Docker 最开始解决的一个核心问题就是:
程序部署到不同机器
↓
环境可能不同
↓
依赖库可能缺失
↓
版本可能不兼容
↓
需要人工配置
Docker 将:
程序
+
依赖
+
用户态 rootfs
统一做成 Image 后,部署流程可以逐渐变成:
Node-A → pull image → run container
Node-B → pull image → run container
Node-C → pull image → run container
节点越多,这种标准化交付的价值越明显。
1. 为什么 Docker 特别适合多节点、多服务场景
假设系统只有一台机器、一个服务,手动安装环境可能暂时还能接受。
但是如果变成:
100 台机器
×
每台多个服务
那么传统方式可能变成:
Node-1:安装依赖、配环境、部署服务
Node-2:安装依赖、配环境、部署服务
Node-3:安装依赖、配环境、部署服务
...
很容易出现:
Node-A:运行正常
Node-B:缺库
Node-C:库版本不对
Node-D:配置文件不同
而 Docker 可以将部署单位统一成 Image:
同一个 Image
↓
Node-A
Node-B
Node-C
Node-D
所以多节点、多服务、频繁部署的场景会天然放大 Docker 的价值。
需要注意的是:
Docker 并不是只有多节点场景才有价值。
即使单机环境,它也能够解决:
服务之间依赖冲突
开发 / 测试 / 生产环境一致性
快速创建和销毁环境
环境复现
只是当系统规模扩大以后,这些优势会更加明显。
十五、Docker 为什么会和云计算、大数据、微服务联系起来
1. 云计算:底层本来就是大规模资源池
像大型云平台,底层通常由大量物理主机和虚拟化资源组成。
可以先建立一个简化模型:
大量 Physical Host
↓
组成资源池
↓
上面运行 VM / Container / Service
一台物理主机上可能进一步运行:
Physical Host
├── VM-1
│ ├── Web Service
│ └── Redis
│
├── VM-2
│ └── MySQL
│
└── Containers
├── Service-A
├── Service-B
└── Service-C
这里需要注意,“主机”“节点”“服务器”几个词在不同语境中容易混用。
更准确地说:
一个物理或虚拟节点上,可以部署和运行多个服务实例,而服务实例又可能直接运行在宿主机、虚拟机或者容器内部。
当节点规模非常大时,如果每个服务都需要人工配置环境,维护成本会迅速上升。
Docker 正好可以将:
应用 + 用户态运行环境
标准化成 Image,再统一分发到不同节点。
所以 Docker 和云计算天然具有很强的结合关系。
2. 大数据:经常天然就是多组件、多节点部署
大数据系统中经常会出现:
多个计算节点
多个存储节点
多个消息组件
多个协调组件
例如一些分布式计算、存储、消息系统,往往并不是只部署一个进程就结束,而是需要在多个节点上维护一组服务。
节点数量增加后,同样会遇到:
环境一致性
依赖版本
组件部署
快速扩容
这些问题。
Docker 并不是用来“处理大数据”的计算框架。
更准确的关系是:
大数据系统本身具有多节点、多组件、部署复杂的特点,而 Docker 可以帮助它们标准化部署环境。
3. 微服务:服务数量增加以后,容器化价值更明显
微服务会将原来单体应用中的模块拆分为:
LoginService
MessageService
FriendService
AdminService
...
每一个服务都需要:
独立部署
独立启动
独立升级
独立扩容
服务数量一多,如果每个服务都依赖宿主机手动配置,就会非常麻烦。
因此微服务和 Docker 同样天然契合。
可以先形成:
微服务
→ 服务实例数量增加
→ 部署频率增加
→ 环境管理复杂度增加
→ Docker 将每个服务标准化成 Image
→ 容器化部署
十六、为什么后面又会自然出现 Kubernetes
当 Docker 解决了:
一个应用怎样标准化打包
一个容器怎样在单台主机上运行
以后,规模继续扩大就会出现另一个问题。
假设现在有:
100 台服务器
10000 个容器
这时即使每一个容器都已经能够通过 Docker 很方便地启动,也不可能继续靠人工决定:
这个容器部署在哪台机器?
哪个节点还有资源?
容器挂了以后谁重启?
压力变大以后应该增加多少实例?
压力降低以后应该减少多少实例?
服务之间怎样发现?
于是进一步引出了:
容器编排
其中最典型的就是:
Kubernetes
所以可以进一步建立这条生态链:
应用
↓
Docker Image
↓
Container
↓
大量 Container
↓
容器编排
↓
Kubernetes
↓
大量服务器节点
↓
云平台 / 大规模集群
因此所谓 Docker 生态,并不是说“大数据、云计算、微服务本身就是 Docker”。
更准确地说:
这些场景普遍具有多节点、多服务、部署频繁、环境复杂等特点,而 Docker 恰好解决了应用环境标准化和容器化运行问题,因此 Docker 在这些场景中被大量采用,并进一步形成了云原生、容器编排等周边生态。
十七、最终建立完整心智模型
学习到这里,可以把本轮所有知识重新压缩成一条逻辑链。
1. 容器实现层
Linux Kernel
↓
Namespace:隔离资源视图
Cgroup:限制资源使用
Mount / rootfs:构造文件系统环境
↓
LXC / libcontainer
把底层原子能力组织成容器语义
↓
runc
一次性执行真正的容器动作
↓
shim
长期承接具体容器 / task
↓
containerd
长期管理容器生命周期
↓
dockerd
Docker 服务端总入口
2. Docker Client / Server 层
用户
↓
Docker CLI
↓
按照 Docker Engine API 构造 HTTP 请求
↓
Unix Domain Socket / TCP
↓
dockerd
↓
真正执行 Docker 业务逻辑
这里最关键的认知是:
客户端只负责控制,真正的容器一定运行在 Docker Host。
3. 镜像与容器层
可执行程序
+
依赖库
+
配置
+
用户态 rootfs
↓
Docker Image
↓
结合 Linux Kernel 的 Namespace / Cgroup 等能力
↓
Container
因此:
Image
= 静态运行模板
Container
= 真正运行起来的实例
4. Registry 层
Image
↓ push
Registry
↓ pull
目标 Docker Host
↓
Container
Registry 的核心职责就是:
镜像存储
+
镜像分发
5. Docker 生态层
节点越来越多
+
服务越来越多
+
部署越来越频繁
↓
环境一致性与交付问题越来越突出
↓
Docker 的标准化镜像 + 容器化部署价值被放大
↓
微服务 / 云计算 / 大数据等大量采用
↓
容器规模继续扩大
↓
Kubernetes / 云原生生态
总结
本轮学习的重点并不是记住 Docker 有多少组件,而是将它们按照“为什么出现”的逻辑连接起来。
最底层,Docker 并没有创造容器。真正提供资源隔离和限制能力的是 Linux Kernel:
Namespace
+
Cgroup
+
Mount
+
其他 Linux 能力
LXC 首先将这些底层能力组合成更高级的容器语义。Docker 早期可以依赖 LXC,但是随着自身发展,为了获得更强的底层控制能力,逐渐使用自己的 libcontainer。
随后 libcontainer 又进一步被组织到 runc 中,使 runc 成为真正执行容器底层动作的低级运行时。
但是 runc 是一次性的动作执行器,执行结束以后就会退出,因此又需要 containerd 这样的长期管理进程,以及 shim 这样的长期运行时代理来承接具体容器的生命周期。
在更上层,Docker 又采用 Client / Server 架构:
Docker CLI
↓
Docker Engine API + HTTP
↓
Unix Domain Socket / TCP
↓
dockerd
客户端只是控制端,真正的容器执行能力始终位于 Docker Host。
与此同时,Docker 还需要解决“程序及其运行环境如何交付”的问题,于是出现 Image:
程序
+
依赖
+
配置
+
用户态 rootfs
↓
Image
镜像本身并不是正在运行的容器,它只是一个静态运行模板。真正创建容器时,再利用宿主机 Linux Kernel 的 namespace、cgroup 等能力启动进程。
当镜像需要在不同节点之间传播时,又自然引出了 Registry:
构建节点
↓ push
Registry
↓ pull
部署节点
最终,当系统从单节点逐渐扩大到多节点、多服务、大规模集群时,Docker 对环境一致性、镜像分发和标准化部署的价值会越来越明显,也就自然进入了微服务、云计算、大数据以及后续 Kubernetes / 云原生生态。
因此,如果只用一句话总结这一整轮知识,可以是:
Docker 的核心不是“发明了容器”,而是以 Linux Kernel 的容器能力为基础,通过运行时体系完成容器生命周期管理,再通过 Image + Registry 完成应用运行环境的标准化封装与分发,最终让应用能够更稳定、更一致地部署到不同节点。

转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/2301_80260194/article/details/165220164




