Zrozumienie warstw dokerów

27

Mamy następujący blok w naszym 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

Powiedziano mi, że powinniśmy zjednoczyć te RUNpolecenia, aby ograniczyć utworzone warstwy dokerów:

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

Jestem bardzo nowy w dockerze i nie jestem pewien, czy całkowicie rozumiem różnice między tymi dwiema wersjami określania wielu poleceń RUN. Kiedy należy łączyć RUNpolecenia w jedno i kiedy sensowne jest posiadanie wielu RUNpoleceń?

alecxe
źródło

Odpowiedzi:

35

Obraz dokera jest w rzeczywistości połączoną listą warstw systemu plików. Każda instrukcja w Dockerfile tworzy warstwę systemu plików, która opisuje różnice w systemie plików przed i po wykonaniu odpowiedniej instrukcji. docker inspectKomenda może być stosowany na obrazie Döcker ujawnić swój charakter będąc związany lista warstw systemu plików.

Ważna jest liczba warstw użytych w obrazie

  • podczas pchania lub ciągnięcia obrazów, ponieważ wpływa to na liczbę jednoczesnych wysyłanych lub pobieranych plików.
  • podczas uruchamiania kontenera, ponieważ warstwy są łączone razem, aby utworzyć system plików używany w kontenerze; im więcej warstw jest zaangażowanych, tym gorsza jest wydajność, ale wpływ na to ma różny backend systemu plików.

Ma to kilka konsekwencji dla sposobu budowania obrazów. Pierwsza i najważniejsza rada, jaką mogę udzielić, to:

Porada 1 Upewnij się, że kroki kompilacji, w których jest zaangażowany kod źródłowy, pojawią się w pliku Docker tak późno, jak to możliwe i nie są powiązane z poprzednimi poleceniami za pomocą a &&lub a ;.

Powodem tego jest to, że wszystkie poprzednie kroki zostaną zapisane w pamięci podręcznej, a odpowiednie warstwy nie będą musiały być pobierane w kółko. Oznacza to szybsze kompilacje i szybsze wydania, co prawdopodobnie jest tym, czego chcesz. Co ciekawe, zaskakująco trudno jest optymalnie wykorzystać pamięć podręczną dokera.

Moja druga rada jest mniej ważna, ale uważam ją za bardzo przydatną z punktu widzenia konserwacji:

Rada # 2 Nie pisz skomplikowanych poleceń w Dockerfile, ale raczej używaj skryptów, które mają być kopiowane i wykonywane.

Dockerfile po tej rady będzie wyglądać

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

i tak dalej. Porada dotycząca wiązania kilku poleceń &&ma jedynie ograniczony zakres. Znacznie łatwiej jest pisać za pomocą skryptów, w których można używać funkcji itp., Aby uniknąć nadmiarowości lub do celów dokumentacji.

Osoby zainteresowane procesorami wstępnymi i chcące uniknąć niewielkiego narzutu spowodowanego przez te COPYkroki i faktycznie generują w locie plik dokowania, w którym

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

sekwencje są zastępowane przez

RUN base64 --decode … | sh -x

gdzie jest wersją zakodowaną w base64 apt_setup.sh.

Moja trzecia rada jest dla osób, które chcą ograniczyć rozmiar i liczbę warstw przy możliwym koszcie dłuższych wersji.

Rada # 3 Użyj with-idiom, aby uniknąć plików znajdujących się w warstwach pośrednich, ale nie w wynikowym systemie plików.

Plik dodany przez jakąś instrukcję dokera i usunięty przez późniejszą instrukcję nie jest obecny w wynikowym systemie plików, ale jest wspomniany dwa razy w warstwach dokera tworzących obraz dokera w budowie. Raz, z nazwą i pełną zawartością w warstwie wynikającej z dodania instrukcji, a raz jako powiadomienie o usunięciu warstwy w wyniku usunięcia instrukcji.

Załóżmy na przykład, że tymczasowo potrzebujemy kompilatora C i jakiegoś obrazu, i rozważmy

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

(Bardziej realistycznym przykładem byłoby zbudowanie jakiegoś oprogramowania za pomocą kompilatora, zamiast jedynie zapewnienia jego obecności za pomocą --versionflagi).

Fragment pliku Dockerfile tworzy trzy warstwy, pierwsza zawiera pełny pakiet gcc, więc nawet jeśli nie jest obecny w końcowym systemie plików, odpowiednie dane są nadal częścią obrazu w ten sam sposób i muszą być pobierane, przesyłane i rozpakowywane za każdym razem, gdy ostateczny obraz to.

