Produktionsleistung mit Stackdriver Profiler analysieren

Während Client-App- und Frontend-Webentwickler häufig Tools wie den Android Studio CPU Profiler oder die in Chrome enthaltenen Profiling-Tools verwenden, um die Leistung ihres Codes zu verbessern, sind entsprechende Techniken für Backend-Dienste nicht annähernd so zugänglich oder weit verbreitet. Stackdriver Profiler bietet diese Funktionen auch für Dienstentwickler, unabhängig davon, ob ihr Code auf der Google Cloud Platform oder an einem anderen Ort ausgeführt wird.

Das Tool erfasst Informationen zur CPU-Nutzung und Arbeitsspeicherzuweisung aus Ihren Produktionsanwendungen. Anschließend werden diese Informationen dem Quellcode der Anwendung zugeordnet. So können Sie feststellen, welche Teile der Anwendung die meisten Ressourcen beanspruchen, und außerdem die Leistungsmerkmale des Codes unter die Lupe nehmen. Der geringe Overhead der vom Tool verwendeten Erfassungstechniken macht es für den kontinuierlichen Einsatz in Produktionsumgebungen geeignet.

In diesem Codelab erfahren Sie, wie Sie Stackdriver Profiler für ein Go-Programm einrichten und welche Art von Informationen zur Anwendungsleistung das Tool liefern kann.

Lerninhalte

  • So konfigurieren Sie ein Go-Programm für die Profilerstellung mit Stackdriver Profiler.
  • Wie Sie die Leistungsdaten mit Stackdriver Profiler erfassen, aufrufen und analysieren.

Voraussetzungen

  • Ein Google Cloud Platform-Projekt
  • Ein Browser, z. B. Chrome oder Firefox
  • Erfahrung mit standardmäßigen Linux-Texteditoren wie Vim, EMACs oder Nano

Wie werden Sie diese Anleitung verwenden?

Nur lesen Lesen und Übungen durchführen

Wie würden Sie Ihre Erfahrung mit der Google Cloud Platform bewerten?

Anfänger Mittelstufe Fortgeschritten

Einrichtung der Umgebung im eigenen Tempo

Wenn Sie noch kein Google-Konto (Gmail oder Google Apps) haben, müssen Sie eins erstellen. Melden Sie sich in der Google Cloud Platform Console (console.cloud.google.com) an und erstellen Sie ein neues Projekt:

Screenshot vom 10.02.2016, 12:45:26.png

Notieren Sie sich die Projekt-ID, also den projektübergreifend nur einmal vorkommenden Namen eines Google Cloud-Projekts. Der oben angegebene Name ist bereits vergeben und kann leider nicht mehr verwendet werden. Sie wird in diesem Codelab später als PROJECT_ID bezeichnet.

Als Nächstes müssen Sie die Abrechnung in der Cloud Console aktivieren, um Google Cloud-Ressourcen verwenden zu können.

Dieses Codelab sollte Sie nicht mehr als ein paar Dollar kosten, aber es könnte mehr sein, wenn Sie sich für mehr Ressourcen entscheiden oder wenn Sie sie laufen lassen (siehe Abschnitt „Bereinigen“ am Ende dieses Dokuments).

Neuen Nutzern der Google Cloud Platform steht eine kostenlose Testversion mit einem Guthaben von 300$ zur Verfügung.

Google Cloud Shell

Während Sie Google Cloud von Ihrem Laptop aus per Fernzugriff nutzen können, wird in diesem Codelab Google Cloud Shell verwendet, eine Befehlszeilenumgebung, die in der Cloud ausgeführt wird.

Google Cloud Shell aktivieren

Klicken Sie in der GCP Console oben rechts in der Symbolleiste auf das Cloud Shell-Symbol:

Klicken Sie dann auf "Cloud Shell starten":

Die Bereitstellung und Verbindung mit der Umgebung dauert nur einen Moment:

Diese virtuelle Maschine verfügt über sämtliche Entwicklertools, die Sie benötigen. Sie bietet ein Basisverzeichnis mit 5 GB nichtflüchtigem Speicher und läuft auf der Google Cloud, wodurch Netzwerkleistung und Authentifizierung deutlich verbessert werden. Sie können die meisten, wenn nicht sogar alle Schritte in diesem Lab einfach mit einem Browser oder Ihrem Google Chromebook durchführen.

Sobald Sie mit Cloud Shell verbunden sind, sollten Sie sehen, dass Sie bereits authentifiziert sind und das Projekt bereits auf Ihre PROJECT_ID eingestellt ist.

Führen Sie in Cloud Shell den folgenden Befehl aus, um zu prüfen, ob Sie authentifiziert sind:

gcloud auth list

Befehlsausgabe

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

Befehlsausgabe

[core]
project = <PROJECT_ID>

Ist dies nicht der Fall, können Sie die Einstellung mit diesem Befehl vornehmen:

