从源码视角拆解Docker:InfoQ高赞文章没讲透的那些底层细节

为什么我们非要啃Docker源码?

💡 推荐阅读:Nginx反向代理配置参数全解:从入门到生产环境调优,看这一篇就够了

很多朋友在后台私信我,说看了InfoQ上不少关于Docker的分享,感觉云里雾里。说实话,InfoQ上的文章质量参差不齐,有些偏向于概念普及,有些则是纯广告软文。真正想搞懂Docker,绕不开的一条路就是直接读源码

笔者(站长)在早期学习容器技术时,也走过不少弯路。市面上99%的文章都在教你怎么用docker run,但很少有人告诉你,当你敲下这行命令时,操作系统底层究竟发生了什么。今天这篇文章,咱们就顺着Docker的调用链,从源码层面把那些InfoQ上没讲透的细节,掰开揉碎了讲清楚。

第一站:Docker CLI 到底是个什么玩意儿?

很多人以为Docker是一个单体程序,其实不然。Docker采用标准的C/S(客户端/服务器)架构。你平时在终端里敲的docker命令,只是客户端(CLI),它本身不干任何重活,只负责把你的指令翻译成RESTful API请求,然后发给Docker Daemon(守护进程)。

从源码目录来看,CLI部分的代码主要在cli/目录下,入口文件是cmd/docker/docker.go。这里使用了Cobra这个命令行框架来管理所有的子命令。

// 简化自 cmd/docker/docker.go
func main() {
    dockerCli, err := command.NewDockerCli()
    if err != nil {
        logrus.Fatal(err)
    }
    cmd := &cobra.Command{
        Use: "docker",
        // 长命令描述...
    }
    // 注册所有子命令,例如 run, ps, build 等
    commands.AddCommands(cmd, dockerCli)
    if err := cmd.Execute(); err != nil {
        logrus.Fatal(err)
    }
}

这里有个细节值得注意:Docker CLI 与 Daemon 的通信协议。默认情况下,它们通过本地的/var/run/docker.sock这个Unix Socket进行通信。如果你配置了远程访问,则会走TCP协议。这意味着,你完全可以在自己的笔记本上,通过设置DOCKER_HOST环境变量,去操作远程服务器上的Docker Daemon。

第二站:Daemon 的启动与模块化设计

接下来是重头戏——Docker Daemon。它的入口在cmd/dockerd/dockerd.go。启动过程非常复杂,涉及配置加载、API路由注册、容器运行时初始化等多个步骤。

在InfoQ的文章里,经常提到Docker的模块化。在源码中,这种模块化体现得淋漓尽致。Daemon 启动时会初始化以下几个关键模块:

  • containerd:负责管理容器的完整生命周期(创建、启动、停止、销毁)。
  • runc:真正干活的底层工具,负责与内核交互,创建隔离的进程。
  • networkd:负责容器网络模式的配置(Bridge、Host、Overlay等)。
  • volumes:负责数据卷的管理。

我们以dockerd启动时加载镜像存储驱动为例。Docker支持Overlay2、Aufs、DeviceMapper等多种存储驱动。源码中通过graphdriver.New()函数,根据你的系统配置和内核支持情况,自动选择最优的驱动。

// 简化自 daemon/graphdriver/driver.go
func New(name string, config Config) (Driver, error) {
    // 根据 name 选择对应的驱动初始化函数
    switch name {
    case "overlay2":
        return overlay2.Init(config)
    case "aufs":
        return aufs.Init(config)
    // ... 其他驱动
    }
}

这里建议大家在测试环境或者生产环境,尽量使用Overlay2驱动,它在性能和稳定性上表现最佳。如果你用的是CentOS 7,记得升级内核到3.10以上,否则只能退而求其次用古老的DeviceMapper。

第三站:深入 containerd 与 runc 的交互

很多初学者分不清Docker、containerd和runc三者的关系。简单来说,Docker是”包工头”,containerd是”项目经理”,runc是”一线工人”。

当我们执行docker run时,调用链是这样的:

  1. Docker CLI 发送请求给 Docker Daemon。
  2. Daemon 收到请求后,通过 gRPC 调用 containerd 的接口。
  3. containerd 接收到任务,从镜像仓库拉取镜像(如果本地没有),然后解压镜像到本地存储。
  4. containerd 通过 task.Create() 调用 runc。
  5. runc 利用 Linux 内核的 Namespace 和 Cgroups 技术,创建出隔离的进程。

在源码层面,containerd 与 runc 的交互是通过exit code管道来传递状态的。runc 启动容器后,会监听容器的状态变化,并通过文件锁和信号量通知 containerd。