with-Idiom jest powszechną formą programowania funkcjonalnego w celu wyodrębnienia własności zasobów i zasobów zwolnieniu z logiką jej stosowania. Łatwo jest przetransponować ten idiom na wykonywanie skryptów powłoki, a my możemy przeformułować poprzednie polecenia jako następujący skrypt, z którego można korzystać COPY & RUNtak jak w Poradzie nr 2.

# 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

Złożone polecenia można przekształcić w funkcję, aby można je było przekazać do with_c_compiler. Możliwe jest również łączenie wywołań kilku with_whateverfunkcji, ale może nie jest to bardzo pożądane. (Używając bardziej ezoterycznych cech powłoki, z pewnością możliwe jest with_c_compilerprzyjmowanie złożonych poleceń, ale we wszystkich aspektach lepiej jest zawinąć te złożone polecenia w funkcje).

Jeśli chcemy zignorować poradę nr 2, wynikowy fragment pliku Dockerfile będzie

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

który nie jest tak łatwy do odczytania i utrzymania z powodu zaciemnienia. Zobacz, jak wariant skryptu powłoki kładzie nacisk na ważną część, gcc --versionpodczas gdy &&wariant łańcuchowy zakrywa tę część w środku hałasu.

Michael Le Barbier Grünewald
źródło
1
Czy możesz dołączyć wynik rozmiaru pudełka po zbudowaniu za pomocą skryptu i użyciu wielu poleceń w jednej instrukcji RUN?
030
1
Wydaje mi się, że złym pomysłem jest połączenie konfiguracji bazy obrazu (tj. Systemu operacyjnego), a nawet bibliotek lib z ustawieniami źródła, które napisałeś. Mówisz „Upewnij się, że kroki kompilacji, w które zaangażowany jest Twój kod źródłowy, nadchodzą możliwie późno”. Czy jest jakiś problem z uczynieniem tej części całkowicie niezależnym artefaktem?
JimmyJames
1
@ 030 Co masz na myśli mówiąc o „pudełku”? Nie mam pojęcia, do którego pola się odnosisz.
Michael Le Barbier Grünewald
1
Miałem na myśli rozmiar obrazu dokera
030
1
@JimmyJames To zależy w dużej mierze od twojego scenariusza wdrażania. Jeśli założymy, że skompilowany program, „właściwą rzeczą do zrobienia” byłoby spakowanie go i zainstalowanie zależności pakietu oraz samego pakietu jako dwóch odrębnych kroków, które są prawie do ukończenia. Ma to na celu maksymalizację użyteczności pamięci podręcznej dokera i uniknięcie pobierania w kółko warstw z tymi samymi plikami. Łatwiej jest mi udostępniać przepisy dotyczące budowania obrazów dokerów niż budować długie łańcuchy zależności obrazów, ponieważ te ostatnie utrudniają odbudowę.
Michael Le Barbier Grünewald
13

Każda instrukcja utworzona w pliku Docker powoduje utworzenie nowej warstwy obrazu. Każda warstwa przynosi dodatkowe dane, które nie zawsze są częścią wynikowego obrazu. Na przykład, jeśli dodasz plik na jednej warstwie, ale usuniesz go później na innej warstwie, ostateczny rozmiar obrazu będzie zawierał dodany rozmiar pliku w postaci specjalnego pliku „białego”, mimo że go usunąłeś.

Załóżmy, że masz następujący plik Dockerfile:

FROM centos:6

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

Wynikowy rozmiar obrazu to

bigimage     latest        3c5cbfbb4116        2 minutes ago    407MB

Przeciwnie, z „podobnym” Dockerfile:

FROM centos:6

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

Wynikowy rozmiar obrazu to

smallimage     latest        7edeafc01ffe        3 minutes ago    384MB

Otrzymasz jeszcze mniejszy rozmiar, jeśli wyczyścisz pamięć podręczną yum w pojedynczej instrukcji RUN.

Chcesz zachować równowagę między czytelnością / łatwością konserwacji a liczbą warstw / wielkości obrazu.

oryades
źródło
4

Te RUNwypowiedzi reprezentują każdą jedną warstwę. Wyobraź sobie, że ktoś pobiera pakiet, instaluje go i chciałby go usunąć. Jeśli użyjemy trzech RUNinstrukcji, rozmiar obrazu nie zmniejszy się, ponieważ istnieją osobne warstwy. Jeśli uruchomisz wszystkie polecenia za pomocą jednej RUNinstrukcji, rozmiar obrazu dysku może zostać zmniejszony.

030
źródło