Grundlegendes zu Docker-Ebenen

27

Wir haben folgenden Block in unserem Dockerfile:

RUN yum -y update
RUN yum -y install epel-release
RUN yum -y groupinstall "Development Tools"
RUN yum -y install python-pip git mysql-devel libxml2-devel libxslt-devel python-devel openldap-devel libffi-devel openssl-devel

Mir wurde gesagt, dass wir diese RUNBefehle vereinen sollten, um die Anzahl der erstellten Docker-Ebenen zu verringern:

RUN yum -y update \
    && yum -y install epel-release \
    && yum -y groupinstall "Development Tools" \
    && yum -y install python-pip git mysql-devel libxml2-devel libxslt-devel python-devel openldap-devel libffi-devel openssl-devel

Ich bin sehr neu in Docker und nicht sicher, ob ich die Unterschiede zwischen diesen beiden Versionen der Angabe mehrerer RUN-Befehle vollständig verstehe. Wann würde man RUNBefehle zu einem einzigen zusammenfassen und wann ist es sinnvoll, mehrere RUNBefehle zu haben ?

Alecxe
quelle

Antworten:

35

Ein Docker-Image ist eine verknüpfte Liste von Dateisystemebenen. Jede Anweisung in einer Docker-Datei erstellt eine Dateisystemebene, die die Unterschiede im Dateisystem vor und nach der Ausführung der entsprechenden Anweisung beschreibt. Der docker inspectUnterbefehl kann in einem Docker-Image verwendet werden, um die Art der Verknüpfung mit einer Liste von Dateisystemebenen aufzuzeigen.

Die Anzahl der in einem Bild verwendeten Ebenen ist wichtig

  • beim Verschieben oder Ziehen von Bildern, da dies die Anzahl der gleichzeitigen Uploads oder Downloads beeinflusst.
  • Beim Starten eines Containers werden die Ebenen kombiniert, um das im Container verwendete Dateisystem zu erstellen. Je mehr Ebenen beteiligt sind, desto schlechter ist die Leistung, aber die verschiedenen Dateisystem-Backends sind unterschiedlich davon betroffen.

Dies hat verschiedene Konsequenzen für die Art und Weise, wie Bilder erstellt werden sollen. Der erste und wichtigste Rat, den ich geben kann, ist:

Tipp Nr. 1 Stellen Sie sicher, dass die Erstellungsschritte, bei denen Ihr Quellcode beteiligt ist, so spät wie möglich in der Docker-Datei eingehen und nicht mit den vorherigen Befehlen mit a &&oder a verknüpft sind ;.

Der Grund dafür ist, dass alle vorherigen Schritte zwischengespeichert werden und die entsprechenden Ebenen nicht immer wieder heruntergeladen werden müssen. Dies bedeutet schnellere Builds und schnellere Releases, was Sie wahrscheinlich wollen. Interessanterweise ist es überraschend schwierig, den Docker-Cache optimal zu nutzen.

Mein zweiter Rat ist weniger wichtig, aber ich finde ihn unter dem Gesichtspunkt der Wartung sehr nützlich:

Tipp Nr. 2 Schreiben Sie keine komplexen Befehle in die Docker-Datei , sondern verwenden Sie Skripte, die kopiert und ausgeführt werden sollen.

Ein Dockerfile, das diesen Rat befolgt, würde so aussehen

COPY apt_setup.sh /root/
RUN sh -x /root/apt_setup.sh
COPY install_pacakges.sh /root/
RUN sh -x /root/install_packages.sh

und so weiter. Der Ratschlag, mehrere Befehle mit zu binden, &&hat nur einen begrenzten Umfang. Es ist viel einfacher, mit Skripten zu schreiben, in denen Sie Funktionen usw. verwenden können, um Redundanz zu vermeiden, oder zu Dokumentationszwecken.

Leute, die an Vorprozessoren interessiert sind und bereit sind, den geringen Overhead zu vermeiden, der durch die COPYSchritte verursacht wird , und die tatsächlich im laufenden Betrieb eine Docker - Datei erzeugen, in der die

COPY apt_setup.sh /root/
RUN sh -x /root/apt_setup.sh

Sequenzen werden durch ersetzt

RUN base64 --decode … | sh -x

wo das ist die Base64-codierte Version apt_setup.sh.

Mein dritter Rat ist für Leute, die die Größe und die Anzahl der Lagen zu den möglichen Kosten längerer Bauten begrenzen möchten.

withTipp # 3 Verwenden Sie das -idiom, um Dateien zu vermeiden, die in Zwischenebenen, nicht jedoch im resultierenden Dateisystem vorhanden sind.

Eine Datei, die durch eine Docker-Anweisung hinzugefügt und durch eine spätere Anweisung entfernt wurde, ist im resultierenden Dateisystem nicht vorhanden, wird jedoch in den Docker-Ebenen, die das Docker-Bild in der Konstruktion bilden, zweimal erwähnt. Einmal mit dem Namen und dem vollständigen Inhalt in der Ebene, der sich aus dem Hinzufügen der Anweisung ergibt, und einmal als Löschhinweis in der Ebene, der sich aus dem Entfernen der Anweisung ergibt.

Nehmen wir zum Beispiel an, wir brauchen vorübergehend einen C-Compiler und ein Image und betrachten das

# !!! THIS DISPLAYS SOME PROBLEM --- DO NOT USE !!!
RUN apt-get install -y gcc
RUN gcc --version
RUN apt-get --purge autoremove -y gcc

(Ein realistischeres Beispiel wäre, eine Software mit dem Compiler zu erstellen, anstatt nur das Vorhandensein des Compilers mit dem --versionFlag zu bestätigen.)

