Dlaczego aplikacja jest wolna tylko w produkcji: checklisty, metryki i tracing

0
5
Rate this post

Realne pytania, które zwykle pojawiają się w takiej sytuacji: dlaczego aplikacja jest wolna tylko w produkcji, skoro lokalnie działa szybko? Czy winna jest aplikacja, baza, infrastruktura, dane, ruch czy zewnętrzna integracja? Od czego zacząć, gdy użytkownicy zgłaszają „wolno”, ale dashboard nie krzyczy na czerwono? Kiedy wystarczy checklista operacyjna, kiedy trzeba wejść w metryki, a kiedy bez tracingu nie da się już iść dalej?

wolna aplikacja na produkcji, diagnostyka wydajności, checklista incydentu, metryki latency throughput errors saturation, tracing rozproszony, różnice między środowiskami, wolne endpointy, bottleneck bazy danych, connection pool exhaustion, lock contention, kolejki i cache, observability w praktyce

Nawigacja:

Aplikacja szybka lokalnie, wolna na produkcji — co to naprawdę zmienia?

Objaw jest ten sam, warunki już nie

Jeśli użytkownik mówi, że aplikacja działa wolno, dostajesz objaw, a nie diagnozę. To ważna różnica. „Wolno” nie oznacza jeszcze: za wysokie CPU, źle napisany SQL albo za mało RAM-u. Ten sam objaw może wynikać z bardzo różnych przyczyn: czekania na bazę danych, zatoru w puli połączeń, przeciążonego storage, zewnętrznego API, które odpowiada z opóźnieniem, albo z danych produkcyjnych, które uruchamiają mało wydajną ścieżkę biznesową.

Pierwsze pytanie diagnostyczne brzmi więc nie „co jest wolne w kodzie?”, tylko kiedy, komu i gdzie robi się wolno? Czy problem dotyczy wszystkich użytkowników? Jednego endpointu? Jednego klienta? Tylko ruchu mobilnego? Jednej pory dnia? Jednego regionu? Jednego typu danych? Jeśli już na starcie nie rozbijesz objawu na konkretne scenariusze, później bardzo łatwo pomylić skutek z przyczyną.

Produkcja różni się od local i stagingu nie tylko skalą. Różni się też rozkładem danych, współbieżnością, zachowaniem użytkowników, retry po stronie klientów, aktywnością background jobów, ustawieniami cache, limitami w proxy, topologią sieci i jakością zależności zewnętrznych. Lokalnie możesz mieć piękne, małe i przewidywalne rekordy. Produkcja potrafi dostarczyć długie listy, skrajne filtry, nietypowe kombinacje flag i rekordy o dużej cardinality, które zmieniają plan wykonania zapytania.

„Wolno” to sygnał z zewnątrz, nie hipoteza o przyczynie

Typowy błąd wygląda tak: ktoś widzi opóźnienie i od razu otwiera profiler kodu albo zaczyna analizować pojedyncze zapytanie SQL. Tyle że problem może nie być w samym kodzie, tylko w tym, że żądanie czeka na wolny zasób. CPU bywa niskie, a aplikacja i tak odpowiada fatalnie, bo wszystkie requesty stoją w kolejce do bazy, cache masowo pudłuje albo połączenia do zewnętrznej usługi wiszą aż do timeoutu.

Drugi częsty błąd to zbyt szybkie uznanie, że skoro staging działa dobrze, to „kod jest OK”. To nieuprawniony skrót myślowy. Staging zwykle nie odtwarza produkcji pod kątem prawdziwego wolumenu danych, współbieżności, aktywnych jobów, nierównego rozkładu ruchu ani ograniczeń sieciowych. W praktyce staging częściej potwierdza poprawność funkcjonalną niż rzeczywistą wydajność pod realnym obciążeniem.

