跳到主要内容

容器核心原理-namespace-cgroup-unionfs

1. 概念

容器不是"小型虚拟机"。它本质是 Linux 进程,外加三种内核机制让它看起来像独立机器:

机制作用
namespace隔离视图(进程、网络、文件系统、用户、主机名等)
cgroup限制资源(CPU、内存、IO)
UnionFS分层文件系统,多层只读 + 一层读写

2. namespace

Linux 内核提供 8 种 namespace:

类型隔离的东西
pid进程 ID(容器内 PID 1 是应用)
net网络(独立网卡、路由表、iptables)
mnt挂载点(独立 / 视图)
uts主机名、域名
ipcSystem V IPC、POSIX 消息队列
userUID/GID 映射(容器内 root 可映射到宿主非 root)
cgroupcgroup 视图
time系统时间(5.6+)

进入容器看 namespace:

# 容器进程 PID
docker inspect -f '&#125;&#125;.State.Pid&#125;&#125;' <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 + 完整 OSnamespace + 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. 延伸阅读