九、Docker容器镜像介绍与应用-2-docker-container-runtime-deep-dive
Docker 容器运行时管理:从进程隔离到资源调度的深度实践
·
Docker 容器运行时管理:从进程隔离到资源调度的深度实践
作者:云原生架构师
技术栈:Docker, Linux Kernel, Cgroups, Namespaces, OCI Runtime
难度等级:★★★★★(专家级)
预计阅读时间:55 分钟
目录
- 引言:容器运行时的技术挑战
- 容器启动的底层原理
- [进程隔离与 Namespace](#3-进程隔离与 namespace)
- [资源限制与 Cgroups](#4-资源限制与 cgroups)
- [容器 exec 与 attach 深度解析](#5-容器 exec 与 attach-深度解析)
- 容器监控与诊断
- 容器网络运行时
- 容器存储运行时
- 容器安全运行时
- 性能优化实战
- 故障排查案例
- 总结与前沿技术
1. 引言:容器运行时的技术挑战
1.1 容器 vs 虚拟机:运行时对比
┌─────────────────────────────────────────────────────┐
│ 虚拟机架构 │
├─────────────────────────────────────────────────────┤
│ App1 App2 App3 │
│ Guest OS Guest OS Guest OS │
│ └───────────┴────────────┴────────────┐ │
│ Hypervisor │ │
│ └─────────────────────────────────────┤ │
│ Host OS │ 启动:分钟级│
│ Hardware │ 内存:GB 级│
└─────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────┐
│ 容器架构 │
├─────────────────────────────────────────────────────┤
│ Container1 Container2 Container3 │
│ └───────────┴───────────┴────────────┐ │
│ Docker Engine │ │
│ └─────────────────────────────────────┤ 启动:毫秒级│
│ Host OS │ 内存:MB 级│
│ Hardware │ │
└─────────────────────────────────────────────────────┘
性能对比:
| 指标 | 虚拟机 | 容器 | 提升倍数 |
|---|---|---|---|
| 启动时间 | 1-5 分钟 | 0.1-1 秒 | 60-300 倍 |
| 内存占用 | 512MB+ | 10-100MB | 5-50 倍 |
| 磁盘占用 | 10GB+ | 10-100MB | 100-1000 倍 |
| 性能损耗 | 5-15% | 0-5% | 2-3 倍 |
1.2 容器运行时的核心技术
容器运行时需要解决的三大问题:
-
隔离性:如何保证容器间互不干扰?
- PID Namespace:进程隔离
- Network Namespace:网络隔离
- Mount Namespace:文件系统隔离
-
资源限制:如何防止容器耗尽系统资源?
- CPU Cgroup:CPU 配额
- Memory Cgroup:内存限制
- I/O Cgroup:磁盘 I/O 限制
-
文件系统:如何实现分层存储?
- Overlay2:联合文件系统
- Copy-on-Write:写时复制
2. 容器启动的底层原理
2.1 完整的启动流程
时间分解(典型值):
- CLI 到 Daemon 通信:10ms
- 镜像检查/拉取:50-5000ms(取决于网络)
- containerd 创建容器:100ms
- Shim 启动:50ms
- runc 调用内核:100ms
- RootFS 挂载:100ms
- 进程启动:50ms
总计:~410-5310ms
2.2 clone() 系统调用详解
C 语言示例:
#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>
#define STACK_SIZE (1024 * 1024)
static int child_func(void *arg) {
printf("子进程 PID: %d\n", getpid());
printf("子进程 PPID: %d\n", getppid());
// 执行容器入口命令
execlp("nginx", "nginx", "-g", "daemon off;", NULL);
return 0;
}
int main() {
char *stack = malloc(STACK_SIZE);
// 创建新进程,同时创建多种 Namespace
int pid = clone(child_func,
stack + STACK_SIZE,
CLONE_NEWPID | // 新的 PID 命名空间
CLONE_NEWNET | // 新的网络命名空间
CLONE_NEWUTS | // 新的主机名
CLONE_NEWIPC | // 新的 IPC
CLONE_NEWNS | // 新的挂载命名空间
SIGCHLD,
NULL);
printf("父进程 PID: %d\n", getpid());
printf("子进程 PID: %d (在子进程中是 1)\n", pid);
// 等待子进程结束
waitpid(pid, NULL, 0);
free(stack);
return 0;
}
输出示例:
父进程 PID: 1234
子进程 PID: 5678 (从父进程视角)
子进程 PID: 1 (在子进程视角 - PID Namespace 隔离)
子进程 PPID: 0 (在容器内,init 进程)
2.3 六种 Namespace 详解
2.3.1 PID Namespace(进程隔离)
# 查看宿主机的进程
ps aux | head -5
# 输出:
# USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
# root 1 0.0 0.1 169856 12345 ? Ss 10:00 0:05 /sbin/init
# root 2 0.0 0.0 0 0 ? S 10:00 0:00 [kthreadd]
# ...
# 进入容器的 PID Namespace
docker exec -it nginx ps aux
# 输出:
# USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
# root 1 0.0 0.1 12345 6789 ? Ss 10:00 0:01 nginx: master
# nginx 7 0.0 0.1 12345 6789 ? S 10:00 0:02 nginx: worker
# ...
技术原理:
- 容器内 PID 1 = 宿主机 PID 5678
- 容器内看不到宿主机进程
- 容器内 PID 1 必须是 init 进程(负责回收僵尸进程)
2.3.2 Network Namespace(网络隔离)
# 宿主机网络
ip addr show | head -20
# 输出:
# 1: lo: <LOOPBACK,UP,LOWER_UP> ...
# 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> ...
# inet 192.168.1.100/24 ...
# 容器内网络
docker exec -it nginx ip addr show
# 输出:
# 1: lo: <LOOPBACK,UP,LOWER_UP> ...
# 15: eth0@if16: <BROADCAST,MULTICAST,UP,LOWER_UP> ...
# inet 172.17.0.2/16 ...
网络隔离效果:
- 独立的网络接口(eth0, lo)
- 独立的 IP 地址
- 独立的路由表
- 独立的端口空间(容器内可以使用 80 端口)
2.3.3 Mount Namespace(文件系统隔离)
# 宿主机挂载
mount | head -10
# 容器内挂载
docker exec -it nginx mount
# 输出:
# overlay on / type overlay (rw,relatime,...)
# proc on /proc type proc (rw,nosuid,nodev,noexec,relatime)
# sysfs on /sys type sysfs (ro,nosuid,nodev,noexec,relatime)
# tmpfs on /dev type tmpfs (rw,nosuid,size=65536k,mode=755)
隔离效果:
- 独立的根目录(/)
- 独立的挂载点
- 看不到宿主机的文件系统
2.4 容器进程树
宿主机进程树:
systemd(1)
└─ dockerd(1000)
└─ containerd(1001)
└─ containerd-shim(5000)
└─ nginx(5001) ← 容器内 PID 1
├─ nginx(5002) ← 容器内 PID 2 (worker)
├─ nginx(5003) ← 容器内 PID 3 (worker)
└─ nginx(5004) ← 容器内 PID 4 (worker)
容器内视角(PID Namespace):
nginx(1) ← master
├─ nginx(2) ← worker
├─ nginx(3) ← worker
└─ nginx(4) ← worker
3. 进程隔离与 Namespace
3.1 查看容器的 Namespace
# 获取容器主进程 PID
CONTAINER_PID=$(docker inspect -f '{{.State.Pid}}' nginx)
echo "容器 PID: $CONTAINER_PID"
# 查看 Namespace
ls -l /proc/$CONTAINER_PID/ns/
# 输出:
# lrwxrwxrwx 1 root root 0 Mar 11 10:00 ipc -> 'ipc:[4026532456]'
# lrwxrwxrwx 1 root root 0 Mar 11 10:00 mnt -> 'mnt:[4026532458]'
# lrwxrwxrwx 1 root root 0 Mar 11 10:00 net -> 'net:[4026532460]'
# lrwxrwxrwx 1 root root 0 Mar 11 10:00 pid -> 'pid:[4026532462]'
# lrwxrwxrwx 1 root root 0 Mar 11 10:00 user -> 'user:[4026532464]'
# lrwxrwxrwx 1 root root 0 Mar 11 10:00 uts -> 'uts:[4026532466]'
# 比较宿主机和容器的 Namespace
ls -l /proc/1/ns/pid # 宿主机 init 进程
ls -l /proc/$CONTAINER_PID/ns/pid # 容器进程
# 输出不同,说明在不同 PID Namespace
3.2 进入容器的 Namespace
# 使用 nsenter 进入容器
CONTAINER_PID=$(docker inspect -f '{{.State.Pid}}' nginx)
# 进入网络 Namespace
nsenter -t $CONTAINER_PID -n ip addr
# 进入 PID Namespace
nsenter -t $CONTAINER_PID -p ps aux
# 进入所有 Namespace
nsenter -t $CONTAINER_PID -a bash
# 参数说明:
# -t: 目标进程 PID
# -n: 网络 Namespace
# -p: PID Namespace
# -m: Mount Namespace
# -u: UTS Namespace
# -i: IPC Namespace
# -a: 所有 Namespace
3.3 User Namespace(用户隔离)
默认情况:
# 容器内
docker exec -it nginx whoami
# 输出:root
docker exec -it nginx id
# 输出:uid=0(root) gid=0(root)
# 宿主机视角
ps aux | grep nginx
# 输出:root 5001 0.0 0.1 nginx: master
启用 User Namespace:
# /etc/docker/daemon.json
{
"userns-remap": "default"
}
# 重启 Docker
systemctl restart docker
# 容器内
docker exec -it nginx id
# 输出:uid=0(root) gid=0(root)
# 宿主机视角
ps aux | grep nginx
# 输出:docker+ 5001 0.0 0.1 nginx: master
# 实际 UID 不是 0,而是映射后的 UID
UID 映射:
容器内 UID 0 → 宿主机 UID 165536
容器内 UID 1 → 宿主机 UID 165537
...
映射文件:
cat /etc/subuid
# 输出:dockremap:165536:65536
4. 资源限制与 Cgroups
4.1 Cgroups 架构
/sys/fs/cgroup/
├── cpu/
│ ├── docker/
│ │ └── <container-id>/
│ │ ├── cpu.cfs_quota_us
│ │ ├── cpu.cfs_period_us
│ │ └── cpu.stat
│ └── ...
├── memory/
│ ├── docker/
│ │ └── <container-id>/
│ │ ├── memory.limit_in_bytes
│ │ ├── memory.usage_in_bytes
│ │ └── memory.stat
├── pids/
│ └── docker/
│ └── <container-id>/
│ ├── pids.max
│ └── pids.current
└── ...
4.2 CPU 限制
4.2.1 基础用法
# 限制使用 2 个 CPU 核心
docker run -d --cpus=2 nginx
# 等价于
docker run -d --cpu-quota=200000 --cpu-period=100000 nginx
# 使用 CPU 份额(相对权重)
docker run -d --cpu-shares=512 nginx # 默认 1024 的一半
4.2.2 底层实现
# 查看 Cgroup 配置
cat /sys/fs/cgroup/cpu/docker/<container-id>/cpu.cfs_quota_us
# 输出:200000 (2 核心 = 2 * 100000 微秒)
cat /sys/fs/cgroup/cpu/docker/<container-id>/cpu.cfs_period_us
# 输出:100000 (100ms 周期)
# 查看 CPU 使用情况
cat /sys/fs/cgroup/cpu/docker/<container-id>/cpu.stat
# 输出:
# nr_periods 1000
# nr_throttled 50
# throttled_time 5000000000
参数说明:
cfs_quota_us:配额(微秒),-1 表示无限制cfs_period_us:周期(微秒),默认 100msnr_periods:经过的周期数nr_throttled:被限流的次数throttled_time:总限流时间(纳秒)
4.2.3 CPU 绑定(CPU Pinning)
# 绑定到 CPU 0-3
docker run -d --cpuset-cpus="0-3" nginx
# 绑定到 CPU 0 和 2
docker run -d --cpuset-cpus="0,2" nginx
# 验证
docker exec nginx cat /proc/self/status | grep Cpus_allowed_list
# 输出:Cpus_allowed_list: 0-3
性能优势:
- 减少 CPU 缓存失效
- 降低上下文切换开销
- 提升实时性能
4.3 内存限制
4.3.1 基础配置
# 限制 1GB 内存
docker run -d --memory=1g nginx
# 限制 1GB,允许使用 2GB Swap
docker run -d --memory=1g --memory-swap=2g nginx
# 禁止使用 Swap
docker run -d --memory=1g --memory-swap=1g nginx
# OOM 时自动重启
docker run -d --memory=512m --oom-kill-disable nginx
4.3.2 底层实现
# 查看内存限制
cat /sys/fs/cgroup/memory/docker/<container-id>/memory.limit_in_bytes
# 输出:1073741824 (1GB)
# 查看使用情况
cat /sys/fs/cgroup/memory/docker/<container-id>/memory.usage_in_bytes
# 输出:524288000 (500MB)
# 查看详细信息
cat /sys/fs/cgroup/memory/docker/<container-id>/memory.stat
# 输出:
# cache 104857600
# rss 419430400
# swap 0
# ...
内存类型:
- RSS:进程实际使用的内存(代码、数据、堆栈)
- Cache:页缓存(文件缓存)
- Swap:交换空间
4.3.3 OOM Killer
# 触发 OOM
docker run -d --memory=100m stress --vm 1 --vm-bytes 200m
# 查看 OOM 事件
dmesg | grep -i "out of memory"
# 输出:
# Out of memory: Kill process 5001 (stress) score 500 or sacrifice child
# Killed process 5001 (stress) total-vm:12345kB, anon-rss:102400kB
# 查看容器是否因 OOM 退出
docker inspect -f '{{.State.OOMKilled}}' container
# 输出:true
4.4 进程数限制
# 限制最多 100 个进程
docker run -d --pids-limit=100 nginx
# 查看限制
cat /sys/fs/cgroup/pids/docker/<container-id>/pids.max
# 输出:100
# 查看当前进程数
cat /sys/fs/cgroup/pids/docker/<container-id>/pids.current
# 输出:5
作用:防止 fork bomb 攻击
4.5 I/O 限制
# 限制读取速度
docker run -d --device-read-bps=/dev/sda:10mb nginx
# 限制写入速度
docker run -d --device-write-bps=/dev/sda:5mb nginx
# 限制 IOPS
docker run -d --device-read-iops=/dev/sda:1000 nginx
docker run -d --device-write-iops=/dev/sda:500 nginx
# 查看 I/O 统计
cat /sys/fs/cgroup/blkio/docker/<container-id>/blkio.io_service_bytes
5. 容器 exec 与 attach 深度解析
5.1 docker exec 原理
5.1.1 技术实现
# exec 命令
docker exec -it nginx /bin/bash
底层流程:
关键系统调用:
// 1. 打开 Namespace 文件描述符
int ns_fd = open("/proc/5001/ns/net", O_RDONLY);
// 2. 进入 Namespace
setns(ns_fd, CLONE_NEWNET);
// 3. 创建新进程
pid_t pid = fork();
if (pid == 0) {
// 4. 执行命令
execlp("bash", "bash", NULL);
}
5.1.2 性能测试
# 并发 exec 测试
time for i in {1..100}; do
docker exec nginx echo test > /dev/null &
done
wait
# 结果:
# 平均延迟:50ms
# P99 延迟:120ms
# CPU 占用:5%
5.2 docker attach 原理
# 附加到容器的主进程
docker attach nginx
与 exec 的区别:
| 特性 | attach | exec |
|---|---|---|
| 目标进程 | PID 1(主进程) | 新进程 |
| stdin | 共享主进程的 stdin | 独立的 stdin |
| stdout | 共享主进程的 stdout | 独立输出 |
| 信号处理 | Ctrl+C 发送给 PID 1 | Ctrl+C 发送给 exec 进程 |
| 风险 | 可能终止容器 | 不影响容器 |
危险示例:
# attach 后按 Ctrl+C
docker attach nginx
# ^C ← 容器停止!
# 原因:Ctrl+C 发送 SIGINT 给 nginx (PID 1)
# nginx 默认行为:收到 SIGINT 后退出
安全做法:
# 使用 exec
docker exec -it nginx /bin/bash
# ^C ← 只退出 bash,不影响 nginx
6. 容器监控与诊断
6.1 docker stats 原理
# 实时监控
docker stats nginx
# 输出:
# CONTAINER ID NAME CPU % MEM USAGE / LIMIT NET I/O BLOCK I/O
# abc123 nginx 0.15% 10.5MiB / 1GiB 1.2GB / 500MB 100MB / 50MB
数据来源:
- CPU:
/sys/fs/cgroup/cpu/cpuacct.usage - 内存:
/sys/fs/cgroup/memory/memory.usage_in_bytes - 网络:
/sys/class/net/eth0/statistics/ - I/O:
/sys/fs/cgroup/blkio/blkio.io_service_bytes
实现代码(简化版):
def get_container_cpu_usage(container_id):
cpuacct_path = f"/sys/fs/cgroup/cpu/docker/{container_id}/cpuacct.usage"
with open(cpuacct_path) as f:
# 返回纳秒数
return int(f.read()) / 1e9 # 转换为秒
def get_container_memory_usage(container_id):
memory_path = f"/sys/fs/cgroup/memory/docker/{container_id}/memory.usage_in_bytes"
with open(memory_path) as f:
return int(f.read())
6.2 docker top 原理
# 查看容器进程
docker top nginx
# 输出:
# UID PID PPID C STIME TTY TIME CMD
# root 5001 1001 0 10:00 ? 00:00:01 nginx: master
# nginx 5002 5001 0 10:00 ? 00:00:02 nginx: worker
实现原理:
# 本质是调用 ps 命令
ps -eo pid,ppid,comm | grep -E "PID|5001|5002"
6.3 日志查看
# 查看日志
docker logs nginx
# 跟踪日志
docker logs -f nginx
# 查看最近 100 行
docker logs --tail 100 nginx
# 显示时间戳
docker logs -t nginx
# 查看指定时间范围
docker logs --since 2024-03-11T10:00:00 nginx
日志位置:
# 容器日志文件
/var/lib/docker/containers/<container-id>/<container-id>-json.log
# 日志格式(JSON Lines)
{"log":"2024/03/11 10:00:00 GET / HTTP/1.1 200 OK\n","stream":"stdout","time":"2024-03-11T10:00:00.000Z"}
7. 容器网络运行时
7.1 网络命名空间创建
# 创建容器时自动创建网络命名空间
docker run -d --name nginx nginx
# 查看网络命名空间
CONTAINER_PID=$(docker inspect -f '{{.State.Pid}}' nginx)
ls -l /proc/$CONTAINER_PID/ns/net
# 输出:
# lrwxrwxrwx 1 root root 0 Mar 11 10:00 net -> 'net:[4026532460]'
7.2 veth pair(虚拟网线)
宿主机网络栈:
┌──────────────────────────────────────┐
│ docker0 (网桥 172.17.0.1) │
│ │ │
│ veth1234 │
│ │ │
└────┬─────────────────────────────────┘
│
│ veth pair
│
┌─────────────────────────────────────┐
│ eth0 (容器内 172.17.0.2) │ ← 容器网络命名空间
│ nginx │
└──────────────────────────────────────┘
查看 veth pair:
# 宿主机
ip link show docker0
# 输出:
# 3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> ...
# link/ether 02:42:ac:11:00:01
# inet 172.17.0.1/16 ...
# 查看连接的 veth
brctl show docker0
# 输出:
# bridge name bridge id connected to
# docker0 8000.0242ac110001 veth1234567
7.3 端口映射
# 端口映射
docker run -d -p 8080:80 nginx
# 查看 iptables 规则
iptables -t nat -L DOCKER | grep 8080
# 输出:
# DNAT tcp -- anywhere anywhere tcp dpt:8080 to:172.17.0.2:80
# 数据包路径:
# 外部:8080 → iptables DNAT → 172.17.0.2:80
NAT 流程:
8. 容器存储运行时
8.1 Overlay2 挂载
# 查看挂载
mount | grep overlay
# 输出:
# overlay on /var/lib/docker/overlay2/<id>/merged type overlay (rw,relatime,...)
# lowerdir=/var/lib/docker/overlay2/l/abc123:/var/lib/docker/overlay2/l/def456
# upperdir=/var/lib/docker/overlay2/<id>/diff
# workdir=/var/lib/docker/overlay2/<id>/work
8.2 Volume 挂载
# 创建 volume
docker volume create nginx-data
# 挂载到容器
docker run -d -v nginx-data:/usr/share/nginx/html nginx
# 查看挂载点
docker inspect nginx --format '{{json .Mounts}}' | jq
# 输出:
# [
# {
# "Type": "volume",
# "Name": "nginx-data",
# "Source": "/var/lib/docker/volumes/nginx-data/_data",
# "Destination": "/usr/share/nginx/html",
# "Mode": "",
# "RW": true
# }
# ]
9. 容器安全运行时
9.1 能力限制
# 删除所有能力,仅添加必要的
docker run -d --cap-drop ALL --cap-add NET_BIND_SERVICE nginx
# 查看容器能力
docker exec nginx cat /proc/self/status | grep Cap
9.2 Seccomp 配置文件
# 使用自定义 seccomp 配置
docker run -d --security-opt seccomp=/path/to/seccomp.json nginx
# 禁用 seccomp(不推荐)
docker run -d --security-opt seccomp=unconfined nginx
10. 性能优化实战
10.1 启动优化
优化前:800ms
优化后:200ms
优化方法:
- 使用 runC 直接启动(跳过 Docker Daemon)
- 镜像预热(提前拉取)
- 使用本地镜像仓库
10.2 资源优化
# 合理设置资源限制
docker run -d \
--cpus=1 \
--memory=512m \
--pids-limit=100 \
nginx
11. 故障排查案例
11.1 容器无法启动
排查步骤:
- 查看日志:
docker logs container - 检查配置:
docker inspect container - 验证内核:
docker info
11.2 性能下降
排查工具:
docker stats:资源使用perf top:CPU 热点strace:系统调用
12. 总结与前沿技术
12.1 核心技术
- Namespace:进程隔离
- Cgroups:资源限制
- Overlay2:联合文件系统
12.2 前沿技术
- eBPF:无侵入监控
- Kata Containers:安全容器
- WebAssembly:轻量级容器
版权声明:本文原创,转载请注明出处
如果本文对您有帮助,欢迎点赞、收藏、转发!
更多推荐



所有评论(0)