Testy obciążeniowe Go przed pierwszym zatrudnieniem backendowym
Użyj testów obciążeniowych Go, aby modelować realistyczny ruch, mierzyć opóźnienie p95, presję na bazę, pamięć i błędy, a następnie podjąć decyzję o zatrudnieniu.

Dziesięć tysięcy miesięcznie aktywnych użytkowników nie jest wymaganiem dotyczącym wydajności. To liczba do rozliczeń lub analityki. Usługa Go może obsłużyć znacznie więcej osób, gdy żądania są lekkie i rozłożone na cały miesiąc, albo przestać działać przy kilkuset użytkownikach, jeśli każda sesja uruchamia wolne zapytania, przesyłanie plików i wywołania usług zewnętrznych.
Ważne pytanie brzmi: czy wygenerowany backend spełnia określony cel usługowy przy spodziewanym ruchu szczytowym, z zapasem na wzrost i zwykłą awarię. Możesz odpowiedzieć na nie przed zatrudnieniem inżyniera backendu, ale test obciążeniowy musi przypominać Twój produkt i jednocześnie rejestrować, co dzieje się w API, środowisku wykonawczym Go i PostgreSQL. Zielony wykres średniego opóźnienia prawie niczego nie dowodzi.
Tego testu wymagałbym, zanim powiedziałbym założycielowi, że wygenerowany backend Go jest gotowy na 10 000 miesięcznie aktywnych użytkowników. Daje powtarzalny wynik pozytywny lub negatywny, ujawnia pierwsze wąskie gardło i oddziela problem z przepustowością od problemu z poprawnością.
Miesięczni użytkownicy muszą zostać przeliczeni na żądania szczytowe
Przed wyborem poziomu obciążenia przelicz prognozę użytkowników na liczbę żądań na sekundę. Miesięcznie aktywni użytkownicy ukrywają dwie zmienne napędzające backend: ile sesji pojawia się w najbardziej ruchliwym oknie oraz ile pracy tworzy każda sesja.
Jeśli istnieje zamknięta beta, zacznij od obserwowanych danych. Policz sesje w najbardziej ruchliwych 15 minutach, żądania na sesję i miks tras. Jeśli ruchu jeszcze nie ma, zapisz założenia tak, aby każdy mógł je zakwestionować. Załóżmy na przykład, że 10 000 aktywnych użytkowników tworzy osiem sesji miesięcznie, 15 żądań API na sesję, a 20 procent dziennego ruchu przypada na najbardziej ruchliwą godzinę. Daje to około 8 żądań na sekundę w przeciętny ruchliwy dzień. Premiery, powiadomienia, terminy wypłat lub wspólna strefa czasowa mogą sprawić, że rzeczywisty szczyt będzie kilkukrotnie wyższy.
Nie zamieniaj tej arytmetyki w fałszywą precyzję. Użyj jej do określenia trzech poziomów testu:
- Oczekiwany szczyt: największe obciążenie, którego obecnie się spodziewasz.
- Szczyt wzrostu: dwa razy większy niż oczekiwany, chyba że firma ma lepszą prognozę.
- Poziom stresowy: zwiększaj ruch, aż cel usługowy nie zostanie spełniony lub zasób się nasyci.
Testy oczekiwanego wzrostu odpowiadają, czy planowane uruchomienie ma zapas. Test stresowy pokazuje, co psuje się pierwsze i jak awaria wygląda dla użytkowników. To ważne, bo usługę, która szybko odrzuca nadmiar pracy, łatwiej utrzymać niż taką, która zużywa każde połączenie z bazą i blokuje niepowiązane trasy.
Do testu przepustowości użyj otwartego modelu obciążenia. W otwartym modelu żądania rozpoczynają się ze stałą intensywnością nawet wtedy, gdy wcześniejsze żądania zwalniają. Zamknięty model ze stałą liczbą wirtualnych użytkowników często ukrywa załamanie: wolniejsze odpowiedzi sprawiają, że użytkownicy wysyłają mniej nowych żądań, więc oferowane obciążenie spada dokładnie wtedy, gdy usługa ma problem. Dokumentacja Grafana k6 wyjaśnia tę różnicę za pomocą executorów intensywności napływu i raportuje dropped_iterations, gdy generator nie może rozpocząć zaplanowanej pracy. Traktuj pominięte iteracje jako awarię generatora testu, a nie sukces serwera.
Uruchamiaj każdy stały poziom przez co najmniej 30 minut po rozgrzaniu. Pięciominutowe testy nie wykrywają rotacji połączeń, cykli odśmiecania pamięci, wyrzucania danych z cache, zadań w tle ani stopniowego wzrostu zużycia pamięci. Po zaliczeniu krótszego testu dodaj osobny dwugodzinny test długotrwały przy oczekiwanym szczycie.
Zapisz przeliczenie jako mały arkusz i pokaż każdą jednostkę. Miesięczna liczba użytkowników pomnożona przez sesje na użytkownika i żądania na sesję daje miesięczną liczbę żądań. Dziel dopiero po przypisaniu ruchu do dni działania i najbardziej ruchliwej godziny. Następnie dodaj ruch ponowień, zadania w tle, webhooki i odpytywanie, których analityka użytkowników może nie liczyć. Frontend odpytywany co dziesięć sekund może tworzyć więcej pracy API niż kliknięcia, które otworzyły stronę.
Modeluj skoki ruchu osobno od stałego szczytu. Logowanie po powiadomieniu, zakończony import lub ponowienia klientów po krótkiej awarii mogą skoncentrować pracę w jednej minucie. Dodaj etap skoku, który szybko osiąga oczekiwaną intensywność, utrzymuje ją wystarczająco długo, by wypełnić kolejki, a potem wraca do normy. Usługa powinna odzyskać sprawność bez rosnącej kolejki i bez ręcznego restartu. Zapisz czas odzyskania jako wynik. System może zaliczyć stały test, a mimo to być niebezpieczny, jeśli krótki skok blokuje jego pulę lub workerów.
Nie mnoż końcowej intensywności przez dowolny współczynnik bezpieczeństwa i nie nazywaj wyniku realistycznym. Powiąż zapas z niepewnością biznesową: błędem prognozy, planowaną kampanią, niedostępnością jednej repliki lub czasem potrzebnym na dodanie wydajności. Testuj każde założenie, na którym zamierzasz polegać. Trzymaj arkusz obok wyniku, ponieważ zaliczony test traci sens, gdy nikt nie pamięta, z jakiej prognozy i jakich założeń dotyczących skoku wynikał cel.
Miks ruchu musi przypominać prawdziwą sesję
Realistyczny test obciążeniowy zachowuje częstotliwość tras, rozmiar danych, uwierzytelnianie, rozkład danych, czas namysłu i rywalizację o zapisy. Wysyłanie 100 żądań na sekundę do /health mierzy handler stanu zdrowia, a nie aplikację.
Jeśli to możliwe, zbuduj miks na podstawie logów dostępu. Grupuj trasy według działania biznesowego, a nie surowego URL, ponieważ /projects/123 i /projects/456 mają ten sam kształt. Wiarygodny wczesny miks SaaS może przydzielać 45 procent do odczytów list i szczegółów, 20 procent do wyszukiwania, 15 procent do tworzenia lub aktualizacji, 10 procent do logowania i odświeżania tokenów oraz 10 procent do eksportów lub innej ciężkiej pracy. Twoje liczby powinny wynikać z przepływu produktu, nie z tego przykładu.
Używaj wielu kont testowych i rekordów. Ponowne wykorzystywanie jednego konta może tworzyć nierealistycznie gorący cache, serializować aktualizacje jednego wiersza albo uruchamiać limity intensywności, które prawdziwy ruch rozłoży. Przygotuj małych, średnich i dużych klientów. Uwzględnij brakujące rekordy, nieprawidłowe dane wejściowe i błędy autoryzacji, ponieważ ścieżki błędów często inaczej odpytyją bazę lub alokują treść odpowiedzi niż ścieżki udane.
Duże przesyłane pliki i długie eksporty utrzymuj w osobnym scenariuszu, jeśli mają inny cel usługowy. Uruchamiaj je jednak równocześnie z normalnym ruchem. W przeciwnym razie test pominie dokładnie ten incydent, który zauważają użytkownicy: jedna klasa eksportów zajmuje pulę, podczas gdy prosta strona ustawień czeka za nią.
W końcowym teście przepustowości nie stosuj atrap PostgreSQL, magazynu obiektów, kolejek ani usług wychodzących. Atrapa pomaga odizolować koszt handlera, ale usuwa zależności, które najpewniej wyznaczają przepustowość. Skieruj test na środowisko staging o tych samych rozmiarach instancji, ustawieniach bazy, indeksach, limitach połączeń i ścieżce sieciowej co produkcja. Oczyszczone dane podobne do produkcyjnych są lepsze od tysiąca identycznych wierszy startowych.
Nie testuj przez cache sieci dostarczania treści, jeśli API zwykle nie jest cacheowane. Z drugiej strony zachowaj prawdziwy cache w ścieżce, gdy korzysta z niego produkcja. Celem nie jest sprawienie, by backend wyglądał na zajęty. Chodzi o odtworzenie pracy, którą faktycznie wywołuje żądanie użytkownika.
Uruchamialny test k6 powinien opisywać kontrakt
Przechowuj progi i etapy ruchu w kontroli wersji, aby wynik testu nie stał się zrzutem ekranu interpretowanym po fakcie. k6 traktuje progi jako kryteria pozytywnego lub negatywnego wyniku i kończy działanie kodem różnym od zera, gdy nie zostaną spełnione. Dzięki temu wynik nadaje się do kontroli wydania.
Poniższy szablon prowadzi mieszaną sesję z intensywnością napływu, sprawdza semantykę odpowiedzi i stosuje oddzielne limity opóźnień dla zwykłych odczytów i ciężkich eksportów. Zastąp trasy, dane i cele wartościami uzgodnionymi dla Twojego produktu. Kod w teście jest celowo prosty, aby nieudane sprawdzenie można było powiązać z działaniem użytkownika.
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);
}
Przykładowe cele są punktami wyjścia, a nie uniwersalnymi obietnicami. Ustal p95 dla każdej klasy tras na podstawie opóźnienia, które użytkownik jest w stanie zaakceptować, oraz wymagań produktu. Nie używaj jednego globalnego progu 300 ms do oceny asynchronicznego eksportu i żądania autouzupełniania.
Uruchamiaj skrypt z maszyny, która nie hostuje aplikacji. Potwierdź, że generator ma zapas CPU i nie pomija iteracji. Zapisz dokładny commit, konfigurację środowiska, identyfikator migawki danych, polecenie i surowe wyjście testu. Bez nich późniejsze porównanie opiera się głównie na pamięci i optymizmie.
Skalibruj generator, zanim zaufasz długiemu uruchomieniu. Skieruj go do małego handlera bez pracy z bazą, zwiększ żądaną intensywność ponad planowany test i sprawdź, czy generator utrzymuje ją bez wyczerpania własnego CPU, gniazd lub sieci. Gdy k6 dodaje wirtualnych użytkowników albo raportuje pominięte iteracje, ograniczeniem może być maszyna generująca obciążenie. Rozdziel generowanie między maszyny tylko wtedy, gdy jedna nie może dostarczyć wymaganej pracy, i synchronizuj ich zegary, aby wykresy serwera i klienta się pokrywały.
Zapewnij każdemu uruchomieniu spokojny poziom bazowy. Zatrzymaj migracje, importy danych i niepowiązane zadania staging, chyba że działają też podczas rzeczywistego szczytu. Następnie zaplanuj drugi test z włączoną prawdziwą pracą w tle. Para tych testów pokazuje czystą przepustowość API oraz przepustowość operacyjną, którą otrzymają użytkownicy. Jeśli zalicza tylko spokojny test, plan uruchomienia opiera się na fikcji stagingu.
Zbuduj drugi skrypt dla pojedynczej ścieżki użytkownika i uruchom go z jednym wirtualnym użytkownikiem przed dodaniem obciążenia. Sprawdź każdą odpowiedź, utworzony rekord i działanie czyszczące. Pozwala to wykryć zły token, sprawdzenie zawsze zwracające prawdę albo dane testowe kolidujące po pierwszej iteracji. Wynik przepustowości nie ma znaczenia, gdy skrypt testuje stronę błędu lub ciągle odczytuje ten sam obiekt z cache.
p95 potrzebuje kontekstu na poziomie trasy
Używaj opóźnienia p95, ponieważ średnie ukrywają wolną mniejszość, ale nigdy nie odczytuj samego p95. Przy p95 równym 800 ms jedno na dwadzieścia żądań trwa co najmniej tyle, co może sprawić, że strona wykonująca wiele żądań będzie stale odczuwalnie wolna. Percentyl staje się też niestabilny przy trasach z bardzo małą liczbą próbek, dlatego podawaj obok niego liczbę żądań.
Rejestruj p50, p95, p99, maksimum, przepływność i współczynnik błędów dla każdego nazwanego działania biznesowego. p50 pokazuje normalne zachowanie, p95 jest praktyczną bramką usługową, a p99 odsłania ogon bez pozwalania, by pojedyncze maksimum zdominowało rozmowę. Dziel wyniki według kodu statusu. Szybka odpowiedź 500 nie może poprawiać historii o opóźnieniach.
Mierz czas działania handlera po stronie serwera oraz czas obserwowany przez klienta. Różnica obejmuje ustanawianie połączenia, proxy, czas sieci i przesyłanie odpowiedzi. Jeśli p95 klienta rośnie, a p95 handlera pozostaje płaskie, szukaj poza handlerem. Jeśli oba rosną i rośnie czas oczekiwania bazy, żądanie prawdopodobnie czeka na połączenie lub zapytanie.
Wyniki rozgrzewania i stanu ustalonego muszą pozostać osobne. W skompilowanym binarnym programie Go nie następuje kompilacja, ale zimny cache, nowe połączenia z bazą, leniwa inicjalizacja i autoskalowanie mogą zniekształcić pierwsze minuty. Użytkownicy także doświadczają zimnego zachowania, więc zachowaj je jako osobny wynik, zamiast je usuwać.
Średnie nadal przydają się do rozliczania zasobów. Całkowity czas bazy podzielony przez liczbę wywołań może wykryć zapytanie umiarkowanie wolne, ale bardzo częste. Nie zastąpi jednak percentylowego celu usługowego. Często zaciera się różnicę między opóźnieniem a przepustowością: opóźnienie opisuje, jak długo trwała ukończona praca, a przepustowość określa, ile oferowanej pracy usługa może utrzymać bez rosnących kolejek lub błędów. Dobre opóźnienie przy niskiej faktycznie osiągniętej liczbie żądań nie dowodzi przepustowości.
Zdefiniuj awarię przed uruchomieniem. Test uruchomienia uznałbym za niezaliczony, gdy którakolwiek krytyczna trasa nie spełnia celu p95, nieoczekiwane awarie HTTP przekraczają uzgodniony współczynnik, kontrole biznesowe zawodzą, zaplanowane iteracje są pomijane lub zasób pozostaje nasycony. Zaliczanie czterech z pięciu bramek oznacza nieudane uruchomienie z przydatnymi danymi diagnostycznymi.
Oczekiwanie na połączenie ujawnia ukryte kolejki bazy danych
Przed testem zinstrumentuj database/sql, ponieważ opóźnienie aplikacji nie powie Ci, czy PostgreSQL działa wolno, czy aplikacja czeka na dostęp do niego. DB.Stats() w Go raportuje OpenConnections, InUse, Idle, WaitCount i WaitDuration wraz z licznikami zamknięć. Eksportuj je do systemu metryk co kilka sekund.
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())
}
}
}
Oblicz zmianę skumulowanych liczników w stałym oknie. Rosnący WaitCount oznacza, że żądania musiały czekać na wolne połączenie. Zmiana WaitDuration podzielona przez zmianę WaitCount daje średni czas oczekiwania na pulę w tym przedziale. Wykreśl InUse względem MaxOpenConnections; płaska linia na limicie wraz z rosnącym oczekiwaniem oznacza nasycenie puli.
Nie reaguj zwiększeniem SetMaxOpenConns, dopóki wykres nie wygląda lepiej. To popularne rozwiązanie przenosi kolejkę do PostgreSQL i może zwiększyć rywalizację, zużycie pamięci oraz opóźnienie zapytań. Najpierw ustal, dlaczego połączenia pozostają zajęte: wolne zapytania, transakcje trzymane podczas wywołań sieciowych, odczytywanie wierszy pojedynczo lub pominięte wywołania Rows.Close(). Potem dobierz rozmiar puli w ramach budżetu połączeń bazy dla każdej repliki aplikacji i workera.
Dokumentacja Go mówi, że nie dodatnia wartość SetMaxOpenConns pozostawia pulę bez limitu. Brak limitu jest niebezpiecznym domyślnym ustawieniem produkcyjnym, gdy kilka replik może jednocześnie otwierać połączenia. Ustaw wyraźny limit, świadomie ustaw zachowanie połączeń bezczynnych i czas życia oraz zarezerwuj wydajność bazy na migracje, administrację i pracę w tle.
Śledź osobno czas trwania transakcji. Handler może zwrócić odpowiedź w 200 ms, podczas gdy odroczone czyszczenie lub porzucona transakcja znacznie dłużej trzyma połączenie. Metryki puli pokazują presję, ale ślady lub pomiar czasu transakcji ujawniają właściciela.
Dowody wolnych zapytań muszą pochodzić z PostgreSQL
Włącz pg_stat_statements w środowisku testowym i wykonuj migawki przed oraz po każdym uruchomieniu. Dokumentacja PostgreSQL opisuje go jako narzędzie śledzące statystyki planowania i wykonania dla znormalizowanych instrukcji. Widok zawiera liczbę wywołań, wiersze, całkowity i średni czas wykonania, aktywność bloków oraz aktywność bloków tymczasowych. To znacznie lepszy dowód niż zgadywanie na podstawie zapytania, które pojawiło się w jednym śladzie.
Używaj różnicy między migawkami, ponieważ widok jest skumulowany. Resetuj go wyłącznie w odizolowanej testowej bazie danych, bo reset niszczy dowody dotyczące innej pracy. To zapytanie znajduje instrukcje, które zużyły najwięcej czasu wykonania w czystym oknie testowym:
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;
Całkowity czas wskazuje częste zapytania dominujące pracę bazy. Średni czas wskazuje instrukcje pojedynczo wolne. Żaden z nich nie daje p95 zapytania, ponieważ pg_stat_statements agreguje wywołania. Użyj śledzenia lub histogramów czasu trwania, gdy istotne jest opóźnienie ogona pojedynczej instrukcji. To kolejna często zacierana różnica: wolne zapytanie może oznaczać wysoki średni czas wykonania, długi ogon albo po prostu ogromny całkowity koszt wynikający z częstych wywołań. Każde wymaga innej poprawki.
Dla najważniejszych instrukcji uruchom EXPLAIN (ANALYZE, BUFFERS) na bezpiecznych, reprezentatywnych parametrach poza mierzonym testem obciążeniowym. ANALYZE wykonuje instrukcję, więc instrukcje zmieniające dane opakuj w transakcję, którą wycofasz, albo analizuj je na jednorazowej kopii. Szukaj szacunków wyraźnie różniących się od rzeczywistej liczby wierszy, powtarzanych pętli, skanów sekwencyjnych dużych selektywnych tabel, sortowań przelewających się na dysk i wielu odczytanych bloków współdzielonych.
Wygenerowany kod często tworzy wzorzec N+1, który wygląda dobrze na danych startowych: pobiera 25 projektów, a potem wykonuje jedno zapytanie o właściciela i jedno o liczbę elementów dla każdego projektu. Przy dziesięciu żądaniach na sekundę ten jeden endpoint może tworzyć ponad 500 instrukcji bazy na sekundę, zanim ruszy jakakolwiek inna trasa. Naprawą może być złączenie, wsadowe WHERE id = ANY($1) lub wcześniej obliczona liczba. Podniesienie limitu puli pozostawia marnotrawstwo bez zmian.
Rejestruj też oczekiwanie na blokady. Szybkie w izolacji zapytanie może zatrzymać się przy równoczesnych aktualizacjach tego samego klienta, konta lub sekwencji. Gdy opóźnienie rośnie tylko w scenariuszu zapisu, sprawdź aktywne oczekiwania i granice transakcji, zamiast odruchowo dodawać indeks.
Pamięć musi się ustabilizować przy stałym obciążeniu
Oceniaj pamięć po jej kształcie w czasie, a nie po pojedynczym szczycie. Proces Go, który rośnie podczas rozgrzewania, a następnie oscyluje wokół stabilnego poziomu, zachowuje się inaczej niż proces, którego poziom bazowy po odśmiecaniu rośnie przez cały dwugodzinny test długotrwały.
Rejestruj rezydentną pamięć procesu, alokację sterty Go, obiekty sterty, liczbę gorutin, częstotliwość odśmiecania, czas pauz i tempo alokacji. Pamięć kontenera ma znaczenie, ponieważ system operacyjny zabija proces na podstawie limitu kontenera, a nie wyłącznie sterty Go. Porównuj pamięć po odśmiecaniu przy podobnym obciążeniu. Usuwa to znaczną część normalnego ząbkowania i uwidacznia zatrzymywanie pamięci.
Udostępnij standardowe metryki środowiska wykonawczego Go albo chroniony endpoint profilowania na stagingu. Zrób profil sterty blisko początku i końca testu długotrwałego, a następnie porównaj miejsca zatrzymanych alokacji za pomocą go tool pprof. Zrób też profil gorutin. Rosnąca liczba gorutin może ujawnić żądania zablokowane na kanałach, treści odpowiedzi, które nigdy nie zostały zamknięte, albo pracę w tle rozpoczętą bez anulowania.
Nie ustawiaj GOMEMLIMIT dokładnie na limit kontenera. Proces potrzebuje pamięci także na stosy gorutin, mapowania wykonywalne, bufory sterownika bazy i inne alokacje poza stertą. Zostaw zapas i udowodnij go przy największych danych wejściowych. Test z małymi obiektami JSON niewiele mówi o endpoincie, który wczytuje do pamięci plik 20 MB.
Wymuś trudne przypadki: największe akceptowane treści żądań, duże wyniki zapytań, anulowanych klientów, limity czasu i powtarzane eksporty. Sprawdź, czy pamięć wraca po zakończeniu pracy. Obserwuj także CPU, ponieważ intensywne odśmiecanie może utrzymać pamięć poniżej limitu, jednocześnie niszcząc opóźnienia.
Praktyczny warunek zaliczenia łączy pułap i trend. Wymagaj, by rezydentna pamięć pozostawała bezpiecznie poniżej limitu wdrożenia przy szczycie wzrostu, a następnie by poziom bazowy po odśmiecaniu i liczba gorutin przestały rosnąć podczas testu długotrwałego. Nie ma uniwersalnego bezpiecznego procentu. Wybierz zapas dla zachowania platformy przy restarcie, zmienności ruchu i tego, czy inna replika może przejąć ruch po restarcie.
Awarie muszą obejmować błędne odpowiedzi i zachowanie przy przeciążeniu
Licz osobno awarie transportu, awarie kodów HTTP, limity czasu, paniki i błędne odpowiedzi. http_req_failed wyłapuje nieudane żądania HTTP zgodnie z callbackiem odpowiedzi k6, ale odpowiedź 200 z pustą listą, zduplikowaną płatnością lub brakującym rekordem nadal jest awarią. Dlatego przykład emituje business_errors z kontroli treści.
Oznacz oczekiwane odrzucenia, takie jak nieprawidłowe dane wejściowe lub celowy limit intensywności, aby nie zanieczyszczały współczynnika nieoczekiwanych błędów. Następnie sprawdź ich kontrakt: status jest prawidłowy, treść ma ograniczony rozmiar, a odrzucenie następuje szybko. System pod przeciążeniem nie powinien zużywać 30 sekund przed zwróceniem 503.
Obserwuj logi serwera pod kątem odzyskiwania po panice, błędów przekroczenia terminu kontekstu, opóźnień pozyskania połączenia, błędów serializacji PostgreSQL i anulowanych zapytań. Grupuj błędy według stabilnej przyczyny, a nie pełnego komunikatu, aby identyfikatory nie tworzyły tysięcy kategorii. Zapisuj reprezentatywne ślady dla pierwszego wystąpienia oraz dla ogona o wysokim opóźnieniu.
Po czystym teście przepustowości wykonaj jeden test degradacji. Zmniejsz liczbę dostępnych połączeń do bazy, dodaj kontrolowane opóźnienie do jednej zależności wychodzącej albo uruchom ponownie jedną replikę aplikacji przy trwającym ruchu. Rób to wyłącznie w odizolowanym środowisku testowym. Celem jest potwierdzenie, że limity czasu, anulowanie i kontrole zdrowia ograniczają awarię, zamiast pozwalać kolejkom zużyć każdy zasób.
Skonfiguruj serwer HTTP jawnie. Dokumentacja net/http w Go mówi, że zerowe lub ujemne wartości ReadTimeout, WriteTimeout i IdleTimeout mogą oznaczać brak limitu czasu, zależnie od pola. Wygenerowane usługi często wywołują http.ListenAndServe z ustawieniami domyślnymi i nigdy nie podejmują tej decyzji. Własny http.Server, terminy ograniczone do żądania i ograniczone rozmiary treści zapobiegają wiecznemu trzymaniu zasobów przez wolnych klientów i zablokowane zależności.
Po uruchomieniu sprawdź poprawność. Policz utworzone rekordy, zweryfikuj idempotencję tam, gdzie klienci ponawiali żądania, sprawdź, czy zadania w tle zakończyły się tylko raz, i potwierdź, że po nieudanych żądaniach nie pozostał częściowy stan. Testy obciążeniowe wykryły w moich projektach więcej błędów duplikowania pracy niż najbardziej pomysłowy przegląd kodu.
Decyzja o zatrudnieniu wynika z pierwszego limitu
Możesz uruchomić produkt bez inżyniera backendu, gdy system wielokrotnie zalicza testy oczekiwanego szczytu i wzrostu, test długotrwały osiąga stabilną linię bazową pamięci, kolejki bazy pozostają pod kontrolą, a zespół potrafi wyjaśnić pierwszą awarię stresową. Jedno szczęśliwe zielone uruchomienie nie jest dowodem. Uruchom ten sam commit co najmniej trzy razy i zbadaj dużą zmienność.
Dla każdego uruchomienia zachowaj zwięzły zapis wyniku:
- Commit i środowisko, w tym rozmiary replik oraz konfigurację bazy danych.
- Skalę zbioru danych, miks ruchu, intensywności napływu i czas testu.
- p95 i p99 tras, osiągniętą przepływność, błędy biznesowe i pominięte iteracje.
- Szczytowe CPU i pamięć, trend pamięci po odśmiecaniu oraz trend gorutin.
- Oczekiwanie na pulę, najważniejsze SQL według całkowitego czasu, oczekiwanie na blokady i zaobserwowany punkt załamania.
Zatrudnij lub zakontraktuj pomoc backendową przed uruchomieniem, gdy nikt nie potrafi wyjaśnić rosnącego oczekiwania na pulę, zatrzymywanej pamięci, rywalizacji o blokady lub niespójnych zapisów. Zatrudnij też wtedy, gdy jedyna osoba potrafiąca uruchomić test nie może bezpiecznie zmienić wygenerowanego kodu. To luka w odpowiedzialności, nie próg liczby żądań na sekundę.
Nieudany test nie uzasadnia automatycznie zatrudnienia na pełny etat. Brakujący indeks, zapytanie N+1 lub nieograniczony eksport mogą być ograniczoną naprawą. Powtarzające się problemy z projektem transakcji, obserwowalnością, anulowaniem i zachowaniem wdrożenia wskazują na stałą pracę inżynierską. Różnica polega na tym, czy znaleziono jedną usterkę, czy odkryto, że nikt nie odpowiada za zachowanie systemu.
Koder.ai może generować i eksportować backend Go, wdrażać go oraz zachowywać migawki do wycofywania zmian, ale generowanie nie znosi potrzeby planowania przepustowości. Trzymaj skrypt obciążeniowy i zmiany obserwowalności razem z kodem źródłowym, aby każda istotna zmiana backendu musiała spełnić ten sam kontrakt.
Nie obiecuj firmie, że 10 000 użytkowników miesięcznie jest bezpieczne. Obiecaj zmierzoną intensywność napływu, miks tras, cel opóźnienia, budżet błędów i zakres zasobów. Gdy produkt się zmienia, zmień te dane wejściowe i uruchom test ponownie.
Często zadawane pytania
Czy backend Go obsłuży 10 000 miesięcznie aktywnych użytkowników?
Często tak, ale miesięczni aktywni użytkownicy nie opisują obciążenia backendu. Przelicz prognozę na szczytową liczbę żądań na sekundę i miks tras, a następnie przetestuj ten ruch względem jasno określonych limitów opóźnień, błędów, bazy danych i pamięci.
Ile żądań na sekundę odpowiada 10 000 użytkowników miesięcznie?
Nie ma stałego przelicznika. Potrzebujesz liczby sesji na użytkownika, żądań na sesję, udziału ruchu w najbardziej ruchliwym okresie oraz informacji o zdarzeniach, które powodują jednoczesne pojawienie się użytkowników.
Jakie opóźnienie p95 jest akceptowalne dla API Go?
Ustal cele według działania użytkownika, a nie języka czy frameworka. Interaktywne odczyty mogą wymagać kilkuset milisekund, a zaakceptowane zadania w tle mogą mieć inny cel, ale zespół musi wybrać te wartości przed poznaniem wyników testu.
Jak długo powinien trwać test obciążeniowy backendu?
Utrzymuj każdy stały poziom obciążenia przez co najmniej 30 minut po rozgrzaniu, a potem wykonaj dłuższy test długotrwały przy oczekiwanym szczycie. Krótkie testy mogą nie wykryć rotacji połączeń, zatrzymywanej pamięci, pracy w tle i stopniowego wzrostu kolejek.
Czy w testach obciążeniowych używać wirtualnych użytkowników czy intensywności napływu?
Do deklarowania przepustowości użyj modelu ze stałą intensywnością napływu, ponieważ nadal dostarcza pracę, gdy usługa zwalnia. Model ze stałą liczbą wirtualnych użytkowników może podczas spowolnienia zmniejszyć liczbę żądań i ukryć moment, w którym kolejki zaczynają rosnąć.
Jak wykryć wyczerpanie puli połączeń z bazą danych w Go?
Eksportuj DB.Stats() i obserwuj InUse, OpenConnections, WaitCount oraz WaitDuration. Rosnące liczniki oczekiwania, gdy używane połączenia utrzymują się na skonfigurowanym maksimum, pokazują, że żądania czekają na pulę.
Czy zwiększać pulę połączeń SQL w Go, gdy żądania czekają?
Dopiero gdy wiesz, dlaczego połączenia pozostają zajęte i jaką przepustowość ma PostgreSQL. Większa pula może przenieść kolejkę do bazy i pogorszyć rywalizację o zasoby.
Jak znaleźć wolne zapytania PostgreSQL podczas testu obciążeniowego?
Zrób migawki pg_stat_statements przed i po teście, a następnie uszereguj różnice zapytań według całkowitego i średniego czasu wykonania. Do analizy opóźnień ogona użyj reprezentatywnych śladów lub histogramów, ponieważ widok zbiorczy nie podaje p95 dla pojedynczych zapytań.
Jak rozpoznać wyciek pamięci w usłudze Go?
Uruchom stabilny test długotrwały i porównuj pamięć po odśmiecaniu przy podobnym obciążeniu. Rosnąca linia bazowa, zwłaszcza przy rosnącej liczbie obiektów sterty lub gorutin, wymaga porównania profili sterty i gorutin.
Kiedy startup powinien zatrudnić inżyniera backendu?
Zatrudnij, gdy problemy z wydajnością ujawniają stałą potrzebę odpowiedzialności za projekt bazy, obserwowalność, współbieżność, poprawność lub eksploatację. Pojedyncza poprawka indeksu lub zapytania może nie wymagać etatu, lecz niewyjaśnione zachowanie pod obciążeniem już tak.