把进程骗成“世界只有它”:容器与隔离技术的原理与实战
如果能在一台机器上跑成百上千个应用,每个都以为自己独占系统、网卡、文件系统,却彼此毫无干扰,靠的是什么?答案既不是“轻量虚拟机”,也不是魔法,而是 Linux 内核提供的一组“隔离+限额+分层”的能力:namespaces、cgroups 和 union/overlay 文件系统,以及围绕它们的工具链(Docker、Podman、LXC)。本文从原理到实操,系统讲清楚容器的底层技术与常见工具的差异与选择。注意:本文不涉及 Kubernetes 等编排与服务网格。
目录
- 容器是什么:不是虚拟机,是“隔离的进程”
- Namespaces:把一个世界切成许多“局部”
- UTS/PID/NET/MOUNT/IPC/USER/Cgroup namespace
- 动手:用 unshare 手工拼一个“迷你容器”
- 容器里的 PID 1、capabilities 与常见坑
- cgroups v2:CPU/内存/IO 的精细化资源治理
- 统一层级与关键控制器
- 动手:创建 cgroup、限内存与 CPU
- 与 systemd 的关系与 rootless 委托
- Rootless 容器:把“假 root”关在盒子里
- user namespace 与 UID/GID 映射
- 安全收益与功能限制
- 动手:Podman rootless 快速体验
- OverlayFS:容器分层存储的秘密
- lower/upper/work/merged 与写时复制
- whiteout/opaque、rename 与性能考量
- rootless 下的 fuse-overlayfs
- 镜像:构建、优化与签名验证
- OCI 镜像格式与层
- Dockerfile/BuildKit、Buildah/Podman 实践
- 可复现构建与多阶段
- Cosign/Notary 的镜像签名
- Docker、Podman、LXC:定位、架构与用法对比
- 安全与运维最佳实践清单
- 小实验合集:三步带你看见“隔离”与“限额”
- 参考资料与延伸阅读
- 结语:你的容器栈怎么选?欢迎留言
容器是什么:不是虚拟机,是“隔离的进程”
容器的核心是一组内核特性叠加起来提供“看起来像独立系统”的错觉。它不虚拟 CPU 指令集,不提供完整虚拟硬件,而是让普通进程在被隔离的视角中运行:
- namesapces:改写进程“看到的世界”(PID 树、网络栈、挂载点、主机名、用户 ID 等)。
- cgroups v2:对一组进程施加资源配额与压力反馈(CPU、内存、IO、PIDs)。
- overlay/union FS:将镜像做成“分层只读 + 最上层可写”,高效复用并按需写。
- LSM 与 seccomp:用安全策略进一步收紧系统调用与访问。
因此,“容器就是强隔离的进程组”。这句话比“轻量虚拟机”更精确:容器共享宿主内核,成本极低,但也意味着安全边界与内核漏洞风险的考量不同于虚拟机。
Namespaces:把一个世界切成许多“局部”
Linux 提供多种命名空间,把“系统视图”按维度切分。一个进程可同时处在多个新 namespace 中,从而以“独占世界”的视角运行。
- UTS(主机名与域名)
- 隔离主机名、NIS 域名。容器内可随意设 hostname,不影响宿主。
- PID(进程 ID)
- 每个 PID namespace 有独立的 PID 树。容器内看到的 PID 从 1 开始;向下可见,但向上不可见(父命名空间看得到子)。
- NET(网络)
- 独立网络栈、接口、路由、iptables/nftables。常见模式是 veth + bridge + NAT;rootless 常配合 slirp4netns。
- MOUNT(挂载)
- 独立挂载表,容器内 mount/umount 不影响宿主;透过私有/共享传播可控。
- IPC(进程间通信)
- 隔离 System V IPC 和 POSIX message queues。
- USER(用户)
- 映射 UID/GID,使容器内的 root(0)映射为宿主上的非特权用户;rootless 的关键。
- Cgroup(控制组)
- 隔离 cgroup 视图(v2);进程在容器内可只看到被委托的 cgroup 子树。
此外还有 Time namespace(隔离时钟偏移)等较新特性。
动手:用 unshare 手工拼一个“迷你容器”
示例以下需较新内核,某些发行版默认禁用无特权 userns 或 netns;如遇权限问题可使用 sudo 体验。
- 以 root 体验全套隔离(UTS/PID/NET/MOUNT/IPC/USER):
sudo unshare -Ur -p -m -n -i -u -f --mount-proc bash
# 现在在新命名空间的 bash 中
hostname demo-ns
echo "我是容器内的 PID:" $$
ps -ef | head
ip link set lo up
ip addr show lo
mkdir -p /mnt/ns-test && mount -t tmpfs none /mnt/ns-test
touch /mnt/ns-test/hello
mount | grep ns-test
exit
# 回到宿主,确认挂载与主机名未受影响
hostname
mount | grep ns-test || echo "宿主无此挂载"
- 仅用 user/mount/pid 的“rootless”体验(可能受限于发行版设置):
unshare -Ur -p -m -f --mount-proc bash
hostname demo-rootless
id -u # 显示为 0(容器内),但宿主上是非特权 UID
要点解释:
-U/-r打开 userns 并把容器内 0 映射到宿主的一个普通 UID(依赖 /etc/subuid/subgid)。- 在 PID ns 中,容器内的第一个进程是 PID 1,信号与僵尸回收行为因此不同。
--mount-proc在新的 mount ns 挂载 proc,使 ps 等工具正常工作。
容器里的 PID 1、capabilities 与常见坑
- PID 1 行为特殊:不接收默认信号处理、不会被 OOM killer 首先杀死、负责回收孤儿进程。因此在容器内用 bash 充当 PID 1 时要注意僵尸回收。生产中通常使用 tini、dumb-init 或正确处理 SIGTERM/SIGINT 的入口进程。
- Linux capabilities 将 root 权限拆分为细粒度能力(如 CAP_NET_ADMIN)。在 userns 中,容器内 root 获得的是“映射后的”能力,仅在该 namespace 有效。
- NET ns 与 iptables/nft 变更在容器内默认只影响该 NET ns,不影响宿主。
- MOUNT ns 隔离挂载,结合只读根与 tmpfs 可做最小可写面。
cgroups v2:CPU/内存/IO 的精细化资源治理
v2 统一了层级,所有控制器挂在单一树下,提供一致、可组合的接口和更强的内存压力信号。常见控制器:
- cpu:
cpu.max(定额,格式:quota period)、cpu.weight(权重,1-10000) - memory:
memory.max(硬上限)、memory.high(软上限/节流)、memory.swap.max、memory.oom.group - io:
io.max(读写速率/IOPS 限制,按设备 major:minor) - pids:
pids.max(进程/线程数) - cpuset:CPU 核与内存节点绑定
- bpf:BPF 程序挂载点(高级用途)
树形调度要点:
- 要在某个层级使用控制器,须在其父节点的
cgroup.subtree_control写入+controller打开委托。 - 将进程放入某 cgroup:向该目录的
cgroup.procs写入 PID。
动手:创建 cgroup、限内存与 CPU(v2)
以下示例以 root 身份,且系统为 cgroup v2(常见于新发行版)。谨慎操作生产环境。
# 1) 准备层级与控制器
cd /sys/fs/cgroup
echo '+cpu +memory +pids' > cgroup.subtree_control
mkdir demo
# 2) 设置额度
echo 200M > demo/memory.max
echo 20000 100000 > demo/cpu.max # 20000/100000=20% CPU
echo 256 > demo/pids.max
# 3) 将一个 shell 放入该 cgroup
bash -c 'echo $$ > /sys/fs/cgroup/demo/cgroup.procs; \
cat /proc/self/cgroup; \
dd if=/dev/zero of=/dev/shm/big bs=1M count=500 status=progress'
# 当写入 /dev/shm 触发 tmpfs 分页占用内存,达到上限后进程会被 OOM 杀
小贴士:
memory.high不会直接 OOM,而是触发内核的回压机制,适合温和限流;memory.max是硬封顶。- 与 systemd:大多数发行版使用 systemd 驱动 cgroup 层级。你可以用
systemd-run --scope临时创建带配额的 cgroup,例如:
systemd-run --unit=demo --scope -p MemoryMax=200M -p CPUQuota=25% sleep 1000 - rootless 场景:systemd 会为每个用户创建可委托的子树,容器引擎得以在用户空间使用 CPU/内存控制器(需发行版支持)。
Rootless 容器:把“假 root”关在盒子里
rootless 的关键是 user namespace:容器内 UID 0 被映射为宿主的一个非特权 UID,通过 /etc/subuid 和 /etc/subgid 指定可映射的 ID 段。例如:
# /etc/subuid 与 /etc/subgid 中的行(通常由工具自动写入)
alice:100000:65536
这样,容器内的 0…65535 将映射为宿主的 100000…165535。容器内看似 root 的进程,实际上对宿主来说只是普通用户。
安全收益:
- 大幅降低“容器逃逸后拿到宿主 root”的风险面。很多需要 CAP_SYS_ADMIN 的危险操作在 userns 内被内核限制或模拟。
- 结合 seccomp、AppArmor/SELinux 与只读根文件系统,能将攻击面进一步缩小。
功能限制与取舍:
- 网络:rootless 不能直接创建宿主 bridge/veth;常借助 slirp4netns/rootlesskit 提供用户态 NAT 与端口映射,性能略低于内核态。
- 存储:内核 overlayfs 对无特权挂载有限制;常用 fuse-overlayfs 作为替代,性能略低。
- cgroup:需要 systemd 为用户委托子树,或容器引擎使用替代限额方法;早期内核/发行版对 rootless 的 cgroup 支持不完善。
- 低号端口:容器内能绑定 <1024 端口,但通过用户态转发暴露到宿主对应端口。
动手:Podman rootless 快速体验
Podman 天生支持无守护进程与 rootless 模式(建议 crun 运行时,配合 cgroup v2)。
# 普通用户,无需 sudo
podman run --rm -it --name hello alpine:3.20 sh -c 'id -u; hostname; echo ok'
podman info | grep -i -e rootless -e overlay
# 映射端口(用户态转发)
podman run -p 8080:80 --rm nginx:alpine
如果提示缺少 subuid/subgid,按发行版文档添加或运行 podman system migrate 进行用户映射准备。
OverlayFS:容器分层存储的秘密
镜像的“多层只读 + 最上层可写”依赖 OverlayFS:
- lowerdir:一个或多个只读层(镜像层)。
- upperdir:当前容器的可写层。
- workdir:OverlayFS 必需的工作目录。
- merged:最终合成视图(容器内看到的文件系统)。
写时复制(CoW):
- 读取文件:直接来自最近的 lower 或 upper。
- 修改/删除文件:第一次写会把目标从 lower 拷贝到 upper,然后修改;删除则在 upper 放置 whiteout 条目,隐藏 lower 的同名文件。
- 目录“opaque”:upper 上设置 opaque 标记后,隐藏 lower 中同名目录的子项。
常见细节与性能点:
- whiteout 与 opaque 标记由内核与 xattr 协同处理;OCI 层的 tar 里以
.wh.*与.wh..wh..opq表示。 - rename 跨层会触发“redirect_dir”或复制开销;大量小文件随机写是 overlay 的弱项。
- inode 增长与层级过多会带来性能劣化;最佳实践是“尽量少层、合并小文件、减少构建期间写放大”。
rootless 与 fuse-overlayfs:
- 无特权用户通常不能直接挂载内核 overlayfs;Podman/rootless 会使用 fuse-overlayfs,在用户态模拟分层,兼容性好但性能略降。
- 可通过
podman info查看正在使用的存储驱动。
动手感受(root):
mkdir -p /tmp/ol/{lower,upper,work,merged}
echo hello > /tmp/ol/lower/a.txt
mount -t overlay overlay -o lowerdir=/tmp/ol/lower,upperdir=/tmp/ol/upper,workdir=/tmp/ol/work /tmp/ol/merged
cat /tmp/ol/merged/a.txt
rm /tmp/ol/merged/a.txt
ls -la /tmp/ol/upper # 可观察到 whiteout/opaque 相关变更
umount /tmp/ol/merged
镜像:构建、优化与签名验证
OCI 镜像格式
- layers:一组 gzip/zstd 压缩的 tar,按顺序叠加。
- config:容器配置(Entrypoint、Env、User、层的 diffID、历史)。
- manifest:引用 config 与 layers 的 digest;manifest list 支持多架构(amd64/arm64 等)。
镜像是内容寻址的:按 digest(sha256)寻址,确保不可篡改与缓存命中。
构建与优化
- Dockerfile + BuildKit(推荐):
- 多阶段构建减小体积。
- 使用
--mount=type=cache缓存包管理器目录,加速增量构建。 --mount=type=secret挂载构建时机密而不写入层。.dockerignore减小构建上下文。- 固定版本与锁定文件(requirements.txt/go mod)提升可复现性。
示例(Docker BuildKit):
# 启用 BuildKit:DOCKER_BUILDKIT=1
# 多阶段 + 缓存 + 最小运行时
# 文件: Dockerfile
# syntax=docker/dockerfile:1.6
FROM golang:1.22-alpine AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
COPY . .
RUN --mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o app ./cmd/app
FROM gcr.io/distroless/static:nonroot
USER 65532:65532
COPY --from=build /src/app /app
ENTRYPOINT ["/app"]
- Podman/Buildah:
- 无守护进程构建,rootless 友好。
- 亦支持多阶段、
.containerignore等实践。 - 结合 Skopeo 进行镜像复制与检视。
Podman 构建示例:
podman build -t demo:latest .
podman run --rm demo:latest
可复现构建小贴士:
- 固定基础镜像 digest:FROM alpine@sha256:…
- 设定 LANG/TZ 与 PATH,避免隐式差异。
- 在构建脚本中显式设置 umask、排序与时间戳(SOURCE_DATE_EPOCH)。
镜像签名与验证
供应链安全的重要一环是“镜像签名 + 拉取时策略校验”。
- Cosign(Sigstore 生态,推荐):
- 支持 keyless(OIDC,签名记录于透明日志 Rekor)。
- 与多容器引擎、注册中心兼容。
# 生成密钥对
cosign generate-key-pair
# 签名镜像
cosign sign --key cosign.key registry.example.com/demo:1.0
# 验证签名
cosign verify --key cosign.pub registry.example.com/demo:1.0
- Docker Content Trust(Notary v1)与即将普及的 Notary v2:
- v1 已逐步退场;v2 更贴近 OCI 工件与仓库原生签名。
- Podman 的信任策略:
- 通过
/etc/containers/policy.json定义拉取策略(仅接受特定公钥签名的镜像),搭配skopeo copy/podman pull生效。
- 通过
Docker、Podman、LXC:定位、架构与用法对比
-
核心定位
- Docker:生态与体验起家,开发者友好;现代 Docker 后端依赖 containerd + runc。
- Podman:无守护进程、rootless 一等公民;默认运行时常为 crun(cgroup v2、rootless 适配更佳)。
- LXC:更接近“系统容器”,倾向运行完整发行版用户空间,常见于长期运行的轻量系统隔离场景(与 LXD 管理层配套更强)。
-
架构与守护进程
- Docker:dockerd 常驻;客户端-守护进程模式。
- Podman:每次命令直连内核与 runc/crun;无需常驻守护进程。
- LXC:库与工具集合(liblxc、lxc-*),可 rootless。
-
Rootless 支持
- Docker:支持,但起步相对晚;部分功能依赖发行版能力。
- Podman:天然支持 rootless,结合 fuse-overlayfs、slirp4netns、cgroup v2 委托。
- LXC:支持无特权容器(需配置 subuid/subgid、AppArmor/SELinux 等)。
-
安全默认值
- 三者均可使用 seccomp、AppArmor/SELinux、capabilities 削减;具体默认策略与提供的配置项有所差异。
- crun 对 cgroup v2、seccomp notify、新特性支持更积极。
-
网络与存储
- Docker:默认 docker0 网桥 + iptables/nft NAT;overlay2 存储驱动。
- Podman:CNI/Netavark 双栈选择;rootless 走 slirp4netns;overlay/fuse-overlayfs。
- LXC:灵活;桥接、macvlan、SR-IOV 等易用,存储可用目录、LVM、ZFS 等后端。
-
适用场景建议
- 应用容器(微服务、CLI 工具):Docker/Podman 均可;重视 rootless 与系统整合可偏向 Podman。
- 系统容器(运行完整 OS 用户空间、需要 init/systemd):LXC/LXD 更省心。
- 资源与安全红线严格的多租户:优先 rootless + 强策略;必要时考虑更强沙箱(如 gVisor/Kata 等)——超出本文范围。
安全与运维最佳实践清单
- 最小权限
- 使用非 root 用户运行:Dockerfile 中
USER 10001:10001;或 rootless 引擎。 - 去掉不必要 capabilities:
--cap-drop=ALL --cap-add=NET_BIND_SERVICE等。 - 打开
--security-opt no-new-privileges,阻止 setuid 提权链。
- 使用非 root 用户运行:Dockerfile 中
- 文件系统防护
- 只读根:
--read-only;可写目录单独挂载 tmpfs 或持久卷。 - 将
/proc、/sys以只读形式或挂载最小化子集。 - 限制设备访问:
--device白名单化;避免--privileged。
- 只读根:
- 资源与风暴控制
--memory,--memory-swap,--cpus/--cpu-quota,--pids-limit。- ulimit:
--ulimit nofile=...、nproc等。 - 为 PID 1 准备好信号处理与僵尸回收。
- 供应链与镜像治理
- 固定基础镜像 digest;定期重建更新。
- 开启签名验证策略(cosign + policy.json)。
- 减少层数与镜像体积,降低攻击面与传输成本。
- 观察与故障排查
- 将 stdout/stderr 收敛到标准日志;避免把日志写入层(放到卷或外部系统)。
- 健康探针与自检脚本。
- 结合 eBPF 工具、
nsenter、ctr/crictl等进行深入诊断(注意生产环境风险)。
小实验合集:三步看见“隔离”与“限额”
-
看见 PID/UTS 隔离
sudo unshare -Ur -p -u -f --mount-proc bash- 内部
hostname myns; echo $$; ps -o pid,ppid,comm | head - 外部
ps -ef | grep bash,对比 PID 不同。
-
用 cgroups v2 限内存触发 OOM
cd /sys/fs/cgroup; echo '+memory' > cgroup.subtree_control; mkdir demo; echo 150M > demo/memory.maxbash -c 'echo $$ > /sys/fs/cgroup/demo/cgroup.procs; dd if=/dev/zero of=/dev/shm/big bs=1M count=500 status=progress'- 观察
dmesg | tail有 OOM 日志。
-
rootless 容器与端口映射
podman run -p 8080:80 --rm nginx:alpine- 浏览器访问
http://127.0.0.1:8080,体验用户态转发。 podman top查看容器进程与映射 UID。
-
overlay 分层可见性
- 以 root 挂 overlay(见前文示例)
- 在 merged 中删除 lower 的文件
- 在 upper 中观察 whiteout 现象
参考资料与延伸阅读
- man-pages:
man 7 namespaces、man 7 user_namespaces、man 7 cgroups、man 7 pid_namespaces - 内核文档:Documentation/admin-guide/cgroup-v2.rst、filesystems/overlayfs.rst
- OCI 规范与容器运行时:
- Open Container Initiative(image-spec、runtime-spec)
- runc、crun
- 工具链:
- Docker/BuildKit 文档
- Podman/Buildah/Skopeo 文档
- LXC 文档
- 供应链安全:
- Sigstore/Cosign
- Notary v2 设计文档
结语:你的容器栈怎么选?欢迎留言
容器不是“迷你虚拟机”,而是“隔离与限额的进程”。掌握 namespaces、cgroups v2 与 overlayfs 的原理,就能理解 Docker/Podman/LXC 的行为与差异,并做出合适的架构与安全取舍。你更偏爱哪种容器引擎?是否在生产中使用 rootless?在 cgroups v2 或 overlay 上有过哪些坑与经验?欢迎在评论区分享你的实践与问题。

868

被折叠的 条评论
为什么被折叠?