gcloud config set project <PROJECT_ID>

Befehlsausgabe

Updated property [core/project].

Rufen Sie in der Cloud Console die Profiler-Benutzeroberfläche auf, indem Sie in der linken Navigationsleiste auf „Profiler“ klicken:

Alternativ können Sie die Suchleiste der Cloud Console verwenden, um zur Profiler-Benutzeroberfläche zu gelangen. Geben Sie einfach „Stackdriver Profiler“ ein und wählen Sie das gefundene Element aus. In beiden Fällen sollte die Profiler-Benutzeroberfläche mit der Meldung „Keine anzuzeigenden Daten“ angezeigt werden (siehe unten). Das Projekt ist neu, daher wurden noch keine Profiling-Daten erhoben.

Jetzt ist es an der Zeit, etwas zu profilieren.

Wir verwenden eine einfache synthetische Go-Anwendung, die auf GitHub verfügbar ist. Führen Sie im Cloud Shell-Terminal, das Sie noch geöffnet haben (und während in der Profiler-Benutzeroberfläche noch die Meldung „Keine Daten zum Anzeigen“ angezeigt wird), den folgenden Befehl aus:

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

Wechseln Sie dann in das Anwendungsverzeichnis:

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

Das Verzeichnis enthält die Datei „main.go“, eine synthetische App, in der der Profiling-Agent aktiviert ist:

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)
        }
        ...
}

Der Profiling-Agent erfasst standardmäßig CPU-, Heap- und Thread-Profile. Der Code hier ermöglicht die Erfassung von Mutex-Profilen (auch als „Konflikt“ bezeichnet).

Führen Sie das Programm nun aus:

$ go run main.go

Während das Programm ausgeführt wird, erfasst der Profiler-Agent in regelmäßigen Abständen Profile der fünf konfigurierten Typen. Die Erfassung erfolgt zufällig über die Zeit (mit einer durchschnittlichen Rate von einem Profil pro Minute für jeden Typ). Es kann also bis zu drei Minuten dauern, bis jeder Typ erfasst wird. Das Programm informiert Sie, wenn ein Profil erstellt wird. Die Meldungen werden durch das Flag DebugLogging in der obigen Konfiguration aktiviert. Andernfalls wird der Agent im Hintergrund ausgeführt:

$ 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
...

Die Benutzeroberfläche wird kurz nach der Erfassung des ersten Profils aktualisiert. Danach wird sie nicht mehr automatisch aktualisiert. Wenn Sie die neuen Daten sehen möchten, müssen Sie die Profiler-Benutzeroberfläche manuell aktualisieren. Klicken Sie dazu zweimal auf die Schaltfläche „Jetzt“ in der Zeitintervallauswahl:

Nachdem die Benutzeroberfläche aktualisiert wurde, sehen Sie in etwa Folgendes:

In der Auswahl für den Profiltyp werden die fünf verfügbaren Profiltypen angezeigt:

Sehen wir uns nun die einzelnen Profiltypen und einige wichtige Funktionen der Benutzeroberfläche an und führen dann einige Tests durch. An diesem Punkt benötigen Sie das Cloud Shell-Terminal nicht mehr. Sie können es beenden, indem Sie STRG+C drücken und „exit“ eingeben.

Nachdem wir einige Daten erhoben haben, sehen wir sie uns genauer an. Wir verwenden eine synthetische App (der Quellcode ist auf GitHub verfügbar), die Verhaltensweisen simuliert, die für verschiedene Arten von Leistungsproblemen in der Produktion typisch sind.

CPU-intensiver Code

Wählen Sie den CPU-Profiltyp aus. Nachdem die UI geladen wurde, sehen Sie im Flammen-Diagramm die vier Blattblöcke für die Funktion load, die zusammen den gesamten CPU-Verbrauch ausmachen:

Diese Funktion wurde speziell dafür entwickelt, viele CPU-Zyklen zu verbrauchen, indem sie eine enge Schleife ausführt:

main.go

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

Die Funktion wird indirekt über vier Aufrufpfade von busyloop() aufgerufen: busyloop → {foo1, foo2} → {bar, baz} → load. Die Breite eines Funktionskästchens gibt die relativen Kosten des jeweiligen Aufrufpfads an. In diesem Fall haben alle vier Pfade ungefähr die gleichen Kosten. In einem echten Programm sollten Sie sich auf die Optimierung von Aufruf-Pfaden konzentrieren, die in Bezug auf die Leistung am wichtigsten sind. Im Flammen-Diagramm werden die teureren Pfade durch größere Kästen visuell hervorgehoben, sodass sie leicht zu erkennen sind.

