W
procesie budowy obrazów kontenerów wykorzystywanych do uruchamiania aplikacji lub serwerów najbardziej czasochłonną częścią jest instalacja zależności w postaci paczek systemowych. Proces ten jest z reguły powolny, ponieważ menedżery pakietów są stworzone w taki sposób, aby przedkładać stabilność instalacji nad jej wydajnością. I to ma sens: jeżeli podczas instalacji wydarzy się niezapowiedziana awaria zasilania lub zawieszenie procesu, system musi pozostać w stanie nadającym się do użytku. Jednak w przypadku budowania obrazów kontenerów stabilność nie stanowi priorytetu. jeżeli proces instalacji pakietów lub kompilacji źródeł się nie powiedzie – system budujący obraz go odrzuci, zakończy proces błędem i będziemy musieli zacząć od nowa. Dlatego gdybyśmy mogli zasugerować menedżerowi pakietów, iż nie przejmujemy się zbytnio integralnością danych, moglibyśmy przyspieszyć nasze kompilacje.
Jak możemy przeczytać w sekcji FAQ dotyczącej dpkg – działa tak wolno podczas korzystania z nowych systemów plików, takich jak btrfs lub ext4, ponieważ stara się zagwarantować, iż dane zawsze będą spójne i bezpieczne. W tym celu dpkg wykonuje wywołania fsync(2) na swojej bazie danych i plikach rozpakowanych z pakietów. Najnowsze systemy plików (jak wspomniane btrfs lub ext4), które implementują opóźnioną alokację, wymagają wywołań systemowych fsync(2), ponieważ zamieniają bezpieczeństwo danych na wydajność i oczekują, iż to programy będą wykonywać wywołania fsync(2) we właściwym czasie. Jednocześnie programy spełniające te wymagania mogą wykazywać słabszą wydajność, aby w przypadku awarii systemu nie tworzyć uszkodzonych lub plików o zerowej długości. Dla przypomnienia fsync() przesyła wszystkie zmodyfikowane dane pliku (np. w stronach pamięci podręcznej), do którego odnosi się deskryptor pliku, na urządzenie dyskowe (lub inne trwałe urządzenie pamięci masowej). Dzięki czemu wszystkie zmienione informacje można odzyskać choćby jeżeli system zostanie niespodziewanie zamknięty lub ponownie uruchomiony. Obejmuje to zapisywanie lub opróżnianie pamięci podręcznej dysku, jeżeli taką dysponuje. Wywołanie to blokuje operacje do czasu, aż urządzenie zaraportuje zakończenie transferu wszystkich operacji. Oprócz zapisu danych pliku, fsync() zapisuje także metadane skojarzone z plikiem.
W przypadku procesu budowy obrazów kontenerów możemy wykorzystać dwie opcje, aby przyśpieszyć proces instalacji pakietów. Pierwszą jest opcja dostępna od conajmniej wersji 1.15.8.6, czyli --force-unsafe-io – co pozwoli uniknąć wywołań fsync(2) na plikach rozpakowanych w systemie plików, ale przez cały czas będzie wykonywać je na bazie danych dpkg. jeżeli opcja ta jest dla nas nie wystarczająco dobra, możemy skorzystać z biblioteki libatemydata. Jest to biblioteka, której autorem jest Stewart Smith. Stworzył on ją jako wynik wykładu, który wygłosił na konferencji linux.conf.au w 2007 roku o tytule: Eat My Data: How Everybody gets File IO Wrong. Biblioteka ta dzięki LD_PRELOAD wyłącza wszelkie formy bezpiecznego zapisu danych na dysku. fsync() staje się NO-OP, a O_SYNC, O_DSYNC itp. zostają usunięte. Przykład działania eatmydata na budowę prostego obrazu kontenera Docker, gdzie obraz bazowy ubuntu:22.04 został już ściągnięty na lokalny serwer budujący:
FROM ubuntu:22.04 RUN echo 'APT::Install-Suggests "0";' >> /etc/apt/apt.conf.d/00-docker RUN echo 'APT::Install-Recommends "0";' >> /etc/apt/apt.conf.d/00-docker RUN echo 'APT::Acquire::Retries "3";' >> /etc/apt/apt.conf.d/00-docker RUN DEBIAN_FRONTEND=noninteractive \ apt-get update \ && apt-get install -y python3 mc apache2 htop \ && rm -rf /var/lib/apt/lists/* RUN useradd -ms /bin/bash apprunner USER apprunnerBudowa obrazu w trzech próbach zajęła:
agresor@darkstar:~$ time docker build . -t myubuntu-pure ... Successfully built b29857680f0b Successfully tagged myubuntu-pure:latest ... real 2m58.782s real 2m56.545s real 3m16.473sPo usunięciu obrazów i modyfikacji pliku Dockerfile:
FROM ubuntu:22.04 RUN echo 'APT::Install-Suggests "0";' >> /etc/apt/apt.conf.d/00-docker RUN echo 'APT::Install-Recommends "0";' >> /etc/apt/apt.conf.d/00-docker RUN echo 'APT::Acquire::Retries "3";' >> /etc/apt/apt.conf.d/00-docker RUN DEBIAN_FRONTEND=noninteractive \ apt-get update \ && apt-get -qq install eatmydata RUN DEBIAN_FRONTEND=noninteractive \ && eatmydata apt-get install -y python3 mc apache2 htop \ && rm -rf /var/lib/apt/lists/* RUN useradd -ms /bin/bash apprunner USER apprunnerBudowa obrazu w trzech próbach zajęła:
agresor@darkstar:~$ time docker build . -t myubuntu-eatd ... Successfully built a08e1c085a3e Successfully tagged myubuntu-eatd:latest ... real 0m43.075s real 1m26.041s real 0m40.903sNawet jeżeli uwzględnimy kaprysy sieci przy ściąganiu pakietów to przez cały czas zyskujemy ponad minutę czasu w pojedynczej konstrukcji obrazu. Daje nam to realny zysk szczególnie, jeżeli nasze serwery muszą budować dużo obrazów lub gdy korzystamy z płatnego dostawcy CI/CD, który pobiera opłatę za każdą sekundę wykorzystanego czasu. Na innym, większym projekcie biblioteka wykazała około 33% wzrostu wydajności. W dodatku samej biblioteki możemy używać:
ENV LD_PRELOAD /usr/lib/x86_64-linux-gnu/libeatmydata.soz dowolnym programem, który wykonuje testy oprogramowania i dużo operacji dyskowych.
Więcej informacji: eatmydata(1), A simple wrapper to make dpkg faster (and unsafe), Speeding up Docker Builds With eatmydata, Is an fsync-less tool such as eatmydata still relevant?