Funktionsweise des Erstellens eines Docker-Containers (von Docker-Lauf zu Runc)

Die Übersetzung des Artikels wurde am Vorabend des Kurses "Infrastrukturplattform basierend auf Kubernetes" vorbereitet .








In den letzten Monaten habe ich viel Zeit damit verbracht, zu lernen, wie Linux-Container funktionieren. Insbesondere, was genau macht es docker run. In diesem Artikel werde ich zusammenfassen, was ich herausgefunden habe, und versuchen zu zeigen, wie die einzelnen Elemente ein Gesamtbild bilden. Wir beginnen unsere Reise mit der Erstellung eines alpinen Containers mit dem Docker-Lauf:



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


Dieser Container wird in der folgenden Ausgabe verwendet. Wenn der Docker-Ausführungsbefehl aufgerufen wird, analysiert er die in der Befehlszeile an ihn übergebenen Parameter und erstellt ein JSON-Objekt, das das Objekt darstellt, das der Docker erstellen muss. Dieses Objekt wird dann über den UNIX-Domänensocket /var/run/docker.sock an den Docker-Daemon gesendet. Um API-Aufrufe zu überwachen , können wir das Dienstprogramm strace verwenden :



$ 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


Hier beginnt der wahre Spaß. Sobald der Docker-Dämon die Anforderung empfängt, analysiert er die Ausgabe und kommuniziert mit Containerd über die gRPC-API , um die Laufzeit (oder Laufzeit) des Containers mithilfe der in der Befehlszeile übergebenen Parameter zu konfigurieren. Um diese Interaktion zu beobachten, können wir das Dienstprogramm ctr verwenden:



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


Das Konfigurieren der Laufzeit eines Containers ist eine ziemlich wichtige Aufgabe. Namespaces müssen konfiguriert sein, das Image muss gemountet sein, Sicherheitskontrollen müssen aktiviert sein (Anwendungsschutzprofile, Seccomp-Profile, Funktionen) usw. usw. usw. Sie können sich eine ziemlich gute Vorstellung machen Alles, was zum Einrichten der Laufzeit erforderlich ist, indem Sie sich die Ausgabe docker inspect containeridund die Laufzeitspezifikationsdatei ansehen config.json(mehr dazu gleich).



Genau genommen erstellt Containerd keine Container-Laufzeit. Es richtet die Umgebung ein und ruft dann Containerd-Shim aufum die Container-Laufzeit über die konfigurierte OCI-Laufzeit auszuführen (gesteuert durch den enthaltenen Parameter "–runtime"). Die meisten modernen Systeme führen die Container-Laufzeit basierend auf runc aus . Wir können dies mit dem Dienstprogramm pstree beobachten :



$ 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


Da pstree den Prozessnamen entfernt, können wir die PID mit ps überprüfen :



$ 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


Als ich anfing, die Wechselwirkungen zwischen Dockerd, Containerd und Shim zu untersuchen , verstand ich nicht ganz, wofür Shim gedacht war . Glücklicherweise führte Google zu einem hervorragenden Schreiben von Michael Crosby . Shim dient mehreren Zwecken:



  1. Ermöglicht das Starten von Containern ohne Dämonen.
  2. STDIO FD containerd docker.
  3. containerd .


Der erste und der zweite wichtige Punkt sind sehr wichtig. Mit diesen Funktionen können Sie den Container vom Docker- Daemon entkoppeln , sodass Dockerd aktualisiert oder neu gestartet werden kann, ohne die laufenden Container zu beeinträchtigen. Sehr effektiv! Ich erwähnte, dass Shim für das Ausführen des Runc verantwortlich ist , um den Container tatsächlich zu starten. Runc benötigt zwei Dinge , um seine Arbeit zu erledigen : die Spezifikationsdatei und den Pfad zum Root-Dateisystem-Image (eine Kombination davon wird als Bundle bezeichnet ). Um zu sehen , wie das funktioniert, können wir schaffen rootfs durch den Export der Alpen Docker Bild :



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


Die Exportoption akzeptiert einen Container, den Sie in der Ausgabe finden docker ps -a. Um eine Spezifikationsdatei zu erstellen, können Sie den Befehl runc spec verwenden :



$ runc spec


Dadurch wird eine Spezifikationsdatei mit dem Namen config.jsonin Ihrem aktuellen Verzeichnis erstellt. Diese Datei kann an Ihre Bedürfnisse und Anforderungen angepasst werden. Sobald Sie mit der Datei zufrieden sind, können Sie runc mit dem Verzeichnis rootfs als einzigem Argument ausführen (die Containerkonfiguration wird aus der Datei gelesen config.json):



$ runc run rootfs


In diesem einfachen Beispiel wird eine alpine Ascheverpackung erstellt:



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


Die Möglichkeit, Container zu erstellen und mit der Laufzeitspezifikation runc zu spielen, ist unglaublich leistungsfähig. Sie können verschiedene Anwendungsprofile auswerten, Linux-Funktionen testen und mit jedem Aspekt der Container-Laufzeit experimentieren, ohne Docker installieren zu müssen. Ich habe nur ein wenig an der Oberfläche gekratzt und kann das Lesen der Runc- und Containerd- Dokumentation nur empfehlen . Sehr coole Werkzeuge!






Erfahren Sie mehr über den Kurs.







All Articles