创建Docker容器的过程如何工作(从docker run到runc)

本文的翻译是在课程“基于Kubernetes的基础架构平台”的开始时准备的








在过去的几个月中,我花了很多个人时间来学习Linux容器的工作方式。特别是,它到底是做什么的docker run在本文中,我将总结发现的内容,并尝试说明各个元素如何构成一幅大图。我们将通过使用docker run创建一个高山容器开始我们的旅程:



$ docker run -i -t --name alpine alpine ash


该容器将在下面的输出中使用。调用docker run命令时,它将解析在命令行中传递给它的参数,并创建一个JSON对象来表示docker需要创建的对象。然后,该对象通过UNIX域套接字/var/run/docker.sock发送到docker守护程序。要监视API调用,我们可以使用strace实用程序



$ strace -s 8192 -e trace=read,write -f docker run -d alpine


[pid 13446] write(3, "GET /_ping HTTP/1.1\r\nHost: docker\r\nUser-Agent: Docker-Client/1.13.1 (linux)\r\n\r\n", 79) = 79
[pid 13442] read(3, "HTTP/1.1 200 OK\r\nApi-Version: 1.26\r\nDocker-Experimental: false\r\nServer: Docker/1.13.1 (linux)\r\nDate: Mon, 19 Feb 2018 16:12:32 GMT\r\nContent-Length: 2\r\nContent-Type: text/plain; charset=utf-8\r\n\r\nOK", 4096) = 196
[pid 13442] write(3, "POST /v1.26/containers/create HTTP/1.1\r\nHost: docker\r\nUser-Agent: Docker-Client/1.13.1 (linux)\r\nContent-Length: 1404\r\nContent-Type: application/json\r\n\r\n{\"Hostname\":\"\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[],\"Cmd\":null,\"Image\":\"alpine\",\"Volumes\":{},\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":{},\"HostConfig\":{\"Binds\":null,\"ContainerIDFile\":\"\",\"LogConfig\":{\"Type\":\"\",\"Config\":{}},\"NetworkMode\":\"default\",\"PortBindings\":{},\"RestartPolicy\":{\"Name\":\"no\",\"MaximumRetryCount\":0},\"AutoRemove\":false,\"VolumeDriver\":\"\",\"VolumesFrom\":null,\"CapAdd\":null,\"CapDrop\":null,\"Dns\":[],\"DnsOptions\":[],\"DnsSearch\":[],\"ExtraHosts\":null,\"GroupAdd\":null,\"IpcMode\":\"\",\"Cgroup\":\"\",\"Links\":null,\"OomScoreAdj\":0,\"PidMode\":\"\",\"Privileged\":false,\"PublishAllPorts\":false,\"ReadonlyRootfs\":false,\"SecurityOpt\":null,\"UTSMode\":\"\",\"UsernsMode\":\"\",\"ShmSize\":0,\"ConsoleSize\":[0,0],\"Isolation\":\"\",\"CpuShares\":0,\"Memory\":0,\"NanoCpus\":0,\"CgroupParent\":\"\",\"BlkioWeight\":0,\"BlkioWeightDevice\":null,\"BlkioDeviceReadBps\":null,\"BlkioDeviceWriteBps\":null,\"BlkioDeviceReadIOps\":null,\"BlkioDeviceWriteIOps\":null,\"CpuPeriod\":0,\"CpuQuota\":0,\"CpuRealtimePeriod\":0,\"CpuRealtimeRuntime\":0,\"CpusetCpus\":\"\",\"CpusetMems\":\"\",\"Devices\":[],\"DiskQuota\":0,\"KernelMemory\":0,\"MemoryReservation\":0,\"MemorySwap\":0,\"MemorySwappiness\":-1,\"OomKillDisable\":false,\"PidsLimit\":0,\"Ulimits\":null,\"CpuCount\":0,\"CpuPercent\":0,\"IOMaximumIOps\":0,\"IOMaximumBandwidth\":0},\"NetworkingConfig\":{\"EndpointsConfig\":{}}}\n", 1556) = 1556
[pid 13442] read(3, "HTTP/1.1 201 Created\r\nApi-Version: 1.26\r\nContent-Type: application/json\r\nDocker-Experimental: false\r\nServer: Docker/1.13.1 (linux)\r\nDate: Mon, 19 Feb 2018 16:12:32 GMT\r\nContent-Length: 90\r\n\r\n{\"Id\":\"b70b57c5ae3e25585edba898ac860e388582391907be4070f91eb49f4db5c433\",\"Warnings\":null}\n", 4096) = 281


