8 Min

Go-Lasttests vor der ersten Backend-Einstellung

Mit Go-Lasttests modellieren Sie realistischen Traffic und messen p95-Latenz, Datenbankdruck, Speicher und Fehler vor der Einstellungsentscheidung.

Go-Lasttests vor der ersten Backend-Einstellung

Zehntausend monatlich aktive Nutzer sind keine Kapazitätsanforderung. Die Zahl gehört in die Abrechnung oder Analyse. Ein Go-Dienst kann erheblich mehr Nutzer bedienen, wenn die Anfragen leicht sind und sich über den Monat verteilen. Er kann aber auch bei einigen Hundert Nutzern zusammenbrechen, wenn jede Sitzung langsame Abfragen, Uploads und Aufrufe externer Dienste auslöst.

Die brauchbare Frage lautet, ob das generierte Backend unter der erwarteten Spitzenlast ein festgelegtes Dienstziel erreicht und genug Reserve für Wachstum und einen gewöhnlichen Ausfall behält. Das lässt sich vor der Einstellung eines Backend-Entwicklers beantworten, aber nur mit einem Lasttest, der Ihr Produkt abbildet und gleichzeitig erfasst, was API, Go-Laufzeit und PostgreSQL tun. Ein grünes Diagramm der durchschnittlichen Latenz beweist fast nichts.

Diesen Test würde ich verlangen, bevor ich einem Gründer sage, dass ein generiertes Go-Backend für 10.000 monatlich aktive Nutzer bereit ist. Er liefert ein wiederholbares Ergebnis, deckt den ersten Engpass auf und trennt ein Kapazitätsproblem von einem Korrektheitsfehler.

Aus monatlichen Nutzern müssen Spitzenanfragen werden

Rechnen Sie die Nutzerprognose in Anfragen pro Sekunde um, bevor Sie eine Laststufe wählen. Monatlich aktive Nutzer verbergen die beiden Größen, die ein Backend treiben: die Anzahl der Sitzungen im verkehrsreichsten Zeitraum und die Arbeit, die jede Sitzung erzeugt.

Beginnen Sie mit beobachteten Daten, falls es bereits eine private Beta gibt. Zählen Sie die Sitzungen in den verkehrsreichsten 15 Minuten, die Anfragen pro Sitzung und die Verteilung auf Routen. Wenn noch kein Traffic vorliegt, schreiben Sie die Annahmen so auf, dass jeder sie infrage stellen kann. Nehmen wir an, 10.000 aktive Nutzer erzeugen acht Sitzungen pro Monat und 15 API-Anfragen pro Sitzung, während 20 Prozent des täglichen Traffics in der verkehrsreichsten Stunde anfallen. An einem durchschnittlich geschäftigen Tag ergibt das rund 8 Anfragen pro Sekunde. Produkteinführungen, Benachrichtigungen, Abrechnungstermine oder eine gemeinsame Zeitzone können die wirkliche Spitze vervielfachen.

Machen Sie aus dieser Rechnung keine Scheingenauigkeit. Definieren Sie damit drei Teststufen:

  • Erwartete Spitze: die höchste Last, die Sie derzeit erwarten.
  • Wachstumsspitze: das Doppelte der erwarteten Spitze, sofern das Unternehmen keine bessere Prognose hat.
  • Belastungsgrenze: Erhöhen Sie den Traffic, bis ein Dienstziel verfehlt wird oder eine Ressource ausgelastet ist.

Die Tests für erwartete Last und Wachstum zeigen, ob die geplante Einführung Reserven hat. Der Belastungstest zeigt, was zuerst versagt und wie Nutzer den Ausfall erleben. Diese Antwort zählt, denn ein Dienst, der zu viel Arbeit schnell ablehnt, lässt sich leichter betreiben als einer, der alle Datenbankverbindungen verbraucht und unabhängige Routen blockiert.

Verwenden Sie für den Kapazitätstest ein offenes Lastmodell. Es startet Anfragen mit einer festen Ankunftsrate, auch wenn vorherige Anfragen langsamer werden. Ein geschlossenes Modell mit einer festen Anzahl virtueller Nutzer verschleiert einen Kollaps häufig: Langsamere Antworten führen dazu, dass diese Nutzer weniger neue Anfragen senden. Die angebotene Last sinkt genau dann, wenn der Dienst kämpft. Die k6-Dokumentation von Grafana unterscheidet diese Modelle über Executors mit Ankunftsrate und meldet dropped_iterations, wenn der Generator geplante Arbeit nicht starten kann. Behandeln Sie verworfene Iterationen als Fehler des Testgenerators, nicht als Erfolg des Servers.

