前言
用了这么久容器,是时候回答那个经典问题了:“容器到底是什么?” 答案:被内核特性"圈"起来的一组进程。三大支柱:Namespace(隔离)、Cgroup(限额)、OverlayFS(镜像分层)。今天亲手搓一个。
一、Namespace:六重视野隔离
每个 namespace 让进程对某种系统资源产生"独立的幻觉":
| Namespace | 隔离什么 | 效果 |
|---|---|---|
| PID | 进程编号 | 容器内 PID 1(看不到宿主机进程) |
| NET | 网络栈 | 独立网卡/IP/路由/iptables |
| MNT | 挂载点 | 独立的文件系统视图 |
| UTS | 主机名 | 容器有自己的 hostname |
| IPC | 信号量/共享内存 | 进程间通信隔离 |
| USER | 用户映射 | 容器内 root ≠ 宿主 root |
眼见为实:
docker run -d --name box busybox sleep 300
# 1. 宿主机视角: 它就是个普通进程
docker inspect -f '{{.State.Pid}}' box # 假设 8642
ps -p 8642 -o pid,cmd
# 2. 容器视角: 自己是 PID 1
docker exec box ps
# PID USER COMMAND
# 1 root sleep 300
# 3. 网络命名空间证据
ls -l /proc/8642/ns/
# net -> net:[4026532281] ← 与宿主机不同的编号 = 不同空间
docker exec box ip addr # eth0@ifxx 172.17.0.x
# 4. UTS: 独立主机名
docker exec box hostname # 一串容器ID
hostname # 宿主机自己的
二、Cgroup:资源的水坝
Namespace 管"看得到什么",cgroup 管"能用多少":
# 验证 docker 的限额(容器 0.5 核 / 256M)
docker run -d --name lim --cpus=0.5 --memory=256m busybox sleep 300
# cgroup v2 机器(RHEL9/CgroupUnified)
cat /sys/fs/cgroup/system.slice/docker-<容器ID>.scope/cpu.max
# 50000 100000 ← 每100ms里最多用50ms CPU = 0.5核
cat /sys/fs/cgroup/system.slice/docker-<ID>.scope/memory.max
# 268435456 ← 256MB
# 压测验证
docker exec lim sh -c 'while :; do :; done' &
docker stats --no-stream lim # CPU 稳定 ~50%
CPU 超限 = 节流(变慢);内存超限 = OOM Kill(exit 137)——这就是 K8s 里 limits 语义的底层来源。
三、OverlayFS:镜像的千层饼
docker inspect -f '{{json .GraphDriver.Data}}' box | python3 -m json.tool
# LowerDir: 只读镜像层(多个, 用:分隔)
# UpperDir: 容器可写层
# MergedDir: 联合挂载点(容器看到的)
# 分层实验
mkdir -p /tmp/ovl/{lower,upper,work,merged}
echo "from image layer" > /tmp/ovl/lower/app.conf
echo "modified by container" > /tmp/ovl/upper/app.conf
mount -t overlay overlay \
-o lowerdir=/tmp/ovl/lower,upperdir=/tmp/ovl/upper,workdir=/tmp/ovl/work \
/tmp/ovl/merged
cat /tmp/ovl/merged/app.conf # modified by container(上层盖住下层)
cat /tmp/ovl/lower/app.conf # from image layer(原文件未被改动!)
# 写时复制: 在 merged 里改 lower 的文件, 实际写进 upper
touch /tmp/ovl/merged/newfile
ls /tmp/ovl/upper/ # newfile 出现在可写层
删除也是写:rm 一个镜像层的文件 → upper 层放一个 whiteout 标记盖住它,lower 的文件其实还在。
四、手搓容器:一百行原理拆解
不用 Docker,用 unshare 亲手圈一个"容器":
#!/usr/bin/env bash
# mycontainer.sh —— 极简容器原理演示
set -euo pipefail
ROOTFS=/tmp/myrootfs
# ---- 1. 准备 rootfs(用 busybox 填充最小系统)----
if [ ! -d "$ROOTFS/bin" ]; then
mkdir -p "$ROOTFS"
dnf install -y busybox 2>/dev/null || true
for d in bin sbin etc proc sys dev usr; do mkdir -p "$ROOTFS/$d"; done
cp /usr/bin/busybox "$ROOTFS/bin/"
ln -sf busybox "$ROOTFS/bin/sh"
fi
# ---- 2. unshare: 一次性进多个 Namespace ----
# --mount 独立挂载空间
# --uts 独立主机名
# --pid --fork 独立PID(当前命令成为容器内 PID 1)
# --net 独立网络(只有 lo)
# --map-root-user 用户映射(无特权场景)
unshare --mount --uts --pid --fork --net --map-root-user bash -c '
# ---- 3. 搭建容器内文件系统 ----
mount -t proc proc /proc 2>/dev/null || true
mount -t sysfs sys /sys 2>/dev/null || true
hostname mini-container # UTS: 独立主机名
echo "=== 欢迎进入手搓容器 ==="
echo "hostname: $(hostname)"
echo "我的 PID: $$(容器内视角)"
ps aux | head -3 # 只能看到自己!
echo "网卡:"; ip addr | grep -E "^[0-9]+:" # 只有 lo
'
chmod +x mycontainer.sh && ./mycontainer.sh
# === 欢迎进入手搓容器 ===
# hostname: mini-container
# 我的 PID: 1
# 网卡: 1: lo ...
这个 30 行的脚本与 Docker 的关系:Docker = 这些内核特性的自动化封装 + 镜像管理 + 网络/存储插件生态。
五、加一层资源限额(cgroup 手搓)
mkdir /sys/fs/cgroup/mytest
echo "100000 200000" > /sys/fs/cgroup/mytest/cpu.max # 0.5 核
echo $$ > /sys/fs/cgroup/mytest/cgroup.procs # 把当前 shell 圈进去
# 死循环烧 CPU, top 观察: 只能到 50%
while :; do :; done &
top -p $!
# 完事后清理
kill %1; rmdir /sys/fs/cgroup/mytest
六、这些原理解释的现象
| 现象 | 原理 |
|---|---|
| 容器内 top/ps 只看到自己 | PID namespace |
| 容器删了数据就没了 | 数据写在 OverlayFS 可写层 |
| docker cp/exec 能进容器 | 宿主机进程视角 + nsenter 到目标 namespace |
| CPU limit 下服务变慢 | cgroup throttling |
| 容器内 root 映射宿主普通用户 | USER namespace(rootless 容器) |
| 容器里看不到宿主磁盘空间 | MNT namespace + overlay 挂载 |
| k8s Pod 内容器互通 localhost | 同 Pod 共享 NET namespace |
七、安全边界提醒
容器隔离弱于虚拟机(共享内核):
- 内核漏洞(如脏管道)可能容器逃逸 → 及时升级内核
- 别用
--privileged(约等于交出宿主机 root) - 挂载 docker.sock 给容器 = 送钥匙
- 敏感多租户场景:Kata Containers(轻量 VM 跑容器)/ gVisor
小结
| 支柱 | 作用 | 关键词 |
|---|---|---|
| Namespace | 视野隔离 | pid/net/mnt/uts/ipc/user |
| Cgroup | 资源限额 | CPU 节流 / 内存 OOM |
| OverlayFS | 分层镜像 | lower 只读 + upper 可写 + whiteout |
| 容器本质 | 一组被圈起来的进程 | docker run ≈ unshare + cgroup + overlay |
本文是「云原生」系列第 13 篇。