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 调用 containerd
的 CreateTask
接口。
四、Containerd 与 runc 的协作
当 containerd
收到创建任务请求后,它会:
- 生成符合 OCI 规范的
config.json
- 调用
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 源码的小技巧
-
编译调试版 dockerd:
make BIND_DIR=. shell go build -o dockerd ./cmd/dockerd ./dockerd -D --debug
-
查看 containerd 日志:
sudo journalctl -u containerd -f
-
进入容器命名空间调试:
nsenter -t $(docker inspect -f '{{.State.Pid}}' <container>) -n ip a
-
使用 delve 调试 Go 进程:
dlv --listen=:2345 --headless=true --api-version=2 exec ./dockerd
七、总结
通过本次源码分析,我们可以清晰地看到 Docker 引擎的工作流程:
- CLI → API → Daemon:命令层层传递
- Daemon → containerd → runc:职责解耦,运行时抽象
- runc → kernel:通过 Namespaces + Cgroups + UnionFS 实现容器隔离
Docker 的成功不仅在于其易用性,更在于其良好的架构设计:分层解耦、接口标准化(OCI)、依赖内核原生能力。
延伸阅读
结语
掌握 Docker 底层原理,不仅能帮助我们更好地排查问题,也能为后续学习 Kubernetes、Serverless 等云原生技术打下坚实基础。希望这篇源码分析能为你打开一扇通往容器世界的大门。
如果想更扎实更系统的掌握Docker,欢迎学习我的**Go + AI 从0到1开发Docker引擎**,你收获的将不仅仅是offer~
https://coding.imooc.com/class/956.html
如果你觉得这篇文章有帮助,欢迎点赞、分享或留言讨论!
共同学习,写下你的评论
评论加载中...
作者其他优质文章