- Published on
容器与 Docker 内部完全攻略 — Namespace、cgroups、OverlayFS、seccomp、Capabilities 直到 Kubernetes(2025)
- Authors

- Name
- Youngju Kim
- @fjvbn20031
0. “容器不是 VM”意味着什么
很多开发者把容器理解成“更轻的 VM”。实际上二者完全不同:
| 项目 | VM | Container |
|---|---|---|
| 隔离单位 | 硬件 | OS namespace |
| 内核 | 每个 VM 独立 | 与宿主机共享 |
| 启动时间 | 数秒 ~ 数分钟 | 数十 ms |
| 内存开销 | 数百 MB | 数 MB |
| 磁盘开销 | GB | MB |
| 隔离强度 | 强 | 弱 |
容器 = 进程 + 内核功能(namespace、cgroup、rootfs)的组合。并不存在一个独立的 OS。
本文剖析 docker run nginx 这一行在 Linux 内核里究竟如何运作,以及它背后长达十余年的工程史。
1. 容器并不短暂的历史
1.1 1979 — chroot
Unix v7 的 chroot() 系统调用。“改变这个进程的文件系统根目录” → 只能看见受限的目录。
但仅靠 chroot 还不够:
- 进程列表、网络、用户依旧照常可见。
- 逃逸手法早已广为人知(
chdir、mount 等)。
1.2 2000 — FreeBSD Jails
chroot 的扩展版。把网络、进程、用户也一并隔离。真正的“轻量级虚拟化”由此开始。
1.3 2004 — Solaris Zones
完整的隔离技术。容器概念在商业上的第一次成功。
1.4 2006 — cgroups,2007 — LXC
Google 的工程师为了内部的资源隔离开发了“process containers” → 以 cgroups 的形式进入 Linux。namespace 也在分阶段补齐。
LXC(Linux Containers)把它们组合起来,提供了第一个实用的容器运行时。
1.5 2013 — Docker
Solomon Hykes 的 dotCloud 把 LXC 包装成易用的 API。docker run 一行命令的魔法。
杀手级功能:镜像层(OverlayFS)+ 镜像仓库(Docker Hub)。开发者终于可以“把我的环境原样共享出去”。
1.6 2015 — OCI,2016 — containerd,2017 — Kubernetes 崛起
Open Container Initiative(Docker + CoreOS + Linux Foundation)确立了镜像/运行时的标准规范(OCI)。
Docker 把运行时拆分成 containerd,演化为更小、更可复用的组件。
Kubernetes 成为编排标准,“docker 引擎”逐渐被抽象掉。
1.7 2024 — 现在
Docker Desktop、Podman(daemonless)、Kubernetes + containerd + runc、ECS、Cloud Run、Fly.io。容器如今已是所有云端部署的基本单位。
2. Linux Namespace — 隔离的 7 个维度
namespace 限制“这个进程能看到哪些 OS 资源”。共有 7 种(2024 年为准):
2.1 PID Namespace
进程 ID 隔离。在容器里执行 ps 会看到:
PID TTY TIME CMD
1 ? 00:00:00 nginx
10 ? 00:00:00 worker
11 ? 00:00:00 worker
容器里的 nginx 是 PID 1。从宿主机上看,它却是 PID 12345。这是两个彼此不同的编号空间。
PID 1 的特殊之处:
- 内核期待它承担僵尸进程回收(init 角色)。
- 它退出时所有子进程也一并退出。
- 大多数应用并不是按 init 设计的 → 于是有了
tini、dumb-init这类小型 init。
2.2 Mount Namespace
文件系统挂载点隔离。每个容器都拥有自己的 / 树。
chroot 的进化形态。可以只共享宿主机的特定挂载。
2.3 Network Namespace
每个容器都有自己的网络接口、路由表和 iptables 规则。
# 从宿主机进入容器网络
sudo nsenter -t <container_pid> -n ip addr
Docker 的 bridge 模式:用虚拟网桥 docker0 + veth 对连接。
2.4 UTS Namespace
Unix Timesharing System。主机名 + 域名的隔离。容器可以设置自己的主机名。
2.5 IPC Namespace
System V IPC、POSIX 消息队列的隔离。容器之间的共享内存被分开。
2.6 User Namespace
UID/GID 映射。容器里的 root(UID 0)在宿主机上是非特权用户 UID 100000。
对安全至关重要,但配置复杂 → Docker 默认不启用。Podman rootless 用到了它。
2.7 Cgroup Namespace
隔离 cgroup 层级。容器把自己的 cgroup 路径看成 /。2016 年引入。
2.8 Time Namespace
Linux 5.6(2020)新增。每个容器可以拥有不同的启动时间(CLOCK_BOOTTIME)。用于检查点/恢复(CRIU)。
2.9 操作 namespace
unshare --pid --mount --net --fork bash # 用新的 namespace 启动 bash
nsenter -t PID -n -p # 进入已有的 namespace
Docker 的 docker exec 其实就是把目标容器的那些 namespace 组合起来,创建一个新进程。
3. cgroups — 资源限制
3.1 cgroups v1(2007-)
每种资源(CPU、内存、网络等)都有各自独立的层级:
/sys/fs/cgroup/
├── cpu/
│ ├── docker/
│ │ └── <container_id>/
│ │ ├── cpu.cfs_quota_us
│ │ └── cpu.cfs_period_us
├── memory/
├── blkio/
└── ...
各层级彼此独立 → 复杂。
3.2 cgroups v2(2016-,systemd 232+)
单一的统一层级:
/sys/fs/cgroup/
├── user.slice/
├── system.slice/
│ └── docker-<id>.scope/
│ ├── cpu.max # "100000 100000" = 1 CPU
│ ├── memory.max # "536870912" = 512MB
│ ├── io.max
│ └── pids.max
- 一棵树里容纳所有资源。
- 层级化的资源分配。
- 更简单的模型。
3.3 CPU 限制的实际含义
cpu.max = "50000 100000" 的意思是:“每 100ms 周期最多使用 50ms CPU”,也就是 0.5 CPU。
但 是否允许突发 取决于配置。JVM、Node 只要短时间内 CPU 冲高就会被 throttle → 响应延迟。
Docker 的 --cpus=1 = cpu.max = "100000 100000"。
3.4 内存限制
memory.max:上限。超过就 OOM。memory.high:soft limit。越过后会有 reclaim 压力。memory.min、memory.low:受保护的内存。
容器达到 memory.max 时,会触发 容器内部的 OOM killer。宿主机安然无恙。
3.5 JVM、Node、Go 对 cgroup 的感知
JVM 10+(-XX:+UseContainerSupport 默认开启):读取 cgroup 限额来决定堆大小。
Go 1.19+ 的 GOMEMLIMIT:手动反映 cgroup limit。
Node 的 --max-old-space-size:直接设置。
不做这件事,就会误以为“宿主机内存有 64GB”而把堆开得很大 → OOM。
4. OverlayFS — 镜像层的秘密
4.1 为什么要分层
Docker 镜像是 只读层的堆叠:
Layer 4 (10KB): 应用代码
Layer 3 (50MB): npm install 结果
Layer 2 (80MB): apt packages
Layer 1 (70MB): ubuntu base
- 每一层都 独立且不可变。
- 多个镜像共享 base 层。
- 即使有 100 GB 的镜像,也只需 push/pull 变化过的 top layer。
4.2 OverlayFS 的结构
┌─────────────────────┐
│ upperdir (RW) │ ← 容器写入
├─────────────────────┤
│ merged view │ ← 应用看到的文件系统
├─────────────────────┤
│ lowerdir (RO) ×N │ ← 镜像层
└─────────────────────┘
- 读取:upperdir 里没有就去 lowerdir 找。
- 写入:一律写到 upperdir。
- 修改:Copy-up — 先把原文件复制到 upperdir 再修改。
4.3 Copy-up 的代价
修改大文件时:
- 把原文件 整份复制 到 upperdir。
- 之后才轮到修改。
对 100MB 的文件改 1 字节 = 复制 100MB。这就是不该把大体积 DB 文件放在容器内部的原因。
解决办法:Volume mount。DB 数据交给宿主机卷。
4.4 Whiteout — 文件删除
执行 rm 并不会把文件从 lowerdir 里抹掉。取而代之的是:
- 在 upperdir 生成特殊的“whiteout”文件。
- merged view 里看不到该文件。
- 真实文件依然留在下面。
结果就是:“即便在 Dockerfile 里用 RUN rm 想减小体积”,层的大小也不会缩小。必须在同一个 RUN 里完成生成+删除,才不会残留在层中。
4.5 StorageDriver 的演进
Docker 的 storage driver 变迁史:
- aufs(2013-):早期方案。没有进入内核 upstream。
- devicemapper(2014-):thin provisioning。慢。
- btrfs、zfs:依赖文件系统。
- overlay(2014):upstream 内核,快。
- overlay2(2016-):当前默认。支持同时使用多个 lowerdir。
Podman 还可以选用 fuse-overlayfs(支持 rootless)。
5. 容器运行时 — runc 和它的伙伴们
5.1 Docker 的内部层次
docker CLI
↓
dockerd (daemon)
↓
containerd (容器 lifecycle)
↓
containerd-shim (每个容器一个)
↓
runc (OCI runtime,实际创建容器)
↓
Linux kernel (namespace, cgroup, ...)
5.2 runc 的职责
实现 OCI Runtime Specification。输入:rootfs + config.json。输出:运行中的容器。
用 C / Go 实现,纯粹地调用系统调用:
clone()+ namespace flags。- 用
setns()进入 namespace。 - 设置 cgroup。
- 挂载 rootfs。
- 用
execve()启动用户进程。
5.3 替代运行时
- crun:用 C 编写,比 runc 快(没有 Go 运行时开销)。
- youki:Rust 实现。
- Kata Containers:把每个容器跑在超轻量 VM 里 → VM 级别的隔离。
- gVisor:Google 出品。用用户态内核拦截系统调用 → 内核级隔离。
- Firecracker:AWS Lambda 的基础。microVM。
安全要求高的多租户环境(Lambda、Fargate)会采用 VM 隔离的运行时。
5.4 shim 的职责
containerd-shim 对每个容器都存在一份:
- containerd 守护进程挂掉,容器依然存活。
- 收集 stdout/stderr。
- 记录 exit code。
这也是 Docker 没有直接接到 kubelet 上的原因。结构是 kubelet → CRI → containerd → shim → runc。
6. 安全 — 容器为什么不是“真正的隔离”
6.1 共享内核的风险
容器们共享同一个内核 → 内核漏洞 = 所有容器逃逸。实际案例:
- Dirty COW(CVE-2016-5195):权限提升。可以逃逸出容器。
- runc CVE-2019-5736:覆写 runc 二进制以夺取宿主机 root。
- Leaky Vessels(2024):runc、containerd 的多个漏洞。
应对:勤更新内核、AppArmor/SELinux、seccomp profile。
6.2 Linux Capabilities
传统 Unix:root(UID 0)= 全部权限。对容器来说太危险了。
Capabilities 把 root 权限 细分:
CAP_NET_ADMIN:网络配置。CAP_SYS_ADMIN:大部分危险操作。CAP_NET_BIND_SERVICE:绑定 1024 以下端口。- 其余还有数十种。
Docker 默认:受限的 capability set(最小权限)。用 --cap-add、--cap-drop 调整。--privileged 等于全部 cap,几乎等同于宿主机 root。
6.3 seccomp — 系统调用过滤器
“这个容器只允许特定的系统调用”:
{
"defaultAction": "SCMP_ACT_ERRNO",
"syscalls": [
{ "names": ["read", "write", "open", ...], "action": "SCMP_ACT_ALLOW" }
]
}
Docker 的默认 seccomp profile 只允许约 300 个 syscall。keyctl、ptrace、mount 等会被拦截。
6.4 AppArmor / SELinux
文件系统层面的 MAC(Mandatory Access Control)。用策略限制容器可以尝试的文件访问。
Ubuntu 用 AppArmor,RHEL 用 SELinux。
6.5 Rootless Container
容器守护进程也以非特权用户运行。用 User namespace 做 UID 映射:
- Podman 的默认方式。
- Docker 也支持 rootless 模式。
- 安全性大幅增强,部分功能受限(1024 以下端口等)。
6.6 安全检查清单
- 最新内核。
- 保持 默认 seccomp profile(禁止
--security-opt seccomp=unconfined)。 - 最小化 capability(
--cap-drop ALL+ 只 add 必要的)。 - 用 非 root 用户 运行应用(
USER指令)。 - 只读 rootfs(
--read-only+ tmpfs 卷)。 - Image scanning:Trivy、Grype、Docker Scout。
- Runtime monitoring:Falco。
7. 制作镜像的技术
7.1 层的优化
# 不好:每个 RUN 都是新的一层
FROM node:20
COPY package.json .
RUN npm install # 层 1
COPY . .
RUN npm run build # 层 2
RUN rm -rf /tmp/cache # 层 3(第 1 层的 cache 原封不动地留着)
# 好:临时文件在同一个 RUN 里处理掉
FROM node:20
COPY package.json .
RUN npm install && rm -rf /tmp/* /var/cache/*
COPY . .
RUN npm run build
7.2 Multi-stage Build
# Stage 1: 构建
FROM node:20 AS builder
WORKDIR /app
COPY . .
RUN npm install && npm run build
# Stage 2: 运行时(小体积)
FROM node:20-alpine
COPY /app/dist /app
COPY /app/node_modules /app/node_modules
CMD ["node", "/app/index.js"]
构建工具(gcc、webpack 等)不会出现在最终镜像里 → 体积缩小 5 倍以上。
7.3 Distroless 镜像
Google 打造的最小镜像(连 shell 都没有):
FROM gcr.io/distroless/nodejs20
COPY /app /app
CMD ["/app/index.js"]
- 体积:数十 MB。
- 攻击面:没有 shell,RCE 更困难。
- 缺点:调试困难(无法 exec 进去)。
7.4 Alpine vs Debian
- Alpine(
node:20-alpine):5MB base。musl libc。 - Debian slim(
node:20-slim):80MB。glibc。
Alpine 的注意点:musl 兼容性问题(DNS resolver、pthread 行为的细微差异)。构建 Python 的 C extensions 时需要额外工具。
7.5 BuildKit 与缓存
DOCKER_BUILDKIT=1 docker build .
- 并行 stage 构建。
- 缓存的导出/导入:
--cache-to=type=registry,ref=...。 - 安全:构建期 secrets(
--secret)。 - BuildKit 是 containerd 原生的。
8. 网络 — 从 docker0 到 CNI
8.1 Docker 默认网桥
docker0 (bridge, 172.17.0.1)
├── veth0 → container1 eth0 (172.17.0.2)
├── veth1 → container2 eth0 (172.17.0.3)
└── veth2 → container3 eth0 (172.17.0.4)
- veth 对:一端在宿主机,另一端在容器。
- NAT:用
iptables MASQUERADE连到外部。 - 端口转发:
-p 80:8080就是 iptables DNAT。
8.2 网络模式
- bridge(默认):如上所述。
- host:直接用宿主机网络(没有隔离,最快)。
- none:没有网络。
- container:与其他容器共享(类似 pod)。
8.3 Kubernetes CNI
Kubernetes 的理念是“所有 pod 都拥有扁平的 IP”。只靠 Docker bridge 不够。
- Flannel:VXLAN 隧道,简单。
- Calico:基于 BGP,支持 eBPF,网络策略。
- Cilium:eBPF 原生,可观测性/安全能力丰富。
- AWS VPC CNI:把 ENI 直接分配给 pod。
CNI = Container Network Interface。让编排器可以替换网络插件的标准。
9. 与 Kubernetes 的整合
9.1 Kubernetes 为什么抛弃了 Docker(2020)
Kubernetes 1.20 公布了“dockershim deprecation” → 在 1.24(2022)中移除。
理由:
- Docker 引擎包含大量 kubelet 用不到的功能(镜像构建、swarm 等)。
- 为了对齐 CRI(Container Runtime Interface)标准,需要额外的一层 shim。
- containerd 本来就是 Docker 的核心。
结果:kubelet → CRI → containerd 直连。更简单、更快。从用户视角看,OCI 镜像照常工作。
9.2 Pod = 共享 namespace 的一组容器
Pod 是多个容器 共享 network、IPC、UTS namespace。Mount 和 PID(默认)保持独立。
pause container (namespace 所有者)
├── app container
└── sidecar container
pause 是一个什么都不做的迷你进程。只负责维持 namespace。
9.3 Init container 与 sidecar
- Init container:Pod 启动前顺序执行。
- Sidecar(native 2023+):与应用并行运行,可以独立重启。
Envoy(Istio)、Fluent Bit 这类辅助进程都以 sidecar 形式存在。
9.4 编排的意义
- Scheduling:放到哪个节点上运行?
- Health check、自动重启。
- Rolling update、canary。
- Service discovery、负载均衡。
- Storage、Config、Secret。
- 水平自动伸缩。
Kubernetes 用声明式 YAML 完成这一切。
10. 实战贴士
10.1 调试工具箱
docker ps -a # 所有容器
docker logs -f <container> # 日志 stream
docker exec -it <container> sh # 进入
docker inspect <container> # 详细信息(JSON)
docker stats # 实时资源使用
docker events # 事件 stream
Kubernetes:
kubectl logs -f <pod>
kubectl exec -it <pod> -- sh
kubectl describe pod <pod>
kubectl get events --sort-by='.lastTimestamp'
10.2 在 Distroless 里调试
没有 shell,无法用 exec 进入。替代方案:
kubectl debug <pod> --image=busybox --target=app
# 或者
docker run --rm -it --pid=container:<id> --net=container:<id> busybox
共享 pid/net namespace,用调试容器接入。
10.3 镜像体积分析
docker history <image>:按层查看大小。divetool:浏览层的内容。docker image ls --format="..."+ 格式化。
10.4 性能观测
# 容器资源使用
docker stats --no-stream
# 直接查看 cgroup
cat /sys/fs/cgroup/memory.current
cat /sys/fs/cgroup/cpu.stat
# 用 perf 做剖析
docker run --cap-add SYS_ADMIN --pid host ...
11. 结语 — docker run 一行的深度
docker run nginx 这一行实际做的事:
- 从 Docker Hub 下载 nginx 镜像层。
- 用 OverlayFS 把层堆叠组装起来。
- containerd 生成 containerd-shim。
- shim 启动 runc。
- runc 设置 7 个 namespace + cgroup + rootfs。
- 应用 seccomp/AppArmor profile。
- 用
execve("nginx")启动进程。 - 配置容器网络(veth + bridge)。
- 添加 iptables 端口转发。
50ms 之内就结束了。和 2000 年代启动 VM 要花上几分钟相比,这简直是奇迹。它的背后是 2006 cgroups、2008 namespaces、2013 Docker、2015 OCI、2016 overlay2、2020 CRI 等十余年的工程积累。
与此同时,这份便利的代价是 共享内核 = 安全弱点。这正是 Lambda、Fargate 这类商用平台使用 microVM 的原因。如果是多租户场景,不要全然相信“容器 = 隔离”。
下一篇打算深入 Kubernetes 内部 — etcd 的 Raft 共识、调度器的过滤/打分、控制器模式、自定义资源、CRI/CNI/CSI 的插件体系。一张 YAML 在数百台节点上跑起来的全过程。
参考资料
- Jérôme Petazzoni — “Anatomy of a Container: Namespaces, cgroups & some filesystem magic”(LinuxCon 2015)。
- Michael Kerrisk — “Understanding Linux Namespaces”(LWN 系列)。
- OCI Image/Runtime Specifications(GitHub)。
- Julia Evans — “Container Networking” 系列。
- Liz Rice — “Container Security”(O'Reilly, 2020)。
- Kubernetes 官方文档 — Pods、CNI、CRI。
- Red Hat crun 博客。
- Firecracker 论文(NSDI 2020)。
- Google gVisor 论文。
- “The Kubernetes Book” — Nigel Poulton。