Leistungsanalyse und -optimierung – ein leistungsfähiges Werkzeug zur Überprüfung der Leistungsanforderungen für Kunden.
Leistungsanalyse kann angewendet werden, um Engpässe in Programmen zu identifizieren, indem ein wissenschaftlicher Ansatz zur Überprüfung von Optimierungsexperimenten verwendet wird. Dieser Artikel beschreibt einen allgemeinen Ansatz zur Leistungsanalyse und -optimierung anhand eines Webservers auf Go.
Go eignet sich hier besonders gut, da es Profilierungswerkzeuge besitzt pprof in der Standardbibliothek.

Strategie
Lassen Sie uns eine Zusammenfassung für unsere strukturelle Analyse erstellen. Wir werden versuchen, einige Daten zur Entscheidungsfindung zu verwenden, anstatt Änderungen basierend auf Intuition oder Vermutungen vorzunehmen. Dazu tun wir Folgendes:
- Definieren Sie die Grenzen der Optimierung (Anforderungen);
- Wir berechnen die Transaktionslast für das System;
- Wir führen den Test durch (erstellen Daten);
- Wir beobachten;
- Wir analysieren – werden alle Anforderungen erfüllt?
- Wir konfigurieren wissenschaftlich und formulieren eine Hypothese;
- Wir führen ein Experiment durch, um diese Hypothese zu überprüfen.

Architektur eines einfachen HTTP-Servers
Für diesen Artikel verwenden wir einen kleinen HTTP-Server in Golang. Den gesamten Code zu diesem Artikel finden Sie .
Die zu analysierende Anwendung ist ein HTTP-Server, der bei jeder Anfrage PostgreSQL abfragt. Zusätzlich gibt es Prometheus, Node_exporter und Grafana zur Sammlung und Darstellung von Metriken der Anwendung und des Systems.

Zur Vereinfachung nehmen wir an, dass für die horizontale Skalierung (und zur Vereinfachung der Berechnungen) jeder Dienst und jede Datenbank zusammen bereitgestellt werden:

Ziele definieren
In diesem Schritt legen wir das Ziel fest. Was versuchen wir zu analysieren? Wie wissen wir, wann wir aufhören sollten? In diesem Artikel nehmen wir an, dass wir Kunden haben und dass unser Dienst 10.000 Anfragen pro Sekunde bearbeiten wird.
In beschreibt detailliert Methoden zur Auswahl und Modellierung. Wir werden es ebenso machen und Modelle aufbauen:
- Latenz: 99 % der Anfragen sollten in weniger als 60 ms verarbeitet werden;
- Kosten: Der Dienst sollte einen minimalen Geldbetrag erfordern, der uns als vernünftig erscheint. Dazu maximieren wir die Bandbreite;
- Kapazitätsplanung: Es ist notwendig zu verstehen und zu dokumentieren, wie viele Instanzen der Anwendung gestartet werden müssen, einschließlich der Gesamtfunktion der Skalierung sowie wie viele Instanzen erforderlich sind, um die anfänglichen Leistungsanforderungen zu erfüllen und Redundanz zu gewährleisten. .
Die Latenz kann Optimierungen erfordern, zusätzlich zur Analyse, aber die Bandbreite muss eindeutig analysiert werden. Bei der Verwendung des SRE SLO-Prozesses stammt die Anforderung zur Latenz vom Kunden oder vom Geschäft, das vom Produktinhaber vertreten wird. Und unser Dienst wird dieses Commitment von Anfang an ohne Anpassungen erfüllen!
Testumgebung einrichten
Mit der Testumgebung können wir eine dosierte Belastung auf unser System ausüben. Für die Analyse werden Leistungsdaten des Webdienstes erstellt.
Transaktionslast
Diese Umgebung verwendet zum Erstellen einer anpassbaren HTTP-Anfragefrequenz, bis sie gestoppt wird:
$ make load-test LOAD_TEST_RATE=50
echo "POST http://localhost:8080" | vegeta attack -body tests/fixtures/age_no_match.json -rate=50 -duration=0 | tee results.bin | vegeta reportÜberwachung
Während der Ausführung wird eine transaktionale Last angewendet. Neben den Anwendungsmetriken (Anzahl der Anfragen, Antwortverzögerungen) und den Betriebssystemmetriken (Speicher, CPU, IOPS) wird eine Anwendungsprofilierung durchgeführt, um zu verstehen, wo Probleme bestehen und wie die CPU-Zeit verbraucht wird.
Profilierung
Profilierung ist eine Art der Messung, die zeigt, wohin die CPU-Zeit bei der Ausführung der Anwendung fließt. Sie ermöglicht es, genau zu bestimmen, wo und wie viel CPU-Zeit verwendet wird:

