Analizowanie wydajności środowiska produkcyjnego za pomocą Stackdriver Profiler

Programiści aplikacji klienckich i stron internetowych często używają narzędzi takich jak profiler procesora Android Studio czy narzędzia do profilowania w Chrome, aby poprawić wydajność kodu. Podobne techniki nie są jednak tak łatwo dostępne ani tak powszechnie stosowane przez osoby pracujące nad usługami backendu. Stackdriver Profiler udostępnia te same możliwości programistom usług, niezależnie od tego, czy ich kod jest uruchamiany na platformie Google Cloud Platform, czy w innym miejscu.

Narzędzie zbiera informacje o wykorzystaniu procesora i alokacji pamięci przez aplikacje produkcyjne. Przypisuje te informacje do kodu źródłowego aplikacji, co pomaga identyfikować części aplikacji, które zużywają najwięcej zasobów, i w inny sposób wyjaśniać charakterystykę wydajności kodu. Niskie obciążenie związane z technikami zbierania danych stosowanymi przez to narzędzie sprawia, że nadaje się ono do ciągłego użytku w środowiskach produkcyjnych.

Z tego ćwiczenia z programowania dowiesz się, jak skonfigurować Stackdriver Profiler dla programu w języku Go i jakie informacje o wydajności aplikacji może on przedstawiać.

Czego się nauczysz

  • Jak skonfigurować program w Go do profilowania za pomocą Stackdriver Profiler.
  • Jak zbierać, wyświetlać i analizować dane o wydajności za pomocą Stackdriver Profiler.

Czego potrzebujesz

  • projekt Google Cloud Platform,
  • przeglądarka, np. Chrome lub Firefox;
  • Znajomość standardowych edytorów tekstu systemu Linux, takich jak Vim, EMACS lub Nano.

Jak zamierzasz korzystać z tego samouczka?

Tylko przeczytaj Przeczytaj i wykonaj ćwiczenia

Jak oceniasz korzystanie z Google Cloud Platform?

Początkujący Średnio zaawansowany Zaawansowany

Samodzielne konfigurowanie środowiska

Jeśli nie masz jeszcze konta Google (Gmail lub Google Apps), musisz je utworzyć. Zaloguj się w konsoli Google Cloud Platform (console.cloud.google.com) i utwórz nowy projekt:

Screenshot from 2016-02-10 12:45:26.png

Zapamiętaj identyfikator projektu, czyli unikalną nazwę we wszystkich projektach Google Cloud (podana powyżej nazwa jest już zajęta i nie będzie działać w Twoim przypadku). W dalszej części tego laboratorium będzie on nazywany PROJECT_ID.

Następnie musisz włączyć płatności w konsoli Cloud, aby móc korzystać z zasobów Google Cloud.

Wykonanie tego samouczka nie powinno kosztować więcej niż kilka dolarów, ale może okazać się droższe, jeśli zdecydujesz się wykorzystać więcej zasobów lub pozostawisz je uruchomione (patrz sekcja „Czyszczenie” na końcu tego dokumentu).

Nowi użytkownicy Google Cloud Platform mogą skorzystać z bezpłatnego okresu próbnego, w którym mają do dyspozycji środki w wysokości 300 USD.

Google Cloud Shell

Google Cloud można obsługiwać zdalnie z laptopa, ale aby uprościć konfigurację w tym laboratorium, będziemy używać Google Cloud Shell, czyli środowiska wiersza poleceń działającego w chmurze.

Aktywuj Google Cloud Shell

W konsoli GCP kliknij ikonę Cloud Shell na pasku narzędzi w prawym górnym rogu:

Następnie kliknij „Uruchom Cloud Shell”:

Udostępnienie środowiska i połączenie się z nim powinno zająć tylko kilka chwil:

Ta maszyna wirtualna zawiera wszystkie potrzebne narzędzia dla programistów. Zawiera stały katalog domowy o pojemności 5 GB i działa w Google Cloud, co znacznie zwiększa wydajność sieci i uwierzytelniania. Większość, jeśli nie wszystkie, zadań w tym laboratorium można wykonać za pomocą przeglądarki lub Chromebooka Google.

Po połączeniu z Cloud Shell zobaczysz, że jesteś już uwierzytelniony, a projekt jest już ustawiony na PROJECT_ID.

Aby potwierdzić, że masz autoryzację, uruchom w Cloud Shell to polecenie:

gcloud auth list

Wynik polecenia

Credentialed accounts:
 - <myaccount>@<mydomain>.com (active)
gcloud config list project

Wynik polecenia

[core]
project = <PROJECT_ID>

Jeśli nie, możesz ustawić go za pomocą tego polecenia:

gcloud config set project <PROJECT_ID>

Wynik polecenia

Updated property [core/project].

W konsoli Cloud otwórz interfejs Profilera, klikając „Profiler” na pasku nawigacyjnym po lewej stronie:

Możesz też użyć paska wyszukiwania w konsoli Cloud, aby przejść do interfejsu Profilera: wpisz „Stackdriver Profiler” i wybierz znaleziony element. W obu przypadkach powinien wyświetlić się interfejs programu profilującego z komunikatem „Brak danych do wyświetlenia” podobnym do tego poniżej. Projekt jest nowy, więc nie ma jeszcze żadnych zebranych danych profilowania.