Lassen Sie jede konstante Stufe nach dem Aufwärmen mindestens 30 Minuten laufen. Fünfminütige Tests übersehen den Austausch von Verbindungen, Zyklen der Speicherbereinigung, Cache-Verdrängung, Hintergrundaufgaben und langsames Speicherwachstum. Führen Sie nach einem erfolgreichen Kurztest einen separaten zweistündigen Dauertest bei der erwarteten Spitze aus.

Schreiben Sie die Umrechnung als kleines Arbeitsblatt und behalten Sie alle Einheiten bei. Monatliche Nutzer mal Sitzungen pro Nutzer mal Anfragen pro Sitzung ergeben monatliche Anfragen. Teilen Sie erst, nachdem Sie den Traffic auf Betriebstage und die verkehrsreichste Stunde verteilt haben. Ergänzen Sie Wiederholungen, Hintergrundaufgaben, Webhooks und Polling, die in der Nutzeranalyse womöglich fehlen. Ein Frontend, das alle zehn Sekunden abfragt, kann mehr API-Arbeit erzeugen als die Klicks, mit denen die Seite geöffnet wurde.

Modellieren Sie Lastspitzen getrennt von der konstanten Spitzenlast. Anmeldungen nach einer Benachrichtigung, das Ende eines Imports oder Wiederholungen nach einem kurzen Ausfall können Arbeit in einer Minute bündeln. Fügen Sie eine Stufe hinzu, die schnell die erwartete Spitzenrate erreicht, lange genug zum Füllen der Warteschlangen anhält und dann zur Normallast zurückkehrt. Der Dienst muss sich ohne wachsenden Rückstand und manuellen Neustart erholen. Erfassen Sie die Erholungszeit. Ein System kann den konstanten Test bestehen und trotzdem unsicher sein, wenn eine kurze Spitze Pool oder Worker festsetzt.

Multiplizieren Sie die Endrate nicht mit einem beliebigen Sicherheitsfaktor und nennen Sie das realistisch. Binden Sie die Reserve an eine geschäftliche Unsicherheit: Prognosefehler, geplante Kampagne, Ausfall einer Instanz oder Zeit zum Hinzufügen von Kapazität. Testen Sie jede Annahme, auf die Sie sich verlassen wollen. Legen Sie das Arbeitsblatt zum Ergebnis, denn ein bestandener Lauf verliert seine Aussage, wenn niemand mehr weiß, welche Prognose und Spitzenannahmen das Ziel ergaben.

Die Traffic-Verteilung muss einer echten Sitzung entsprechen

Ein realistischer Lasttest erhält Routenhäufigkeit, Payload-Größe, Authentifizierung, Datenverteilung, Denkzeit und Schreibkonflikte. Hundert Aufrufe von /health pro Sekunde messen den Health-Handler, nicht die Anwendung.

Ermitteln Sie die Verteilung nach Möglichkeit aus Zugriffsprotokollen. Gruppieren Sie Routen nach Geschäftsaktion statt nach roher URL, denn /projects/123 und /projects/456 haben dieselbe Form. Ein plausibler Mix für ein junges SaaS-Produkt könnte 45 Prozent Listen- und Detailabfragen, 20 Prozent Suchen, 15 Prozent Erstellungen oder Änderungen, 10 Prozent Anmeldungen und Token-Erneuerungen sowie 10 Prozent Exporte oder andere schwere Arbeit enthalten. Ihre Werte müssen aus dem Produktablauf stammen, nicht aus diesem Beispiel.

Verwenden Sie viele Testkonten und Datensätze. Ein einziges Konto kann einen unrealistisch heißen Cache erzeugen, Änderungen an einer Zeile serialisieren oder Ratenbegrenzungen auslösen, die echte Nutzer verteilen würden. Legen Sie kleine, mittlere und große Mandanten an. Testen Sie fehlende Datensätze, ungültige Eingaben und abgewiesene Berechtigungen, denn Fehlerpfade fragen die Datenbank ab oder reservieren Antwortspeicher oft anders als erfolgreiche Pfade.

Legen Sie große Uploads und lange Exporte in ein eigenes Szenario, wenn dafür ein anderes Dienstziel gilt. Lassen Sie sie dennoch gleichzeitig mit dem normalen Traffic laufen. Sonst verpasst der Test genau den Vorfall, den Nutzer bemerken: Ein Export belegt den Pool, während eine einfache Einstellungsseite dahinter wartet.

Ersetzen Sie im abschließenden Kapazitätslauf weder PostgreSQL noch Objektspeicher, Warteschlangen oder ausgehende Dienste durch Attrappen. Attrappen helfen beim Isolieren der Handlerkosten, entfernen aber die Abhängigkeiten, die wahrscheinlich die Kapazität begrenzen. Testen Sie einen Staging-Stack mit denselben Instanzgrößen, Datenbankeinstellungen, Indizes, Verbindungslimits und Netzwerkwegen wie in Produktion. Bereinigte Daten in Produktionsform sind besser als tausend identische Seed-Zeilen.