Diese Daten können während der Analyse verwendet werden, um Einblicke in unnötig verschwendete CPU-Zeit und ausgeführte unnötige Arbeiten zu erhalten. Go (pprof) kann Profile generieren und diese als Flame Graph visualisieren, indem es einen Standardwerkzeugsatz verwendet. Ich werde ihre Verwendung und eine Anleitung zur Einrichtung weiter unten im Artikel erklären.
Ausführung, Überwachung, Analyse.
Lassen Sie uns ein Experiment durchführen. Wir werden beobachten, auswerten und analysieren, bis die Leistung unseren Erwartungen entspricht. Wir wählen willkürlich eine niedrige Last, um erste Ergebnisse zu erzielen. In jedem weiteren Schritt erhöhen wir die Last mit einem Skalierungsfaktor, der mit einem gewissen Spielraum ausgewählt wird. Jeder Durchlauf des Lasttests erfolgt mit einer Anpassung der Anzahl der Anfragen: make load-test LOAD_TEST_RATE=X.
50 Anfragen pro Sekunde

Bitte beachten Sie die beiden oberen Diagramme. Das obere linke zeigt, dass unsere Anwendung 50 Anfragen pro Sekunde verarbeitet (nach ihrer Einschätzung), und das obere rechte die Dauer jeder Anfrage. Beide Parameter helfen uns, zu beobachten und zu analysieren: Ob wir innerhalb unserer Leistungsgrenzen bleiben oder nicht. Die rote Linie im Diagramm HTTP-Anforderungslatenz zeigt das SLO von 60 ms. Die Linie zeigt, dass wir deutlich unter unserer maximalen Antwortzeit liegen.
Betrachten wir die Kostenperspektive:
10000 Anfragen pro Sekunde / 50 Anfragen pro Server = 200 Server + 1
Wir können diese Kennzahl weiterhin verbessern.
500 Anfragen pro Sekunde
Interessantere Dinge passieren, wenn die Last 500 Anfragen pro Sekunde erreicht:

Wieder ist im linken, oberen Diagramm zu sehen, dass die Anwendung eine normale Last registriert. Wenn dies nicht der Fall ist, gibt es ein Problem mit dem Server, auf dem die Anwendung läuft. Das Diagramm zur Antwortverzögerung oben rechts zeigt, dass 500 Anfragen pro Sekunde eine Antwortverzögerung von 25-40 ms verursachten. Der 99. Perzentil liegt immer noch hervorragend im gewählten SLO von 60 ms.
Hinsichtlich der Kosten:
10.000 Anfragen pro Sekunde / 500 Anfragen pro Server = 20 Server + 1
Es gibt immer noch Verbesserungsmöglichkeiten.
1.000 Anfragen pro Sekunde

Ein großartiger Start! Die Anwendung zeigt, dass sie 1.000 Anfragen pro Sekunde verarbeitet hat, jedoch wurde die Grenze für die Verzögerung seitens SLO überschritten. Dies wird durch die p99-Linie im oberen rechten Diagramm sichtbar. Obwohl die p100-Linie deutlich höher ist, liegen die tatsächlichen Verzögerungen über dem Maximum von 60 ms. Lassen Sie uns in das Profiling eintauchen, um herauszufinden, was die Anwendung tatsächlich macht.
Profilierung
Für das Profiling setzen wir die Last auf 1.000 Anfragen pro Sekunde und verwenden dann pprof Um herauszufinden, wo die Anwendung CPU-Zeit verbraucht, aktivieren Sie den HTTP-Endpunkt. pprof, danach können Sie die Ergebnisse unter Last mit curl speichern:
$ curl http://localhost:8080/debug/pprof/profile?seconds=29 > cpu.1000_reqs_sec_no_optimizations.profDie Ergebnisse können wie folgt angezeigt werden:
$ go tool pprof -http=:12345 cpu.1000_reqs_sec_no_optimizations.prof
Das Diagramm zeigt, wo und wie viel CPU-Zeit die Anwendung verbraucht. Die Beschreibung stammt von :
Die X-Achse zeigt die Stapelprofilierung, alphabetisch sortiert (dies ist nicht die Zeit), die Y-Achse zeigt die Stapeltiefe, beginnend bei null im [top]. Jedes Rechteck ist ein Stapelrahmen. Je breiter der Rahmen, desto häufiger tritt er in den Stacks auf. Was oben ist, läuft auf der CPU, und darunter sind die Kind-Elemente. Die Farben bedeuten in der Regel nichts und werden einfach zufällig gewählt, um die Frames zu unterscheiden.
Analyse – Hypothese
Bei der Konfiguration konzentrieren wir uns darauf, unnötige Ausgaben von CPU-Zeit zu identifizieren. Wir werden die größten Quellen für unnötige Ausgaben suchen und entfernen. Angesichts der Tatsache, dass das Profiling sehr genau zeigt, wo genau die Anwendung CPU-Zeit verbraucht, könnte es notwendig sein, dies mehrmals durchzuführen und auch den Quellcode der Anwendung zu ändern, Tests neu zu starten und zu beobachten, wie sich die Leistung dem erwarteten Wert nähert.
Gemäß den Empfehlungen von Brendan Gregg lesen wir das Diagramm von oben nach unten. Jede Zeile stellt einen Stack-Frame (Funktionsaufruf) dar. Die erste Zeile ist der Einstiegspunkt in das Programm, der übergeordnete Teil aller anderen Aufrufe (mit anderen Worten, alle anderen Aufrufe haben sie in ihrem Stack). Die nächste Zeile weicht bereits ab:
![]()
Wenn Sie mit der Maus über den Funktionsnamen im Diagramm fahren, wird die gesamte Zeit angezeigt, die sie während des Debuggings im Stack verbrachte. Die Funktion HTTPServe war dabei 65% der Zeit aktiv, andere Funktionen wie runtime, runtime.mcall, mstart und gc, machten den Rest der Zeit aus. Interessanter Fakt: 5% der Gesamtzeit wurden für DNS-Anfragen aufgewendet:

