Docker 容器运行时管理:从进程隔离到资源调度的深度实践

作者:云原生架构师
技术栈:Docker, Linux Kernel, Cgroups, Namespaces, OCI Runtime
难度等级:★★★★★(专家级)
预计阅读时间:55 分钟


目录

  1. 引言:容器运行时的技术挑战
  2. 容器启动的底层原理
  3. [进程隔离与 Namespace](#3-进程隔离与 namespace)
  4. [资源限制与 Cgroups](#4-资源限制与 cgroups)
  5. [容器 exec 与 attach 深度解析](#5-容器 exec 与 attach-深度解析)
  6. 容器监控与诊断
  7. 容器网络运行时
  8. 容器存储运行时
  9. 容器安全运行时
  10. 性能优化实战
  11. 故障排查案例
  12. 总结与前沿技术

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 容器运行时的核心技术

容器运行时需要解决的三大问题:

  1. 隔离性:如何保证容器间互不干扰?

    • PID Namespace:进程隔离
    • Network Namespace:网络隔离
    • Mount Namespace:文件系统隔离
  2. 资源限制:如何防止容器耗尽系统资源?

    • CPU Cgroup:CPU 配额
    • Memory Cgroup:内存限制
    • I/O Cgroup:磁盘 I/O 限制
  3. 文件系统:如何实现分层存储?

    • Overlay2:联合文件系统
    • Copy-on-Write:写时复制

2. 容器启动的底层原理

2.1 完整的启动流程

Linux Kernel runc containerd-shim containerd Docker Daemon Docker CLI 用户 Linux Kernel runc containerd-shim containerd Docker Daemon Docker CLI 用户 CLONE_NEWPID | CLONE_NEWNET | ... docker run nginx POST /containers/create 验证参数 Pull image (如果需要) 解析镜像层 镜像就绪 Create container spec Spawn shim process Create container 1. clone() 创建进程 2. 配置 Cgroups 3. 挂载 RootFS (Overlay2) 4. 设置 Hostname 5. 配置网络接口 6. exec() 执行入口进程 容器进程运行 容器启动成功 容器状态:Running 容器 ID 返回容器 ID 显示容器 ID

时间分解(典型值):

  1. CLI 到 Daemon 通信:10ms
  2. 镜像检查/拉取:50-5000ms(取决于网络)
  3. containerd 创建容器:100ms
  4. Shim 启动:50ms
  5. runc 调用内核:100ms
  6. RootFS 挂载:100ms
  7. 进程启动: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:周期(微秒),默认 100ms
  • nr_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

底层流程

Linux Kernel containerd-shim containerd Docker Daemon Docker CLI Linux Kernel containerd-shim containerd Docker Daemon Docker CLI POST /containers/{id}/exec 创建 exec 配置 exec ID POST /exec/{id}/start ExecProcess Exec in container namespace 1. 打开 /proc/<pid>/ns/* 2. setns() 进入 Namespace 3. fork() 创建进程 4. exec() 执行命令 命令运行 进程已启动 成功 流式输出

关键系统调用

// 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 流程

外部请求 :8080

PREROUTING 链

DOCKER 链

DNAT 172.17.0.2:80

docker0 网桥

veth pair

容器 eth0:80

Nginx


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

优化方法

  1. 使用 runC 直接启动(跳过 Docker Daemon)
  2. 镜像预热(提前拉取)
  3. 使用本地镜像仓库

10.2 资源优化

# 合理设置资源限制
docker run -d \
    --cpus=1 \
    --memory=512m \
    --pids-limit=100 \
    nginx

11. 故障排查案例

11.1 容器无法启动

排查步骤

  1. 查看日志:docker logs container
  2. 检查配置:docker inspect container
  3. 验证内核:docker info

11.2 性能下降

排查工具

  • docker stats:资源使用
  • perf top:CPU 热点
  • strace:系统调用

12. 总结与前沿技术

12.1 核心技术

  1. Namespace:进程隔离
  2. Cgroups:资源限制
  3. Overlay2:联合文件系统

12.2 前沿技术

  1. eBPF:无侵入监控
  2. Kata Containers:安全容器
  3. WebAssembly:轻量级容器

版权声明:本文原创,转载请注明出处


如果本文对您有帮助,欢迎点赞、收藏、转发!

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