Testen Sie nicht durch einen Content-Cache, wenn die API normalerweise nicht gecacht wird. Behalten Sie den echten Cache dagegen im Pfad, wenn Produktion ihn verwendet. Es geht nicht darum, das Backend beschäftigt aussehen zu lassen. Der Test soll die Arbeit reproduzieren, die eine Nutzeranfrage tatsächlich auslöst.

Ein ausführbarer k6-Test muss den Vertrag festschreiben

Speichern Sie Schwellenwerte und Traffic-Stufen in der Versionsverwaltung, damit ein Testlauf nicht zu einem nachträglich interpretierten Screenshot wird. k6 behandelt Schwellenwerte als Bestehensregeln und beendet den Prozess bei einer Verletzung mit einem von null verschiedenen Code. Das macht das Ergebnis für eine Release-Prüfung geeignet.

Das folgende Gerüst führt eine gemischte Sitzung mit einer Ankunftsrate aus, prüft die inhaltliche Antwort und setzt getrennte Latenzgrenzen für gewöhnliche Lesezugriffe und schwere Exporte. Ersetzen Sie Routen, Payloads und Ziele durch die für Ihr Produkt vereinbarten Werte. Der Code ist bewusst einfach, damit sich jede fehlgeschlagene Prüfung einer Nutzeraktion zuordnen lässt.

import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate } from 'k6/metrics';

const businessErrors = new Rate('business_errors');

export const options = {
  scenarios: {
    expected_peak: {
      executor: 'ramping-arrival-rate',
      startRate: 5,
      timeUnit: '1s',
      preAllocatedVUs: 40,
      maxVUs: 200,
      stages: [
        { target: 10, duration: '5m' },
        { target: 10, duration: '30m' },
        { target: 20, duration: '10m' },
        { target: 20, duration: '30m' },
      ],
    },
  },
  thresholds: {
    'http_req_duration{name:project_list}': ['p(95)<300'],
    'http_req_duration{name:project_create}': ['p(95)<500'],
    'http_req_duration{name:export}': ['p(95)<2000'],
    http_req_failed: ['rate<0.01'],
    business_errors: ['rate<0.005'],
    dropped_iterations: ['count==0'],
  },
};

export function setup() {
  const response = http.post(`${__ENV.BASE_URL}/api/login`, JSON.stringify({
    email: __ENV.TEST_EMAIL,
    password: __ENV.TEST_PASSWORD,
  }), { headers: { 'Content-Type': 'application/json' } });
  check(response, { 'login succeeds': r => r.status === 200 });
  return { token: response.json('token') };
}

export default function (data) {
  const headers = { Authorization: `Bearer ${data.token}`, 'Content-Type': 'application/json' };
  const list = http.get(`${__ENV.BASE_URL}/api/projects?limit=25`, {
    headers,
    tags: { name: 'project_list' },
  });
  businessErrors.add(!check(list, {
    'list status is 200': r => r.status === 200,
    'list has items': r => Array.isArray(r.json('items')),
  }));

  if (Math.random() < 0.25) {
    const create = http.post(`${__ENV.BASE_URL}/api/projects`, JSON.stringify({
      name: `load-${__VU}-${__ITER}`,
    }), { headers, tags: { name: 'project_create' } });
    businessErrors.add(!check(create, { 'create status is 201': r => r.status === 201 }));
  }

  if (Math.random() < 0.03) {
    const runExport = http.post(`${__ENV.BASE_URL}/api/exports`, '{}', {
      headers,
      tags: { name: 'export' },
    });
    businessErrors.add(!check(runExport, { 'export accepted': r => r.status === 202 }));
  }

  sleep(Math.random() * 2 + 1);
}

Die Beispielziele sind Ausgangspunkte, keine allgemeinen Zusagen. Setzen Sie p95 je Routenklasse anhand der Verzögerung, die Nutzer tolerieren, und der Anforderungen des Produkts. Bewerten Sie einen asynchronen Export und eine Autovervollständigung nicht mit demselben globalen Grenzwert von 300 ms.

Starten Sie das Skript auf einem Rechner, der die Anwendung nicht hostet. Prüfen Sie, ob der Generator freie CPU hat und keine Iterationen verwirft. Speichern Sie den genauen Commit, die Umgebungskonfiguration, die Kennung des Daten-Snapshots, den Befehl und die Rohdaten. Ohne diese Angaben beruht ein späterer Vergleich überwiegend auf Erinnerung und Optimismus.

Kalibrieren Sie den Generator vor einem langen Lauf. Richten Sie ihn auf einen kleinen Handler ohne Datenbankarbeit, erhöhen Sie die angeforderte Rate über den geplanten Testwert und prüfen Sie, ob er sie ohne Erschöpfung der eigenen CPU, Sockets oder Netzwerkverbindung hält. Wenn k6 virtuelle Nutzer ergänzt oder verworfene Iterationen meldet, kann der Lastrechner die Grenze sein. Verteilen Sie die Erzeugung erst dann auf mehrere Rechner und synchronisieren Sie deren Uhren, damit Server- und Client-Diagramme zusammenpassen.

