Rozgryźmy Cache Task w Azure Pipelines, czyli mechanizm, dzięki któremu oszczędzisz setki godzin.

Procesy Ciągłej Integracji i Ciągłego Dostarczania, wraz z rozwojem oprogramowania, trwają coraz dłużej. Gdy nieodpowiednio urosną, tracimy nie tylko czas potrzebny na ich wykonanie, ale również odczuwamy to w innych obszarach. Wcześniej przenieśmy się do biura.

W biurze

Piątek, 17:15. Jestem sam. Towarzystwa dotrzymuje mi ekran wraz z mrugającą świetlówką. Mruga, rozprasza i wydaje ten charakterystyczny brzdęk, rozbijając jednostajną symfonię szumu komputerów i szmerów chłodnego powietrza wdmuchiwanego z klimatyzatorów. 

Ten chłód. Ta bryza o niepodrabianym, stęchliwym, lekko grzybiczym, zapachu, wdzierająca się przez nozdrza przy każdym wdechu. Ignorowana przez mózg. On już się przyzwyczaił, a może po prostu ignoruje. Jest skupiony na czymś innym. Na tym, aby poprawić już tego buga i pójść spokojnie do domu. 

Ostanie stuknięcia w klawiaturę git commit -m "fix bux", potem git push. Chwila magii na czarnej konsoli, a mózg, przełącza się na inny tryb. Wyłapuje bodźce z otoczenia, bo czeka. Czeka. I czeka, bo…

Najwolniejsza część Pipeline

… serwer CI, nie wiadomo dlaczego, po każdym commicie pobiera półtora gigabajta pakietów z internetu. Gdyby nie powolny npm install to skończyłbym pracę już dawno temu. Jednak takie mamy czasy. Czekam. Na zakończenie następującego pipeline:

pool:
  vmImage: 'windows-2019'

steps:
- task: Npm@1
  inputs:
    command: 'install'
    workingDir: 'src/frontend'

Może to koszt szybkiego dewelopmentu, gotowych rozwiązań, automatyzacji? Najgorzej, jeżeli będę musiał puścić build jeszcze raz. Za każdym razem pobiorę wszystkie pakiety znów, a to trwa….

build time without cache task in Azure Pipelines
Build Time Without Cache Task in Azure Pipelines - One Run

i trwa, i trwa, i jest całkowicie bez sensu, bo przecież te pliki już tam były 5 minut temu. Odnoszę wrażenie, że ten zgrzybiały zapach wydobywa się jednak z serwera CI, a nie z klimy.

A gdyby to naprawić, odświeżyć, przyspieszyć?

Cache Task w Azure Pipelines

Jest na to sposób. Serwery CI radzą sobie z tym na różne sposoby. W rozwiązaniu od Microsoftu istnieje Cache Task w Azure Pipelines. Umożliwia ponowne użycie plików pomiędzy kolejnymi wywołaniami Pipeline. Dzięki czemu możemy uniknąć niepotrzebnego pobierania gigabajtów z internetu i oszczędzić czas na to potrzebny.

Oto jego najprostsza konfiguracja:

pool:
  vmImage: 'windows-2019'

steps:
- task: Cache@2
  inputs:    
    key: 'src/frontend/package.json'
    path: 'src/frontend/node_modules/'

- task: Npm@1
  inputs:
    command: 'install'
    workingDir: 'src/frontend'

w której:

  • key: 'src/frontend/package.json' – jest definicją klucza, na podstawie którego zostanie wyliczony hash. Ten hash będzie użyty jako klucz do zachowania i odtworzenia plików określonych w path.
    W tym przykładzie jest to zawartość pliku package.json. Co za tym idzie, jeżeli zmieni się zawartość tego pliku, cache nie zostanie pobrany.
  • path: 'src/frontend/node_modules/' – jest to ścieżka do plików, które chcemy użyć ponownie w kolejnych wywołaniach. Zostaną one zapisane na koniec konkretnego pipeline w specjalnym zadaniu Post-job: Cache, a następnie przywrócone, jeżeli hash wyliczony z key będzie taki sam.

Przy takiej konfiguracji pierwsze uruchomienie pipeline, będzie dłuższe, ponieważ musimy pobrać paczki z internetu oraz dodać je do cache.  Co widać na screenie.

First Run After Adding Cache Task in Azure Pipelines

Dodatkowo pierwsze wywołania również są wolniejsze, ale z czasem powinno się to ustabilizować. Tak to wygląda na moim przykładzie:

build time without cache task in Azure Pipelines - next runs.png

package.json w Cache Task to za mało

Prawdopodobnie zwróciłeś uwagę, że użyłem w key package.json. Nie jest to dobry pomysł, ponieważ może się okazać, że:

  • Zmienią się wersje używanych pakietów, a pakietów nie zmieniając package.json.
  • Zmienisz package.json ,nie ruszając definicji pakietów, np. dodając coś do sekcji scripts