Jaki masz cel na tym etapie? Jeśli chcesz szybko zawęzić obszar awarii, potrzebujesz warstwowej ścieżki decyzji. Najpierw prosty filtr operacyjny, później metryki pokazujące trendy i miejsce nasycenia, a jeśli problem nadal pozostaje niejednoznaczny — tracing, który rozcina pojedynczą ścieżkę requestu przez usługi i zależności.

Klasy problemów, które produkcja ujawnia najczęściej

Żeby nie błądzić, dobrze od razu rozróżnić kilka klas źródeł spowolnień. Nie chodzi o akademicką kategoryzację, tylko o praktyczny podział, który porządkuje kolejne kroki.

  • Warstwa aplikacyjna — nieefektywna ścieżka kodu, zbyt kosztowna serializacja, niekontrolowane retry, N+1, duża liczba synchronicznych wywołań.
  • Baza danych — wolne zapytania, lock contention, słaba selektywność filtrów, brak indeksu pasującego do realnych danych, wyczerpana pula połączeń.
  • Infrastruktura — saturacja I/O, throttling CPU, problemy storage, limity kontenerów, niestabilność autoscalingu, backlog na workerach.
  • Sieć i topologia — dodatkowe hopy, wolny DNS, opóźnienia między regionami, load balancer, reverse proxy, limity po drodze.
  • Integracje zewnętrzne — auth, płatności, API partnera, webhooki, storage obiektowy, kolejki.
  • Dane i ruch — duże rekordy, wysoka cardinality, bursty ruchu, hot key, konkretni klienci generujący nietypowe zapytania.
  • Konfiguracja — feature flags, timeouty, cache TTL, limity połączeń, różne wersje bibliotek, zmiany po deployu.

To właśnie dlatego odpowiedź na pytanie „dlaczego aplikacja jest wolna tylko w produkcji?” bardzo rzadko brzmi „bo produkcja jest większa”. Zwykle chodzi o kombinację skali, danych, współbieżności i zależności, której nie masz poza produkcją.

Najpierw porównaj środowiska, nie kod — typowe różnice, które fałszują obraz wydajności

Co faktycznie bywa inne między local, staging i production

Najbardziej zdradliwa różnica to nie „mocniejszy serwer” albo „większa baza”, ale kształt danych. Lokalnie i na stagingu dane są zwykle mniejsze, bardziej jednorodne i czystsze. Produkcja zawiera rekordy historyczne, skrajne przypadki, duże listy powiązań, pola tekstowe o nietypowej długości, wartości puste i kombinacje filtrów, których nikt świadomie nie odtworzył w testach. To wystarczy, żeby zapytanie, które zawsze działało szybko, zaczęło wybierać inny plan wykonania albo zwracać wielokrotnie więcej danych do aplikacji.

Kobieta z laptopem w nowoczesnym centrum danych między serwerami
Źródło: Pexels | Autor: Christina Morillo

Druga różnica to współbieżność. Nawet jeżeli pojedynczy request działa lokalnie idealnie, pięćdziesiąt lub pięćset takich samych requestów może ujawnić problem z pulą połączeń, lockami, cache stampede albo kolejką jobów. Lokalnie testujesz głównie czas obsługi jednostkowego wywołania. Produkcja testuje zdolność systemu do utrzymania płynności przy wielu jednoczesnych żądaniach i zależnościach konkurujących o te same zasoby.

Trzecia rzecz to konfiguracja środowiska. Inne timeouty, inny rozmiar connection poola, włączone lub wyłączone feature flags, odmienny cache TTL, różne wersje bibliotek, inne limity reverse proxy albo kontenera — każdy z tych elementów może mieć większy wpływ niż sam kod. Czasem po deployu wszystko jest „to samo”, ale nowa wersja usługi ma inny domyślny rozmiar puli, inaczej pracuje GC albo zmienia sposób wykonywania zapytań przez ORM.

Współbieżność, bursty i nierówny rozkład ruchu

