artykuły

ReadyToRun – nowa broń w walce z cold-startami

Brak komentarzy

Długi czas oczekiwania na wynik pierwszego uruchomienia lambdy towarzyszy nam od momentu, gdy AWS zaczął wspierać platformę .NET core w swoim sztandarowym serwisie. Na przestrzeni lat, wraz ze wsparciem dla kolejnych wersji .NET core, sytuacja ulegała stopniowej poprawie, mimo wszystko ten uciążliwy problem wciąż występuje. Jednak w najnowszej wersji 3.1 dostaliśmy do dyspozycji nowe narzędzie, które, przy odrobinie wysiłku, pozwoli nam znacząco skrócić czas cold-startów.

Obrazy ReadyToRun

Jedną z przyczyn występowania cold-startów jest fakt wykorzystywania przez platformę .NET core techniki kompilacji podczas uruchomienia (Just-In-Time compilation). W tym procesie kod aplikacji tłumaczony jest z języka pośredniego CIL na kod binarny, który to dopiero może być uruchomiony na docelowej platformie sprzętowej. Zajęcie to jest wymagające obliczeniowo, a potrzebny czas rośnie proporcjonalnie do wielkości naszego kodu i wykorzystanych bibliotek.

Jedną z nowości wprowadzonych przez Microsoft w .NET core 3.0 jest opcja ReadyToRun (w skrócie R2R). Umożliwia ona, już na etapie kompilacji kodu źródłowego, przygotowanie zestawów (assemblies) w taki sposób, aby zawierały one docelowy kod binarny. Dzięki temu kompilacja JIT staje się zbędna, a aplikacja powinna, w teorii, uruchomić się zdecydowanie szybciej. Optymalizowany jest zarówno nasz kod, jak i wszystkie zewnętrzne zależności.

Niestety każda magia ma swoją cenę. W tym przypadku wynikowe pliki będą większe, bo dla kompatybilności z innymi platformami muszą zawierać również standardową wersję CIL. Nie ma również możliwości cross-kompilacji – wersji dla linuxa nie przygotujemy na windowsie i vice-versa.

Docker na ratunek

Jeśli programujemy w C#, to jest spora szansa, że robimy to na windowsie, natomiast kontenery, na których uruchamiane są lambdy, bazują na Amazon Linuxie. Szczęśliwie, problem ten możemy łatwo obejść korzystając z dockera. Wykorzystując poniższy Dockerfile, stworzymy dockerowy obraz, który umożliwi nam budowanie lambdowych projektów z włączoną opcją ReadyToRun.

FROM amazonlinux:2

ENV PROJECT Functions

RUN yum -y update
RUN rpm -Uvh https://packages.microsoft.com/config/centos/7/packages-microsoft-prod.rpm
RUN yum -y install dotnet-sdk-3.1
RUN yum -y install zip

RUN dotnet tool install -g Amazon.Lambda.Tools
ENV PATH="/root/.dotnet/tools:${PATH}"

RUN  mkdir /work
WORKDIR /work

CMD dotnet lambda package --project-location $PROJECT --msbuild-parameters "/p:PublishReadyToRun=true --self-contained false"

Bazujemy na dystrybucji linuxa od Amazona, na której zainstalowane zostaną dotnet SDK oraz pakiet Amazon.Lambda.Tools. Projekt budowany będzie za pomocą standardowej komendy dotnet lambda package z dodatkowym przełącznikiem --msbuild-parameters.

Przed pierwszym użyciem musimy zbudować obraz i zapisać go w naszym lokalnym repozytorium

 docker build -t rozchmurzeni/lambdadotnetr2r .

Następnie możemy budować nasz projekt za pomocą komendy

docker run --rm -v C:\Path\To\Solution:/work -e PROJECT=LambdaProjectName rozchmurzeni/lambdadotnetr2r

Za pomocą przełącznika -v montujemy katalog naszej solucji do katalogu /work wewnątrz kontenera, a do zmiennej PROJECT przypisujemy nazwę folderu zawierającego nasze lambdy. Dzięki przełącznikowi --rm kontener zostanie usunięty zaraz po zbudowaniu projektu.

Tak zbudowany projekt możemy wdrożyć na AWS-ie wykonując w katalogu solucji polecenie

dotnet lambda deploy-serverless `
    --project-location LambdaProjectName `
    --package LambdaProjectName/bin/release/netcoreapp3.1/Functions.zip `
    --stack-name my-r2r-stack `
    --s3-bucket my-deploy-bucket `
    --template LambdaProjectName/template.yaml `
    --region eu-central-1 `

Porównując tę samą lambdę opublikowaną z wyłączonym i włączonym R2R zauważymy zdecydowaną różnicę w rozmiarze paczki. W moim przykładowym projekcie rozmiar wzrósł z 3.2MB do 7.9 MB.

Jak ReadyToRun wpływa na cold-start?

Na potrzeby testu stworzyłem prosty projekt z dwoma sporymi zależnościami: Entity Framework Core (najpopularniejszy ORM) oraz EPPlus (biblioteka do pracy z plikami Excela). Aby zależności nie zostały wycięte w procesie optymalizacji, lambda przy uruchomieniu tworzy bazę danych sqlite, wykonuje na niej zapytanie, a następnie tworzy arkusz Excela.

Cold-start zmierzyłem uruchamiając lambdę w regionie eu-central-1 i porównując czas odpowiedzi z zimnego i rozgrzanego kontenera. Start nowego kontenera wymuszałem zmieniając losowo opis w konfiguracji lamdby. Wynik ostateczny to średnia z 5 pomiarów. Wyniki prezentują się następująco:

Rozmiar pamięci (pośrednio moc CPU)Cold-start bez R2RCold-start z R2R
128 MB23,4606 s11,4732 s
256 MB11,3722 s5,3842 s
512 MB5,6064 s2,7544 s
1024 MB2,7528 s1,451 s
2048 MB1,629 s0,9834 s
3008 MB1,686 s0,1958 s

Jak widać wyniki są dość jednoznaczne, cold-starty zostały zredukowane o połowę prawie we wszystkich przypadkach.

Źródła testowego projektu oraz aplikacji do pomiaru cold-startów dostępne jest na naszym repozytorium.

Podsumowanie

Wyniki testu są bardzo obiecujące. Bez ingerencji w istniejący kod (poza ewentualną migracją do .NET core 3.1), jesteśmy w stanie skrócić cold-starty o połowę. To bardzo dobry wynik.

Konieczność budowania projektu na Linuxie jest pewną niedogodnością, ale jak pokazałem, można z tym sobie łatwo poradzić. Nie wiadomo natomiast czy użycie ReadyToRun jest zawsze bezproblemowe i nie powoduje jakichś problemów z kompatybilnością. Zapewne dowiemy się tego, gdy technika ta zacznie być szerzej wykorzystywana.

Tags:

Powiązane artykuły

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

Wypełnij to pole
Wypełnij to pole
Proszę wprowadzić prawidłowy adres email.
You need to agree with the terms to proceed

Menu