Como funciona o processo de criação de um contêiner do docker (de docker run para runc)

A tradução do artigo foi preparada na véspera do início do curso "Plataforma de infraestrutura baseada em Kubernetes" .








Nos últimos meses, passei muito do meu tempo pessoal aprendendo como os contêineres Linux funcionam. Em particular, o que exatamente faz docker run. Neste artigo, vou resumir o que descobri e tentar mostrar como os elementos individuais formam um quadro geral. Começaremos nossa jornada criando um contêiner alpino usando o docker run:



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


Este contêiner será usado na saída abaixo. Quando o comando docker run é chamado, ele analisa os parâmetros passados ​​a ele na linha de comando e cria um objeto JSON para representar o objeto que o docker precisa criar. Este objeto é então enviado ao docker daemon pelo soquete de domínio UNIX /var/run/docker.sock. Para monitorar chamadas de API, podemos usar o utilitário 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


Aqui é onde a verdadeira diversão começa. Assim que o docker daemon receber a solicitação, ele analisará a saída e se comunicará com o containerd por meio da API gRPC para configurar o tempo de execução (ou tempo de execução) do contêiner usando os parâmetros passados ​​na linha de comando. Para observar essa interação, podemos usar o utilitário 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 o tempo de execução de um contêiner é uma tarefa bastante significativa. Os namespaces devem ser configurados, a imagem deve ser montada, os controles de segurança devem ser habilitados (perfis de proteção de aplicativos, perfis seccomp, recursos), etc., etc., etc. Você pode ter uma boa ideia tudo o que é necessário para configurar o tempo de execução, observando a saída docker inspect containeride o arquivo de especificações de tempo de execução config.json(mais sobre isso em instantes ).



A rigor, o containerd não cria um tempo de execução do contêiner. Ele configura o ambiente e, em seguida, chama containerd-shimpara executar o tempo de execução do contêiner por meio do tempo de execução OCI configurado (controlado pelo parâmetro "–runtime" do containerd). A maioria dos sistemas modernos executa o tempo de execução do contêiner com base no runc . Podemos observar isso usando o utilitário 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


Como o pstree remove o nome do processo, podemos verificar o PID com 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


Quando comecei a explorar as interações entre dockerd, containerd e shim , não entendi totalmente para que servia o shim . Felizmente, o Google resultou em excelentes textos de Michael Crosby . Shim serve a vários propósitos:



  1. Permite iniciar contêineres sem daemons.
  2. STDIO FD containerd docker.
  3. containerd .


O primeiro e o segundo pontos-chave são muito importantes. Esses recursos permitem que você desacople o contêiner do daemon do docker , permitindo que o dockerd seja atualizado ou reiniciado sem afetar os contêineres em execução. Muito efetivo! Mencionei que o shim é responsável por executar o runc para realmente iniciar o contêiner. O Runc precisa de duas coisas para fazer seu trabalho : o arquivo de especificação e o caminho para a imagem do sistema de arquivos raiz (uma combinação dos quais é chamada de pacote ). Para ver como isso funciona, podemos criar rootfs por exportar a janela de encaixe alpino imagem :



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


A opção de exportação aceita um contêiner, que você pode encontrar na saída docker ps -a. Para criar um arquivo de especificação, você pode usar o comando runc spec :



$ runc spec


Isso criará um arquivo de especificações nomeado config.jsonem seu diretório atual. Este arquivo pode ser personalizado de acordo com suas necessidades e requisitos. Quando estiver satisfeito com o arquivo, você pode executar runc com o diretório rootfs como o único argumento (a configuração do contêiner será lida do arquivo config.json):



$ runc run rootfs


Este exemplo simples criará um invólucro de cinza alpino:



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


A capacidade de criar contêineres e brincar com a especificação de tempo de execução runc é incrivelmente poderosa. Você pode avaliar diferentes perfis de aplicativo, testar recursos do Linux e experimentar todos os aspectos do tempo de execução do contêiner sem precisar instalar o docker. Eu apenas arranhei a superfície um pouco e recomendo a leitura da documentação do runc e do containerd . Ferramentas muito legais!






Saiba mais sobre o curso.







All Articles