Nie każdy ruch jest równy. Produkcja ma piki, bursty, użytkowników automatyzujących działania, klientów wysyłających retry i boty trafiające w ten sam endpoint. To oznacza, że średni throughput może wyglądać spokojnie, a system i tak ma problem, bo krótki burst zapełnia kolejkę, wyczerpuje pulę połączeń albo powoduje falę timeoutów wtórnych.

Warto zapytać: czy problem pojawia się przy określonej porze? Tuż po pełnej godzinie? Po uruchomieniu jobów cyklicznych? Po imporcie danych? W czasie wysyłki notyfikacji? Po wznowieniu wielu sesji jednocześnie? To nie są szczegóły. To często bezpośrednia droga do przyczyny. Jeśli spowolnienie koreluje z tłem, nie szukasz już tylko wolnego endpointu, ale rywalizacji o zasoby między ruchem użytkowników a procesami wewnętrznymi.

Do tego dochodzi nierówny rozkład endpointów. Czasem 90% ruchu jest tanie, a 10% bardzo kosztowne. Na dashboardzie średnia wygląda dobrze, a użytkownicy konkretnej funkcji mają fatalne doświadczenie. Dlatego porównanie środowisk nie może kończyć się na pytaniu „czy ogólnie działa szybko?”. Trzeba zejść do poziomu per endpoint, per typ operacji, a czasem per klient lub per segment danych.

Topologia sieci i zależności, których lokalnie prawie nie widać

Lokalnie aplikacja rozmawia często z usługami uruchomionymi obok, na tej samej maszynie albo z mockami. Produkcja niemal nigdy tak nie działa. Masz load balancer, reverse proxy, osobny serwis autoryzacji, zewnętrzne API, storage, kolejki, DNS i czasem komunikację między regionami. Każdy dodatkowy hop to nowa szansa na opóźnienie, timeout, retry i kaskadę spowolnień.

Zdradliwy bywa też cache. Lokalnie bywa ciepły, prosty i przewidywalny. Produkcja ma prawdziwe TTL-e, wygaszanie kluczy, hot keye i okresowe fale cache missów. Jeśli jedna grupa kluczy nagle wygasa, aplikacja może zacząć masowo odpalać ciężkie operacje odświeżające. Użytkownik widzi „aplikacja zwolniła”, ale prawdziwy problem siedzi w tym, że cache przestał amortyzować obciążenie.

Krótki realistyczny scenariusz: lokalnie zapytanie po filtrze działa błyskawicznie na małej próbce danych. W produkcji ten sam filtr trafia na rekordy o wysokiej cardinality, zwraca ogromny wynik pośredni i baza zmienia plan wykonania. Kod się nie zmienił. Zmieniły się warunki, a razem z nimi koszt całej operacji.

Praktyczny wniosek przed grzebaniem w kodzie

Zanim zaczniesz szukać „wolnej funkcji”, sprawdź, czy naprawdę porównujesz te same warunki. Jeśli nie porównujesz tego samego wolumenu danych, tego samego wzorca ruchu, tej samej konfiguracji i tych samych zależności, to local kontra production nie jest uczciwym porównaniem. Wtedy nawet bardzo dobra analiza kodu może zaprowadzić w złą stronę.

Trzy warianty diagnozy: checklista, metryki, tracing — czym się różnią i czego nie zobaczą

Wariant 1 — checklista operacyjna jako najszybszy filtr

Checklista jest najtańszym i najszybszym ruchem, gdy potrzebujesz odpowiedzi tu i teraz. Działa dobrze przy incydencie, gdy liczy się zawężenie przestrzeni problemu w kilkanaście minut, a nie budowa pełnego obrazu systemu. Jej zadanie nie polega na znalezieniu jednej „winnej linijki”, tylko na wychwyceniu sygnałów, które od razu zmieniają kierunek diagnozy: wzrost timeoutów, kolejki, wyczerpanie puli połączeń, locki w bazie, skok cache missów, regresja po deployu.