Lepiej oprzeć się na pliku package-lock.json, ponieważ na jego podstawie jesteśmy w stanie określić konkretne wersje pakietów do zainstalowania.

Czyli nasza konfiguracja, będzie wyglądać następująco:

pool:
  vmImage: 'windows-2019'

steps:
- task: Cache@2
  inputs:
    key: 'src/frontend/package-lock.json'
    path: 'src/frontend/node_modules'

- task: Npm@1
  inputs:
    command: 'install'
    workingDir: 'src/frontend'

Już zyskujesz. Co dalej?

Używając powyższej konfiguracji, oszczędzasz. Czas wykonania spadł z 3:30-5:40 do 2:08-2:42, czyli o jedną/dwie minuty. Wydaje się to niewiele, ale jeżeli jest to uruchamiane setki razy dziennie, to daje olbrzymie liczby.

Procesy CI/CD powinny być rozwijane przyrostowo, wraz z rozwojem oprogramowania. Dzięki czemu ten proces wciąż będzie działał sprawnie i wydajnie.

Pamiętaj, że jest to początkowa konfiguracja i możesz ją znacznie ulepszyć, aby oszczędzać jeszcze więcej czasu i stabilności. Przeczytasz o tym kolejnej części


Zaawansowany Cache Task W Azure Pipelines,
Czyli Ekstremalne Przyspieszenie

Czy już dodałeś Cache Task do Azure Pipelines? A jeżeli nie to dlaczego?
Daj mi o tym znać w komentarzu.


8 Komentarzy

Bartek · 21 sierpnia, 2020 o 17:51

Czy wiesz może czy istnieje odpowiednich Cache Task dla Azure DevOps 2019? Sprawdzałem w Marketplace i jest tylko Hash and Cache.

Ireneusz Patalas · 21 sierpnia, 2020 o 14:51

Cześć 🙂

Temat jak wiesz u mnie dość na czasie, także dzięki.. wypróbuję. Ale widzę tutaj jeszcze jedno miejsce do poprawy. Od dłuższego czasu do instalacji npm’ów w CI powinno się używać komendy `npm ci`, a nie `npm install`.
Ta pierwsza omija (całkiem sporą) część logiki zwykłego installa, ale jak masz package-lock.json to i tak ściągamy konkretne wersje bibliotek. Polecam, u nas czas instalacji spadł prawie o połowę (!).
Instalacja npm w CI trwa ~30 sekund, więc już nie wiele możemy zyskać na cachu.

Za to dużym problemem jest NuGet restore. Trwa to u nas 2-3 minuty. Niestety z tego co się dowiedziałem domyślne cachowanie nie działa na Hosted agentach, a takimi dysponujemy.
Tutaj już Cache task mógłby pomóc, ale… jaki ustawić klucz?
Mamy projekt .NET Core 2.2 i tam informacje o zależnościach są w plikach .csproj od każdego projektu. Nie ma już packages.config jak kiedyś. Masz pomysł jak to ugryźć? 🙂

    Jerzy Wickowski · 22 sierpnia, 2020 o 14:17

    Cześć @Ireneusz,
    Używając npm ci nie zalecane jest keszowanie node_modules. Możesz to przeczytać w dokumentacji od MS, ale jeszcze przyjrzę się tematowi i dam znać.

    Odnośnie NuGeta to możesz ustawiść klucz z każdym csprojem, czyli np. src/backend/project1.csproj | src/backend/project2.csproj , ale możesz to zrobić w bardziej cywilizowany sposób, czyli src/backend/**/*.csproj. Oczywiście pozostaje jeszcze opcja, czy to wszystko, czy może coś będzie trzeba dodać.

    Więcej info na stronie MS: https://docs.microsoft.com/en-us/azure/devops/pipelines/release/caching?view=azure-devops

Piotr · 21 sierpnia, 2020 o 08:18

Tutaj może link do dokumentacji Microsoftu
https://docs.microsoft.com/en-us/azure/devops/pipelines/release/caching?view=azure-devops

Piotr · 21 sierpnia, 2020 o 08:13

Dzięki za dzielenie się doświadczeniem, Jerzy.

Rozumiem, że to też może odnieść się do paczek NuGet. Zgadza się? Czy spotkałeś się z jakimiś problemami w związku z cachowaniem?

    Jerzy Wickowski · 22 sierpnia, 2020 o 14:23

    Cześć @Piotr,

    Tak. Zastosujesz to również dla NuGeta, ale musisz to dostosować.
    Problem? Owszem, ale wynikał z mojego błędu. Źle skonfigurowałem odświeżanie cache i pobierało się nie to, co powinno. Opiszę to w kolejnym artykule.

Jak NIE zarządzać konfiguracją. Chyba że lubisz kłopoty! · 30 września, 2020 o 12:11

[…] A jak już jesteśmy przy marnowaniu czasu. Sprawdź jak go oszczędzić wprowadzając Cache Task do Azure Pipelines. […]

Możliwość dodawania komentarzy nie jest dostępna.