10 lat temu, Google stanęło w obliczu krytycznego wąskiego gardła spowodowanego ekstremalnie długim czasem kompilacji C++ i potrzebowało zupełnie nowego sposobu na rozwiązanie tego problemu. Inżynierowie Google podjęli wyzwanie, tworząc nowy język o nazwie Go (aka Golang). Nowy język Go zapożycza najlepsze części C++ (przede wszystkim jego wydajność i funkcje bezpieczeństwa) i łączy je z szybkością Pythona, aby umożliwić Go szybkie wykorzystanie wielu rdzeni przy jednoczesnej możliwości implementacji współbieżności.

Tutaj, w Coralogix, parsujemy logi naszych klientów, aby dać im wgląd w czasie rzeczywistym, alerty i meta-dane na temat ich logów. Aby to zrobić, faza parsowania, która jest bardzo złożona i obciążona tonami reguł dla każdej usługi linii logów, musi być niezwykle szybka. To jeden z powodów, dla których zdecydowaliśmy się na użycie języka Go.

Nowa usługa działa teraz w pełnym wymiarze godzin na produkcji i choć widzimy świetne rezultaty, musi działać na wysokowydajnych maszynach. Ponad kilkadziesiąt miliardów logów jest przetwarzanych każdego dnia przez usługę Go, która działa na instancji AWS m4.2xlarge z 8 procesorami i 36 GB pamięci.

Na tym etapie, mogliśmy zakończyć dzień z poczuciem, że wszystko działa dobrze, ale to nie jest sposób, w jaki działamy w Coralogix. Chcieliśmy mieć więcej funkcji (wydajność, itp.) używając mniej (instancje AWS). Aby to poprawić, musieliśmy najpierw zrozumieć naturę naszych wąskich gardeł i jak możemy je zredukować lub całkowicie wyeliminować.

Postanowiliśmy uruchomić profilowanie Golanga na naszej usłudze i sprawdzić, co dokładnie powoduje wysokie zużycie CPU, aby zobaczyć, czy możemy to zoptymalizować.

Po pierwsze, uaktualniliśmy do najnowszej stabilnej wersji Go (kluczowa część cyklu życia oprogramowania). Byliśmy na wersji Go v1.12.4, a najnowsza to 1.13.8. Wydanie 1.13, zgodnie z dokumentacją, miało znaczące ulepszenia w bibliotece runtime i kilku innych komponentach, które głównie wykorzystywały użycie pamięci. Po drugie, aby lepiej zrozumieć nasz proces i zrozumieć, gdzie spędzamy czas i zasoby, zaczęliśmy profilować.

Profilowanie różnych usług i języków programowania może wydawać się skomplikowane i onieśmielające, ale w rzeczywistości jest całkiem proste w Go i można je opisać za pomocą kilku poleceń. Go posiada dedykowane narzędzie o nazwie 'pprof’, które powinno być włączone w twojej aplikacji poprzez nasłuchiwanie trasy (domyślny port 6060) i użycie pakietu Go do zarządzania połączeniami http:

import _ "net/http/pprof"

Potem zainicjuj następujące elementy w swojej głównej funkcji lub pod pakietem trasy:

go func() {log.Println(http.ListenAndServe("localhost:6060", nil))}()

Teraz możesz uruchomić swoją usługę i połączyć się z

http://localhost:6060/debug/pprof

Pełną dokumentację Go można znaleźć tutaj.

Domyślnym profilowaniem dla pprof będzie 30 sekundowe próbkowanie użycia procesora. Istnieje kilka różnych ścieżek, które umożliwiają próbkowanie dla użycia CPU, użycia sterty i innych.

Skupiliśmy się na naszym użyciu CPU, więc wykonaliśmy 30 sekundowe profilowanie w produkcji i odkryliśmy to, co widzisz na poniższym obrazku (przypomnienie: to jest po aktualizacji naszej wersji Go i zmniejszeniu wewnętrznych części Go do minimum):