Die Adressen, die das Programm sucht, gehören zu Postgresql. Klicken Sie auf FindByAge:

Interessant, das Programm zeigt, dass es im Grunde drei Hauptquellen gibt, die Verzögerungen verursachen: das Öffnen und Schließen von Verbindungen, Datenanforderungen und die Verbindung zur Datenbank. Auf dem Diagramm ist zu erkennen, dass die DNS-Anfragen sowie das Öffnen und Schließen von Verbindungen etwa 13% der gesamten Ausführungszeit in Anspruch nehmen.
Hypothese: Die Wiederverwendung von Verbindungen über einen Pool sollte die Zeit einer HTTP-Anfrage verkürzen und eine höhere Bandbreite sowie geringere Latenzen ermöglichen..
App-Konfiguration – Experiment
Wir aktualisieren den Quellcode und versuchen, die Verbindung zu Postgresql bei jeder Anfrage zu eliminieren. Die erste Option ist die Verwendung auf Anwendungsebene. In diesem Experiment werden wir Verbindungspool mit dem SQL-Treiber für Go einrichten:
db, err := sql.Open("postgres", dbConnectionString)
db.SetMaxOpenConns(8)
if err != nil {
return nil, err
}Ausführung, Beobachtung, Analyse
Nach dem Neustart des Tests mit 1000 Anfragen pro Sekunde zeigt sich, dass die p99-Latenzen mit einem SLO von 60 ms in den Normalbereich zurückgekehrt sind!
Wie sieht es mit den Kosten aus?
10000 Anfragen pro Sekunde / 1000 Anfragen pro Server = 10 Server + 1
Lass uns noch besser werden!
2000 Anfragen pro Sekunde

Die Verdopplung der Last zeigt dasselbe, das obere linke Diagramm zeigt, dass die Anwendung in der Lage ist, 2000 Anfragen pro Sekunde zu bearbeiten, p100 liegt unter 60 ms, p99 erfüllt das SLO.
Hinsichtlich der Kosten:
10000 Anfragen pro Sekunde / 2000 Anfragen pro Server = 5 Server + 1
3000 Anfragen pro Sekunde

Hier kann die Anwendung 3000 Anfragen mit einer p99-Latenz von weniger als 60 ms verarbeiten. Das SLO wird nicht verletzt, und die Kosten werden wie folgt angenommen:
10000 Anfragen pro Sekunde / 3000 Anfragen pro Server = 4 Server + 1 (der Autor hat aufgerundet, Anm. der Übersetzer)
Lassen Sie uns eine weitere Analyse-Runde versuchen.
Analyse – Hypothese
Wir sammeln und zeigen die Debugging-Ergebnisse der Anwendung bei 3000 Anfragen pro Sekunde an:

Immer noch werden 6% der Zeit für die Herstellung von Verbindungen aufgewendet. Die Pool-Konfiguration hat die Leistung verbessert, aber man sieht immer noch, dass die Anwendung weiterhin neue Verbindungen zur Datenbank herstellt.
Hypothese: Verbindungen werden trotz vorhandenen Pools weiterhin abgelehnt und bereinigt, daher muss die Anwendung sie neu herstellen. Die Festlegung der Anzahl wartender Verbindungen auf die Poolgröße sollte helfen, die Latenz zu minimieren, indem die Zeit reduziert wird, die die Anwendung für die Herstellung einer Verbindung benötigt..
App-Konfiguration – Experiment
Versuchen wir es mit der Festlegung von entspricht der Größe des Pools (siehe auch ):
db, err := sql.Open("postgres", dbConnectionString)
db.SetMaxOpenConns(8)
db.SetMaxIdleConns(8)
if err != nil {
return nil, err
}Ausführung, Beobachtung, Analyse
3000 Anfragen pro Sekunde

p99 unter 60ms mit deutlich geringerem p100!

Die Überprüfung des Flamegraphs zeigt, dass die Verbindungserkennung nicht mehr auffällt! Lassen Sie uns genauer prüfen pg(*conn).query — auch hier bemerken wir die Verbindungserkennung nicht.

Fazit
Leistungsanalyse ist entscheidend für das Verständnis, ob die Erwartungen der Kunden und die nicht-funktionalen Anforderungen erfüllt werden. Eine Analyse durch den Abgleich von Beobachtungen mit den Erwartungen der Kunden kann helfen, zu bestimmen, was akzeptabel ist und was nicht. Go bietet effektive Werkzeuge, die in die Standardbibliothek integriert sind, um die Analyse einfach und zugänglich zu machen.
Quelle: habr.com
