Po co testować obciążenie

Strona działa świetnie, gdy testuje ją jeden programista na swoim komputerze. Ale co się stanie, gdy 500 użytkowników wejdzie na nią jednocześnie? Albo 5000? Load testing (testy obciążeniowe) to jedyny sposób, aby odpowiedzieć na to pytanie, zanim odpowiedzą na nie niezadowoleni klienci.

Testy obciążeniowe pozwalają zidentyfikować wąskie gardła zanim staną się problemem produkcyjnym. Wolne zapytania do bazy danych, niewystarczająca pamięć, źle skonfigurowany worker pool — te problemy ujawniają się dopiero pod obciążeniem.

Apache Benchmark (ab) — szybki test z linii poleceń

ab to najprostsze narzędzie do testów obciążeniowych, wbudowane w pakiet Apache. Nie wymaga konfiguracji ani skryptów:

ab -n 10000 -c 100 https://example.com/

To polecenie wysyła 10 000 żądań z poziomem współbieżności 100 (100 jednoczesnych połączeń). Wynik zawiera kluczowe metryki:

  • Requests per second — przepustowość serwera
  • Time per request — średni czas odpowiedzi
  • Percentage of requests served within a certain time — rozkład percentylowy czasów odpowiedzi

Wady ab: obsługuje tylko żądania GET (lub POST z jednym plikiem), nie obsługuje HTTP/2, nie pozwala na złożone scenariusze. To narzędzie do szybkiej weryfikacji, nie do kompleksowych testów.

wrk — wydajny generator obciążenia

wrk to nowoczesna alternatywa dla ab, napisana w C z obsługą wielowątkowości i skryptowania w Lua:

wrk -t4 -c200 -d30s https://example.com/

Cztery wątki, 200 jednoczesnych połączeń, test trwający 30 sekund. wrk jest znacznie wydajniejszy od ab — potrafi wygenerować setki tysięcy żądań na sekundę z jednej maszyny.

Skrypty Lua pozwalają na bardziej realistyczne scenariusze — losowe ścieżki URL, nagłówki autoryzacji, dane POST:

request = function()
    local path = "/api/products/" .. math.random(1, 1000)
    return wrk.format("GET", path)
end

k6 — testy jako kod

k6 (od Grafana Labs) to najnowocześniejsze narzędzie w tej kategorii. Scenariusze testowe piszemy w JavaScript, co daje ogromną elastyczność:

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

export const options = {
    stages: [
        { duration: '1m', target: 50 },
        { duration: '3m', target: 200 },
        { duration: '1m', target: 0 },
    ],
};

export default function () {
    const res = http.get('https://example.com/');
    check(res, {
        'status 200': (r) => r.status === 200,
        'czas < 500ms': (r) => r.timings.duration < 500,
    });
    sleep(1);
}

Kluczowa cecha k6 to stages — definiowanie profilu obciążenia w czasie. Powyższy scenariusz symuluje ramp-up (narastanie ruchu do 50 użytkowników w minutę), plateau (200 użytkowników przez 3 minuty) i ramp-down (stopniowe wygaszanie). To znacznie bliższe rzeczywistemu ruchowi niż stałe obciążenie.

k6 potrafi też testować WebSockety, gRPC, generować raporty HTML i integrować się z systemami monitoringu.

Interpretacja wyników

Surowe liczby nic nie mówią bez kontekstu. Oto na co zwracamy uwagę:

Percentyle, nie średnie. Średni czas odpowiedzi 50 ms brzmi świetnie, ale jeśli p99 wynosi 3 sekundy, co setne żądanie jest dramatycznie wolne. Zawsze analizujemy:

  • p50 (mediana) — typowe doświadczenie użytkownika
  • p95 — doświadczenie "pechowego" użytkownika
  • p99 — najgorsze przypadki (poza skrajnościami)

Throughput vs latency. Wysoka przepustowość (requests/second) przy niskiej latencji to ideał. Ale często zwiększanie współbieżności podnosi throughput kosztem latencji. Trzeba znaleźć punkt, w którym latencja zaczyna gwałtownie rosnąć — to punkt nasycenia serwera.

Error rate. Procent żądań zakończonych błędem (5xx, timeout). Akceptowalny poziom to poniżej 0,1% przy normalnym obciążeniu. Jeśli error rate rośnie powyżej 1% — serwer jest przeciążony.

Stabilność w czasie. Stały czas odpowiedzi przez cały test to dobry znak. Narastająca latencja sugeruje wyciek pamięci, zapychanie się puli połączeń lub narastającą kolejkę żądań.

Identyfikacja wąskich gardeł

Gdy testy ujawnią problem, trzeba ustalić jego źródło. Najczęstsze wąskie gardła:

Baza danych — wolne zapytania SQL, brak indeksów, zbyt mała pula połączeń. Monitoruj czas odpowiedzi bazy i liczbę aktywnych połączeń. Jedno brakujące indeksowanie potrafi obniżyć wydajność stukrotnie.

Pamięć — serwer zaczyna swapować, garbage collector Go pracuje intensywnie. Monitoruj zużycie RSS i częstotliwość GC. W Go polecenie GODEBUG=gctrace=1 wypisuje statystyki garbage collectora.

CPU — renderowanie szablonów, kompresja, szyfrowanie TLS. Sprawdź top lub htop podczas testu. Jeśli CPU jest na 100% — potrzebujesz optymalizacji kodu lub silniejszego serwera.

Sieć — nasycenie pasma, zbyt wiele otwartych połączeń. Szczególnie istotne przy testach z zewnętrznej maszyny — upewnij się, że to serwer jest wąskim gardłem, a nie łącze testowe.

Reverse proxy — zbyt mała liczba worker connections w Nginx, brak keepalive do backendu. Sprawdź logi Nginx pod kątem błędów upstream timed out lub no live upstreams.

Dobre praktyki testowania

Kilka zasad, których się trzymamy:

  • Testuj z zewnętrznej maszyny — testy z localhost nie uwzględniają opóźnień sieciowych i TLS handshake
  • Testuj środowisko zbliżone do produkcji — inne dane, inna maszyna, inna konfiguracja dadzą inne wyniki
  • Stopniowo zwiększaj obciążenie — ramp-up pozwala znaleźć punkt nasycenia, a nie tylko sprawdzić, czy serwer się nie przewróci
  • Monitoruj zasoby serwera podczas testu — CPU, pamięć, I/O dysku, połączenia sieciowe
  • Powtarzaj testy — jeden przebieg to za mało, wyniki mogą się różnić
  • Zapisuj wyniki — porównywanie z poprzednimi testami pozwala wychwycić regresje wydajności

Podsumowanie

Load testing to nie luksus, lecz konieczność dla każdej strony, która ma obsługiwać realny ruch. Zaczynaj od prostego ab lub wrk dla szybkiej weryfikacji, a dla kompleksowych scenariuszy sięgnij po k6. Analizuj percentyle zamiast średnich i zawsze monitoruj zasoby serwera podczas testów.

Potrzebujesz audytu wydajności swojej strony lub API? Skontaktuj się z nami — przeprowadzimy testy obciążeniowe i pomożemy zidentyfikować wąskie gardła.