Geben Sie jedem Lauf eine ruhige Basis. Stoppen Sie Migrationen, Datenimporte und fremde Staging-Jobs, sofern diese nicht auch während der echten Produktionsspitze laufen. Planen Sie danach einen zweiten Test mit echten Hintergrundaufgaben. Das Paar zeigt sowohl die saubere API-Kapazität als auch die Betriebskapazität der Nutzer. Wenn nur der ruhige Lauf besteht, hängt der Einführungsplan von einer künstlichen Staging-Situation ab.

Bauen Sie ein zweites Skript für eine einzelne Nutzerreise und führen Sie es vor jeder Last mit einem virtuellen Nutzer aus. Prüfen Sie jede Antwort, jeden erstellten Datensatz und jede Bereinigung. So finden Sie ein falsches Token, eine immer wahre Prüfung oder Testdaten, die nach der ersten Iteration kollidieren. Ein Kapazitätsergebnis ist bedeutungslos, wenn das Skript eine Fehlerseite aufruft oder immer dasselbe gecachte Objekt liest.

p95 braucht den Kontext einzelner Routen

Nutzen Sie p95, weil Mittelwerte eine langsame Minderheit verbergen, aber lesen Sie den Wert nie allein. Bei p95 von 800 ms dauert eine von zwanzig Anfragen mindestens so lange. Eine Seite mit mehreren Anfragen kann sich deshalb regelmäßig langsam anfühlen. Bei Routen mit wenigen Stichproben schwankt das Perzentil stark, also geben Sie die Anzahl der Anfragen daneben an.

Erfassen Sie p50, p95, p99, Maximum, Durchsatz und Fehlerrate für jede benannte Geschäftsaktion. p50 zeigt das normale Verhalten, p95 ist ein praktisches Dienstkriterium und p99 zeigt das Ende der Verteilung, ohne ein einzelnes Maximum zum Maßstab zu machen. Trennen Sie Ergebnisse nach Statuscode. Eine schnelle 500-Antwort darf die Latenzwerte nicht verbessern.

Messen Sie zusätzlich zur clientseitigen Dauer die Laufzeit des serverseitigen Handlers. Die Differenz umfasst Verbindungsaufbau, Proxies, Netzwerkzeit und Antwortübertragung. Wenn clientseitiges p95 steigt und der Handler konstant bleibt, suchen Sie außerhalb des Handlers. Wenn beide steigen und Datenbankwartezeit hinzukommt, wartet die Anfrage wahrscheinlich auf eine Verbindung oder Abfrage.

Trennen Sie Aufwärm- und stationäre Ergebnisse. In einem bereitgestellten Go-Binary findet keine Kompilierung statt, aber kalte Caches, neue Datenbankverbindungen, späte Initialisierung und automatische Skalierung können die ersten Minuten verzerren. Nutzer erleben dieses kalte Verhalten ebenfalls, also bewahren Sie es als eigenes Ergebnis auf.

Mittelwerte helfen weiterhin bei der Ressourcenrechnung. Gesamte Datenbankzeit geteilt durch Aufrufe kann eine mäßig langsame, sehr häufige Abfrage zeigen. Sie ersetzt aber kein Perzentilziel. Die Branche vermischt oft Latenz und Kapazität: Latenz beschreibt die Dauer abgeschlossener Arbeit, Kapazität die Menge angebotener Arbeit, die der Dienst ohne wachsende Warteschlangen oder Fehler trägt. Gute Latenz bei niedrigem erreichtem Durchsatz beweist keine Kapazität.

Definieren Sie das Versagen vor dem Lauf. Ich würde einen Einführungstest als fehlgeschlagen werten, wenn eine wichtige Route ihr p95-Ziel verfehlt, unerwartete HTTP-Fehler die vereinbarte Rate überschreiten, Geschäftsprüfungen scheitern, geplante Iterationen verworfen werden oder eine Ressource dauerhaft ausgelastet bleibt. Vier von fünf erfüllten Kriterien sind ein fehlgeschlagener Lauf mit nützlichen Diagnosedaten.

Verbindungswartezeiten decken versteckte Datenbankwarteschlangen auf

Schlechte Performance-Änderung zurückrollen
Snapshots und Rollback geben generierten Backend-Änderungen einen Rückweg, wenn Ziele verfehlt werden.

