1. Übersicht
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. Cloud Profiler bietet diese Funktionen auch für Dienstentwickler, unabhängig davon, ob ihr Code in 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. Die geringen Gemeinkosten der vom Tool verwendeten Erfassungstechniken machen es für den kontinuierlichen Einsatz in Produktionsumgebungen geeignet.
In diesem Codelab erfahren Sie, wie Sie Cloud Profiler für ein Go-Programm einrichten und welche Art von Informationen zur Anwendungsleistung das Tool liefern kann.
Lerninhalte
- Hier erfahren Sie, wie Sie ein Go-Programm für die Profilerstellung mit Cloud Profiler konfigurieren.
- Wie Sie die Leistungsdaten mit Cloud Profiler erfassen, aufrufen und analysieren.
Voraussetzungen
- 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?
Wie würden Sie Ihre Erfahrung mit der Google Cloud Platform bewerten?
2. Einrichtung und Anforderungen
Umgebung zum selbstbestimmten Lernen einrichten
- Melden Sie sich in der Cloud Console an und erstellen Sie ein neues Projekt oder verwenden Sie ein vorhandenes Projekt. Wenn Sie noch kein Gmail- oder Google Workspace-Konto haben, müssen Sie eines erstellen.



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 später in diesem Codelab 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.
Die Durchführung dieses Codelabs sollte keine oder nur geringe Kosten verursachen. Folgen Sie bitte der Anleitung im Abschnitt „Bereinigen“, in der Sie erfahren, wie Sie Ressourcen herunterfahren können, damit nach Abschluss dieser Anleitung keine Gebühren anfallen. Neue Nutzer von Google Cloud kommen für das Programm für kostenlose Testversionen mit einem Guthaben von 300$ infrage.
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.
Cloud Shell aktivieren
- Klicken Sie in der Cloud Console auf Cloud Shell aktivieren
.

Wenn Sie die Cloud Shell zuvor noch nicht gestartet haben, wird ein Fenster mit einer Beschreibung eingeblendet. Klicken Sie in diesem Fall einfach auf Weiter. So sieht dieses Fenster aus:

Das Herstellen der Verbindung mit der Cloud Shell sollte nur wenige Augenblicke dauern.

Auf dieser virtuellen Maschine sind alle Entwicklungstools installiert, die Sie benötigen. Sie bietet ein Basisverzeichnis mit 5 GB nichtflüchtigem Speicher und läuft in Google Cloud, was die Netzwerkleistung und Authentifizierung erheblich verbessert. Die meisten, wenn nicht sogar alle Aufgaben in diesem Codelab können mit einem Browser oder Ihrem Chromebook erledigt werden.
Sobald die Verbindung mit der Cloud Shell hergestellt ist, sehen Sie, dass Sie bereits authentifiziert sind und für das Projekt schon Ihre Projekt-ID eingestellt ist.
- Führen Sie in der Cloud Shell den folgenden Befehl aus, um zu prüfen, ob Sie authentifiziert sind:
gcloud auth list
Befehlsausgabe
Credentialed Accounts
ACTIVE ACCOUNT
* <my_account>@<my_domain.com>
To set the active account, run:
$ gcloud config set account `ACCOUNT`
- Führen Sie den folgenden Befehl in Cloud Shell aus, um zu bestätigen, dass der gcloud-Befehl Ihr Projekt kennt:
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].
3. Cloud Profiler aufrufen
Rufen Sie in der Cloud Console die Profiler-UI auf, indem Sie in der linken Navigationsleiste auf „Profiler“ klicken:

Alternativ können Sie die Suchleiste der Cloud Console verwenden, um die Profiler-UI aufzurufen. Geben Sie einfach „Cloud 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“ wie unten angezeigt werden. Das Projekt ist neu, daher wurden noch keine Profiling-Daten erhoben.

Jetzt ist es an der Zeit, etwas zu profilieren.
4. Benchmark 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. Mit diesem Code wird die Erfassung von Mutex-Profilen (auch als „Konflikt“-Profile bezeichnet) aktiviert.
Führen Sie das Programm nun aus:
$ go run main.go
Während das Programm ausgeführt wird, erfasst der Profiler-Agent regelmäßig Profile der fünf konfigurierten Typen. Die Erfassung erfolgt im Zeitverlauf zufällig (mit einer durchschnittlichen Rate von einem Profil pro Minute für jeden Typ). Es kann also bis zu drei Minuten dauern, bis alle Typen erfasst sind. Das Programm informiert Sie, wenn ein Profil erstellt wird. Die Nachrichten 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.
5. Profiler-Daten analysieren
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 ist speziell darauf ausgelegt, 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 entspricht den relativen Kosten des jeweiligen Aufrufpfads. 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 „Stapeldarstellung“ mit dem Filterstring „baz“ hinzu. Die Ausgabe sollte in etwa so aussehen wie im Screenshot unten. Dort werden nur zwei der vier Aufruf-Pfade zu load() angezeigt. 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 angezeigte 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 ein Flame-Diagramm angezeigt, 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 ausgeht. 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 verbringt, z. B. mit der Ein-/Ausgabe. 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, um beispielsweise 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 (unter dem Messwert „Contentions“) 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 erfasst, 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 10, 5 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)
}
}
6. Zusammenfassung
In diesem Lab haben Sie gelernt, wie ein Go-Programm für die Verwendung mit Cloud 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.
7. Glückwunsch!
Sie haben gelernt, wie Sie Cloud Profiler konfigurieren und verwenden.
Weitere Informationen
- Cloud Profiler: https://cloud.google.com/profiler/
- Go-Laufzeit-/pprof-Paket, das von Cloud Profiler verwendet wird: https://golang.org/pkg/runtime/pprof/
Lizenz
Dieser Text ist mit einer Creative Commons Attribution 2.0 Generic License lizenziert.