Leistungsanalyse und Optimierung — ein leistungsstarkes Werkzeug zur Überprüfung der Leistungsübereinstimmung für Kunden.
Die Leistungsanalyse kann verwendet werden, um Engpässe im Programm zu identifizieren, indem ein wissenschaftlicher Ansatz zur Überprüfung von Optimierungsexperimenten angewendet wird. Dieser Artikel beschreibt einen allgemeinen Ansatz zur Leistungsanalyse und Optimierung mit einem Go-Webserver als Beispiel.
Go eignet sich dafür besonders gut, da es über Profiling-Tools verfügt pprof in der Standardbibliothek.

Strategie
Lassen Sie uns eine zusammenfassende Liste für unsere strukturelle Analyse erstellen. Wir versuchen, einige Daten zur Entscheidungsfindung zu verwenden, anstatt Änderungen auf Basis von Intuition oder Vermutungen vorzunehmen. Dafür machen wir Folgendes:
- Wir definieren die Grenzen der Optimierung (Anforderungen);
- Wir berechnen die Transaktionslast für das System;
- Wir führen einen Test durch (erstellen Daten);
- Wir beobachten;
- Wir analysieren — werden alle Anforderungen erfüllt?
- Wir optimieren wissenschaftlich, stellen eine Hypothese auf;
- 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 aus 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 Anzeige 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 die Datenbank zusammen bereitgestellt werden:

Wir definieren Ziele
In diesem Schritt definieren wir das Ziel. Was versuchen wir zu analysieren? Wie erkennen 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 verarbeiten wird.
Im detailliert besprochen, wie die Auswahl erfolgt und Modellierungen durchgeführt werden. Wir gehen ebenso vor und erstellen Modelle:
- Latenz: 99% der Anfragen müssen in weniger als 60 ms ausgeführt werden;
- Kosten: Der Dienst sollte die minimal denkbare Summe an Geld verbrauchen, die uns sinnvoll erscheint. Dafür maximieren wir die Durchsatzrate;
- Kapazitätsplanung: Es ist wichtig zu verstehen und zu dokumentieren, wie viele Instanzen der Anwendung benötigt werden, einschließlich der allgemeinen Skalierungsfunktion, sowie wie viele Instanzen erforderlich sind, um die anfänglichen Lastanforderungen und Redundanz n+1 zu erfüllen. .
Die Latenz könnte eine Optimierung zusätzlich zur Analyse erfordern, aber die Bandbreite muss eindeutig analysiert werden. Bei der Verwendung des SRE SLO-Prozesses stammt die Anforderung zur Latenz von dem Kunden oder dem Unternehmen, das vom Produktinhaber vertreten wird. Unsere Dienstleistung wird diese Verpflichtung von Anfang an ohne Anpassungen erfüllen!
Testumgebung einrichten
Mit der Testumgebung können wir progressiv Lasten auf unser System anwenden. Es werden Leistungsdaten des Webdienstes erfasst.
Transaktionslast
Diese Umgebung verwendet um anpassbare HTTP-Anforderungsraten zu generieren, 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 Transaktionslast angewendet. Neben den Anwendungsmetriken (Anzahl der Anfragen, Antwortlatenz) und der Betriebssystemmetriken (Speicher, CPU, IOPS) wird das Profiling der Anwendung gestartet, um zu verstehen, wo es Probleme gibt und wie die CPU-Zeit verbraucht wird.
Profiling
Profiling ist eine Art der Messung, die zeigt, wohin die CPU-Zeit während der Ausführung der Anwendung geht. Es ermöglicht die Bestimmung, wo und wie viel CPU-Zeit verwendet wird:

