Dlaczego REST API odpowiada wolno? Zacznij od pomiaru
Praktyczna diagnoza opóźnień REST API: percentyle, baza danych, zewnętrzne wywołania, kolejki i pomiary pod obciążeniem.
Dlaczego REST API odpowiada wolno? Zacznij od pomiaru
Endpoint ma kilkanaście linii kodu, procesor nie wygląda na przeciążony, a użytkownik czeka dwie sekundy. W takiej sytuacji łatwo zacząć od zwiększenia puli wątków albo dodania cache. Najpierw warto ustalić, na co aplikacja naprawdę czeka.
Czas odpowiedzi obejmuje więcej niż wykonanie metody kontrolera. Znaczenie mają kolejki, baza danych, sieć, serwisy zewnętrzne i rozmiar odpowiedzi. Pomiar z perspektywy klienta może dodatkowo obejmować DNS, nawiązanie połączenia i infrastrukturę przed aplikacją.
Średnia nie opisuje wszystkich użytkowników
Jeżeli większość żądań trwa krótko, a mała część bardzo długo, średnia może wyglądać dobrze. Dlatego oprócz niej warto śledzić percentyle. P95 informuje, że około 95% obserwacji mieści się w danym czasie; P99 pokazuje dalszy fragment ogona rozkładu.
Zawsze trzeba dopisać kontekst: endpoint, okno czasu, liczbę próbek, poziom obciążenia i odsetek błędów. Percentyl z kilkunastu żądań nie daje takiego obrazu jak pomiar reprezentatywnego ruchu.
W Spring Boot pomocnym punktem wyjścia są metryki HTTP w Actuatorze. Samo zbieranie czasu żądań nie oznacza jeszcze, że histogramy lub percentyle są skonfigurowane zgodnie z potrzebami. Ich konfigurację trzeba sprawdzić dla używanego systemu monitoringu.
Rozpisz drogę pojedynczego żądania
Poniższe liczby są przykładem dydaktycznym, a nie wynikiem benchmarku:
| Etap wykonywany kolejno | Przykładowy czas |
|---|---|
| Oczekiwanie na zasób wykonawczy | 90 ms |
| Odczyt zamówienia z bazy | 35 ms |
| Odczyt dodatkowych danych z bazy | 170 ms |
| Zewnętrzny serwis | 420 ms |
| Mapowanie i zapis odpowiedzi | 25 ms |
Optymalizacja mapowania z 25 do 10 ms poprawi wynik o 15 ms. Może być słuszna, ale widać, że największy koszt znajduje się gdzie indziej. Przy wywołaniach równoległych nie sumujemy bezmyślnie wszystkich czasów — liczy się ścieżka, która warunkuje zakończenie żądania.
Śledzenie rozproszone pomaga zobaczyć tę ścieżkę. Jeśli nie jest dostępne, można zacząć od pomiarów wybranych operacji. Należy przy tym ograniczyć liczbę etykiet metryk: identyfikator każdego zamówienia jako etykieta szybko stworzy ogromną liczbę szeregów czasowych.
Baza danych: sprawdź zapytania i oczekiwanie
W aplikacji z ORM problem może wynikać z wielu dodatkowych zapytań wykonywanych podczas przechodzenia po relacjach. Zwracanie encji bezpośrednio z kontrolera potrafi dodatkowo ukryć odczyty uruchamiane przy serializacji.
Przy diagnozie sprawdziłbym liczbę SQL-i, plan wykonania, liczbę zwracanych wierszy i czas oczekiwania na połączenie. Projekcja DTO lub właściwy plan pobierania danych mogą usunąć niepotrzebne odczyty. To jednak nie powód, żeby wszędzie dodać JOIN FETCH: połączenie pobierania kolekcji z paginacją wymaga szczególnej uwagi, co opisuje przewodnik Hibernate HQL.
Powiększenie puli połączeń nie jest automatyczną optymalizacją. Może przenieść kolejkę do bazy i zwiększyć rywalizację o jej zasoby.
Wywołania zewnętrzne: ogranicz czas czekania
Klient HTTP powinien mieć świadomie skonfigurowane limity połączenia i odpowiedzi. Endpoint nie może czekać dowolnie długo tylko dlatego, że zależność nie odpowiada.
Ponawianie też ma koszt. Trzy próby po kilkaset milisekund mogą zużyć cały budżet czasu żądania. Przy operacji zmieniającej dane trzeba dodatkowo rozwiązać problem idempotencji: brak odpowiedzi nie oznacza, że pierwsza próba nie została wykonana.
Równoległe pobieranie niezależnych danych bywa korzystne. Warto je wprowadzać dopiero po ustaleniu limitów współbieżności i sposobu obsługi częściowych błędów.
Cache i wątki są narzędziami do konkretnego problemu
Cache ma sens, gdy dane można bezpiecznie wykorzystać ponownie. Klucz musi uwzględniać kontekst, np. użytkownika lub organizację, a reguła wygasania powinna odpowiadać dopuszczalnej nieaktualności.
Wątki wirtualne mogą pomóc, gdy wiele żądań czeka na I/O. Nie skrócą samego wolnego zapytania SQL i nie zwiększą przepustowości przeciążonej zależności. Podobnie zmiana MVC na WebFlux nie zastąpi diagnozy.
Porównuj cały wynik
Po zmianie powtórz test z podobnym zbiorem danych, konfiguracją i obciążeniem. Sprawdź P95/P99, przepustowość, błędy, CPU, pamięć oraz obciążenie zależności. Rozgrzej JVM i odróżnij scenariusz zimnego cache od ciepłego.
Nie wystarczy zmierzyć pojedyncze wywołanie lokalnie. Generowanie nowego żądania wyłącznie po zakończeniu poprzedniego może ukryć narastające kolejki. Test powinien odzwierciedlać sposób napływu ruchu, który aplikacja ma obsłużyć.
Najlepszą zmianą jest ta, której korzyść potrafimy wykazać i której koszt rozumiemy. Bez tego szybszy wynik na laptopie może oznaczać tylko inne warunki pomiaru.