Wielokrotne URUCHOMIENIE kontra URUCHAMIANIE pojedynczo łańcuchowe w Dockerfile, co jest lepsze?

132

Dockerfile.1wykonuje wiele RUN:

FROM busybox
RUN echo This is the A > a
RUN echo This is the B > b
RUN echo This is the C > c

Dockerfile.2 dołącza do nich:

FROM busybox
RUN echo This is the A > a &&\
    echo This is the B > b &&\
    echo This is the C > c

Każdy RUNtworzy warstwę, więc zawsze zakładałem, że mniej warstw jest lepszych, a więc Dockerfile.2lepiej.

Jest to oczywiście prawdą, gdy a RUNusuwa coś dodanego przez poprzednie RUN(tj. yum install nano && yum clean all), Ale w przypadkach, gdy każdy RUNcoś dodaje, jest kilka punktów, które musimy wziąć pod uwagę:

  1. Warstwy mają po prostu dodać różnicę powyżej poprzedniej, więc jeśli późniejsza warstwa nie usuwa czegoś dodanego w poprzedniej, nie powinno być zbyt wiele korzyści w zakresie oszczędzania miejsca na dysku ...

  2. Warstwy są pobierane równolegle z Docker Hub, więc Dockerfile.1chociaż prawdopodobnie nieco większe, teoretycznie byłyby pobierane szybciej.

  3. Dodanie czwartego zdania (tj. echo This is the D > d) I lokalna przebudowa Dockerfile.1spowodowałoby szybszą kompilację dzięki pamięci podręcznej, ale Dockerfile.2musiałoby ponownie uruchomić wszystkie 4 polecenia.

Zatem pytanie: jaki jest lepszy sposób na zrobienie pliku Dockerfile?

Yajo
źródło
1
Nie można ogólnie odpowiedzieć, ponieważ zależy to od sytuacji i zastosowania obrazu (zoptymalizuj pod kątem rozmiaru, szybkości pobierania lub szybkości budowania)
Henryk

Odpowiedzi:

99

Jeśli to możliwe, zawsze łączę ze sobą polecenia, które tworzą pliki z poleceniami, które usuwają te same pliki w jeden RUNwiersz. Dzieje się tak, ponieważ każda RUNlinia dodaje warstwę do obrazu, a wynikiem są dosłownie zmiany systemu plików, które można zobaczyć docker diffw tymczasowym kontenerze, który tworzy. Jeśli usuniesz plik, który został utworzony w innej warstwie, jedyne, co robi system plików unii, rejestruje zmianę systemu plików w nowej warstwie, plik nadal istnieje w poprzedniej warstwie i jest przesyłany przez sieć i przechowywany na dysku. Jeśli więc pobierzesz kod źródłowy, wyodrębnisz go, skompilujesz do pliku binarnego, a na końcu usuniesz pliki tgz i źródłowe, naprawdę chcesz, aby wszystko to zostało zrobione w jednej warstwie, aby zmniejszyć rozmiar obrazu.

Następnie osobiście podzieliłem warstwy na podstawie ich możliwości ponownego wykorzystania w innych obrazach i oczekiwanego użycia pamięci podręcznej. Jeśli mam 4 obrazy, wszystkie z tym samym obrazem podstawowym (np. Debian), mogę pobrać kolekcję typowych narzędzi do większości z tych obrazów do pierwszego polecenia uruchomienia, aby inne obrazy skorzystały z buforowania.

Kolejność w pliku Dockerfile jest ważna przy ponownym wykorzystaniu pamięci podręcznej obrazów. Patrzę na wszystkie komponenty, które aktualizują się bardzo rzadko, prawdopodobnie tylko wtedy, gdy aktualizuje się obraz podstawowy i umieszcza je wysoko w pliku Dockerfile. Pod koniec Dockerfile dołączam wszelkie polecenia, które będą działały szybko i mogą się często zmieniać, np. Dodanie użytkownika z identyfikatorem UID hosta lub utworzenie folderów i zmiana uprawnień. Jeśli kontener zawiera zinterpretowany kod (np. JavaScript), który jest aktywnie opracowywany, jest on dodawany tak późno, jak to możliwe, aby przebudowa wykonywała tylko jedną zmianę.

