前言:理解容器技术的基石
Docker 和 Kubernetes 早已成为现代基础设施的标配,但当你使用
1 | docker run |
启动一个容器时,底层到底发生了什么?容器之所以”轻量”而非像虚拟机一样需要完整的内核模拟,秘密就在于 Linux 内核的两大核心特性:cgroups(控制组) 和 namespaces(命名空间)。理解这两者,才能真正掌握容器技术的底层逻辑。
本文将从零开始,先用命令行的方式手把手创建”手工容器”,然后深入 cgroups 和 namespace 的配置调优,最后讨论容器安全性、资源限制的最佳实践。目标读者是有一定 Linux 基础、希望深入理解容器的开发者和运维人员。
一、Linux Namespace:实现资源隔离
Namespace 是 Linux 内核实现资源隔离的机制。它让一组进程”看到”自己独有的系统资源视图,而与其他进程隔离。每个 namespace 提供的隔离维度都不同,Docker 正是组合利用了多个 namespace 实现容器的独立环境。
1.1 六种核心 Namespace
Linux 内核目前提供了 8 种 namespace,其中容器技术最常用的是以下 6 种:
| Namespace | 隔离资源 | 内核版本 |
|---|---|---|
| Mount (mnt) | 文件系统挂载点 | 2.4.19+ |
| PID (pid) | 进程编号 | 2.6.24+ |
| Network (net) | 网络设备、IP、端口 | 2.6.29+ |
| IPC (ipc) | System V IPC 和 POSIX 消息队列 | 2.6.19+ |
| UTS (uts) | 主机名和域名 | 2.6.19+ |
| User (user) | 用户和用户组 ID | 3.8+ |
1.2 使用 unshare 创建隔离环境
1 | unshare |
是 Linux 自带的工具,可以在新创建的 namespace 中运行程序。先用它感受一下 UTS namespace:
1
2
3
4
5
6
7
8
9
10
11 # 创建一个新 UTS namespace 并设置独立的主机名
sudo unshare --uts /bin/bash
# 在新 namespace 中修改主机名
hostname my-container
hostname
# 输出: my-container
# 在另一个终端检查宿主机的主机名
hostname
# 输出: (原来的主机名,没有变化)
同样的方式,我们可以组合多个 namespace 创建一个更完整的隔离环境:
1
2
3
4
5
6
7
8 # 同时创建 uts + pid + mount + ipc namespace
sudo unshare --uts --pid --mount --ipc --fork /bin/bash
# 注意:在 PID namespace 中,需要先挂载 /proc
mount -t proc none /proc
ps aux
# 输出中 PID 1 就是当前的 bash 进程
这就是 Docker 容器的基本原理!当你启动一个容器时,Docker 实际上是调用了类似
1 | unshare |
的系统调用
1 | clone() |
,参考 clone(2) 手册,传入相应的 namespace 标志位来创建隔离环境。
二、Cgroups:实现资源限制与监控
Namespace 提供了”看到什么”的隔离,但无法限制”能用多少”——如果没有 cgroups,一个容器可以耗尽宿主机所有的 CPU 和内存。Cgroups(Control Groups)就是用来解决这个问题的。
2.1 Cgroups v1 vs v2
Linux 内核目前存在两套 cgroups 实现:
- cgroups v1:每个资源类型(CPU、内存、IO 等)有独立的层级结构,管理复杂,容易导致不一致
- cgroups v2:统一层级结构,更简洁安全。从 Ubuntu 21.10、Fedora 31、Debian 11、RHEL 9 开始默认使用
检查当前系统使用的版本:
1
2
3
4
5
6 # 检查 cgroup 版本
stat -fc %T /sys/fs/cgroup/
# cgroup2fs 表示 v2,tmpfs 表示 v1
# 或者
grep cgroup /proc/filesystems
2.2 手动创建 Cgroup 限制 CPU
以 cgroups v2 为例,手动创建控制组来限制进程的 CPU 使用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 # 1. 创建新的控制组
sudo mkdir /sys/fs/cgroup/example-container
# 2. 设置 CPU 配额:限制为 0.5 核(500000 微秒中的 250000 微秒)
echo "250000 500000" | sudo tee /sys/fs/cgroup/example-container/cpu.max
# 3. 启动一个消耗 CPU 的进程(例如一个无限循环)
dd if=/dev/zero of=/dev/null &
echo $! # 记住 PID
# 假设 PID 为 12345
# 4. 将进程添加到控制组
echo 12345 | sudo tee /sys/fs/cgroup/example-container/cgroup.procs
# 5. 用 top 或 htop 验证 CPU 使用率被限制在 50% 左右
top -p 12345
2.3 Cgroups v1 的内存限制
使用 cgroups v1 限制内存的方式略有不同:
1
2
3
4
5
6
7
8
9
10
11
12 # 创建内存控制组
sudo mkdir /sys/fs/cgroup/memory/example-container
# 限制最大 512MB 内存(单位是字节)
echo "536870912" | sudo tee /sys/fs/cgroup/memory/example-container/memory.limit_in_bytes
# 超过限制时,内核会触发 OOM Killer
# 将进程写入控制组
echo 12345 | sudo tee /sys/fs/cgroup/memory/example-container/cgroup.procs
# 查看内存使用统计
cat /sys/fs/cgroup/memory/example-container/memory.usage_in_bytes
2.4 Cgroups v2 的内存限制
cgroups v2 的接口更加统一:
1
2
3
4
5
6
7
8
9
10
11
12
13
14 # 创建控制组(自动继承父级)
sudo mkdir /sys/fs/cgroup/example-container
# 设置内存硬限制 512MB
echo "536870912" | sudo tee /sys/fs/cgroup/example-container/memory.max
# 设置内存软限制 256MB(尽力而为)
echo "268435456" | sudo tee /sys/fs/cgroup/example-container/memory.low
# 设置内存和 swap 总限制 1GB
echo "1073741824" | sudo tee /sys/fs/cgroup/example-container/memory.swap.max
# 允许使用 swap 但有限制,如需完全禁止 swap:
echo "0" | sudo tee /sys/fs/cgroup/example-container/memory.swap.max
三、组合 Namespace + Cgroups:手搓一个”容器”
现在我们把 namespace 和 cgroups 组合起来,完整地创建一个”手工容器”的运行环境。步骤如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32 #!/bin/bash
# my-container.sh - 手搓容器完整示例
CONTAINER_NAME="my-custom-container"
ROOTFS_DIR="/tmp/${CONTAINER_NAME}/rootfs"
CG_DIR="/sys/fs/cgroup/${CONTAINER_NAME}"
# 0. 准备 rootfs
mkdir -p "$ROOTFS_DIR"
# 用 debootstrap 或 alpine minirootfs 制作最小文件系统
curl -sL https://dl-cdn.alpinelinux.org/alpine/v3.19/releases/x86_64/alpine-minirootfs-3.19.1-x86_64.tar.gz \
| tar -xz -C "$ROOTFS_DIR"
# 1. 创建 cgroup(v2)
mkdir -p "$CG_DIR"
echo "100000 200000" > "$CG_DIR/cpu.max" # 0.5 CPU
echo "268435456" > "$CG_DIR/memory.max" # 256MB 内存限制
echo "0" > "$CG_DIR/memory.swap.max" # 禁止 swap
# 2. 创建 namespace 并启动容器进程
unshare --uts --pid --mount --ipc --net --fork \
--mount-proc="$ROOTFS_DIR/proc" \
/bin/bash << 'INNER'
# 切换根文件系统
exec chroot "$ROOTFS_DIR" /bin/sh -c "
hostname my-container
mount -t sysfs none /sys
echo 'Container started!'
# 将当前 shell PID 写入 cgroup(需要在宿主机执行)
/bin/sh
"
INNER
注:上面的简化脚本示意了组合方式。实际生产环境中,Docker 还使用了更多高级技术,包括 OverlayFS 联合文件系统、veth pair 网络设备、seccomp 安全策略等。