这才是真正的乐趣开始的地方。泊坞窗守护程序接收到请求后,将解析输出并通过gRPC API容器通信以使用在命令行中传递的参数来配置容器的运行时(或多个运行时)。为了观察这种相互作用,我们可以使用ctr实用程序:



$ ctr --address "unix:///run/containerd.sock" events


TIME                           TYPE                           ID                             PID                            STATUS
time="2018-02-19T12:10:07.658081859-05:00" level=debug msg="Calling POST /v1.26/containers/create" 
time="2018-02-19T12:10:07.676706130-05:00" level=debug msg="container mounted via layerStore: /var/lib/docker/overlay2/2beda8ac904f4a2531d72e1e3910babf145c6e68dfd02008c58786adb254f9dc/merged" 
time="2018-02-19T12:10:07.682430843-05:00" level=debug msg="Calling POST /v1.26/containers/d1a6d87886e2d515bfff37d826eeb671502fa7c6f47e422ec3b3549ecacbc15f/attach?stderr=1&stdin=1&stdout=1&stream=1" 
time="2018-02-19T12:10:07.683638676-05:00" level=debug msg="Calling GET /v1.26/events?filters=%7B%22container%22%3A%7B%22d1a6d87886e2d515bfff37d826eeb671502fa7c6f47e422ec3b3549ecacbc15f%22%3Atrue%7D%2C%22type%22%3A%7B%22container%22%3Atrue%7D%7D" 
time="2018-02-19T12:10:07.684447919-05:00" level=debug msg="Calling POST /v1.26/containers/d1a6d87886e2d515bfff37d826eeb671502fa7c6f47e422ec3b3549ecacbc15f/start" 
time="2018-02-19T12:10:07.687230717-05:00" level=debug msg="container mounted via layerStore: /var/lib/docker/overlay2/2beda8ac904f4a2531d72e1e3910babf145c6e68dfd02008c58786adb254f9dc/merged" 
time="2018-02-19T12:10:07.885362059-05:00" level=debug msg="sandbox set key processing took 11.824662ms for container d1a6d87886e2d515bfff37d826eeb671502fa7c6f47e422ec3b3549ecacbc15f" 
time="2018-02-19T12:10:07.927897701-05:00" level=debug msg="libcontainerd: received containerd event: &types.Event{Type:\"start-container\", Id:\"d1a6d87886e2d515bfff37d826eeb671502fa7c6f47e422ec3b3549ecacbc15f\", Status:0x0, Pid:\"\", Timestamp:(*timestamp.Timestamp)(0xc420bacdd0)}" 
2018-02-19T17:10:07.927795344Z start-container                d1a6d87886e2d515bfff37d826eeb671502fa7c6f47e422ec3b3549ecacbc15f                                0
time="2018-02-19T12:10:07.930283397-05:00" level=debug msg="libcontainerd: event unhandled: type:\"start-container\" id:\"d1a6d87886e2d515bfff37d826eeb671502fa7c6f47e422ec3b3549ecacbc15f\" timestamp:<seconds:1519060207 nanos:927795344 > " 
time="2018-02-19T12:10:07.930874606-05:00" level=debug msg="Calling POST /v1.26/containers/d1a6d87886e2d515bfff37d826eeb671502fa7c6f47e422ec3b3549ecacbc15f/resize?h=35&w=115" 