Instrumentieren Sie database/sql vor dem Test, denn Anwendungslatenz verrät nicht, ob PostgreSQL langsam ist oder die Anwendung auf den Zugang wartet. Go liefert über DB.Stats() die Werte OpenConnections, InUse, Idle, WaitCount und WaitDuration sowie Zähler geschlossener Verbindungen. Exportieren Sie sie alle paar Sekunden in Ihr Metriksystem.

func recordDBStats(ctx context.Context, db *sql.DB, g GaugeSet) {
    ticker := time.NewTicker(5 * time.Second)
    defer ticker.Stop()

    for {
        select {
        case <-ctx.Done():
            return
        case <-ticker.C:
            s := db.Stats()
            g.Set("db_open_connections", float64(s.OpenConnections))
            g.Set("db_in_use_connections", float64(s.InUse))
            g.Set("db_idle_connections", float64(s.Idle))
            g.Set("db_wait_count_total", float64(s.WaitCount))
            g.Set("db_wait_seconds_total", s.WaitDuration.Seconds())
        }
    }
}

Berechnen Sie die Änderung der kumulativen Zähler im stationären Fenster. Steigendes WaitCount bedeutet, dass Anfragen auf eine freie Verbindung warten mussten. Die Änderung von WaitDuration geteilt durch die Änderung von WaitCount ergibt die mittlere Poolwartezeit in diesem Intervall. Zeichnen Sie InUse zusammen mit MaxOpenConnections; eine flache Linie am Limit und zunehmende Wartezeit bedeuten Poolsättigung.

Erhöhen Sie SetMaxOpenConns nicht einfach, bis das Diagramm besser aussieht. Dieser beliebte Eingriff verschiebt die Warteschlange nach PostgreSQL und kann Konkurrenz, Speicherbedarf und Abfragelatenz erhöhen. Finden Sie zuerst heraus, warum Verbindungen belegt bleiben: langsame Abfragen, während Netzaufrufen offene Transaktionen, einzeln gelesene Zeilen oder vergessene Aufrufe von Rows.Close(). Dimensionieren Sie den Pool danach innerhalb des Datenbankbudgets für alle Anwendungsinstanzen und Worker.

Laut Go-Dokumentation lässt ein nicht positiver Wert für SetMaxOpenConns den Pool unbegrenzt. Das ist ein gefährlicher Produktionsstandard, wenn mehrere Instanzen gleichzeitig Verbindungen öffnen können. Setzen Sie ein ausdrückliches Limit, wählen Sie Leerlauf- und Lebensdauerregeln bewusst und reservieren Sie Datenbankkapazität für Migrationen, Verwaltung und Hintergrundarbeit.

Erfassen Sie die Transaktionsdauer getrennt. Ein Handler kann nach 200 ms antworten, während eine verzögerte Bereinigung oder eine verlorene Transaktion die Verbindung viel länger hält. Poolmetriken zeigen den Druck, Traces oder Transaktionszeiten den Verursacher.

Belege für langsame Abfragen müssen aus PostgreSQL kommen

Aktivieren Sie pg_stat_statements in der Testumgebung und nehmen Sie vor und nach jedem Lauf Snapshots auf. Die PostgreSQL-Dokumentation beschreibt das Modul als Erfassung von Planungs- und Ausführungsstatistiken normalisierter Anweisungen. Die Sicht enthält Aufrufe, Zeilen, gesamte und mittlere Ausführungszeit, Blockaktivität und temporäre Blöcke. Das ist deutlich besser als eine Vermutung anhand der Abfrage in einem einzelnen Trace.

Verwenden Sie die Differenz zwischen Snapshots, da die Sicht kumulativ ist. Setzen Sie sie nur in einer isolierten Testdatenbank zurück, denn ein Reset vernichtet Belege anderer Arbeit. Diese Abfrage findet die Anweisungen mit der höchsten gesamten Ausführungszeit in einem sauberen Testfenster:

SELECT
  queryid,
  calls,
  round(total_exec_time::numeric, 1) AS total_ms,
  round(mean_exec_time::numeric, 2) AS mean_ms,
  rows,
  shared_blks_read,
  temp_blks_written
FROM pg_stat_statements
WHERE calls > 20
ORDER BY total_exec_time DESC
LIMIT 20;

Gesamtzeit findet häufige Abfragen, die die Datenbankarbeit beherrschen. Mittlere Zeit findet einzeln langsame Anweisungen. Keine davon liefert Abfrage-p95, weil pg_stat_statements Aufrufe zusammenfasst. Verwenden Sie Tracing oder Dauerhistogramme, wenn das Ende der Latenzverteilung einer einzelnen Anweisung zählt. Auch hier wird oft vermischt: Eine langsame Abfrage kann eine hohe mittlere Dauer, eine hohe Randlatenz oder lediglich enorme Gesamtkosten durch häufige Aufrufe haben. Jede Ursache verlangt eine andere Korrektur.

