容器核心原理-namespace-cgroup-unionfs
1. 概念
容器不是"小型虚拟机"。它本质是 Linux 进程,外加三种内核机制让它看起来像独立机器:
| 机制 | 作用 |
|---|---|
| namespace | 隔离视图(进程、网络、文件系统、用户、主机名等) |
| cgroup | 限制资源(CPU、内存、IO) |
| UnionFS | 分层文件系统,多层只读 + 一层读写 |
2. namespace
Linux 内核提供 8 种 namespace:
| 类型 | 隔离的东西 |
|---|---|
| pid | 进程 ID(容器内 PID 1 是应用) |
| net | 网络(独立网卡、路由表、iptables) |
| mnt | 挂载点(独立 / 视图) |
| uts | 主机名、域名 |
| ipc | System V IPC、POSIX 消息队列 |
| user | UID/GID 映射(容器内 root 可映射到宿主非 root) |
| cgroup | cgroup 视图 |
| time | 系统时间(5.6+) |
进入容器看 namespace:
# 容器进程 PID
docker inspect -f '}}.State.Pid}}' <container>
# 看 namespace
ls -la /proc/<pid>/ns/
# net -> net:[4026532567]
# pid -> pid:[4026532569]
每个数字代表一个 namespace 实例。同一 namespace ID 的进程在同一隔离空间。
2.1 PID namespace 实战意义
容器内的应用是 PID 1。Linux 对 PID 1 有特殊要求:
- 必须负责回收僵尸子进程(
wait()) - 必须正确处理信号(SIGTERM 优雅退出)
普通应用没实现 PID 1 职责,导致:
- 多进程容器(fork chrome、子进程)僵尸堆积
docker stop等 10 秒 SIGKILL(应用没响应 SIGTERM)
解决:
# 用 tini 当 PID 1
RUN apk add --no-cache tini
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "server.js"]
或 docker run 加 --init。
3. cgroup(v1 / v2)
控制组限制资源。docker 的 -m、--cpus 本质是写 cgroup 文件。
3.1 cgroup v2(新内核默认)
# 看容器的 cgroup
cat /proc/<pid>/cgroup
# 0::/system.slice/docker-xxx.scope
# 看资源限制
cat /sys/fs/cgroup/system.slice/docker-xxx.scope/memory.max
# 2147483648 ← 2GB
cat /sys/fs/cgroup/system.slice/docker-xxx.scope/cpu.max
# 200000 100000 ← 2 核
3.2 docker 资源参数
docker run -m 2g --cpus 2.0 --memory-swap 2g --pids-limit 1000 myimage
# K8s pod spec
resources:
limits:
memory: 2Gi
cpu: "2"
容器内观察自身限制:
# 早期工具看到的是宿主机数据,会误判
free # 宿主总内存
nproc # 宿主 CPU
# 正确方式
cat /sys/fs/cgroup/memory.max
cat /sys/fs/cgroup/cpu.max
Node.js 在 v12+ 自动识别 cgroup 内存限制(os.totalmem() 返回容器限制)。Java 8u131+ 加 -XX:+UseContainerSupport 也会识别。
4. UnionFS / OverlayFS
容器镜像由多层只读 layer 叠加而成,运行时上面加一层读写层(rw layer)。
┌──────────────────────────┐
│ rw layer(容器写入) │
├──────────────────────────┤
│ app layer(你的代码) │
├──────────────────────────┤
│ npm install layer │
├──────────────────────────┤
│ base image: node:20 │
├──────────────────────────┤
│ base image: alpine │
└──────────────────────────┘
写时复制(CoW):
- 读:从下层找
- 写:复制到 rw 层修改
- 删除:rw 层加 whiteout 标记,下层文件还在
4.1 实战意义
- 多个容器共享同一 base image:内存和磁盘只占一份
- 镜像层缓存:构建时未变的层重用,加快 build
- rm 文件不会缩小镜像:rw 层只记 whiteout,原文件还占空间
# 看镜像分层
docker history nginx:alpine
docker inspect nginx:alpine | jq '.[0].RootFS.Layers'
4.2 OverlayFS
Linux 内核内置 union 文件系统。Docker 默认用 overlay2 driver。
docker info | grep -i storage
# Storage Driver: overlay2
5. 容器 vs 虚拟机
| 虚拟机 | 容器 | |
|---|---|---|
| 隔离 | Hypervisor + 完整 OS | namespace + cgroup |
| 启动 | 秒-分钟 | 毫秒 |
| 镜像 | GB 级 | MB-百 MB |
| 内核 | 独立 | 共享宿主机 |
| 性能 | 有虚拟化损耗 | 接近原生 |
| 安全 | 强(硬件级) | 弱于 VM |
容器不是 VM:内核共享意味着内核漏洞在容器之间会传染,root 容器逃逸风险高于 VM。安全场景用 gVisor / Kata Containers(容器外面套 VM)。
6. Docker 架构
┌──────────────────────────────────┐
│ docker CLI (docker run ...) │
└──────────────────────────────────┘
│ HTTP API
↓
┌──────────────────────────────────┐
│ dockerd (daemon) │
└──────────────────────────────────┘
│
↓
┌──────────────────────────────────┐
│ containerd (容器生命周期) │
└──────────────────────────────────┘
│
↓
┌──────────────────────────────────┐
│ runc / crun (实际运行容器,OCI) │
└──────────────────────────────────┘
K8s 已不用 dockerd,直接对接 containerd(或 CRI-O)。但镜像格式(OCI)和 Dockerfile 仍是事实标准。
7. 常见反模式
- 以为容器是 VM:内核共享,内核漏洞 = 容器逃逸
- 应用做 PID 1 不处理信号:
docker stop等 10 秒强杀 - 容器内 free / top 当真:看到的是宿主数据
- 容器装一堆调试工具:镜像膨胀,攻击面扩大
- rm 文件以为镜像变小:rw 层加白标,原层不变
- 多进程容器不用 init:僵尸进程堆积
8. 延伸阅读
- Linux Containers - Wikipedia
- What is a container? — Docker 官方
- tini: A tiny init for containers
- 《自己动手写 Docker》— 中文了解容器原理最佳
- OCI 规范 — 容器和镜像格式标准