四、容器资源限制实战调优
在生产环境使用 Docker 或 containerd 时,如何合理设置资源限制是运维同学关心的问题。以下是一些经过验证的最佳实践。
4.1 CPU 限制:不要使用分数核的误区
Docker 的
1 | --cpus |
参数表示容器可以使用的 CPU 时间比例,但很多开发者误以为设置
1 | --cpus=1.5 |
就能保证容器获得 1.5 个 CPU 的性能。实际上,如果你的宿主机只有 4 核,设置
1 | --cpus=4 |
也不一定能让容器独占所有 CPU 时间——这取决于其他容器的压力和内核调度策略。
更精细的控制方式:
1
2
3
4
5
6
7
8
9
10 # 保证容器至少获得 0.5 核,最多 2 核
docker run --cpus=2 --cpu-shares=512 my-image
# 绑定到特定 CPU 核心(CPU pinning)
docker run --cpuset-cpus="0-1" my-image
# 设置 CPU 的权重优先级(相对于其他容器)
docker run --cpu-shares=1024 high-priority-container
docker run --cpu-shares=256 low-priority-container
# 当宿主机 CPU 争用时,高优先级的容器获得更多 CPU 时间
4.2 内存限制:OOM 优先级控制
当宿主机内存不足时,内核 OOM Killer 会选择杀掉某些进程。容器的 OOM 优先级可以通过以下方式控制:
1
2
3
4
5
6
7
8
9
10
11
12 # 降低容器被 OOM Killer 选中的概率
docker run --oom-kill-disable=false --oom-score-adj=-500 my-database
# 关键业务容器设置更低的 oom_score_adj(更不容易被杀)
docker run --oom-score-adj=-1000 critical-service
# 批处理任务可以设置较高的值(优先被杀)
docker run --oom-score-adj=500 batch-job
# 注意:--oom-kill-disable 与 memory 限制同时设置才有意义
# 仅设置 --oom-kill-disable 而不设 memory 限制的话,容器理论上可以耗尽宿主机内存
docker run --memory=512m --oom-kill-disable my-service
4.3 磁盘 I/O 限制
对于日志密集型和数据库容器,磁盘 IO 的限制至关重要:
1
2
3
4
5
6
7
8
9 # 限制读写速率(字节/秒)
docker run --device-read-bps=/dev/sda:50mb \
--device-write-bps=/dev/sda:30mb \
my-db-container
# 限制 IOPS
docker run --device-read-iops=/dev/sda:1000 \
--device-write-iops=/dev/sda:500 \
my-log-processor
五、容器运行时安全实践
Namespace 和 cgroups 提供了基础的隔离和限制,但容器并非完全安全的沙箱。以下安全措施能显著降低容器逃逸的风险。
5.1 Seccomp 系统调用过滤
Seccomp(Secure Computing Mode)可以限制容器内进程可以使用的系统调用,是防范容器逃逸的第一道防线。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 # Docker 默认使用 seccomp 白名单,约 300+ 系统调用被允许
# 查看当前容器的 seccomp 配置
docker inspect --format='{{json .HostConfig.SecurityOpt}}' container_name
# 使用自定义 seccomp 策略(限制更严格)
docker run --security-opt seccomp=/path/to/custom-profile.json my-app
# 自定义 profile 示例(仅允许必要的系统调用)
cat > /tmp/strict-seccomp.json << 'EOF'
{
"defaultAction": "SCMP_ACT_ERRNO",
"architectures": ["SCMP_ARCH_X86_64"],
"syscalls": [
{
"names": ["read", "write", "open", "close", "mmap", "munmap",
"brk", "sched_yield", "exit_group", "exit", "futex",
"clock_gettime", "getdents64", "newfstatat", "readlinkat",
"getrandom", "nanosleep", "writev", "readv", "mprotect"],
"action": "SCMP_ACT_ALLOW",
"args": [],
"comment": "Allowed system calls for minimal workloads"
}
]
}
EOF
5.2 Capabilities 缩减
Linux capabilities 将 root 用户的特权拆分为独立单元。容器默认启用了约 14 种 capability,对于大多数应用来说仍过多:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 # 查看容器拥有的 capabilities
docker run --rm alpine capsh --print
# 仅保留真正需要的 capability(安全最佳实践)
docker run --cap-drop=ALL \
--cap-add=NET_BIND_SERVICE \
--cap-add=NET_RAW \
nginx
# 常见应用的 capabilities 需求速查表
# - Web 服务器 (nginx/httpd): NET_BIND_SERVICE, NET_RAW
# - 数据库 (PostgreSQL): 无需额外 capability
# - Redis: 无需额外 capability
# - NTP 服务: SYS_TIME, SYS_NICE
# - DHCP 客户端: NET_BIND_SERVICE, NET_RAW, NET_ADMIN
5.3 使用 Rootless 模式
Docker 的 rootless 模式可以在非 root 用户下运行整个 Docker 守护进程,大幅降低潜在的攻击面:
1
2
3
4
5
6
7
8
9 # 安装 rootless Docker
dockerd-rootless-setuptool.sh install
# 使用 rootless Docker
export DOCKER_HOST=unix:///run/user/$UID/docker.sock
docker run -d nginx
# 验证当前是否是 rootless 模式
docker info 2>&1 | grep -i rootless
要注意的是,rootless 模式也有一些限制:参见 Docker 官方文档,例如 overlay network 不支持,
1 | --privileged |
模式不可用,以及性能开销略高(约 5-10%)。
六、生产环境资源规划清单
将容器部署到生产环境时,建议按照以下清单检查资源配置:
- 内存:设置
1memory
硬限制,同时设置
1memory-reservation软限制(给调度器参考值),通常硬限制比应用预期使用量多 20-30%
- CPU:使用
1--cpus
而非
1--cpu-shares,除非你有明确的优先级需求。对延迟敏感的应用使用
1--cpuset-cpus绑定核心
- 文件系统:日志密集型容器挂载独立数据卷,设置磁盘 IO 限制防止影响其他容器
- 安全:
1--cap-drop=ALL
然后逐一添加需要的 capability;启用 seccomp 默认配置;除非极特殊情况否则不要使用
1--privileged - 健康检查:
1HEALTHCHECK
指令配置合理的间隔(30s)和超时(10s),重试次数 3 次
- 重启策略:对我来说,非关键服务用
1--restart=on-failure:5
,关键服务用
1--restart=always
七、故障排查:cgroups 和 namespace 相关工具
当容器行为异常时,以下工具能帮助你透过现象看本质:
| 工具 | 用途 | 示例 | ||||
|---|---|---|---|---|---|---|
|
列出系统上的所有 namespace |
查看所有 net namespace |
||||
|
进入已有 namespace 执行命令 |
查看某个进程的网络配置 |
||||
|
查看文件系统挂载点和设备 |
查看 overlay 层 |
||||
|
类似 top 但显示 cgroup 资源使用 |
直接运行 |
||||
|
查看进程所属的 cgroup |
看当前 shell 的 cgroup |
||||
|
查看当前进程的 capabilities |
需要在容器内执行 |
1
2
3
4
5
6
7
8
9
10
11
12 # 实际排查场景:查找哪个容器占用了大量内存
# 方法1:通过 cgroup 查找
for cg in /sys/fs/cgroup/system.slice/docker-*.scope; do
echo "=== $(basename $cg) ==="
cat "$cg/memory.current" 2>/dev/null | numfmt --to=iec
done
# 方法2:通过 nsenter 诊断容器网络问题
# 先获取容器的主进程 PID
CONTAINER_PID=$(docker inspect --format '{{.State.Pid}}' container_name)
# 进入容器的网络 namespace 抓包
nsenter -t "$CONTAINER_PID" -n tcpdump -i eth0 -c 100
八、总结与延伸阅读
本文从 namespace 和 cgroups 的底层机制出发,逐步过渡到容器资源限制的生产实践和安全管理。理解这些底层原理,不仅能帮助你更好地配置 Docker/Kubernetes,也能在遇到奇葩问题时直击本质——大多数容器的”玄学”问题,最终都可以归结为 namespace 隔离不彻底或 cgroup 限制配置不当。
延伸阅读资源
- Linux 内核官方 cgroup v2 文档 — 权威参考
- Linux Programmer’s Manual – namespaces(7) — 各 namespace 详细说明
- Docker 安全文档 — 官方安全最佳实践
- Kubernetes Pod Lifecycle — 了解容器在 K8s 中的资源管理
汤不热吧