Führen Sie für die wichtigsten Anweisungen EXPLAIN (ANALYZE, BUFFERS) mit sicheren, repräsentativen Parametern außerhalb des zeitlich gemessenen Lasttests aus. ANALYZE führt die Anweisung tatsächlich aus. Packen Sie ändernde Anweisungen deshalb in eine Transaktion mit Rollback oder untersuchen Sie sie auf einer entbehrlichen Kopie. Suchen Sie nach starken Abweichungen zwischen geschätzten und tatsächlichen Zeilen, wiederholten Schleifen, sequenziellen Scans großer selektiver Tabellen, auf Platte ausgelagerten Sortierungen und vielen gelesenen Shared Blocks.

Generierter Code erzeugt oft ein N+1-Muster, das mit Seed-Daten harmlos wirkt: Er lädt 25 Projekte und führt dann je Projekt eine Eigentümer- und eine Zählabfrage aus. Bei zehn Anfragen pro Sekunde kann allein dieser Endpunkt mehr als 500 Datenbankanweisungen pro Sekunde erzeugen, bevor eine andere Route läuft. Die Korrektur kann ein Join, ein gebündeltes WHERE id = ANY($1) oder eine vorberechnete Anzahl sein. Ein höheres Poollimit lässt die Verschwendung bestehen.

Erfassen Sie auch Sperrwartezeiten. Eine isoliert schnelle Abfrage kann bei gleichzeitigen Änderungen am selben Mandanten, Konto oder Zähler stehen bleiben. Wenn die Latenz nur im Schreibszenario steigt, prüfen Sie aktive Wartezustände und Transaktionsgrenzen, statt reflexartig einen Index hinzuzufügen.

Der Speicher muss sich unter konstanter Last einpendeln

Go-Quellcode testbar halten
Koder.ai exportiert das generierte Backend, damit Lastskript und Metriken daneben liegen.

Beurteilen Sie Speicher anhand seines Verlaufs, nicht einer einzelnen Spitze. Ein Go-Prozess, der beim Aufwärmen wächst und danach um ein stabiles Niveau schwankt, verhält sich anders als einer, dessen Basis nach der Speicherbereinigung während eines zweistündigen Dauertests weiter steigt.

Erfassen Sie Resident Memory des Prozesses, Go-Heapbelegung, Heapobjekte, Goroutine-Anzahl, Häufigkeit und Pausen der Speicherbereinigung sowie Allokationsrate. Containerspeicher zählt, weil das Betriebssystem den Prozess anhand seines Limits beendet, nicht allein anhand des Go-Heaps. Vergleichen Sie Speicherwerte nach einer Bereinigung bei ähnlicher Last. Das entfernt einen Großteil des normalen Sägezahns und macht gebundene Daten sichtbar.

Stellen Sie die Standardmetriken der Go-Laufzeit oder einen geschützten Profiling-Endpunkt in Staging bereit. Erfassen Sie ein Heapprofil nahe am Anfang und Ende des Dauertests und vergleichen Sie gebundene Allokationsstellen mit go tool pprof. Nehmen Sie auch ein Goroutine-Profil auf. Eine wachsende Goroutine-Anzahl kann auf Anfragen hinweisen, die an Kanälen hängen, nicht geschlossene Antwortkörper oder Hintergrundarbeit ohne Abbruch.

Setzen Sie GOMEMLIMIT nicht genau auf das Containerlimit. Der Prozess braucht auch Speicher für Goroutine-Stacks, ausführbare Abbildungen, Puffer des Datenbanktreibers und andere Allokationen außerhalb des Heaps. Lassen Sie Reserve und prüfen Sie sie mit den größten Payloads. Ein Test mit winzigen JSON-Körpern sagt wenig über einen Endpunkt, der einen 20-MB-Upload vollständig in den Speicher liest.

Erzwingen Sie unangenehme Fälle: maximal erlaubte Anfragekörper, große Abfrageergebnisse, abgebrochene Clients, Zeitüberschreitungen und wiederholte Exporte. Prüfen Sie, ob der Speicher nach Abschluss der Arbeit zurückgeht. Beobachten Sie auch CPU, denn intensive Speicherbereinigung kann den Speicher unter dem Limit halten und gleichzeitig die Latenz ruinieren.

Eine praktische Bestehensregel kombiniert Obergrenze und Verlauf. Der Resident Memory muss bei der Wachstumsspitze sicher unter dem Bereitstellungslimit bleiben. Danach dürfen Basiswert nach Speicherbereinigung und Goroutine-Anzahl im Dauertest nicht weiter steigen. Es gibt keinen allgemein sicheren Prozentsatz. Wählen Sie Reserve passend zum Neustartverhalten der Plattform, zur Traffic-Schwankung und dazu, ob eine andere Instanz einen Neustart auffangen kann.

Fehler umfassen falsche Antworten und Überlastverhalten