Czas teraz coś profilować.

Użyjemy prostej syntetycznej aplikacji Go dostępnej w GitHub. W terminalu Cloud Shell, który jest nadal otwarty (i gdy w interfejsie Profilera nadal wyświetla się komunikat „Brak danych do wyświetlenia”), uruchom to polecenie:

$ go get -u github.com/GoogleCloudPlatform/golang-samples/profiler/...

Następnie przejdź do katalogu aplikacji:

$ cd ~/gopath/src/github.com/GoogleCloudPlatform/golang-samples/profiler/hotapp

Katalog zawiera plik „main.go”, który jest syntetyczną aplikacją z włączonym agentem profilowania:

main.go

...
import (
        ...
        "cloud.google.com/go/profiler"
)
...
func main() {
        err := profiler.Start(profiler.Config{
                Service:        "hotapp-service",
                DebugLogging:   true,
                MutexProfiling: true,
        })
        if err != nil {
                log.Fatalf("failed to start the profiler: %v", err)
        }
        ...
}

Agent profilowania domyślnie zbiera profile procesora, sterty i wątków. Ten kod umożliwia zbieranie profili mutexów (nazywanych też „konfliktami”).

Teraz uruchom program:

$ go run main.go

Podczas działania programu agent profilowania będzie okresowo zbierać profile 5 skonfigurowanych typów. Zbieranie danych jest losowe w czasie (średnio 1 profil na minutę dla każdego typu), więc zebranie wszystkich typów może potrwać do 3 minut. Program informuje o utworzeniu profilu. Wiadomości są włączane przez flagę DebugLogging w konfiguracji powyżej. W przeciwnym razie agent działa w trybie cichym:

$ go run main.go
2018/03/28 15:10:24 profiler has started
2018/03/28 15:10:57 successfully created profile THREADS
2018/03/28 15:10:57 start uploading profile
2018/03/28 15:11:19 successfully created profile CONTENTION
2018/03/28 15:11:30 start uploading profile
2018/03/28 15:11:40 successfully created profile CPU
2018/03/28 15:11:51 start uploading profile
2018/03/28 15:11:53 successfully created profile CONTENTION
2018/03/28 15:12:03 start uploading profile
2018/03/28 15:12:04 successfully created profile HEAP
2018/03/28 15:12:04 start uploading profile
2018/03/28 15:12:04 successfully created profile THREADS
2018/03/28 15:12:04 start uploading profile
2018/03/28 15:12:25 successfully created profile HEAP
2018/03/28 15:12:25 start uploading profile
2018/03/28 15:12:37 successfully created profile CPU
...

Interfejs użytkownika zaktualizuje się wkrótce po zebraniu pierwszego profilu. Po tym czasie nie będzie się ona automatycznie aktualizować, więc aby zobaczyć nowe dane, musisz ręcznie odświeżyć interfejs Profilera. Aby to zrobić, kliknij dwukrotnie przycisk Teraz w selektorze przedziału czasu:

Po odświeżeniu interfejsu zobaczysz coś takiego:

Selektor typów profili zawiera 5 dostępnych typów profili:

Przyjrzyjmy się teraz poszczególnym typom profili i ważnym funkcjom interfejsu, a potem przeprowadźmy kilka eksperymentów. Na tym etapie nie potrzebujesz już terminala Cloud Shell, więc możesz go zamknąć, naciskając CTRL-C i wpisując „exit”.

Zebraliśmy już trochę danych, więc przyjrzyjmy się im bliżej. Używamy syntetycznej aplikacji (źródło jest dostępne w GitHubie), która symuluje zachowania typowe dla różnych rodzajów problemów z wydajnością w wersji produkcyjnej.

Kod wymagający dużej mocy obliczeniowej procesora

Wybierz typ profilu procesora. Po wczytaniu interfejsu zobaczysz na wykresie płomieniowym 4 bloki liściowe funkcji load, które łącznie odpowiadają za całe zużycie procesora:

Ta funkcja została specjalnie napisana tak, aby zużywać dużo cykli procesora przez uruchamianie ciasnej pętli:

main.go

func load() {
        for i := 0; i < (1 << 20); i++ {
        }
}

Funkcja jest wywoływana pośrednio z funkcji busyloop() za pomocą 4 ścieżek wywołań: busyloop → {foo1, foo2} → {bar, baz} → load. Szerokość pola funkcji reprezentuje względny koszt konkretnej ścieżki wywołania. W tym przypadku wszystkie 4 ścieżki mają podobny koszt. W prawdziwym programie warto skupić się na optymalizacji ścieżek połączeń, które mają największe znaczenie pod względem skuteczności. Wykres płomieniowy, który wizualnie podkreśla bardziej kosztowne ścieżki za pomocą większych pól, ułatwia ich identyfikację.