配置容器的运行时是一项相当重要的任务。必须配置名称空间,必须挂载映像,必须启用安全控件(应用程序保护配置文件,seccomp配置文件,功能)等,等等。您可以得到一个很好的主意通过查看输出docker inspect containerid和运行时规范文件来设置运行时所需的所有信息config.json稍后会详细介绍)。



严格来说,containered不会创建容器运行时。它设置环境,然后调用容器填充垫片通过配置的OCI运行时运行容器运行时(由容器的“ –runtime”参数控制)。大多数现代系统都基于runc运行容器运行时我们可以使用pstree实用程序观察到这一点



$ pstree -l -p -s -T
systemd,1 --switched-root --system --deserialize 24
  ├─docker-containe,19606 --listen unix:///run/containerd.sock --shim /usr/libexec/docker/docker-containerd-shim-current --start-timeout 2m --debug
  │   ├─docker-containe,19834 93a619715426f613646359863e77cc06fa85502273df931517ec3f4aaae50d5a /var/run/docker/libcontainerd/93a619715426f613646359863e77cc06fa85502273df931517ec3f4aaae50d5a /usr/libexec/docker/docker-runc-current


由于pstree剥离了进程名称,因此我们可以使用ps检查PID



$ ps auxwww | grep [1]9606


root     19606  0.0  0.2 685636 10632 ?        Ssl  13:01   0:00 /usr/libexec/docker/docker-containerd-current --listen unix:///run/containerd.sock --shim /usr/libexec/docker/docker-containerd-shim-current --start-timeout 2m --debug


$ ps auxwww | grep [1]9834


root     19834  0.0  0.0 527748  3020 ?        Sl   13:01   0:00 /usr/libexec/docker/docker-containerd-shim-current 93a619715426f613646359863e77cc06fa85502273df931517ec3f4aaae50d5a /var/run/docker/libcontainerd/93a619715426f613646359863e77cc06fa85502273df931517ec3f4aaae50d5a /usr/libexec/docker/docker-runc-current


当我第一次探索dockerd,containered和shim之间的相互作用时,我并不完全了解shim的用途幸运的是,Google促成 Michael Crosby的出色写作Shim有几个用途:



  1. 允许启动没有守护程序的容器。
  2. STDIO FD containerd docker.
  3. containerd .


第一个和第二个关键点非常重要。这些功能允许您从分离容器泊坞窗守护进程,允许dockerd要被更新或重启不影响运行的容器。非常有效的!我提到过shim负责运行runc以实际启动容器。Runc需要完成两项工作:spec文件和根文件系统映像的路径(这些的组合称为bundle)。要了解其工作原理,我们可以通过导出高山码头工人镜像来创建rootfs



$ mkdir -p alpine/rootfs


$ cd alpine


$ docker export d1a6d87886e2 | tar -C rootfs -xvf -


time="2018-02-19T12:54:13.082321231-05:00" level=debug msg="Calling GET /v1.26/containers/d1a6d87886e2/export" 
.dockerenv
bin/
bin/ash
bin/base64
bin/bbconfig
.....


导出选项接受一个容器,您可以在输出中找到它docker ps -a要创建一个规范文件,可以使用runc spec命令



$ runc spec


这将config.json在您的当前目录中创建一个规范文件该文件可以根据您的需求和要求进行定制。对文件满意后,可以使用rootfs目录作为唯一参数运行runc(将从文件中读取容器配置config.json):



$ runc run rootfs


这个简单的示例将创建一个高山灰包装器:



$ runc run rootfs


/ # cat /etc/os-release
NAME="Alpine Linux"
ID=alpine
VERSION_ID=3.7.0
PRETTY_NAME="Alpine Linux v3.7"
HOME_URL="http://alpinelinux.org"
BUG_REPORT_URL="http://bugs.alpinelinux.org"


创建容器并使用runc运行时 规范的功能非常强大。您可以评估不同的应用程序配置文件,测试Linux功能以及对容器运行时的各个方面进行试验,而无需安装docker。我只是稍微了一下,强烈建议阅读runc容器化的文档很酷的工具!






了解有关该课程的更多信息。







All Articles