Anforderungen in Go verwandeln
Beschreiben Sie Latenz-, Zeitlimit- und Datenbankvorgaben im Chat, während Koder.ai den Backend-Code baut.

Zählen Sie Transportfehler, HTTP-Statusfehler, Zeitüberschreitungen, Panics und falsche Antworten getrennt. http_req_failed erfasst fehlgeschlagene HTTP-Anfragen nach dem k6-Callback. Eine 200-Antwort mit leerer Liste, doppelter Belastung oder fehlendem Datensatz ist trotzdem falsch. Deshalb erzeugt das Beispiel business_errors aus Inhaltsprüfungen.

Kennzeichnen Sie erwartete Ablehnungen wie ungültige Eingaben oder eine gewollte Ratenbegrenzung, damit sie die unerwartete Fehlerrate nicht verfälschen. Prüfen Sie dann ihren Vertrag: richtiger Status, begrenzter Antwortkörper und schnelle Ablehnung. Ein überlastetes System sollte nicht 30 Sekunden warten, bevor es 503 sendet.

Untersuchen Sie Serverprotokolle auf Panic-Recovery, Context-Deadline-Fehler, Verzögerungen bei Verbindungsbeschaffung, PostgreSQL-Serialisierungsfehler und abgebrochene Abfragen. Gruppieren Sie Fehler nach stabiler Ursache statt nach vollständiger Meldung, damit Kennungen nicht Tausende Kategorien erzeugen. Speichern Sie repräsentative Traces des ersten Auftretens und der hohen Latenzwerte.

Führen Sie nach dem sauberen Kapazitätstest einen Degradationstest aus. Verringern Sie verfügbare Datenbankverbindungen, fügen Sie einer externen Abhängigkeit kontrollierte Latenz hinzu oder starten Sie bei laufendem Traffic eine Anwendungsinstanz neu. Tun Sie das nur in der isolierten Testumgebung. Prüfen Sie, ob Zeitlimits, Abbruch und Health-Checks den Fehler begrenzen, statt Warteschlangen alle Ressourcen verbrauchen zu lassen.

Konfigurieren Sie den HTTP-Server ausdrücklich. Die Go-Dokumentation zu net/http sagt, dass null oder negative Werte für ReadTimeout, WriteTimeout und IdleTimeout je nach Feld kein Zeitlimit bedeuten können. Generierte Dienste rufen oft http.ListenAndServe mit Standardwerten auf, ohne die Entscheidung zu treffen. Ein eigener http.Server, an Anfragen gebundene Deadlines und begrenzte Body-Größen verhindern, dass langsame Clients und festhängende Abhängigkeiten Ressourcen unbegrenzt halten.

Prüfen Sie nach dem Lauf die Korrektheit. Zählen Sie erstellte Datensätze, prüfen Sie Idempotenz bei Client-Wiederholungen, stellen Sie die genau einmalige Ausführung von Hintergrundaufgaben sicher und suchen Sie nach Teilzuständen fehlgeschlagener Anfragen. Lasttests haben in meinen Projekten mehr Fehler durch doppelte Arbeit gefunden als noch so clevere Code-Reviews.

Die erste Grenze bestimmt die Einstellungsentscheidung

Sie können ohne Backend-Entwickler starten, wenn das System die erwarteten und wachsenden Lasttests wiederholt besteht, der Dauertest eine stabile Speicherbasis erreicht, Datenbankwarteschlangen kontrolliert bleiben und das Team den ersten Belastungsfehler erklären kann. Ein glücklicher grüner Lauf ist kein Beleg. Führen Sie denselben Commit mindestens dreimal aus und untersuchen Sie große Schwankungen.

Bewahren Sie für jeden Lauf einen kompakten Ergebnisdatensatz auf:

  • Commit und Umgebung einschließlich Instanzgrößen und Datenbankkonfiguration.
  • Datenumfang, Traffic-Mix, Ankunftsraten und Testdauer.
  • p95 und p99 je Route, erreichter Durchsatz, Geschäftsfehler und verworfene Iterationen.
  • Spitzenwerte für CPU und Speicher, Speicherverlauf nach Bereinigung und Goroutine-Verlauf.
  • Poolwartezeiten, wichtigste SQL-Anweisungen nach Gesamtzeit, Sperrwartezeiten und beobachtete Bruchstelle.

Stellen Sie vor der Einführung Backend-Hilfe ein oder beauftragen Sie sie, wenn niemand steigende Poolwartezeit, gebundenen Speicher, Sperrkonkurrenz oder inkonsistente Schreibvorgänge erklären kann. Tun Sie das auch, wenn nur eine Person den Test ausführen kann, den generierten Code aber nicht sicher ändern kann. Das ist eine Eigentümerlücke, kein Schwellenwert für Anfragen pro Sekunde.