Aby jeszcze bardziej doprecyzować wyświetlanie, możesz użyć filtra danych profilu. Spróbuj na przykład dodać filtr „Pokaż stosy” z ciągiem filtra „baz”. Powinien wyświetlić się ekran podobny do tego poniżej, na którym widoczne są tylko 2 z 4 ścieżek połączeń do load(). Te 2 ścieżki są jedynymi, które przechodzą przez funkcję z ciągiem znaków „baz” w nazwie. Takie filtrowanie jest przydatne, gdy chcesz skupić się na części większego programu (np. dlatego, że jesteś właścicielem tylko jego części).

Kod wymagający dużej ilości pamięci

Teraz przełącz się na typ profilu „Heap”. Pamiętaj, aby usunąć wszystkie filtry utworzone w poprzednich eksperymentach. Powinien pojawić się wykres płomieniowy, na którym allocImpl, wywoływany przez alloc, jest wyświetlany jako główny odbiorca pamięci w aplikacji:

Tabela podsumowująca nad wykresem płomieniowym wskazuje, że łączna ilość używanej pamięci w aplikacji wynosi średnio ok.57,4 MiB, a większość z niej jest przydzielana przez funkcję allocImpl. Nie jest to zaskakujące, biorąc pod uwagę implementację tej funkcji:

main.go

func allocImpl() {
        // Allocate 64 MiB in 64 KiB chunks
        for i := 0; i < 64*16; i++ {
                mem = append(mem, make([]byte, 64*1024))
        }
}

Funkcja jest wykonywana raz, przydzielając 64 MiB w mniejszych fragmentach, a następnie przechowując wskaźniki do tych fragmentów w zmiennej globalnej, aby chronić je przed odzyskiwaniem pamięci. Pamiętaj, że ilość pamięci podana przez profiler jest nieco inna niż 64 MiB: profiler sterty Go to narzędzie statystyczne, więc pomiary są mało obciążające, ale nie są dokładne co do bajta. Nie zdziw się, jeśli zobaczysz taką różnicę (ok. 10%).

Kod wymagający dużej liczby operacji wejścia/wyjścia

Jeśli w selektorze typu profilu wybierzesz „Wątki”, wyświetlacz przełączy się na wykres płomieniowy, na którym większość szerokości zajmują funkcje waitwaitImpl:

Z podsumowania nad wykresem płomieniowym wynika, że 100 gorutyn powiększa stos wywołań z funkcji wait. Jest to prawidłowe, ponieważ kod, który inicjuje te oczekiwania, wygląda tak:

main.go

func main() {
        ...
        // Simulate some waiting goroutines.
        for i := 0; i < 100; i++ {
                go wait()
        }

Ten typ profilu jest przydatny do sprawdzania, czy program nie spędza nieoczekiwanie dużo czasu na oczekiwaniu (np. na operacje wejścia-wyjścia). Takie stosy wywołań nie są zwykle próbkowane przez profiler procesora, ponieważ nie zużywają znaczącej części czasu procesora. W przypadku profili wątków często warto używać filtrów „Ukryj stosy” – na przykład, aby ukryć wszystkie stosy kończące się wywołaniem funkcji gopark,, ponieważ są to często nieaktywne gorutyny i są mniej interesujące niż te, które czekają na operacje wejścia/wyjścia.

Typ profilu wątków może też pomóc w zidentyfikowaniu punktów w programie, w których wątki długo czekają na mutex należący do innej części programu, ale do tego celu bardziej przydatny jest następujący typ profilu.

Kod wymagający dużej liczby operacji

Typ profilu rywalizacji określa najbardziej „pożądane” blokady w programie. Ten typ profilu jest dostępny w przypadku programów w Go, ale musi być jawnie włączony przez podanie wartości „MutexProfiling: true” w kodzie konfiguracji agenta. Kolekcja działa poprzez rejestrowanie (w ramach rodzaju danych „Contentions”) liczby przypadków, w których określona blokada, odblokowywana przez gorutynę A, miała inną gorutynę B oczekującą na odblokowanie. Rejestruje też (w ramach danych „Opóźnienie”) czas, przez jaki zablokowana gorutyna czekała na blokadę. W tym przykładzie jest jeden stos rywalizacji, a łączny czas oczekiwania na blokadę wynosił 11,03 sekundy:

Kod, który generuje ten profil, składa się z 4 goroutines walczących o muteks:

main.go

func contention(d time.Duration) {
        contentionImpl(d)
}

func contentionImpl(d time.Duration) {
        for {
                mu.Lock()
                time.Sleep(d)
                mu.Unlock()
        }
}
...
func main() {
        ...
        for i := 0; i < 4; i++ {
                go contention(time.Duration(i) * 50 * time.Millisecond)
        }
}

Z tego modułu dowiedziałeś(-aś) się, jak skonfigurować program w języku Go do używania z Stackdriver Profiler. Dowiedziałeś się też, jak za pomocą tego narzędzia zbierać, wyświetlać i analizować dane o skuteczności. Możesz teraz wykorzystać nową umiejętność w przypadku rzeczywistych usług działających w Google Cloud Platform.

Wiesz już, jak skonfigurować i używać Stackdriver Profiler.

Więcej informacji

Licencja

To zadanie jest licencjonowane na podstawie ogólnej licencji Creative Commons Attribution 2.0.