W każdej z tych grup zmian konsoliduję najlepiej, jak potrafię, aby zminimalizować warstwy. Więc jeśli istnieją 4 różne foldery kodu źródłowego, zostaną one umieszczone w jednym folderze, aby można je było dodać za pomocą jednego polecenia. Wszelkie instalacje pakietów z czegoś takiego jak apt-get są łączone w jedną RUN, jeśli jest to możliwe, aby zminimalizować obciążenie menedżera pakietów (aktualizacja i czyszczenie).


Aktualizacja dla kompilacji wieloetapowych:

O wiele mniej martwię się zmniejszaniem rozmiaru obrazu w nieostatecznych etapach wieloetapowej kompilacji. Gdy te etapy nie są oznaczone i wysyłane do innych węzłów, można zmaksymalizować prawdopodobieństwo ponownego użycia pamięci podręcznej, dzieląc każde polecenie w osobnym RUNwierszu.

Nie jest to jednak idealne rozwiązanie do zgniatania warstw, ponieważ wszystko, co kopiujesz między etapami, to pliki, a nie reszta metadanych obrazu, takich jak ustawienia zmiennych środowiskowych, punkt wejścia i polecenie. A kiedy instalujesz pakiety w dystrybucji Linuksa, biblioteki i inne zależności mogą być rozproszone po całym systemie plików, utrudniając kopiowanie wszystkich zależności.

Z tego powodu używam kompilacji wieloetapowych jako zamiennika do budowania plików binarnych na serwerze CI / CD, więc mój serwer CI / CD musi mieć tylko narzędzia do uruchomienia docker build, a nie mieć jdk, nodejs, go i wszelkie inne zainstalowane narzędzia kompilacji.

BMitch
źródło
30

Oficjalna odpowiedź wymieniona w ich najlepszych praktykach (oficjalne obrazy MUSZĄ być zgodne z tymi)

Zminimalizuj liczbę warstw

Musisz znaleźć równowagę między czytelnością (a tym samym długoterminową łatwością utrzymania) pliku Dockerfile i minimalizacją liczby używanych przez niego warstw. Podejmij strategię i zachowaj ostrożność, jeśli chodzi o liczbę używanych warstw.

Od dokowanym 1.10 COPY, ADDaRUN sprawozdanie dodać nową warstwę do obrazu. Zachowaj ostrożność, używając tych stwierdzeń. Spróbuj połączyć polecenia w jedną RUNinstrukcję. Oddziel to tylko wtedy, gdy jest to wymagane dla czytelności.

Więcej informacji: https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/#/minimize-the-number-of-layers

Aktualizacja: wieloetapowy w dockerze> 17.05

W przypadku kompilacji wieloetapowych można używać wielu plików FROM instrukcji w pliku Dockerfile. Każda FROMinstrukcja jest sceną i może mieć swój własny obraz bazowy. Na ostatnim etapie używasz minimalnego obrazu podstawowego, takiego jak alpine, kopiujesz artefakty kompilacji z poprzednich etapów i instalujesz wymagania dotyczące środowiska wykonawczego. Efektem końcowym tego etapu jest Twój wizerunek. W tym miejscu martwisz się o warstwy, jak opisano wcześniej.

Jak zwykle, docker ma świetne dokumenty na temat kompilacji wieloetapowych. Oto krótki fragment:

W przypadku kompilacji wieloetapowych używasz wielu instrukcji FROM w pliku Dockerfile. Każda instrukcja FROM może korzystać z innej bazy, a każda z nich rozpoczyna nowy etap budowy. Możesz selektywnie kopiować artefakty z jednego etapu do drugiego, pozostawiając wszystko, czego nie chcesz w ostatecznym obrazie.

Świetny wpis na blogu na ten temat można znaleźć tutaj: https://blog.alexellis.io/mutli-stage-docker-builds/