Ein fehlgeschlagener Test rechtfertigt nicht automatisch eine Vollzeitstelle. Ein fehlender Index, eine N+1-Abfrage oder ein unbegrenzter Export können klar begrenzte Reparaturen sein. Wiederholte Fehler in Transaktionsdesign, Beobachtbarkeit, Abbruch und Bereitstellungsverhalten zeigen laufende Entwicklungsarbeit. Entscheidend ist, ob Sie einen einzelnen Defekt fanden oder entdeckten, dass niemand für das Systemverhalten verantwortlich ist.

Koder.ai kann ein Go-Backend erzeugen und exportieren, bereitstellen und Snapshots für ein Rollback bewahren. Die Generierung ersetzt aber keine Kapazitätsplanung. Halten Sie Lastskript und Observability-Änderungen beim Quellcode, damit jede wesentliche Backend-Änderung denselben Vertrag bestehen muss.

Versprechen Sie dem Unternehmen nicht, dass 10.000 monatliche Nutzer sicher sind. Versprechen Sie eine gemessene Ankunftsrate, einen Routenmix, ein Latenzziel, ein Fehlerbudget und einen Ressourcenrahmen. Wenn sich das Produkt ändert, ändern Sie diese Eingaben und führen den Test erneut aus.

FAQ

Kann ein Go-Backend 10.000 monatlich aktive Nutzer bewältigen?

Oft ja, aber monatliche Nutzer beschreiben die Backend-Last nicht. Rechnen Sie die Prognose in Spitzenanfragen pro Sekunde und einen Routenmix um und testen Sie gegen klare Grenzen für Latenz, Fehler, Datenbank und Speicher.

Wie viele Anfragen pro Sekunde entsprechen 10.000 monatlichen Nutzern?

Es gibt keine feste Umrechnung. Sie brauchen Sitzungen pro Nutzer, Anfragen pro Sitzung, den Traffic-Anteil im verkehrsreichsten Zeitraum und Ereignisse, die Nutzer gleichzeitig aktivieren.

Welche p95-Latenz ist für eine Go-API akzeptabel?

Setzen Sie Ziele nach Nutzeraktion statt nach Sprache oder Framework. Interaktive Lesezugriffe können wenige Hundert Millisekunden verlangen, während angenommene Hintergrundarbeit ein anderes Ziel hat. Die Zahlen müssen vor dem Test feststehen.

Wie lange sollte ein Backend-Lasttest laufen?

Halten Sie jede konstante Last nach dem Aufwärmen mindestens 30 Minuten und führen Sie danach einen längeren Dauertest bei erwarteter Spitze aus. Kurze Tests übersehen Verbindungswechsel, gebundenen Speicher, Hintergrundarbeit und wachsende Warteschlangen.

Soll ich virtuelle Nutzer oder eine Ankunftsrate verwenden?

Nutzen Sie für Kapazitätsaussagen ein Modell mit Ankunftsrate, weil es auch bei langsamerem Dienst weiter Arbeit anbietet. Eine feste Nutzerzahl kann bei Verzögerungen die Anfragerate senken und wachsende Warteschlangen verbergen.

Wie erkenne ich einen erschöpften Go-Datenbankpool?

Exportieren Sie DB.Stats() und beobachten Sie InUse, OpenConnections, WaitCount und WaitDuration. Steigende Wartezähler bei dauerhaft maximaler Nutzung zeigen, dass Anfragen auf den Pool warten.

Soll ich den Go-SQL-Pool erhöhen, wenn Anfragen warten?

Nicht bevor Sie wissen, warum Verbindungen belegt bleiben und welche Kapazität PostgreSQL hat. Ein größerer Pool kann die Warteschlange in die Datenbank verschieben und Konflikte verschärfen.

Wie finde ich langsame PostgreSQL-Abfragen im Lasttest?

Erstellen Sie Vorher- und Nachher-Snapshots von pg_stat_statements und sortieren Sie die Differenzen nach gesamter und mittlerer Ausführungszeit. Für Randlatenz brauchen Sie Traces oder Histogramme, weil die aggregierte Sicht kein p95 je Abfrage enthält.

Wie erkenne ich ein Speicherleck in einem Go-Dienst?

Führen Sie einen konstanten Dauertest aus und vergleichen Sie Speicher nach Bereinigungen bei ähnlicher Last. Ein weiter steigender Basiswert, besonders zusammen mit Heapobjekten oder Goroutines, verlangt einen Profilvergleich.

Wann sollte ein Startup einen Backend-Entwickler einstellen?

Stellen Sie ein, wenn Performancefehler laufende Verantwortung für Datenbankdesign, Beobachtbarkeit, Parallelität, Korrektheit oder Betrieb verlangen. Ein einzelner Index oder eine Abfragekorrektur braucht vielleicht keine Vollzeitstelle, unerklärtes Lastverhalten aber schon.

Related posts