手记

Docker 引擎底层源码分析:从架构到核心实现

2025-09-22 18:54:11168浏览

少林码僧

3实战 · 4手记 · 6推荐

Docker 引擎底层源码分析:从架构到核心实现


引言

Docker 自2013年发布以来,已成为容器化技术的事实标准。其简洁的 API 和高效的运行时让开发者能够快速构建、打包和部署应用。但你是否好奇过:Docker 是如何在 Linux 上创建一个隔离的“容器”?它的引擎背后究竟发生了什么?

本文将带你深入 Docker 引擎(dockerd)的源码,剖析其核心架构、关键组件以及容器启动的底层流程。我们将基于 Docker CE 24.0 版本(主要代码托管于 moby/moby 仓库)进行分析,使用 Go 语言作为主线。


一、Docker 架构概览

Docker 并非单一进程,而是一个由多个组件协同工作的系统。其典型架构如下:

+------------------+     +---------------------+
|   Docker CLI     | <-> |   Docker Daemon     |
+------------------+     +----------+----------+
                                     |
                                     v
                          +----------+----------+
                          |   Containerd        |
                          +----------+----------+
                                     |
                                     v
                          +----------+----------+
                          |   runc / OCI Runtime|
                          +----------+----------+
                                     |
                                     v
                          +----------+----------+
                          |    Linux Kernel     |
                          | (Namespaces, Cgroups, FS)
                          +---------------------+
  • Docker CLI:用户命令行工具,发送请求到守护进程。
  • Docker Daemon (dockerd):主服务进程,负责镜像管理、容器生命周期、网络、存储等。
  • Containerd:容器运行时管理器,由 Docker 贡献给 CNCF,负责容器的创建、启动、停止。
  • runc:符合 OCI(Open Container Initiative)规范的轻量级运行时,真正调用 clone() 系统调用创建容器。
  • Linux Kernel:提供 Namespaces、Cgroups、UnionFS 等核心技术支持。

📌 注意:自 Docker 1.11 起,dockerd 将容器运行时职责解耦给 containerd,自身更专注于上层业务逻辑。


二、源码结构解析(moby/moby)

Docker 引擎的核心代码位于 GitHub 仓库:https://github.com/moby/moby

关键目录结构如下:

moby/
├── cmd/dockerd/           # dockerd 主程序入口
├── daemon/                # 守护进程核心逻辑(容器、镜像、网络)
├── container/             # 容器数据结构定义
├── libcontainerd/         # 与 containerd 的 gRPC 客户端
├── api/                   # REST API 路由与处理
├── distribution/          # 镜像拉取与推送逻辑
├── graphdriver/           # 存储驱动(如 overlay2)
├── volume/                # 卷管理
└── runconfig/             # 容器运行配置解析

三、容器启动流程源码追踪

我们以 docker run -d nginx 命令为例,追踪从 CLI 到内核的完整流程。

1. CLI 发起请求

docker run -d nginx

CLI 解析命令后,通过 HTTP 请求发送至 dockerd 的 Unix Socket:POST /containers/create

2. API 层接收请求

路径:api/server/router/container/container_routes.go

func (r *containerRouter) initRoutes() {
    // ...
    r.post("/containers/create", r.postContainersCreate)
}

postContainersCreate 是实际处理函数,位于 daemon/create.go

3. 容器创建逻辑(daemon.Create())

路径:daemon/create.go

func (daemon *Daemon) create(b *builder.ContainerBuilder, params types.ContainerCreateConfig) (*container.Container, error) {
    // 1. 解析镜像配置(镜像层、环境变量、CMD等)
    img, err := daemon.imageService.ImageByName(params.Config.Image)
    
    // 2. 构建容器运行时配置
    container, err := daemon.newBaseContainer(params.Name)
    
    // 3. 分配资源:网络、挂载点、安全策略
    if err := daemon.createRootfs(container); err != nil { ... }
    
    // 4. 注册容器(内存中保存状态)
    if err := daemon.Register(container); err != nil { ... }

    return container, nil
}

其中 createRootfs() 会调用 graphdriver 层(如 overlay2) 来合并镜像层,形成容器可读写的文件系统。

4. 启动容器:调用 containerd

路径:daemon/start.go

func (daemon *Daemon) containerStart(ctx context.Context, container *container.Container, ...) error {
    // ...
    return daemon.startContainer(ctx, container)
}