Plus jest oczywisty: działa od ręki, nawet przy słabszej dojrzałości monitoringu. Minus też jest oczywisty: checklista nie pokaże dobrze złożonej ścieżki pojedynczego requestu przez wiele usług. Jeśli problem jest rozproszony, przerywany albo dotyczy tylko części żądań, sama checklista może powiedzieć „coś jest nie tak”, ale nie odpowie precyzyjnie, gdzie ginie czas.

Wariant 2 — metryki jako warstwa wykrywania trendu i miejsca przeciążenia

Metryki są kolejnym poziomem. Odpowiadają na pytania: gdzie rośnie latency, gdzie spada throughput, gdzie pojawiają się błędy, co się nasyca, gdzie tworzy się kolejka? Dobrze zorganizowany zestaw metryk pozwala odróżnić problem chwilowy od trendu, wykryć korelację z deployem, porą dnia albo określonym obciążeniem oraz zobaczyć, czy aplikacja stoi na CPU, I/O, bazie, cache czy zależności zewnętrznej.

Metryki są najlepsze wtedy, gdy chcesz nie tylko zgasić pojedynczy incydent, ale też zrozumieć wzorzec zachowania systemu. Ich ograniczenie? Same w sobie często nie tłumaczą, dlaczego konkretny request utknął. Mogą pokazać, że 95 percentyl czasu odpowiedzi rośnie i że wzrasta czas odpowiedzi bazy, ale nie powiedzą jeszcze, czy chodzi o konkretną ścieżkę autoryzacji, jeden typ payloadu czy szczególną sekwencję wywołań między usługami.

Wariant 3 — tracing jako narzędzie do rozcięcia jednego requestu

Tracing rozproszony odpowiada na inne pytanie niż checklista i metryki. Nie pyta „co dzieje się średnio?” ani „gdzie trend wygląda źle?”, tylko jak przebiegło jedno konkretne żądanie i na którym odcinku straciło czas. To kluczowe przy architekturze rozproszonej, wielu integracjach, asynchronicznych przejściach i problemach, które dotyczą tylko wybranych requestów.

Bez tracingu bardzo trudno ustalić, czy wolny endpoint spędził czas w API, warstwie autoryzacji, cache, bazie, kolejce czy na zewnętrznym API. Owszem, logi czasem pomagają, ale przy wielu usługach i braku wspólnego identyfikatora korelacyjnego robi się z tego ręczne sklejanie historii. Tracing porządkuje to automatycznie.

Minus? Jeśli nie masz podstawowych metryk i sensownej hipotezy, tracing bywa kosztowny poznawczo. Łatwo zalać się danymi i analizować ślady, które nie reprezentują właściwego problemu. W systemie prostym, z jedną usługą i jedną bazą, pełny tracing może być też zbyt ciężkim rozwiązaniem na początek.

Porównanie wariantów w praktyce

WariantCo wykrywa najlepiejCzego zwykle nie wykrywa dobrzeKiedy działa najlepiejPlusyMinusyTypowe ryzyko błędnej interpretacji
Checklista operacyjnaOczywiste sygnały incydentu: timeouty, błędy, backlog, locki, problemy po deployuRozproszoną ścieżkę pojedynczego requestu i subtelne zależności między usługamiPilny incydent, monolit, nis

Pilny incydent, monolit, niski poziom obserwowalnościSzybki start, niski koszt, dobre do odsiać oczywiste przyczynyFałszywe poczucie, że skoro „wszystko zielone”, to problem nie istnieje
MetrykiTrend, punkt nasycenia, korelacje z ruchem, deployem i zasobamiDokładną historię pojedynczego wolnego requestuPowtarzalne spowolnienia, planowanie pojemności, szukanie bottleneckuDają kontekst w czasie, dobrze pokazują przeciążenie i regresjeŁatwo ukryć problem w średnich i źle dobranych agregacjachPatrzenie na average zamiast percentyli albo globalnie zamiast per endpoint
TracingŚcieżkę jednego requestu przez usługi, bazy, kolejki i integracjeObraz trendu całego systemu bez wsparcia metrykMikroserwisy, złożone integracje, problemy nieregularne i trudne do odtworzeniaPrecyzyjnie pokazuje, gdzie ginie czasWiększa złożoność wdrożenia i analizyAnaliza przypadkowych śladów zamiast reprezentatywnej próbki problemu