Aby odpowiedzieć na Twoje punkty:

  1. Tak, warstwy są czymś w rodzaju różnic. Nie sądzę, aby dodano warstwy, jeśli nie ma absolutnie żadnych zmian. Problem polega na tym, że po zainstalowaniu / pobraniu czegoś w warstwie # 2 nie można tego usunąć w warstwie # 3. Kiedy więc coś zostanie zapisane w warstwie, nie można już zmniejszyć rozmiaru obrazu, usuwając to.

  2. Chociaż warstwy można przeciągać równolegle, co czyni je potencjalnie szybszymi, każda warstwa niewątpliwie zwiększa rozmiar obrazu, nawet jeśli usuwają pliki.

  3. Tak, buforowanie jest przydatne, jeśli aktualizujesz plik Dockera. Ale działa w jednym kierunku. Jeśli masz 10 warstw i zmienisz warstwę # 6, nadal będziesz musiał odbudować wszystko z warstwy # 6- # 10. Nie jest więc zbyt często, że przyspieszy to proces tworzenia, ale gwarantuje niepotrzebne zwiększenie rozmiaru obrazu.


Dzięki @Mohan za przypomnienie mi o zaktualizowaniu tej odpowiedzi.

Menzo Wijmenga
źródło
1
To jest teraz nieaktualne - zobacz odpowiedź poniżej.
Mohan
1
@Mohan dzięki za przypomnienie! Zaktualizowałem post, aby pomóc użytkownikom.
Menzo Wijmenga
19

Wygląda na to, że powyższe odpowiedzi są nieaktualne. Uwaga w dokumentacji:

Przed Docker 17.05, a nawet więcej, przed Docker 1.10 ważne było zminimalizowanie liczby warstw w obrazie. Następujące ulepszenia złagodziły tę potrzebę:

[…]

Docker 17.05 i nowsze zawierają obsługę kompilacji wieloetapowych, które umożliwiają kopiowanie tylko potrzebnych artefaktów do ostatecznego obrazu. Pozwala to na dołączanie narzędzi i informacji debugowania na pośrednich etapach kompilacji bez zwiększania rozmiaru końcowego obrazu.

https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/#minimize-the-number-of-layers

i

Zauważ, że ten przykład również sztucznie kompresuje razem dwa polecenia RUN za pomocą operatora Bash &&, aby uniknąć tworzenia dodatkowej warstwy w obrazie. Jest to podatne na awarie i trudne do utrzymania.

https://docs.docker.com/engine/userguide/eng-image/multistage-build/

Wydaje się, że najlepsza praktyka zmieniła się i polega na używaniu kompilacji wieloetapowych i zachowaniu Dockerfileczytelności.

Mohan
źródło
Chociaż kompilacje wieloetapowe wydają się dobrą opcją, aby zachować równowagę, faktyczna poprawka na to pytanie nastąpi, gdy docker image build --squashopcja wyjdzie poza eksperymentalną.
Yajo,
2
@Yajo - sceptycznie podchodzę squashdo eksperymentów. Ma wiele sztuczek i ma sens tylko przed kompilacjami wieloetapowymi. W przypadku kompilacji wieloetapowych wystarczy zoptymalizować ostatni etap, co jest bardzo łatwe.
Menzo Wijmenga
1
@Yajo Aby to rozwinąć, tylko warstwy na ostatnim etapie mają wpływ na rozmiar ostatecznego obrazu. Więc jeśli umieścisz wszystkie gubbiny kreatora na wcześniejszych etapach, a na ostatnim etapie po prostu zainstalujesz pakiety i skopiujesz pliki z wcześniejszych etapów, wszystko działa pięknie i squash nie jest potrzebny.
Mohan,
3

To zależy od tego, co włączysz do warstw obrazu.

Kluczową kwestią jest udostępnianie jak największej liczby warstw:

Zły przykład:

Dockerfile.1

RUN yum install big-package && yum install package1

Dockerfile 2

RUN yum install big-package && yum install package2

Dobry przykład:

Dockerfile.1

RUN yum install big-package
RUN yum install package1

Dockerfile 2

RUN yum install big-package
RUN yum install package2

Inną sugestią jest to, że usuwanie nie jest tak przydatne tylko wtedy, gdy dzieje się na tej samej warstwie, co czynność dodawania / instalowania.

xdays
źródło
Czy te 2 naprawdę udostępnią dane RUN yum install big-packagez pamięci podręcznej?
Yajo
Tak, będą współdzielić tę samą warstwę, pod warunkiem, że zaczną od tej samej podstawy.
Ondra Žižka