Czy język programowania i region mają wpływ na wydajność Google Cloud Functions?
Jeżeli chociaż raz zastanawiałeś się, w jakim języku programowania napisać funkcję w Google Cloud, to w tym wpisie postaram się pomóc w podjęciu tej decyzji.
Na warsztat weźmiemy wszystkie dostępne na ten moment środowiska uruchomieniowe dla Google Cloud Functions i porównamy czasy odpowiedzi oraz zimne starty (tzw. cold starts). Porównamy nie tylko środowiska uruchomieniowe, ale również regiony w których osadzone są funkcje.
Motywacja
Pisząc swoją pierwszą funkcję w GCP zastanawiałem się, w jakim języku ją napisać? Przecież to prosta funkcja, mogę ją napisać w każdym dostępnym języku. Pisać w Javie, której używam na co dzień? A może w Node.js? Przecież TypeScript też jest dla mnie codziennością…
Motywacją do przeprowadzenia testów był przede wszystkim brak odpowiedzi na moje pytania oraz brak porównań środowisk uruchomieniowych dla Cloud Functions w Internecie.
Środowisko testowe
Google co chwilę rozszerza listę obsługiwanych środowisk uruchomieniowych, dlatego zależało mi na tym, żeby porównanie funkcji było łatwe do przeprowadzenia w przyszłości, z uwzględnieniem nowych języków. Chcąc zautomatyzować całą procedurę i środowisko testowe, wraz z kolegą Jackiem Grobelnym przygotowaliśmy projekt pt. Google Coud Functions Comparison.
Do automatyzacji wykorzystany został Terraform, za pomocą którego przygotowywane jest całe środowisko testowe. Wszystkie osadzane funkcje są definiowane w konfiguracji, dlatego w prosty sposób można uruchomić środowisko testujące wybrane języki oraz regiony.
Testy czasów odpowiedzi zostały napisane w Gatlingu, który listę funkcji odczytuje z tej samej konfiguracji, przez co nie wymaga żadnej dodatkowej ingerencji. Testy zimnych startów wykonywane są natomiast bezpośrednio przez kod napisany w Scali, a wyniki wyświetlane są w formie tabeli ASCII.
Kody wszystkich funkcji znajdują się w folderze /functions
i są to podstawowe funkcje odpowiadające “Hello World”, takie same jak przykładowe funkcje tworzone z poziomu Cloud Console.
Projekt znajduję się na GitHubie - link do repozytorium.
Metodyka testowania
W testach wykorzystałem funkcje uruchomione w następujących środowiskach uruchomieniowych:
- .NET Core 3.1
- Go 1.13
- Java 11
- Node.js 14
- PHP 7.4
- Python 3.9
- Ruby 2.7
Każda funkcja posiadała maksymalnie jedną instancję, przydzieloną pamięć 128 MB i została uruchomiona w regionach:
europe-west3
(Frankfurt, Germany, Europe)us-central1
(Council Bluffs, Iowa, North America)asia-east2
(Hong Kong, APAC)
W celu porównania czasów odpowiedzi, każda funkcja była wywoływana przez 10 minut i 20 równoległych użytkowników. Ilość wykonanych żądań jest zatem uzależniona od samej funkcji oraz Gatlinga (a w zasadzie, to mojego laptopa).
Testy zimnych startów zostały wykonane z zapewnieniem braku istnienia aktywnej instancji. Test polegał na wykonaniu 10 żądań do każdej z funkcji, a następnie porównaniu czasu pierwszej odpowiedzi do średniej arytmetycznej czasów pozostałych 9 odpowiedzi.
Każdy test uruchomiłem dwa razy, o tej samej godzinie czasu polskiego dla wszystkich regionów oraz o tej samej godzinie czasu lokalnego w danym regionie. Wszystkie żądania były wykonywane z mojej stacji roboczej w Poznaniu.
Przyjąłem nazewnictwo język interpretowany dla języków skryptowych i kompilowanych (nie korzystających z maszyny wirtualnej) oraz język uruchamiany w maszynie wirtualnej dla języków kompilowanych i uruchamianych w maszynie wirtualnej.
Czasy odpowiedzi
Godziny uruchomienia testów
Run #1
- środek tygodnia, 17-23 czasu polskiegoRun #2
- środek tygodnia, 20:00 czasu lokalnego w danym regionie:europe-west3
(Frankfurt, Germany, Europe) - 20:00 o 20:00 czasu polskiegous-central1
(Council Bluffs, Iowa, North America) - 20:00 o 03:00 czasu polskiego (-7h)asia-east2
(Hong Kong, APAC) - 20:00 o 13:00 czasu polskiego (+7h)
Wyniki
Runtime | Region | Requests | Min [ms] | 95th pct [ms] | Max [ms] | Mean [ms] | Std Dev [ms] |
---|---|---|---|---|---|---|---|
nodejs14 | europe-west3 | 20796 | 85 | 909 | 1591 | 576 | 182 |
go113 | europe-west3 | 25681 | 83 | 856 | 7607 | 466 | 213 |
java11 | europe-west3 | 20408 | 83 | 1078 | 2675 | 587 | 259 |
python39 | europe-west3 | 20810 | 83 | 893 | 1601 | 575 | 160 |
ruby27 | europe-west3 | 25818 | 82 | 711 | 1791 | 464 | 156 |
dotnet3 | europe-west3 | 17489 | 84 | 1003 | 3912 | 685 | 218 |
php74 | europe-west3 | 25162 | 84 | 793 | 1494 | 476 | 160 |
nodejs14 | us-central1 | 16341 | 192 | 1100 | 2789 | 733 | 244 |
go113 | us-central1 | 19738 | 192 | 1017 | 1757 | 607 | 213 |
java11 | us-central1 | 16545 | 190 | 1400 | 7796 | 724 | 339 |
python39 | us-central1 | 14907 | 192 | 1200 | 2302 | 804 | 248 |
ruby27 | us-central1 | 17968 | 193 | 1091 | 3559 | 667 | 229 |
dotnet3 | us-central1 | 14444 | 192 | 1197 | 3407 | 830 | 240 |
php74 | us-central1 | 16172 | 193 | 1104 | 2088 | 741 | 192 |
nodejs14 | asia-east2 | 25112 | 352 | 780 | 2086 | 477 | 142 |
go113 | asia-east2 | 28831 | 350 | 587 | 1604 | 415 | 112 |
java11 | asia-east2 | 22617 | 352 | 1093 | 5098 | 530 | 292 |
python39 | asia-east2 | 20441 | 373 | 889 | 1786 | 586 | 146 |
ruby27 | asia-east2 | 28630 | 349 | 589 | 1872 | 418 | 106 |
dotnet3 | asia-east2 | 21549 | 364 | 896 | 3197 | 556 | 184 |
php74 | asia-east2 | 26001 | 351 | 715 | 1786 | 461 | 140 |
Runtime | Region | Requests | Min [ms] | 95th pct [ms] | Max [ms] | Mean [ms] | Std Dev [ms] |
---|---|---|---|---|---|---|---|
nodejs14 | europe-west3 | 20487 | 80 | 992 | 1516 | 585 | 191 |
go113 | europe-west3 | 26846 | 83 | 796 | 2314 | 446 | 175 |
java11 | europe-west3 | 22988 | 82 | 906 | 2400 | 521 | 224 |
python39 | europe-west3 | 21902 | 85 | 806 | 1496 | 547 | 155 |
ruby27 | europe-west3 | 27023 | 82 | 703 | 2064 | 443 | 163 |
dotnet3 | europe-west3 | 17760 | 85 | 995 | 2800 | 675 | 189 |
php74 | europe-west3 | 22418 | 82 | 890 | 1712 | 534 | 167 |
nodejs14 | us-central1 | 18468 | 188 | 1006 | 1625 | 649 | 197 |
go113 | us-central1 | 20845 | 185 | 905 | 1805 | 575 | 183 |
java11 | us-central1 | 17753 | 188 | 1290 | 3199 | 675 | 303 |
python39 | us-central1 | 16048 | 188 | 1197 | 2322 | 747 | 232 |
ruby27 | us-central1 | 18306 | 185 | 1013 | 2020 | 655 | 218 |
dotnet3 | us-central1 | 17756 | 187 | 1153 | 3825 | 676 | 290 |
php74 | us-central1 | 16902 | 188 | 1098 | 2102 | 709 | 199 |
nodejs14 | asia-east2 | 22854 | 349 | 835 | 2309 | 524 | 157 |
go113 | asia-east2 | 28844 | 346 | 594 | 1749 | 415 | 112 |
java11 | asia-east2 | 21243 | 347 | 1109 | 5398 | 564 | 316 |
python39 | asia-east2 | 21013 | 367 | 805 | 1902 | 570 | 147 |
ruby27 | asia-east2 | 27958 | 343 | 604 | 1849 | 428 | 127 |
dotnet3 | asia-east2 | 21802 | 357 | 887 | 3104 | 549 | 172 |
php74 | asia-east2 | 25581 | 348 | 711 | 2224 | 468 | 151 |
Regiony
Wyniki pierwszego testu nieco mnie zaskoczyły, ponieważ bardzo dobrze wypadły tutaj funkcje osadzone w Azji (a nie najbliżej mojej geolokalizacji, jak się spodziewałem).
Dlatego postanowiłem wykonać test ponownie, aby wykluczyć różnice w czasie (ponieważ o godzinie 20:00 czasu Polskiego, w Hong Kongu była godzina 03:00). Dzięki temu mogłem sprawdzić, czy wpływ na wyniki ma tutaj fakt, że w środku nocy obciążenie centrum danych może być mniejsze.
Drugi test wykluczył jednak kwestie godziny w danym regionie, ponieważ i tym razem Azja wypadła najlepiej. Z powodu odległości można zaobserwować znacznie wyższe minimalne i maksymalne czasy odpowiedzi, jednak średnio były one i tak nieco niższe niż w przypadku Frankfurtu. W ciągu 10 minut udało się wykonać sumarycznie więcej żądań.
Najgorzej wypadł region w USA, gdzie pomimo niższych minimalnych czasów odpowiedzi, średnio były one znacznie wyższe (co idealnie pokazuje kolumna z 95 percentylem). W efekcie funkcje uruchomione w USA obsłużyły zauważalnie mniejszą liczbę żądań.
Testy starałem się wykonać w środku tygodnia, aby były jak najbardziej wiarygodne. Pod uwagę należy wziąć jednak fakt, że funkcje były bardzo prymitywne - jedyne co robiły, to odpowiadały “Hello World”. Całkiem możliwe, że w przypadku funkcji do których wysyłamy lub które zwracają jakieś dane, wyniki byłyby zupełnie inne. Zależało mi jednak na sprawdzeniu prostych funkcji, ponieważ w tym przypadku łatwo jest porównać środowiska uruchomieniowe (w przypadku bardziej złożonych implementacji duży wpływ na wydajność mogłyby mieć wykorzystane zewnętrzne zależności czy biblioteki).
Podsumowując, gdybym chciał uruchomić prostą funkcję i zależałoby mi na tym, aby obsłużyła jak największy ruch, prawdopodobnie wybrałbym któryś z regionów w Azji.
Środowiska uruchomieniowe
W przypadku środowisk uruchomieniowych spodziewałem się, że języki interpretowane dają lepsze wyniki niż języki uruchamiane w wirtualnej maszynie.
Wyniki testu częściowo potwierdziły moje podejrzenia, ponieważ najszybciej odpowiadały funkcje napisane w Go, Ruby czy PHP. Dużym zaskoczeniem były dla mnie wyniki Node.js, które są dość przeciętne. Spodziewałem się że JavaScript uplasuje się w czołówce, jednak wyniki były bardziej zbliżone do języków uruchamianych w maszynie wirtualnej.
Kompletnie nie zdziwiły mnie za to wyniki funkcji napisanych w Javie czy .NET, jednak nie spisywałbym ich na straty. Środowiska uruchomieniowe wykorzystujące maszyny wirtualne (takie jak właśnie Java - JVM, czy .NET - CLR) potrafią optymalizować uruchomiony kod, jednak nie zrobią tego od razu, ponieważ potrzebują w tym celu zebrać odpowiednią ilość statystyk. Całkiem możliwe, że funkcje które obsługują bardzo dużo żądań w czasie ciągłym (czyli takim, dzięki któremu instancja funkcji będzie żyła bardzo długo) osiągnęłyby z czasem lepsze wyniki.
Jakie z tego wnioski? Jeżeli piszemy prostą funkcję i nie zależy nam na wydajności (albo spodziewamy się małego ruchu), śmiało możemy napisać ją w języku programowania, który znamy najlepiej. Jeżeli jednak zależy nam na obsłudze jak największej ilości żądań (i jednocześnie wiemy, że instancja funkcji nie będzie długowieczna), najlepszym wyborem będą języki, które nie są uruchamiane w wirtualnej maszynie.
Zimne starty
Godziny uruchomienia testów
Run #1
- środek tygodnia, 22 czasu polskiegoRun #2
- środek tygodnia, 20:00 czasu lokalnego w danym regionie:europe-west3
(Frankfurt, Germany, Europe) - 20:00 o 20:00 czasu polskiegous-central1
(Council Bluffs, Iowa, North America) - 20:00 o 03:00 czasu polskiego (-7h)asia-east2
(Hong Kong, APAC) - 20:00 o 13:00 czasu polskiego (+7h)
Wyniki
Runtime | Region | 1st time [ms] | avg remaining [ms] | diff [ms] |
---|---|---|---|---|
nodejs14 | europe-west3 | 1176 | 49 | 1127 |
go113 | europe-west3 | 447 | 43 | 404 |
java11 | europe-west3 | 54 | 101 | -47 |
python39 | europe-west3 | 895 | 59 | 836 |
ruby27 | europe-west3 | 997 | 41 | 956 |
dotnet3 | europe-west3 | 1054 | 95 | 959 |
php74 | europe-west3 | 679 | 45 | 634 |
nodejs14 | us-central1 | 1123 | 250 | 873 |
go113 | us-central1 | 922 | 227 | 695 |
java11 | us-central1 | 1638 | 284 | 1354 |
python39 | us-central1 | 1254 | 259 | 995 |
ruby27 | us-central1 | 1430 | 193 | 1237 |
dotnet3 | us-central1 | 1536 | 261 | 1275 |
php74 | us-central1 | 1271 | 211 | 1060 |
nodejs14 | asia-east2 | 1228 | 381 | 847 |
go113 | asia-east2 | 869 | 375 | 494 |
java11 | asia-east2 | 1227 | 398 | 829 |
python39 | asia-east2 | 1433 | 375 | 1058 |
ruby27 | asia-east2 | 1126 | 388 | 738 |
dotnet3 | asia-east2 | 1023 | 410 | 613 |
php74 | asia-east2 | 904 | 420 | 484 |
Runtime | Region | 1st time [ms] | avg remaining [ms] | diff [ms] |
---|---|---|---|---|
nodejs14 | europe-west3 | 409 | 49 | 360 |
go113 | europe-west3 | 61 | 46 | 15 |
java11 | europe-west3 | 164 | 120 | 44 |
python39 | europe-west3 | 98 | 53 | 45 |
ruby27 | europe-west3 | 53 | 45 | 8 |
dotnet3 | europe-west3 | 325 | 95 | 230 |
php74 | europe-west3 | 169 | 43 | 126 |
nodejs14 | us-central1 | 1214 | 203 | 1011 |
go113 | us-central1 | 269 | 230 | 39 |
java11 | us-central1 | 819 | 284 | 535 |
python39 | us-central1 | 411 | 249 | 162 |
ruby27 | us-central1 | 310 | 215 | 95 |
dotnet3 | us-central1 | 1022 | 284 | 738 |
php74 | us-central1 | 1004 | 240 | 764 |
nodejs14 | asia-east2 | 880 | 496 | 384 |
go113 | asia-east2 | 394 | 409 | -15 |
java11 | asia-east2 | 409 | 454 | -45 |
python39 | asia-east2 | 406 | 387 | 19 |
ruby27 | asia-east2 | 405 | 409 | -4 |
dotnet3 | asia-east2 | 510 | 450 | 60 |
php74 | asia-east2 | 448 | 398 | 50 |
Regiony
W odróżnieniu od wyników czasów odpowiedzi, w przypadku zimnych startów najlepiej wypadł region, który był najbliżej mojej geolokalizacji. Zarówno pierwsza odpowiedź oraz średnia pozostałych, była najniższa w przypadku funkcji osadzonych we Frankfurcie. Najgorzej natomiast wypadła Azja, co pokrywałoby się z poprzednim testem, ponieważ funkcje w Azji miały najwyższy minimalny czas odpowiedzi.
Podobnie jak w poprzednim teście, różnice w czasie między regionami nie miały żadnego znaczenia.
Na co się zdecydować, biorąc pod uwagę wyniki tych testów? Jeżeli potrzebujemy funkcji, która jest rzadko wywoływana (jej instancja jest krótko żyjąca) i zależy nam na jak najszybszej odpowiedzi, najlepiej powinny sprawdzić się funkcje uruchomione najbliżej geolokalizacji użytkownika końcowego. Dzięki bliskości centrum danych, nie tracimy czasu na przesyłanie żądania i odpowiedzi, a wąskim gardłem jest tutaj zimny start, czyli czas potrzebny na uruchomienie instancji funkcji.
Środowiska uruchomieniowe
Tak samo jak w poprzednim teście, w przypadku różnic między środowiskami uruchomieniowymi spodziewałem się lepszych wyników w przypadku funkcji napisanych w językach interpretowanych, od tych uruchamianych w maszynie wirtualnej. Wyniki jednak mnie zaskoczyły, ponieważ czasami jakieś środowisko uruchomieniowe wypadało bardzo dobrze, a czasami dużo gorzej.
W przypadku zimnych startów na pewno większą rolę odgrywa region, w którym osadzimy funkcję, niż język w którym ją napiszemy. Języki programowania, po których spodziewałem się lepszych wyników (Python i Ruby) nie wypadły wcale lepiej od Javy i .NET, które w teorii powinny potrzebować więcej czasu na uruchomienie.
Patrząc na wyniki testu, nie potrafię jednoznacznie stwierdzić w jakim języku napisałbym funkcję, aby zapewnić jak najkrótszy zimny start. Sytuacja mogłaby ulec zmianie w przypadku bardziej złożonych implementacji i wykorzystania zewnętrznych zależności/bibliotek, ponieważ ich rozmiar i implementacja mogłyby odgrywać tutaj kluczową rolę.
Podsumowanie
Na zakończenie chciałbym zaznaczyć, że wykonane przeze mnie testy dotyczyły jedynie prostych implementacji funkcji, a wyniki mogłyby być inne w przypadku bardziej złożonych implementacji lub przesyłania większej ilości danych. Mimo wszystko najczęściej spotykam się z bardzo prostymi funkcjami i z tego też powodu przeprowadziłem takie testy. Starając się porównać środowiska uruchomieniowe, musiałem zapewnić zbliżoną implementację funkcji, aby wykluczyć wpływ dostępnych bibliotek i zależności na wyniki.
Zachęcam również do przeprowadzania własnych testów za pomocą narzędzia Google Coud Functions Comparison, ponieważ jak widać, wyniki potrafią być zaskakujące i nieoczywiste.
-
SENIOR FULLSTACK DEVELOPER (JAVA + ANGULAR) Poznań (hybrydowo) lub zdalnie UoP 14 900 - 20 590 PLN brutto
B2B 19 680 - 27 220 PLN netto -
REGULAR FULLSTACK DEVELOPER (JAVA + ANGULAR) Poznań (hybrydowo) lub zdalnie UoP 11 300 - 15 900 PLN brutto
B2B 14 950 - 21 000 PLN netto -
ZOBACZ WSZYSTKIE OGŁOSZENIA
newsletter
techniczny
Podobne wpisy
Czy wiesz, że w Angular 17 została wprowadzona alternatywa dla *ngIf?
-
SENIOR FULLSTACK DEVELOPER (JAVA + ANGULAR) Poznań (hybrydowo) lub zdalnie UoP 14 900 - 20 590 PLN brutto
B2B 19 680 - 27 220 PLN netto -
REGULAR FULLSTACK DEVELOPER (JAVA + ANGULAR) Poznań (hybrydowo) lub zdalnie UoP 11 300 - 15 900 PLN brutto
B2B 14 950 - 21 000 PLN netto -
ZOBACZ WSZYSTKIE OGŁOSZENIA