Jaki masz cel: ugasić pożar w pół godziny czy zrozumieć, czemu system regularnie dobija do ściany? Od tej odpowiedzi zależy wybór narzędzia. Przy incydencie zwykle zaczyna się od checklisty, potem potwierdza hipotezę metrykami, a tracing wchodzi wtedy, gdy nadal nie wiadomo, który odcinek ścieżki naprawdę boli. Odwrócenie tej kolejności też bywa błędem — łatwo wpaść w analizę śladów, zanim ustalisz, czy problem dotyczy wszystkich żądań, jednej funkcji czy tylko piku o konkretnej porze.

Dobrze działa też prosta zasada: najpierw szeroko, potem wąsko. Najpierw sprawdzasz, czy to problem zasobów, zależności, deployu albo wzorca ruchu. Później schodzisz do percentyli, endpointów i segmentów danych. Na końcu, jeśli trzeba, tniesz pojedynczy request tracingiem. Przykład z praktyki bywa banalny: dashboard pokazuje wzrost p95, ale dopiero rozbicie per endpoint ujawnia, że winny jest jeden kosztowny eksport uruchamiany seriami retry przez klienta integracyjnego.

Jeśli masz wdrożyć tylko jedną rzecz od razu, wybierz taką, która pomoże podjąć następną decyzję. Czasem będzie to kilka podstawowych metryk z podziałem na endpoint i status. Czasem identyfikator korelacyjny w logach, żeby dało się spiąć zdarzenia między usługami. Sama ilość danych rzadko rozwiązuje problem. Liczy się to, czy po pięciu minutach patrzenia w wykres albo ślad umiesz odpowiedzieć: co dokładnie zwolniło, kiedy i pod jakim obciążeniem.

Gdy aplikacja jest wolna tylko na produkcji, problem najczęściej nie polega na tym, że kod „nagle stał się zły”, ale że system działa w innych warunkach, niż zakładano. Im szybciej oddzielisz różnice środowisk od realnego bottlenecku, tym mniej czasu stracisz na strzelanie w ciemno.

Wariant 1: checklista diagnostyczna, gdy trzeba szybko odsiać oczywiste przyczyny

Masz incydent i użytkownicy piszą tylko tyle, że „wszystko muli”? Wtedy nie szukasz jeszcze eleganckiej analizy. Szukasz najkrótszej drogi do zawężenia problemu. Checklista jest dobra właśnie do tego: ma szybko odpowiedzieć, czy problem wygląda na zasoby, bazę, sieć, deploy, integrację czy wzorzec ruchu.

Najważniejsze: checklista nie ma być listą wszystkiego, co da się sprawdzić. Jeśli jest za długa, przestaje pomagać. Lepsza jest krótka sekwencja pytań, która zmienia kierunek działań.

Od czego zacząć, kiedy sygnał jest nieprecyzyjny

Zadaj sobie najpierw trzy pytania:

  • Czy problem dotyczy wszystkich użytkowników, czy tylko części?
  • Czy spowolnienie jest stałe, czy pojawia się falami?
  • Czy coś zmieniło się tuż przed objawem? Deploy, migracja, zmiana konfiguracji, restart, większy ruch, awaria integracji.

To brzmi banalnie, ale od tego zależy dalsza ścieżka. Jeśli problem dotyczy tylko wybranych endpointów albo jednego typu użytkownika, nie zaczynasz od globalnego „CPU wysokie czy nie”. Jeśli wszystko zwolniło naraz po wdrożeniu, podejrzenie pada najpierw na zmianę, nie na losowy zbieg okoliczności.