Diese Daten können während der Analyse verwendet werden, um einen Einblick in die nutzlos verschwendete CPU-Zeit und die ausgeführten überflüssigen Arbeiten zu erhalten. Go (pprof) kann Profile generieren und diese in Form eines Flame Graph visualisieren, indem es das Standard-Toolset verwendet. Ich werde im Folgenden über deren Verwendung und Einrichtung sprechen.
Ausführung, Überwachung, Analyse.
Lass uns ein Experiment durchführen. Wir werden ausführen, beobachten und analysieren, bis die Leistung uns zufriedenstellt. Wir wählen eine willkürlich niedrige Lastgröße, um sie für die ersten Beobachtungen zu verwenden. In jedem folgenden Schritt werden wir die Last mit einem Skalierungsfaktor erhöhen, der mit einem gewissen Streuungsbereich ausgewählt wird. Jeder Start des Lasttests wird mit Anpassung der Anzahl der Anfragen durchgeführt: make load-test LOAD_TEST_RATE=X.
50 Anfragen pro Sekunde

Beachten Sie die beiden oberen Grafiken. Die obere linke zeigt, dass unsere Anwendung 50 Anfragen pro Sekunde verarbeitet (aus ihrer Sicht), während die obere rechte die Dauer jeder Anfrage darstellt. Beide Parameter helfen uns zu sehen und zu analysieren: Ob wir in unseren Leistungsgrenzen bleiben oder nicht. Die rote Linie im Diagramm HTTP-Anfrageverzögerung zeigt SLO von 60 ms. Der Linie ist zu entnehmen, dass wir weit unter unserer maximalen Antwortzeit liegen.
Betrachten wir die Kosten:
10000 Anfragen pro Sekunde / bei 50 Anfragen pro Server = 200 Server + 1
Wir können diese Zahl immer noch verbessern.
500 Anfragen pro Sekunde
Interessantere Dinge beginnen zu geschehen, wenn die Last 500 Anfragen pro Sekunde beträgt:

Wiederum zeigt die linke obere Grafik, dass die Anwendung die normale Last erfasst. Wenn dies nicht der Fall ist, gibt es ein Problem auf dem Server, auf dem die Anwendung läuft. Das Diagramm mit der Antwortverzögerung befindet sich oben rechts und zeigt, dass 500 Anfragen pro Sekunde zu einer Antwortverzögerung von 25-40 ms geführt haben. Der 99. Perzentil liegt immer noch hervorragend im SLO von 60 ms, der oben ausgewählt wurde.
In Bezug auf die Kosten:
10000 Anfragen pro Sekunde / bei 500 Anfragen pro Server = 20 Server + 1
Es kann immer noch verbessert werden.
1000 Anfragen pro Sekunde

