前言

用了这么久容器,是时候回答那个经典问题了:“容器到底是什么?” 答案:被内核特性"圈"起来的一组进程。三大支柱: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 篇。