Das Dockerfile-Snippet erstellt drei Ebenen, von denen die erste die vollständige gcc-Suite enthält, sodass die entsprechenden Daten, auch wenn sie nicht im endgültigen Dateisystem vorhanden sind, immer noch Teil des Images sind und bei jedem Herunterladen, Hochladen und Entpacken der heruntergeladen werden müssen endgültiges Bild ist.

Das with-idiom ist eine gebräuchliche Form in der funktionalen Programmierung, um den Ressourcenbesitz und die Ressourcenfreigabe von der Logik, die es verwendet, zu isolieren. Es ist einfach, diese Redewendung auf Shell-Scripting zu übertragen, und wir können die vorherigen Befehle als das folgende Script umformulieren, das COPY & RUNwie in Hinweis 2 verwendet wird.

# with_c_compiler SIMPLE-COMMAND
#  Execute SIMPLE-COMMAND in a sub-shell with gcc being available.

with_c_compiler()
(
    set -e
    apt-get install -y gcc
    "$@"
    trap 'apt-get --purge autoremove -y gcc' EXIT
)

with_c_compiler\
    gcc --version

Komplexe Befehle können in Funktionen umgewandelt werden, so dass sie an die weitergeleitet werden können with_c_compiler. Es ist auch möglich, Aufrufe mehrerer with_whateverFunktionen zu verketten, dies ist jedoch möglicherweise nicht sehr wünschenswert. (Unter Verwendung von esoterischeren Funktionen der Shell ist es sicherlich möglich with_c_compiler, komplexe Befehle zu akzeptieren, aber es ist in allen Aspekten vorzuziehen, diese komplexen Befehle in Funktionen zu verpacken.)

Wenn wir den Ratschlag Nr. 2 ignorieren möchten, würde das resultierende Dockerfile-Snippet lauten

RUN apt-get install -y gcc\
 && gcc --version\
 && apt-get --purge autoremove -y gcc

was wegen der Verschleierung nicht so einfach zu lesen und zu warten ist. Sehen Sie, wie die Shell-Skript-Variante den wichtigen Teil gcc --versionheraushebt, während die verkettete &&Variante diesen Teil mitten im Rauschen vergräbt.

Michael Le Barbier Grünewald
quelle
1
Können Sie das Ergebnis der Boxgröße nach dem Erstellen eines Skripts und dem Verwenden mehrerer Befehle in einer RUN-Anweisung angeben?
030
1
Es scheint mir eine schlechte Idee zu sein, die Konfiguration der Image-Basis (dh des Betriebssystems) und sogar der Bibliotheken mit dem Setup der Quelle zu mischen, die Sie geschrieben haben. Sie sagen "Stellen Sie sicher, dass die Erstellungsschritte, an denen Ihr Quellcode beteiligt ist, so spät wie möglich erfolgen". Gibt es irgendwelche Probleme, diesen Teil zu einem völlig unabhängigen Artefakt zu machen?
JimmyJames
1
@ 030 Was meinst du mit "Box" -Größe? Ich habe keine Ahnung, auf welche Box Sie sich beziehen.
Michael Le Barbier Grünewald
1
Ich meinte Docker Bildgröße
030
1
@JimmyJames Das hängt stark von Ihrem Bereitstellungsszenario ab. Wenn wir von einem kompilierten Programm ausgehen, wäre es das „Richtige“, es zu packen und die Paketabhängigkeiten und das Paket selbst als zwei verschiedene, kurz vor dem Abschluss stehende Schritte zu installieren. Dies maximiert die Nützlichkeit des Docker-Caches und verhindert, dass immer wieder Ebenen mit denselben Dateien heruntergeladen werden. Ich finde es einfacher, Build-Rezepte zum Erstellen von Docker-Images freizugeben, als lange Abhängigkeitsketten von Images zu erstellen, da letzteres das Neuerstellen schwieriger macht.
Michael Le Barbier Grünewald
13

Jede Anweisung, die Sie in Ihrem Dockerfile erstellen, führt dazu, dass eine neue Bildebene erstellt wird. Jede Ebene enthält zusätzliche Daten, die nicht immer Teil des resultierenden Bildes sind. Wenn Sie beispielsweise eine Datei in einer Ebene hinzufügen, diese aber später in einer anderen Ebene entfernen, enthält die endgültige Bildgröße die hinzugefügte Dateigröße in Form einer speziellen "Whiteout" -Datei, obwohl Sie sie entfernt haben.

Angenommen, Sie haben das folgende Dockerfile:

FROM centos:6

RUN yum -y update 
RUN yum -y install epel-release

Die resultierende Bildgröße ist

bigimage     latest        3c5cbfbb4116        2 minutes ago    407MB

Im Gegensatz dazu mit "ähnlichem" Dockerfile:

FROM centos:6

RUN yum -y update  && yum -y install epel-release

Die resultierende Bildgröße ist

smallimage     latest        7edeafc01ffe        3 minutes ago    384MB

Sie werden noch kleiner, wenn Sie den YUM-Cache in einer einzigen RUN-Anweisung bereinigen.

Sie möchten also das Gleichgewicht zwischen Lesbarkeit / Wartungsfreundlichkeit und Anzahl der Ebenen / Bildgröße halten.

Oryaden
quelle
4

Die RUNAussagen repräsentieren jeweils eine Schicht. Stellen Sie sich vor, Sie laden ein Paket herunter, installieren es und möchten es entfernen. Wenn drei RUNAnweisungen verwendet werden, wird die Bildgröße nicht verkleinert, da separate Ebenen vorhanden sind. Wenn man alle Befehle mit einer RUNAnweisung ausführt, kann die Größe des Disk-Image reduziert werden.

030
quelle