Krótka checklista operacyjna, która naprawdę pomaga

Przy pierwszym przejściu skup się na objawach o dużej wartości diagnostycznej:

  • Latency: czy wzrosły czasy odpowiedzi i dla których endpointów?
  • Error rate: czy razem ze spowolnieniem rosną timeouty, 5xx, retry, odrzucone połączenia?
  • Throughput: czy ruch wzrósł, czy raczej spadł, bo system zaczął się korkować?
  • Saturation: czy dobija CPU, pamięć, pula połączeń, worker pool, wątki, kolejka zadań?
  • Baza danych: czy są locki, długie zapytania, zmiana planów wykonania, kolejka na połączeniach?
  • Cache: czy spadł hit rate, czy wzrosły missy, czy wygasły kluczowe wpisy?
  • I/O i sieć: czy rosną opóźnienia dysku, problemy DNS, timeouty do usług zewnętrznych?
  • Kolejki i async: czy backlog rośnie szybciej, niż jest konsumowany?
  • Deploy i konfiguracja: czy zmieniły się limity, feature flagi, parametry puli, timeouty, rozmiar instancji?

Jeśli po tej rundzie widzisz wyraźny trop, nie komplikuj. Nie wszystko wymaga tracingu. Gdy pula połączeń do bazy jest zapchana, a czasy zapytań skoczyły po migracji, masz już sensowną hipotezę roboczą.

Co checklista wykrywa bardzo dobrze

Ten wariant działa szczególnie dobrze w kilku typowych sytuacjach:

  • spowolnienie zaczęło się zaraz po deployu,
  • problem jest szeroki i dotyczy dużej części ruchu,
  • system jest prosty: jeden serwis, jedna baza, kilka oczywistych zależności,
  • nie masz jeszcze dojrzałej obserwowalności, ale masz podstawowe dashboardy i logi,
  • trzeba zdecydować, czy rollback ma sens.

Krótki przykład: po wdrożeniu wszystko wygląda „jakoś wolniej”. Checklista pokazuje skok czasu odpowiedzi tylko dla endpointów zapisujących dane, a w bazie pojawiają się locki na nowo używanej tabeli. To nadal nie mówi, która linijka kodu jest winna, ale już mówi, że problem nie siedzi w CPU aplikacji ani w sieci.

Gdzie ten wariant kończy się ślepą uliczką

Co już próbowałeś? Jeśli przejrzałeś dashboardy i wszystko jest „w normie”, a użytkownicy nadal skarżą się na wolne odpowiedzi, to klasyczny znak, że sama checklista nie wystarczy.

Najczęstsze ślepe uliczki są trzy:

  • średnie maskują problem — globalna średnia wygląda dobrze, ale p95 konkretnego endpointu jest fatalne,
  • problem dotyczy tylko części requestów — np. jednego rodzaju payloadu, jednego tenant-a, jednego kraju lub jednej integracji,
  • czas rozkłada się na wiele małych opóźnień — każda usługa wygląda „prawie dobrze”, ale razem dają bardzo wolną ścieżkę.

Wtedy checklista zrobiła swoje: odsiać oczywiste rzeczy. Dalej potrzebujesz lepszego obrazu trendu albo pojedynczej ścieżki żądania.

Wariant 2: metryki, gdy trzeba znaleźć trend, punkt nasycenia i miejsce przeciążenia

Jaki masz cel teraz: potwierdzić jednorazowy incydent czy zrozumieć, czemu system regularnie zwalnia? Jeśli to drugie, same odpowiedzi w stylu „CPU skoczyło” są za słabe. Potrzebujesz metryk, które pokażą kiedy, dla czego i przy jakim obciążeniu wszystko się psuje.

Dobre metryki nie muszą być bardzo rozbudowane. Muszą być sensownie pocięte. Globalny wykres całej aplikacji bywa zbyt gruby, żeby zobaczyć prawdziwy problem.