这里有一个非常核心的概念叫Spec。runc 并不关心镜像层,它只认一个叫做 config.json 的文件。containerd 在调用 runc 之前,会将 Docker 镜像的配置(Entrypoint、Cmd、Env、Mounts等)转换为标准的 OCI(Open Container Initiative)规范,生成 config.json 文件。

// 这是 runc 启动容器时的核心调用逻辑(伪代码)
func startContainer(spec *specs.Spec) error {
    // 创建新的进程
    process, err := newProcess(spec.Process)
    // 设置 Namespace
    if err := applyNamespaces(process, spec.Linux.Namespaces); err != nil {
        return err
    }
    // 设置 Cgroups 资源限制
    if err := applyCgroups(process, spec.Linux.Resources); err != nil {
        return err
    }
    // 启动进程
    return process.Start()
}

看到这里,你应该明白了,所谓容器,其实就是一个被”特殊关照”的普通进程。它并没有虚拟机那样的硬件虚拟化层,而是直接共享宿主机内核。

第四站:镜像层的奥秘——联合文件系统

InfoQ上的文章经常提到Docker镜像的分层结构,但很少解释底层是如何实现的。这就要用到我们上文提到的存储驱动了。

以Overlay2为例,它利用Linux内核的OverlayFS文件系统,将多个目录层叠加在一起。Docker镜像的每一层都是一个只读目录,当你在容器内修改文件时,OverlayFS 会执行写时复制(Copy-on-Write)策略。

具体来说,当容器进程试图修改一个存在于底层只读层的文件时,OverlayFS 会先将该文件复制到最顶层的可写层(容器层),然后再进行修改。这样,底层只读层始终保持不变,多个容器可以安全地共享同一个镜像层,极大地节省了磁盘空间。

在源码中,docker image pull 下载的每一层,都会通过docker save导出的tar包格式存储。如果你好奇,可以手动执行docker history ubuntu:latest,看看这个镜像有多少层。

存储驱动 底层技术 适用场景 性能表现
Overlay2 OverlayFS 生产环境首选
fuse-overlayfs FUSE 无特权用户(Rootless)
VFS 测试环境 极低

第五站:动手实践——从源码编译 Docker

光看不练假把式。为了加深理解,站长建议你有机会一定要自己动手编译一次Docker源码。这并不难,但需要一台配置尚可的Linux服务器。

这里顺便提一句,如果你打算深入学习或者跑一些比较吃资源的编译任务,建议不要用本地笔记本,太吃力了。可以搞一台靠谱的云服务器,比如站长的机器,性价比高,网络稳定,编译起来心情也舒畅。毕竟,工欲善其事,必先利其器。

以下是编译步骤:

环境准备

# 安装 Go 语言环境(版本需 >= 1.20)
wget https://go.dev/dl/go1.21.5.linux-amd64.tar.gz
tar -C /usr/local -xzf go1.21.5.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin

# 克隆 Docker 源码(注意切换到稳定分支)
git clone https://github.com/moby/moby.git
cd moby
git checkout v24.0.7

执行编译

# 使用 Docker 官方提供的构建脚本
# 注意:这里会启动一个容器来编译,需要确保 Docker 已安装
make build

# 编译完成后,二进制文件在 bundles/ 目录下
ls bundles/dynbinary-daemon/dockerd

编译过程中,你会看到大量的Go包下载和编译输出。如果网络不好,可能会失败。此时可以配置一下Go的代理:

export GOPROXY=https://goproxy.cn,direct

编译成功后,你可以停掉系统的Docker服务,用自己编译的dockerd启动试试,那种掌控感是看多少篇文章都体会不到的。

写在最后的思考

读源码是一个枯燥但极其有成就感的过程。Docker的源码量巨大,不要指望一口气全部读完。建议从调用链入手,顺着docker run这条线,逐步深入。你会发现,一旦打通了任督二脉,再看K8s、containerd等其他项目,都会轻松很多。

好了,今天的源码分析就到这里。如果你在阅读过程中遇到什么卡壳的地方,欢迎在评论区留言,我们一起探讨。记住,纸上得来终觉浅,绝知此事要躬行。

延伸阅读

💡 【高性价比独享云服务器限时 1 折起】

新手搭建网站或部署容器,推荐选择高性价比独享云服务器。点击下方通道可享受限时折扣与专属优惠券:

👉 点此前往领取云服务器限时优惠券

🎁 全套实操配置文件与 AI 提效资料包免费下载

本文涉及的全套 Docker Compose 配置文件、服务器运维避坑手册及 AI 提效指令库已完整打包上传至夸克网盘,可直接免费极速转存:

👉 点击前往夸克网盘免费极速转存获取

💡 提示:推荐使用手机【夸克网盘 App】打开保存,新用户首月免费赠送 1TB 超大空间与免流量极速下载特权。

滚动至顶部