docker run -d -p 80:80 nginx——一条命令,0.3 秒,一个运行中的 Nginx 容器。但在这 0.3 秒内,发生了什么?
Docker CLI 解析参数,通过 gRPC 调用 dockerd;dockerd 检查本地镜像,不存在则从 Registry 拉取;拉取完成后,containerd 解压镜像层到 OverlayFS;containerd 启动 containerd-shim;shim 调用 runc create;runc 创建 Namespace、配置 Cgroup、挂载 OverlayFS、设置 seccomp、执行容器进程;shim 收集容器 PID 并返回给 containerd;containerd 通知 dockerd;dockerd 配置端口映射;Docker CLI 返回容器 ID。
本章将完整追踪这条调用链的每一步,将前 8 章的知识融会贯通。
一、docker run 的完整调用链
1.1 调用链总览
1.2 涉及的进程
| 进程 | PID 示例 | 角色 | 生命周期 |
|---|---|---|---|
| docker CLI | 10000 | 命令行客户端 | 命令执行期间 |
| dockerd | 1000 | Docker daemon | 持久运行 |
| containerd | 2000 | 容器运行时管理 | 持久运行 |
| containerd-shim | 3000 | 容器进程监督 | 容器运行期间 |
| runc (create) | 4000 | OCI 运行时 | 创建后退出 |
| runc init | 4001 | 容器初始化 | 初始化后 exec 为用户进程 |
| nginx | 4001→ | 容器进程 | 容器运行期间 |
二、阶段一:镜像拉取
2.1 检查本地镜像
// dockerd 检查本地镜像(简化)func (daemon *Daemon) CreateContainer(params CreateParams) (*Container, error) { // 1. 解析镜像名称 ref, err := reference.ParseNormalizedNamed(params.Image)
// 2. 查找本地镜像 image, err := daemon.imageStore.Get(ref) if err != nil { // 本地不存在,拉取镜像 image, err = daemon.PullImage(ref) }
// 3. 创建容器配置 container, err := daemon.newContainer(image, params) return container, nil}2.2 镜像拉取流程
# docker pull nginx:latest 的底层操作
# 1. 解析镜像名称# nginx:latest → docker.io/library/nginx:latest
# 2. 获取 Manifest# GET https://registry-1.docker.io/v2/library/nginx/manifests/latest
# 3. 下载 Config blob# GET https://registry-1.docker.io/v2/library/nginx/blobs/sha256:abc123...
# 4. 下载 Layer blob(按需,跳过已有层)# GET https://registry-1.docker.io/v2/library/nginx/blobs/sha256:def456...
# 5. 解压 Layer 到 OverlayFS# tar -xzf layer1.tar.gz -C /var/lib/docker/overlay2/l1/diff# tar -xzf layer2.tar.gz -C /var/lib/docker/overlay2/l2/diff2.3 镜像层解压与 OverlayFS
# 查看 nginx 镜像的层docker inspect nginx --format '{{json .RootFS.Layers}}' | python3 -m json.tool
# 输出示例(6 层):# "sha256:a1b2c3d4..."# "sha256:e5f6g7h8..."# "sha256:i9j0k1l2..."# "sha256:m3n4o5p6..."# "sha256:q7r8s9t0..."# "sha256:u1v2w3x4..."
# 每层对应 OverlayFS 的一个 lowerdirls /var/lib/docker/overlay2/l/# 每个短链接指向一个层的 diff 目录三、阶段二:容器创建
3.1 dockerd 生成容器配置
dockerd 将 Docker 参数转换为 OCI Runtime Spec 格式的 config.json:
// dockerd 生成容器配置(简化)func (daemon *Daemon) createContainerSpec(container *Container) (*specs.Spec, error) { spec := &specs.Spec{ Version: specs.Version, Process: &specs.Process{ Args: container.Config.Entrypoint + container.Config.Cmd, Env: container.Config.Env, Cwd: container.Config.WorkingDir, User: container.Config.User, Terminal: container.Config.Tty, }, Root: &specs.Root{ Path: "rootfs", Readonly: container.HostConfig.ReadonlyRootfs, }, Hostname: container.Config.Hostname, Mounts: daemon.generateMounts(container), Linux: &specs.Linux{ Namespaces: daemon.generateNamespaces(container), Resources: daemon.generateResources(container), Seccomp: daemon.generateSeccomp(container), }, } return spec, nil}3.2 Docker 参数到 OCI Spec 的映射
| Docker 参数 | OCI Spec 字段 | 说明 |
|---|---|---|
-e FOO=bar | process.env | 环境变量 |
-u nginx | process.user | 运行用户 |
-h myhost | hostname | 主机名 |
--memory=512m | linux.resources.memory.limit | 内存限制 |
--cpus=2 | linux.resources.cpu.quota/period | CPU 限制 |
--cap-add=NET_ADMIN | process.capabilities | Linux Capabilities |
--security-opt seccomp=... | linux.seccomp | seccomp 规则 |
--pid=host | linux.namespaces (无 PID NS) | 共享宿主 PID NS |
--network=host | linux.namespaces (无 Network NS) | 共享宿主网络 |
--read-only | root.readonly | 只读根文件系统 |
-v /host:/container | mounts | 挂载点 |
3.3 containerd 创建容器
// containerd 创建容器(简化)func (m *TaskManager) Create(ctx context.Context, id string, spec *specs.Spec) error { // 1. 准备 rootfs 快照 mounts, err := m.snapshotter.Prepare(ctx, id, parentSnapshot) // 这一步创建 OverlayFS 的 upperdir
// 2. 写入 config.json if err := writeConfig(bundlePath, spec); err != nil { return err }
// 3. 启动 shim shim, err := m.startShim(ctx, bundle, id) // shim 进程启动后,通过 gRPC 与 containerd 通信
// 4. 通过 shim 创建容器 _, err = shim.Create(ctx, &task.CreateRequest{ ID: id, Bundle: bundlePath, Rootfs: mounts, Terminal: spec.Process.Terminal, })
return err}四、阶段三:runc 创建容器
4.1 runc create 的系统调用
# 用 strace 追踪 runc create 的关键系统调用sudo strace -f -e trace=clone,unshare,mount,pivot_root,prctl,capset,write \ -o /tmp/runc-create.log runc create --bundle /tmp/bundle mycontainer
# 关键系统调用序列(简化):# 1. clone(CLONE_NEWPID|CLONE_NEWNS|CLONE_NEWUTS|CLONE_NEWIPC|CLONE_NEWNET)# → 创建新 Namespace 中的子进程# 2. mount("overlay", "/var/lib/docker/overlay2/.../merged", "overlay", ...)# → 挂载 OverlayFS# 3. mount("proc", "/proc", "proc", ...)# → 挂载 procfs# 4. pivot_root("/var/lib/docker/overlay2/.../merged", ...)# → 切换根文件系统# 5. write(5, "200000 100000", ..., "cpu.max")# → 设置 CPU Cgroup# 6. write(5, "536870912", ..., "memory.max")# → 设置内存 Cgroup# 7. capset(CAP_SETPCAP, {CAP_AUDIT_WRITE, CAP_KILL, ...})# → 设置 Capabilities# 8. prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, ...)# → 设置 seccomp 过滤# 9. execve("/docker-entrypoint.sh", ["nginx", "-g", "daemon off;"], ...)# → 执行用户命令4.2 容器创建的完整时序
五、阶段四:容器启动
5.1 runc start
# runc start 发送信号让 runc init 执行用户命令runc start mycontainer
# 底层操作:# 1. 通过管道通知 runc init 执行用户命令# 2. runc init 调用 execve() 替换为用户进程# 3. 容器进程开始运行5.2 端口映射
# dockerd 配置端口映射(-p 80:80)# 1. 创建 iptables DNAT 规则sudo iptables -t nat -A DOCKER -p tcp --dport 80 -j DNAT --to-destination 172.17.0.2:80
# 2. 创建 iptables MASQUERADE 规则sudo iptables -t nat -A POSTROUTING -s 172.17.0.2 -j MASQUERADE
# 3. 创建 iptables ALLOW 规则sudo iptables -A DOCKER -d 172.17.0.2 -p tcp --dport 80 -j ACCEPT
# 查看 Docker 的 iptables 规则sudo iptables -t nat -L DOCKER -n -v5.3 网络配置
# Docker 的网络配置流程# 1. 创建 veth pairsudo ip link add veth123 type veth peer name veth456
# 2. 将一端放入容器 Network Namespacesudo ip link set veth456 netns <container-pid>
# 3. 将另一端连接到 docker0 bridgesudo ip link set veth123 master docker0
# 4. 在容器内配置 IP 地址sudo nsenter -t <container-pid> -n ip addr add 172.17.0.2/16 dev eth0
# 5. 在容器内设置默认路由sudo nsenter -t <container-pid> -n ip route add default via 172.17.0.1六、阶段五:容器运行
6.1 容器运行时的进程与 Namespace 关系
容器启动后,宿主机上可以看到完整的进程和 Namespace 映射:
6.2 容器运行时的状态
# 查看容器的完整状态docker inspect mynginx | python3 -m json.tool
# 关键信息:# - State.Pid: 容器进程在宿主机上的 PID# - State.Running: 是否运行中# - NetworkSettings.IPAddress: 容器 IP# - HostConfig.PortBindings: 端口映射# - HostConfig.Memory: 内存限制# - HostConfig.NanoCpus: CPU 限制6.3 容器进程的 Namespace
# 查看容器进程的所有 NamespacePID=$(docker inspect -f '{{.State.Pid}}' mynginx)ls -la /proc/$PID/ns/
# 输出:# cgroup -> 'cgroup:[4026532700]' ← 独立 Cgroup NS# ipc -> 'ipc:[4026532698]' ← 独立 IPC NS# mnt -> 'mnt:[4026532696]' ← 独立 Mount NS# net -> 'net:[4026532699]' ← 独立 Network NS# pid -> 'pid:[4026532697]' ← 独立 PID NS# user -> 'user:[4026531837]' ← 共享宿主 User NS# uts -> 'uts:[4026532695]' ← 独立 UTS NS6.4 容器进程的 Cgroup
# 查看容器的 Cgroup 路径cat /proc/$PID/cgroup
# 输出(Cgroup v2):# 0::/system.slice/docker-abc123.scope
# 查看容器的资源限制cat /sys/fs/cgroup/system.slice/docker-abc123.scope/cpu.maxcat /sys/fs/cgroup/system.slice/docker-abc123.scope/memory.maxcat /sys/fs/cgroup/system.slice/docker-abc123.scope/pids.max七、完整流程的代码追踪
7.1 用 Go 模拟 docker run
package main
import ( "context" "fmt" "log" "os" "syscall"
containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/containerd/v2/pkg/namespaces" specs "github.com/opencontainers/runtime-spec/specs-go")
func main() { ctx := namespaces.WithNamespace(context.Background(), "default")
// 1. 连接 containerd client, err := containerd.New("/run/containerd/containerd.sock") if err != nil { log.Fatal(err) } defer client.Close()
// 2. 拉取镜像 fmt.Println("Pulling image...") image, err := client.Pull(ctx, "docker.io/library/nginx:alpine", containerd.WithPullUnpack) if err != nil { log.Fatal(err) }
// 3. 创建容器 fmt.Println("Creating container...") container, err := client.NewContainer(ctx, "my-nginx", containerd.WithImage(image), containerd.WithNewSnapshot("nginx-snapshot", image), containerd.WithNewSpec( containerd.WithImageConfig(image), withResourceLimits(), ), ) if err != nil { log.Fatal(err) } defer container.Delete(ctx, containerd.WithSnapshotCleanup)
// 4. 创建并启动任务 fmt.Println("Starting container...") task, err := container.NewTask(ctx, containerd.NewIO(os.Stdin, os.Stdout, os.Stderr)) if err != nil { log.Fatal(err) } defer task.Delete(ctx)
if err := task.Start(ctx); err != nil { log.Fatal(err) }
// 5. 等待退出 statusC, err := task.Wait(ctx) if err != nil { log.Fatal(err) }
fmt.Printf("Container running with PID %d\n", task.Pid()) status := <-statusC fmt.Printf("Container exited with code %d\n", status.ExitCode())}
func withResourceLimits() containerd.SpecOpts { return func(_ context.Context, _ *containerd.Client, c *specs.Spec) error { c.Linux.Resources = &specs.LinuxResources{ Memory: &specs.LinuxMemory{Limit: int64Ptr(512 * 1024 * 1024)}, CPU: &specs.LinuxCPU{ Quota: int64Ptr(200000), Period: uint64Ptr(100000), }, } return nil }}
func int64Ptr(v int64) *int64 { return &v }func uint64Ptr(v uint64) *uint64 { return &v }八、性能分析
8.1 docker run 各阶段耗时
| 阶段 | 典型耗时 | 说明 |
|---|---|---|
| 镜像拉取 | 1-60s | 取决于网络和镜像大小 |
| 镜像解压 | 0.1-5s | 取决于层数和大小 |
| OCI Bundle 生成 | 1-5ms | 生成 config.json |
| shim 启动 | 5-20ms | fork + exec |
| runc create | 20-100ms | Namespace/Cgroup/mount |
| runc start | 1-5ms | 执行用户命令 |
| 网络配置 | 5-50ms | veth/bridge/iptables |
| 总计(镜像已存在) | 50-200ms | — |
8.2 优化建议
# 1. 使用更小的基础镜像docker run -d alpine-based-image # 比 Ubuntu 镜像快 5-10x
# 2. 预拉取镜像docker pull nginx:latest # 提前拉取,避免运行时等待
# 3. 使用 --init 避免 PID 1 问题docker run --init nginx # 使用 tini 作为 PID 1
# 4. 使用 host 网络模式(跳过网络配置)docker run --network=host nginx # 省去 veth/bridge 配置
# 5. 限制日志大小docker run --log-driver=json-file --log-opt max-size=10m nginx九、本章小结
上一章深入解读了containerd-shim 的解耦机制的内部机制。
| 阶段 | 关键操作 | 涉及组件 |
|---|---|---|
| 镜像拉取 | Registry API → Content Store → Snapshot | containerd |
| 容器创建 | 生成 OCI Bundle → 启动 shim | containerd |
| runc create | clone → mount → pivot_root → cgroup → seccomp | runc + 内核 |
| runc start | execve 用户命令 | runc |
| 网络配置 | veth pair → bridge → iptables | dockerd |
| 容器运行 | shim 监督 → 事件通知 | shim + containerd |
理解 docker run 的完整流程,是排查容器问题的关键。当你遇到容器启动失败、网络不通、资源限制不生效等问题时,可以按这条调用链逐步定位——是镜像问题?是 runc 问题?还是内核问题?
排查容器启动失败的常用方法:docker logs 查看容器日志,docker inspect 查看容器状态和退出码,journalctl -u containerd 查看 containerd 日志,dmesg 查看内核日志(OOM、seccomp 拒绝等)。如果容器一启动就退出,先用 docker run -it 交互模式运行,确认入口命令是否正确。
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






