从 docker 到 runC

笔者在前文《RunC 简介》和《Containerd 简介》中分别介绍了 runC 和 containerd。本文咱们将结合 docker 中的其它组件探索 docker 是如何把这些组件组织起来协调工做的。html

Docker 的主要组件

安装 docker ,实际上是安装了 docker 客户端、dockerd 等一系列的组件,其中比较重要的有下面几个。docker

Docker CLI(docker)
docker 程序是一个客户端工具,用来把用户的请求发送给 docker daemon(dockerd)。该程序的安装路径为:ubuntu

/usr/bin/docker

Dockerd
docker daemon(dockerd),通常也会被称为 docker engine。该程序的安装路径为:segmentfault

/usr/bin/dockerd

Containerd
详情请参考《Containerd 简介》。该程序的安装路径为:api

/usr/bin/docker-containerd

Containerd-shim
它是 containerd 的组件,是容器的运行时载体,咱们在 docker 宿主机上看到的 shim 也正是表明着一个个经过调用 containerd 启动的 docker 容器。该程序的安装路径为:bash

/usr/bin/docker-containerd-shim

RunC
详情请参考《RunC 简介》。该程序的安装路径为:服务器

/usr/bin/docker-runc

从 hello world 开始

Docker 很贴心的为咱们提供了 hello-world 镜像来验证安装是否成功,可是透过这个镜像咱们还能看到更多的信息:架构

$ docker run hello-world

上面的输出信息指出,hello-world 容器的运行经历了以下四步:curl

  1. Docker 客户端向 docker daemon 发送请求
  2. Docker daemon 从 Docker Hub 上拉取镜像
  3. Docker daemon 使用镜像运行了一个容器并产生了输出
  4. Docker daemon 把输出的内容发送给了 docker 客户端

这是一个很抽象也很容器理解的过程,可是咱们还想知道更多:docker daemon 是如何建立并运行容器的?
其实容器部分的操做和管理都被 dockerd 外包给 containerd 了,下图描述了运行一个容器时各个组件之间的关系:tcp

Docker Engine API

从本质上说,docker 是一个客户端/服务器架构的应用。Dockerd 以 Engine API (REST)的方式对外提供服务,Engine API 里描述了 dockerd 支持的全部请求。Docker 客户端与 dockerd 之间就是经过 REST 的方式通讯的。在 ubuntu 16.04 中,dockerd 默认是不监听 tcp 端口的,为了方便演示,咱们让 dockerd 监听 tcp 端口。这样就可使用 curl 代替 docker 客户端向 dockerd 发送请求了。具体的操做为,先修改 /lib/systemd/system/docker.service 文件,注释掉默认的 ExecStart 并添加新的 ExecStart 配置:

# ExecStart=/usr/bin/dockerd -H fd://
ExecStart=/usr/bin/dockerd -H tcp://0.0.0.0:2375 -H unix:///var/run/docker.sock

而后重启 docker.service:

$ sudo systemctl daemon-reload
$ sudo systemctl restart docker.service

这样 dockerd 就开始监听 tcp 端口 2375 了:

Docker 与 Dockerd 的交互

Docker 客户端与 dockerd 之间就是经过 REST 的方式通讯的。前面咱们已经让 dockerd 监听 tcp 端口了,因此咱们可使用 curl 来代替 docker 客户端。这里咱们简单的演示如何请求 dockerd 从 docker hub 上下载 hello-world 镜像:

$ curl '127.0.0.1:2375/v1.37/images/create?fromImage=hello-world&tag=latest' -X POST

若是去看看 Engine API,你会发现其它的请求也都是用相似方式发送的,是否是很简单啊!

建立容器

容器镜像的下载是由 dockerd 完成的,但容器的建立和运行就须要 containerd(docker-containerd) 来完成了。Dockerd 与 docker-containerd 之间是经过 grpc 协议通讯的。当 docker-containerd 收到 dockerd 启动容器的请求以后,会作一些初始化工做,而后启动 docker-containerd-shim 进程,并将相关配置做为参数传给它。docker-containerd 负责管理全部本机正在运行的容器,而一个 docker-containerd-shim 进程只负责管理一个运行的容器,它至关于 docker-runc 的一个封装,充当 docker-containerd 和 docker-runc 之间的桥梁,docker-runc 能干的就交给 docker-runc 来作,docker-runc 作不了的就放到这里来作。下面咱们用 ubuntu 镜像运行一个容器:

$ docker run -id ubuntu bash

上图中黄线框起来的是几个主要的进程,它们之间是有父子关系的(systemd 没有出如今上图):

systemd---dockerd---docker-containerd---docker-containerd-shim---bash

细心的朋友必定发现了,上图中没有出现 docker-runc 进程,这是为何呢?
实际上,在容器启动的过程当中,docker-runc 进程是做为 docker-containerd-shim 的子进程存在的。docker-runc 进程根据配置找到容器的 rootfs 并建立子进程 bash 做为容器中的第一个进程。当这一切都完成后 docker-runc 进程退出,而后容器进程 bash 由 docker-runc 的父进程 docker-containerd-shim 接管。

为何须要 docker-containerd-shim?

也许你们会问,为何在容器的启动或运行过程当中须要一个 docker-containerd-shim 进程呢?把它移除掉整个架构会更简洁也更优美一些!事实上 docker-containerd-shim 的存在是很是有必要的,其目的有以下几点:

  • 它容许容器运行时(即 runC)在启动容器以后退出,简单说就是没必要为每一个容器一直运行一个容器运行时(runC)
  • 即便在 containerd 和 dockerd 都挂掉的状况下,容器的标准 IO 和其它的文件描述符也都是可用的
  • 向 containerd 报告容器的退出状态

前两点尤为重要,有了它们就能够在不中断容器运行的状况下升级或重启 dockerd(这对于生产环境来讲意义重大)。 从这里能够看到对 containerd-shim 的一些解释。

总结

笔者先在前文《RunC 简介》和《Containerd 简介》中分别介绍了 runC 和 containerd 等 docker 的核心组件。本文则经过 demo 演示了在建立、运行容器的过程当中这些组件如何配合 docker engine 完成相关的任务,以及相关进程之间的关系和做用。但愿本文能够帮助你们理解 docker 的总体架构及其组件间的协做方式。

参考:
How the docker container creation process works
走进docker:hello-world的背后发生了什么?

相关文章
相关标签/搜索