func (daemon *Daemon) startContainer(ctx context.Context, container *container.Container) error {
    // 准备运行时 spec(OCI Spec)
    spec, err := daemon.createSpec(container)

    // 调用 containerd 创建并启动任务
    err = daemon.containerd.CreateTask(ctx, container.ID, spec, ...)
    if err != nil { ... }

    // 启动任务
    return daemon.containerd.Start(ctx, container.ID, ...)
}

这里通过 libcontainerd 模块,使用 gRPC 调用 containerdCreateTask 接口。


四、Containerd 与 runc 的协作

containerd 收到创建任务请求后,它会:

  1. 生成符合 OCI 规范的 config.json
  2. 调用 runc 命令或直接通过 libcontainer 库创建容器

runc 的核心是调用 Linux 系统调用:

// runc/libcontainer/process_linux.go
func (p *initProcess) start() error {
    cmd := exec.Command("runc.init")
    cmd.SysProcAttr = &syscall.SysProcAttr{
        Cloneflags: syscall.CLONE_NEWPID | 
                    syscall.CLONE_NEWNS |
                    syscall.CLONE_NEWUTS |
                    syscall.CLONE_NEWIPC |
                    syscall.CLONE_NEWUSER |
                    syscall.CLONE_NEWNET,
    }
    return cmd.Run()
}

这些 CLONE_NEW* 标志分别启用:

  • PID Namespace:隔离进程 ID
  • Mount Namespace:隔离文件系统挂载
  • UTS Namespace:隔离主机名
  • IPC Namespace:隔离进程通信
  • USER Namespace:隔离用户权限
  • NET Namespace:隔离网络栈

同时,cgroups 被用于限制 CPU、内存等资源。


五、关键机制深入

1. 存储驱动:Overlay2 如何工作?

Docker 默认使用 overlay2 驱动。其原理是利用 Linux 的联合文件系统(Union File System)。

路径:graphdriver/overlay2/overlay.go

func (d *Driver) CreateReadWrite(id, parent string, opts *applyOpts) error {
    // 创建 merged 目录:upper + lower = merged
    // upper: 容器可写层
    // lower: 只读镜像层(多层合并)
    // work: overlayfs 内部使用
    return d.mount(id)
}

每一层镜像对应一个只读层(lowerdir),容器自己的修改保存在 upperdir,通过 merged 目录对外呈现统一视图。

2. 网络模型:libnetwork

Docker 使用 libnetwork 实现多主机网络。默认桥接模式下:

  • 创建虚拟网桥 docker0
  • 为容器分配 veth pair:一端在宿主机,一端在容器 Net Namespace
  • 通过 iptables 设置 NAT 规则实现外网访问

源码位于 vendor/github.com/docker/libnetwork/


六、调试 Docker 源码的小技巧

  1. 编译调试版 dockerd

    make BIND_DIR=. shell
    go build -o dockerd ./cmd/dockerd
    ./dockerd -D --debug
    
  2. 查看 containerd 日志

    sudo journalctl -u containerd -f
    
  3. 进入容器命名空间调试

    nsenter -t $(docker inspect -f '{{.State.Pid}}' <container>) -n ip a
    
  4. 使用 delve 调试 Go 进程

    dlv --listen=:2345 --headless=true --api-version=2 exec ./dockerd
    

七、总结

通过本次源码分析,我们可以清晰地看到 Docker 引擎的工作流程:

  1. CLI → API → Daemon:命令层层传递
  2. Daemon → containerd → runc:职责解耦,运行时抽象
  3. runc → kernel:通过 Namespaces + Cgroups + UnionFS 实现容器隔离

Docker 的成功不仅在于其易用性,更在于其良好的架构设计:分层解耦、接口标准化(OCI)、依赖内核原生能力


延伸阅读


结语

掌握 Docker 底层原理,不仅能帮助我们更好地排查问题,也能为后续学习 Kubernetes、Serverless 等云原生技术打下坚实基础。希望这篇源码分析能为你打开一扇通往容器世界的大门。

如果想更扎实更系统的掌握Docker,欢迎学习我的**Go + AI 从0到1开发Docker引擎**,你收获的将不仅仅是offer~
https://coding.imooc.com/class/956.html

如果你觉得这篇文章有帮助,欢迎点赞、分享或留言讨论!

1人推荐
随时随地看视频
慕课网APP