Mit dem Filter für Profildaten können Sie die Darstellung weiter eingrenzen. Fügen Sie beispielsweise einen Filter vom Typ „Stapeldarstellung“ hinzu und geben Sie „baz“ als Filterstring an. Es sollte etwa wie im Screenshot unten aussehen, wo nur zwei der vier Aufruf-Pfade zu load() angezeigt werden. Diese beiden Pfade sind die einzigen, die eine Funktion mit dem String „baz“ im Namen durchlaufen. Diese Filterung ist nützlich, wenn Sie sich auf einen Unterabschnitt eines größeren Programms konzentrieren möchten, z. B. weil Sie nur einen Teil davon besitzen.

Speicherintensiver Code

Wechseln Sie nun zum Profiltyp „Heap“. Entfernen Sie alle Filter, die Sie in früheren Tests erstellt haben. Sie sollten jetzt ein Flame-Diagramm sehen, in dem allocImpl, das von alloc aufgerufen wird, als Hauptverbraucher von Arbeitsspeicher in der App angezeigt wird:

Die Übersichtstabelle über dem Flame-Diagramm zeigt, dass die gesamte Speichernutzung in der App durchschnittlich etwa 57,4 MiB beträgt.Der Großteil davon wird von der Funktion allocImpl zugewiesen. Das ist angesichts der Implementierung dieser Funktion nicht überraschend:

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))
        }
}

Die Funktion wird einmal ausgeführt und weist 64 MiB in kleineren Blöcken zu. Anschließend werden Zeiger auf diese Blöcke in einer globalen Variablen gespeichert, um sie vor der Garbage Collection zu schützen. Die vom Profiler angegebene Speichermenge weicht leicht von 64 MiB ab. Der Go-Heap-Profiler ist ein statistisches Tool. Die Messungen sind also ressourcenschonend, aber nicht bytegenau. Ein Unterschied von etwa 10% ist nicht ungewöhnlich.

E/A-intensiver Code

Wenn Sie in der Profiltypauswahl „Threads“ auswählen, wird das Profil als Flame-Diagramm dargestellt, in dem die Funktionen wait und waitImpl den größten Teil der Breite einnehmen:

In der Zusammenfassung über dem Flame-Diagramm sehen Sie, dass es 100 Goroutines gibt, deren Aufrufstack von der Funktion wait aus wächst. Das ist genau richtig, da der Code, der diese Wartezeiten initiiert, so aussieht:

main.go

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

Dieser Profiltyp ist nützlich, um zu sehen, ob das Programm unerwartet viel Zeit mit Warten (z. B. auf die Ein-/Ausgabe) verbringt. Solche Aufrufstacks werden vom CPU-Profiler in der Regel nicht erfasst, da sie keinen erheblichen Teil der CPU-Zeit in Anspruch nehmen. Sie sollten „Stacks ausblenden“-Filter häufig mit Threads-Profilen verwenden, z. B. um alle Stacks auszublenden, die mit einem Aufruf von gopark, enden, da diese oft inaktive Goroutinen sind und weniger interessant als solche, die auf E/A warten.

Der Profiltyp „Threads“ kann auch dabei helfen, Stellen im Programm zu identifizieren, an denen Threads lange auf einen Mutex warten, der zu einem anderen Teil des Programms gehört. Der folgende Profiltyp ist dafür jedoch besser geeignet.

Konkurrenzintensiver Code

Der Typ des Konfliktprofils gibt die am häufigsten benötigten Sperren im Programm an. Dieser Profiltyp ist für Go-Programme verfügbar, muss aber explizit aktiviert werden, indem „MutexProfiling: true“ im Agent-Konfigurationscode angegeben wird. Bei der Erfassung wird mit dem Messwert „Contentions“ (Konflikte) aufgezeichnet, wie oft eine bestimmte Sperre, wenn sie von einer Goroutine A entsperrt wird, eine andere Goroutine B darauf wartet, dass die Sperre entsperrt wird. Außerdem wird unter dem Messwert „Verzögerung“ die Zeit aufgezeichnet, die die blockierte Goroutine auf die Sperre gewartet hat. In diesem Beispiel gibt es einen einzelnen Konfliktstapel und die gesamte Wartezeit für die Sperre betrug 11, 03 Sekunden:

Der Code, mit dem dieses Profil generiert wird, besteht aus vier Goroutinen, die um einen Mutex konkurrieren:

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)
        }
}

In diesem Lab haben Sie gelernt, wie ein Go-Programm für die Verwendung mit Stackdriver Profiler konfiguriert werden kann. Außerdem haben Sie gelernt, wie Sie die Leistungsdaten mit diesem Tool erfassen, aufrufen und analysieren. Sie können Ihre neuen Kenntnisse jetzt auf die echten Dienste anwenden, die Sie in der Google Cloud Platform ausführen.

Sie haben gelernt, wie Sie Stackdriver Profiler konfigurieren und verwenden.

Weitere Informationen

Lizenz

Dieser Text ist mit einer Creative Commons Attribution 2.0 Generic License lizenziert.