За последние несколько месяцев я потратил немало личного времени на изучение работы Linux-контейнеров. В частности, что конкретно делает
docker run
. В этой статье я собираюсь
резюмировать то, что я выяснил, и попытаюсь показать как отдельные
элементы формируют цельную картину. Начнем мы наше путешествие с
создания контейнера alpine с помощью docker run:
$ docker run -i -t --name alpine alpine ash
Этот контейнер будет использоваться в выводе ниже. Когда вызывается команда docker run, она анализирует параметры, переданные ей в командной строке, и создает JSON объект для представления объекта, который нужно создать docker. Затем этот объект отправляется демону docker через сокет домена UNIX /var/run/docker.sock. Для наблюдения за вызовами 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
Вот тут уже начинается настоящее веселье. Как только демон docker получит запрос, он проанализирует вывод и свяжется с containerd через gRPC API для настройки рантайма (или среды выполнения) контейнера с использованием параметров, переданных в командной строке. Для наблюдения за этим взаимодействием мы можем использовать утилиту ctr:
$ ctr --address "unix:///run/containerd.sock" events
TIME TYPE ID PID STATUStime="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 0time="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
(подробнее об этом чуть позже).Строго говоря, containerd не создает рантайм контейнера. Он настраивает среду, а затем вызывает containerd-shim для запуска рантайма контейнера через настроенный рантайм OCI (управляемый параметром containerd runtime). У большинства современных систем среда выполнения контейнера работает на основе runc. Мы можем наблюдать это с помощью утилиты pstree:
$ pstree -l -p -s -Tsystemd,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 отсекает имя процесса, мы можем проверить PID с помощью 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
Когда я впервые начал исследовать взаимодействие между dockerd, containerd и shim, я не до конца понимал, для какой цели служил shim. К счастью Google привел к отличному изложению Майкла Кросби. Shim служит для нескольких целей:
- Позволяет запускать контейнеры без демонов.
- STDIO и остальные FD остаются открытыми в случае смерти containerd и docker.
- Сообщает в containerd о состоянии выхода контейнеров.
Первый и второй ключевые моменты очень важны. Эти функции позволяют отделить контейнер от демона docker, позволяя обновлять или перезапускать dockerd без влияния на работающие контейнеры. Весьма эффективно! Я упомянул, что shim отвечает за запуск runc для фактического запуска контейнера. Для выполнения своей работы runc необходимы две вещи: файл спецификации и путь к образу корневой файловой системы (их комбинация называется бандлом). Чтобы увидеть, как это работает, мы можем создать rootfs, экспортировав образ alpine docker:
$ 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" .dockerenvbin/bin/ashbin/base64bin/bbconfig.....
Опция экспорта принимает контейнер, который вы можете найти в выводе
docker ps -a
. Для создания файла спецификации
вы можете использовать команду спецификации runc:
$ runc spec
Это создаст файл спецификации с именем
config.json
в
вашем текущем каталоге. Этот файл может быть настроен в
соответствии с вашими потребностями и требованиями. После того, как
вы удовлетворены файлом, вы можете запустить runc с каталогом
rootfs в качестве единственного аргумента (конфигурация контейнера
будет считана из файла config.json
):
$ runc run rootfs
Этот простой пример создаст оболочку alpine ash:
$ runc run rootfs
/ # cat /etc/os-releaseNAME="Alpine Linux"ID=alpineVERSION_ID=3.7.0PRETTY_NAME="Alpine Linux v3.7"HOME_URL="http://personeltest.ru/away/alpinelinux.org"BUG_REPORT_URL="http://personeltest.ru/away/bugs.alpinelinux.org"
Возможность создавать контейнеры и играть с runc спецификацией среды выполнения невероятно мощна. Вы можете оценить различные профили приложений, протестировать возможности Linux и поэкспериментировать с каждым аспектом среды выполнения контейнера без необходимости устанавливать докер. Я только слегка коснулся поверхности и очень рекомендую почитать документацию runc и containerd. Очень крутые инструменты!
Узнать подробнее о курсе.