Testy obciążeniowe odpalane tydzień przed wdrożeniem to scenariusz, który przez lata pracy w QA oglądałem kilkanaście razy. Kończy się zwykle tak samo: system nie wytrzymuje zakładanego ruchu, a przebudowa architektury na tym etapie kosztuje wielokrotność tego, co kosztowałaby trzy miesiące wcześniej. Dlatego namawiam zespoły na podejście Performance as Code: skrypt wydajnościowy trzymam w repozytorium obok testów jednostkowych, wersjonuję go i uruchamiam w pipeline przy każdym merge requeście.
Dlaczego k6 nadaje się na quality gate
k6 od Grafany powstało dla inżynierów, którzy pracują w terminalu. Scenariusze pisze się w JavaScripcie, narzędzie działa z linii poleceń i ma oficjalny obraz Dockera, więc wpięcie go w CI zajmuje kwadrans. JMeter wciąż ma u mnie swoje miejsce przy złożonych scenariuszach z korelacją sesji, opisałem go w tekście o testach wydajnościowych w JMeterze, ale w pipeline wybieram k6 z jednego powodu: progi (thresholds).
Progi zamieniają test obciążeniowy w binarną bramę jakościową (quality gate). Albo metryki mieszczą się w limitach i build jedzie dalej, albo k6 kończy pracę niezerowym kodem wyjścia i Jenkins zatrzymuje pipeline.
Thresholds: wymagania wydajnościowe zapisane w kodzie
Kryteria sukcesu definiuję w obiekcie options skryptu testowego. Po teście nie oglądam wykresów i nie zgaduję, czy „wyszło dobrze”, warunki stoją obok scenariusza i przechodzą code review razem z nim.
import http from 'k6/http';
export const options = {
vus: 20,
duration: '3m',
thresholds: {
// 95% żądań poniżej 500 ms
http_req_duration: ['p(95)<500'],
// mniej niż 1% błędów
http_req_failed: ['rate<0.01'],
},
};
export default function () {
http.get('https://aplikacja.example.com/api/oferty');
}Ten zapis robi dwie rzeczy. Dokumentuje wymagania niefunkcjonalne w pliku, do którego deweloper zagląda codziennie, zamiast w dokumencie odświeżanym raz na kwartał. I egzekwuje je: gdy p(95) wyjdzie 600 ms przy limicie 500, k6 zakończy się kodem wyjścia 99, a etap w Jenkinsie zrobi się czerwony.
Częsta pomyłka na starcie: funkcja check() sama z siebie builda nie zatrzyma. Check zlicza, ile asercji przeszło, ale test kończy się „sukcesem” nawet wtedy, gdy poległy wszystkie. Żeby nieudane asercje wywracały pipeline, dokładam próg na metrykę checks, na przykład checks: ['rate>0.99']. Dopiero wtedy brama reaguje zarówno na czasy odpowiedzi, jak i na treść odpowiedzi.
Jenkinsfile: brama w praktyce
Na agentach Jenkinsa niczego nie instaluję, etap testu wydajności działa na oficjalnym obrazie grafana/k6. Deklaratywny pipeline wygląda tak:
pipeline {
agent none
stages {
stage('Performance Test') {
agent {
docker {
image 'grafana/k6:latest'
args '-u root'
}
}
steps {
sh 'k6 run scripts/performance-test.js'
}
}
}
}Parametr -u root bywa potrzebny, bo obraz k6 domyślnie działa na użytkowniku bez praw zapisu do workspace’u Jenkinsa. Więcej konfiguracji nie potrzebuję: niezerowy kod wyjścia k6 Jenkins traktuje jak porażkę etapu, więc ustawiony dalej etap deploymentu nie wystartuje. Wolny kod zatrzymuje się przed produkcją, bez dyskusji na Slacku. Gdzie taki etap umieścić względem testów funkcjonalnych i budowania artefaktów, pokazuję w tekście o pipeline CI/CD z testami automatycznymi.
Raport JUnit: trendy zamiast czerwonej konsoli
Log w konsoli wystarcza mi do diagnozy pojedynczego builda, ale nie pokaże, od którego commita aplikacja zwalnia. Do tego potrzebuję historii. k6 nie ma wbudowanego eksportu JUnit, raport generuję funkcją handleSummary i konwerterem z jslib:
import { jUnit, textSummary } from 'https://jslib.k6.io/k6-summary/0.0.2/index.js';
export function handleSummary(data) {
return {
'results.xml': jUnit(data),
stdout: textSummary(data, { indent: ' ' }),
};
}Wygenerowany plik podpinam w sekcji post Jenkinsfile’a:
post {
always {
junit 'results.xml'
archiveArtifacts artifacts: 'results.xml', fingerprint: true
}
}Jenkins parsuje wyniki i rysuje historię testów: widać, który próg pękł, kiedy i po jakiej zmianie. Regresję wydajności łapię wtedy na poziomie konkretnego merge’a, a nie „gdzieś w ostatnim kwartale”. Te same wykresy pokazuję osobom spoza zespołu, gdy rozmawiamy o kondycji projektu, na takich danych opieram bilans jakości.
Od czego zacząć
- Jeden endpoint. Wybierz najbardziej obciążoną ścieżkę (u moich klientów to zwykle wyszukiwarka albo logowanie) i napisz dla niej pierwszy skrypt z jednym progiem.
- Progi z monitoringu, nie z sufitu. Limity ustaw na podstawie realnych czasów odpowiedzi z produkcji plus rozsądny zapas. Zbyt ostry próg nauczy zespół ignorować czerwone buildy.
- Dwa tryby uruchomienia. Krótki test dymny (1–2 minuty, kilku wirtualnych użytkowników) na każdy merge request; pełny profil obciążenia w nocnym buildzie.
- Brama bez wyjątków. Gdy próg pęka, zespół naprawia wydajność albo zmienia limit w code review, z uzasadnieniem. Wyłączenie etapu „na chwilę” kończy się bramą otwartą na stałe.
Performance as Code nie wymaga projektu wdrożeniowego. Pierwszy skrypt z progiem i etap w Jenkinsie postawiłem u klienta w jedno popołudnie. Trzy tygodnie później ta brama zatrzymała release, który przy 40 równoległych użytkownikach odpowiadał w 4 sekundy, taniej było przeczytać o tym w czerwonym buildzie niż w zgłoszeniach użytkowników.