Profilowanie Go – Coralogix

Jak widać, znaleźliśmy wiele aktywności pakietów runtime, co wskazywało konkretnie na aktywność GC → Prawie 29% naszego CPU (tylko 20 najbardziej zużytych obiektów) jest wykorzystywane przez GC. Ponieważ Go GC jest dość szybki i ładnie zoptymalizowany, najlepszą praktyką jest nie zmieniać go ani nie modyfikować, a ponieważ nasze zużycie pamięci było bardzo niskie (w porównaniu do naszej poprzedniej wersji Go), głównym podejrzanym był wysoki wskaźnik alokacji obiektów.

Jeśli tak jest, są dwie rzeczy, które możemy zrobić:

  • Dostroić aktywność Go GC, aby dostosować się do zachowania naszego serwisu, czyli – opóźnić jego wyzwalacz, aby aktywować GC rzadziej. To zmusi nas do kompensacji z większą ilością pamięci.
  • Znajdź funkcję, obszar lub linię w naszym kodzie, która alokuje zbyt wiele obiektów.

Patrząc na nasz typ instancji, było jasne, że mamy dużo pamięci do stracenia i jesteśmy obecnie związani przez CPU maszyny. Więc po prostu zmieniliśmy ten stosunek. Golang, od swoich wczesnych dni, posiada flagę, której większość programistów nie jest świadoma, zwaną GOGC. Ta flaga, z domyślną wartością 100, po prostu mówi systemowi kiedy uruchomić GC. Domyślnie proces GC zostanie uruchomiony za każdym razem, gdy sterta osiągnie 100% swojego początkowego rozmiaru. Zmiana tej wartości na wyższą spowoduje opóźnienie wyzwalania GC, a obniżenie jej spowoduje szybsze wyzwalanie GC. Zaczęliśmy testować kilka różnych wartości i najlepsza wydajność dla naszego celu została osiągnięta przy użyciu: GOGC=2000.

To natychmiast zwiększyło nasze użycie pamięci z ~200MB do ~2.7GB (To po tym jak zużycie pamięci spadło z powodu naszej aktualizacji wersji Go) i zmniejszyło nasze użycie CPU o ~10%.
Następujący zrzut ekranu demonstruje wyniki benchmarku:

Wyniki GOGC =2000 – benchmark Coralogix

Główne 4 funkcje zużywające CPU to funkcje naszych usług, co ma sens. Całkowite użycie GC wynosi teraz ~13%, mniej niż połowa poprzedniego zużycia(!)

Mogliśmy na tym poprzestać, ale zdecydowaliśmy się odkryć gdzie i dlaczego alokujemy tak wiele obiektów. Wiele razy jest ku temu dobry powód (na przykład w przypadku przetwarzania strumieniowego, gdzie tworzymy wiele nowych obiektów dla każdej otrzymanej wiadomości i musimy się ich pozbyć, ponieważ są nieistotne dla następnej wiadomości), ale są przypadki, w których istnieje łatwy sposób na optymalizację i drastyczne zmniejszenie tworzenia obiektów.

Na początek uruchommy to samo polecenie co poprzednio z jedną małą zmianą, aby wykonać zrzut sterty:

http://localhost:6060/debug/pprof/heap

W celu odpytywania pliku wynikowego możesz uruchomić następujące polecenie w swoim folderze kodu, aby przeanalizować zrzut:

go tool pprof -alloc_objects <HEAP.PROFILE.FILE>

nasz snapshot wyglądał tak:

Wszystko wydawało się sensowne z wyjątkiem trzeciego wiersza, który jest funkcją monitorującą raportującą do naszego eksportera prometeusza na koniec każdej fazy parsowania reguł Coralogix. Aby dostać się głębiej, uruchomiliśmy następującą komendę:

list <FunctionName>

Na przykład:

list reportRuleExecution

I wtedy otrzymaliśmy następujące wyniki:

.

Leave a comment

Twój adres e-mail nie zostanie opublikowany.