Ein großartiger Start! Die Anwendung zeigt, dass sie 1000 Anfragen pro Sekunde verarbeitet hat, jedoch wurde die Grenze für die Verzögerung seitens SLO überschritten. Dies ist an der p99-Linie in der oberen rechten Grafik zu erkennen. Obwohl die p100-Linie viel höher ist, liegen die tatsächlichen Verzögerungen über dem Maximum von 60 ms. Lassen Sie uns in die Profilierung eintauchen, um herauszufinden, was die Anwendung tatsächlich tut.
Profiling
Für die Profilierung setzen wir die Last auf 1000 Anfragen pro Sekunde und verwenden dann pprof um Daten zu erfassen, um zu verstehen, wo die Anwendung Rechenzeit verbraucht. Dies kann durch Aktivierung des HTTP-Endpunkts erfolgen. pprof, nach dem die Ergebnisse unter Last mit Hilfe von curl gespeichert werden:
$ curl http://localhost:8080/debug/pprof/profile?seconds=29 > cpu.1000_reqs_sec_no_optimizations.profDie Ergebnisse können folgendermaßen 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. Aus der Beschreibung von :
Auf der X-Achse ist das Profil der Stapelaufrufe alphabetisch sortiert (das ist keine Zeit), die Y-Achse zeigt die Stapeltiefe, beginnend bei null in [top]. Jedes Rechteck entspricht einem Stapelrahmen. Je breiter der Rahmen, desto häufiger taucht er in den Stacks auf. Oben sind die CPU-Lasten und darunter die Kind-Elemente. Die Farben bedeuten im Allgemeinen nichts und werden zufällig gewählt, um die Rahmen zu unterscheiden.
Analyse – Hypothese
Für die Anpassung konzentrieren wir uns darauf, unnötige CPU-Zeitverwendungen zu finden. Wir werden die bedeutendsten Quellen unnötiger Ausgaben suchen und diese entfernen. Angesichts dessen, dass das Profiling sehr genau zeigt, wo genau die Anwendung ihre CPU-Zeit verbraucht, müssen wir dies möglicherweise mehrmals tun und auch den Quellcode der Anwendung ändern, die Tests neu starten und beobachten, wie die Leistung sich dem Ziel nähert.
Den Empfehlungen von Brendan Gregg folgend, lesen wir das Diagramm von oben nach unten. Jede Zeile zeigt einen Stapelrahmen (Funktionsaufruf). Die erste Zeile ist der Einstiegspunkt in das Programm, der Elternteil aller anderen Aufrufe (mit anderen Worten, alle anderen Aufrufe werden ihn in ihrem Stapel haben). Die nächste Zeile unterscheidet sich bereits:
![]()
Wenn Sie den Mauszeiger über den Funktionsnamen im Diagramm bewegen, wird die Gesamtzeit angezeigt, die sie während des Debuggens im Stapel war. Die Funktion HTTPServe war dort 65 % der Zeit, andere Funktionen zur Laufzeit, runtime.mcall, mstart und gc, beanspruchten die restliche Zeit. Eine interessante Tatsache: 5 % der Gesamtzeit wurden für DNS-Anfragen aufgewendet:

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

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

Die Verdopplung der Last zeigt das gleiche Bild. Das obere linke Diagramm zeigt, dass die Anwendung es schafft, 2000 Anfragen pro Sekunde zu verarbeiten, p100 liegt unter 60 ms, p99 erfüllt das SLO.
In Bezug auf die Kosten:
10000 Anfragen pro Sekunde / auf 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 eingehalten, und die Kosten wurden folgendermaßen angenommen:
10000 Anfragen pro Sekunde / auf 3000 Anfragen pro Server = 4 Server + 1 (der Autor hat aufgerundet, Anm. des Übersetzers)
Lass uns einen weiteren Analyse-Runde versuchen.
Analyse – Hypothese
Wir sammeln und zeigen die Ergebnisse der Debugging-Session bei 3000 Anfragen pro Sekunde an:

Immer noch werden 6% der Zeit für die Verbindungsherstellung aufgewendet. Die Konfiguration des Pools hat die Leistung verbessert, aber es ist immer noch sichtbar, dass die Anwendung weiterhin neue Verbindungen zur Datenbank herstellt.
Hypothese: Verbindungen werden trotz des vorhandenen Pools weiterhin abgelehnt und gelöscht, sodass die Anwendung sie neu einrichten muss. Die Festlegung der Anzahl der wartenden Verbindungen auf die Poolgröße sollte helfen, die Latenz zu minimieren, indem die Zeit verringert wird, die die Anwendung für die Erstellung einer Verbindung benötigt..
App-Konfiguration — Experiment
Wir versuchen, auf die Größe des Pools zu setzen (auch beschrieben ):
db, err := sql.Open("postgres", dbConnectionString)
db.SetMaxOpenConns(8)
db.SetMaxIdleConns(8)
if err != nil {
return nil, err
}Ausführung, Überwachung, Analyse
3000 Anfragen pro Sekunde

p99 liegt unter 60 ms mit deutlich geringerem p100!

Die Überprüfung des Flame Graph zeigt, dass die Verbindungsherstellung nicht mehr bemerkbar ist! Wir prüfen genauer pg(*conn).query — auch hier bemerken wir die Verbindungsherstellung nicht.

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