Cómo funciona el proceso de creación de un contenedor de Docker (de Docker Run a Runc)

La traducción del artículo se preparó en vísperas del inicio del curso "Plataforma de infraestructura basada en Kubernetes" .








Durante los últimos meses, he pasado gran parte de mi tiempo personal aprendiendo cómo funcionan los contenedores de Linux. En particular, qué hace exactamente docker run. En este artículo, resumiré lo que descubrí y trataré de mostrar cómo los elementos individuales forman un panorama general. Comenzaremos nuestro viaje creando un contenedor alpino usando la ventana acoplable:



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


Este contenedor se utilizará en el resultado a continuación. Cuando se llama al comando docker run, analiza los parámetros que se le pasan en la línea de comando y crea un objeto JSON para representar el objeto que el docker necesita crear. Este objeto luego se envía al demonio de la ventana acoplable a través del socket de dominio UNIX /var/run/docker.sock. Para monitorear las llamadas a la API, podemos usar la utilidad 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


Aquí es donde comienza la verdadera diversión. Tan pronto como el daemon docker reciba la solicitud, analizará la salida y se comunicará con containerd a través de la API de gRPC para configurar el tiempo de ejecución (o tiempo de ejecución) del contenedor utilizando los parámetros pasados ​​en la línea de comando. Para observar esta interacción, podemos usar la utilidad 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" 


Configurar el tiempo de ejecución de un contenedor es una tarea bastante importante. Se deben configurar los espacios de nombres, se debe montar la imagen, se deben habilitar los controles de seguridad (perfiles de protección de aplicaciones, perfiles seccomp, capacidades), etc., etc., etc. Puede hacerse una idea bastante buena todo lo que necesita para configurar el tiempo de ejecución mirando la salida docker inspect containeridy el archivo de especificaciones del tiempo de ejecución config.json(más sobre eso en un momento ).



Estrictamente hablando, containerd no crea un tiempo de ejecución de contenedor. Configura el entorno y luego llama a containerd-shimpara ejecutar el tiempo de ejecución del contenedor a través del tiempo de ejecución de OCI configurado (controlado por el parámetro containerd "–runtime"). La mayoría de los sistemas modernos ejecutan el tiempo de ejecución del contenedor en función de runc . Podemos observar esto usando la utilidad 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


Dado que pstree elimina el nombre del proceso, podemos verificar el PID con ps :



$ 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


Cuando comencé a explorar las interacciones entre dockerd, containerd y shim , no entendía completamente para qué era shim . Afortunadamente, Google llevó a una excelente redacción de Michael Crosby . Shim sirve para varios propósitos:



  1. Permite iniciar contenedores sin demonios.
  2. STDIO FD containerd docker.
  3. containerd .


Los puntos clave primero y segundo son muy importantes. Estas características le permiten desacoplar el contenedor del demonio de la ventana acoplable , lo que permite que dockerd se actualice o reinicie sin afectar los contenedores en ejecución. ¡Muy efectivo! Mencioné que shim es responsable de ejecutar runc para iniciar realmente el contenedor. Runc necesita dos cosas para hacer su trabajo : el archivo de especificaciones y la ruta a la imagen del sistema de archivos raíz (una combinación de las cuales se llama paquete ). Para ver cómo funciona esto, podemos crear rootfs por exportar el alpino ventana acoplable imagen :



$ 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
.....


La opción de exportación acepta un contenedor, que puede encontrar en la salida docker ps -a. Puede utilizar el comando runc spec para crear el archivo de especificaciones :



$ runc spec


Esto creará un archivo de especificaciones llamado config.jsonen su directorio actual. Este archivo se puede personalizar según sus necesidades y requisitos. Una vez que esté satisfecho con el archivo, puede ejecutar runc con el directorio rootfs como único argumento (la configuración del contenedor se leerá desde el archivo config.json):



$ runc run rootfs


Este simple ejemplo creará una envoltura de ceniza alpina:



$ 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"


La capacidad de crear contenedores y jugar con la especificación de tiempo de ejecución de runc es increíblemente poderosa. Puede evaluar diferentes perfiles de aplicaciones, probar las capacidades de Linux y experimentar con todos los aspectos del tiempo de ejecución del contenedor sin tener que instalar la ventana acoplable. Solo he arañado un poco la superficie y recomendaría encarecidamente leer la documentación de runc y containerd . ¡Herramientas geniales!






Obtenga más información sobre el curso.







All Articles