7 min

Tuning wydajności Go + Postgres: praktyczny playbook dla API

Playbook do tuningu wydajności Go + Postgres dla AI-generowanych API: pula połączeń, analiza planów zapytań, mądre indeksowanie, bezpieczna paginacja i szybkie kształtowanie JSON.

Tuning wydajności Go + Postgres: praktyczny playbook dla API

Jak „wolno” wygląda dla API w Go na Postgresie

AI-generowane API mogą wydawać się szybkie w wczesnych testach. Odpalasz endpoint kilka razy, zestaw danych jest mały, a żądania przychodzą pojedynczo. Potem pojawia się prawdziwy ruch: mieszane endpointy, nagłe skoki obciążenia, zimniejsze cache i więcej wierszy niż się spodziewałeś. Ten sam kod może zacząć działać losowo wolno, choć nic nie uległo awarii.

Zwykle „wolno” objawia się kilkoma sposobami: skoki latencji (większość żądań jest w porządku, niektóre trwają 5x–50x dłużej), timeouty (mały odsetek kończy się niepowodzeniem) lub wysokie użycie CPU (Postgres wykonuje zapytania, albo Go zużywa CPU na JSON, goroutiny, logowanie i ponawianie).

Częsty scenariusz to endpoint listy z elastycznym filtrem, który zwraca dużą odpowiedź JSON. W testowej bazie skanuje kilka tysięcy wierszy i kończy szybko. W produkcji skanuje kilka milionów, sortuje je i dopiero potem stosuje LIMIT. API wciąż „działa”, ale p95 latencja rośnie wykładniczo i kilka żądań kończy się timeoutem podczas skoków.

Aby oddzielić powolność bazy od powolności aplikacji, trzymaj prosty model myślowy.

Jeśli baza jest wolna, handler w Go spędza większość czasu czekając na zapytanie. Możesz też zobaczyć wiele żądań „w locie”, podczas gdy CPU Go wygląda normalnie.

Jeśli aplikacja jest wolna, zapytanie kończy się szybko, ale czas jest tracony po zapytaniu: budowanie dużych obiektów odpowiedzi, marshaling JSON, dodatkowe zapytania per wiersz lub za dużo pracy na żądanie. CPU Go rośnie, pamięć rośnie, a latencja rośnie wraz z rozmiarem odpowiedzi.

„Dostatecznie dobrze” przed uruchomieniem to nie perfekcja. Dla wielu CRUD-owych endpointów celem jest stabilna p95 latencja (nie tylko średnia), przewidywalne zachowanie przy skokach i brak timeoutów przy oczekiwanym szczycie. Cel jest prosty: brak niespodziewanych wolnych żądań, gdy dane i ruch rosną, oraz wyraźne sygnały, gdy coś odpływa.

Najpierw baza: kilka liczb, które mają znaczenie

Zanim zaczniesz stroić cokolwiek, zdecyduj, co znaczy „dobrze” dla twojego API. Bez bazy wyjściowej łatwo spędzić godziny na zmianach ustawień i nadal nie wiedzieć, czy poprawiłeś sytuację, czy tylko przesunąłeś wąskie gardło.

Zwykle trzy liczby mówią najwięcej:

  • p95 latencja żądań (nie średnia)
  • wskaźnik błędów (HTTP 5xx, timeouty, anulowania)
  • czas DB na żądanie (ile każde żądanie czeka na Postgres)

p95 to metryka „złego dnia”. Jeśli p95 jest wysoki, ale średnia OK, to mały zestaw żądań robi za dużo pracy, blokuje się na lockach lub uruchamia wolne plany.

Uczyń wolne zapytania widocznymi wcześnie. W Postgresie włącz logowanie wolnych zapytań z niskim progiem do testów przed uruchomieniem (np. 100–200 ms) i loguj pełne zapytanie, żeby móc wkleić je do klienta SQL. Zachowaj to tymczasowo. Logowanie każdego wolnego zapytania w produkcji szybko staje się hałaśliwe.

Następnie testuj z prawdziwie wyglądającymi żądaniami, a nie tylko z jedną trasą „hello world”. Wystarczy mały zestaw, jeśli odzwierciedla rzeczywiste użycie: wywołanie listy z filtrami i sortowaniem, strona szczegółów z kilkoma joinami, tworzenie/aktualizacja z walidacją oraz zapytanie w stylu wyszukiwania z dopasowaniami częściowymi.

Jeśli generujesz endpointy ze specyfikacji (np. narzędziem vibe-coding jak Koder.ai), uruchamiaj ten sam kilka żądań wielokrotnie z powtarzalnymi danymi. To ułatwia mierzenie zmian jak indeksy, poprawki paginacji i przepisywanie zapytań.

Na koniec wybierz cel, który możesz wypowiedzieć na głos. Przykład: „Większość żądań poniżej 200 ms p95 przy 50 równoczesnych użytkownikach, błędy poniżej 0,5%.” Dokładne liczby zależą od produktu, ale jasny cel zapobiega niekończącym się drobnym poprawkom.

Pool połączeń, który utrzymuje Postgresa stabilnym

Pula połączeń utrzymuje ograniczoną liczbę otwartych połączeń do bazy i je ponownie wykorzystuje. Bez puli każde żądanie może otwierać nowe połączenie, a Postgres marnuje czas i pamięć na zarządzanie sesjami zamiast wykonywać zapytania.

Celem jest utrzymanie Postgresa zajętego użyteczną pracą, a nie przełączaniem kontekstu między zbyt wieloma połączeniami. To często pierwszy znaczący zysk, szczególnie dla AI-generowanych API, które mogą po cichu stać się gadatliwymi endpointami.

Proste ustawienia startowe

W Go zwykle stroisz max open connections, max idle connections i time-to-live połączeń. Bezpieczny punkt startowy dla wielu małych API to mała wielokrotność rdzeni CPU (często 5–20 łącznie), podobna liczba trzymana jako idle oraz okresowa recyklingacja połączeń (np. co 30–60 minut).

Jeśli uruchamiasz wiele instancji API, pamiętaj, że pula się mnoży. Pula 20 połączeń na 10 instancji to 200 połączeń uderzających w Postgresa — stąd zespoły niespodziewanie napotykają limity połączeń.

Jak stwierdzić, że pula jest problemem

Problemy z pulą czują się inaczej niż wolne SQL.

Jeśli pula jest za mała, żądania czekają zanim w ogóle trafią do Postgresa. Latencja skacze, ale CPU bazy i czasy zapytań mogą wyglądać dobrze.

Jeśli pula jest za duża, Postgres wygląda na przeciążony: dużo aktywnych sesji, presja pamięci i nierównomierna latencja między endpointami.

Szybki sposób rozdzielenia to zmierzyć wywołania DB w dwóch częściach: czas oczekiwania na połączenie vs czas wykonywania zapytania. Jeśli większość to „oczekiwanie”, to pula jest wąskim gardłem. Jeśli większość to „w zapytaniu”, skup się na SQL i indeksach.

Przydatne szybkie kontrole:

  • Loguj statystyki puli (open, in-use, idle) i obserwuj, czy in-use stoi na maxie.
  • Dodaj timeout przy zdobywaniu połączenia, żeby oczekiwania kończyły się szybko w staging.
  • Monitoruj aktywne połączenia w Postgres i jak blisko jesteś max_connections.
  • Potwierdź, że każde żądanie zamyka rows i szybko zwalnia połączenia.
  • Testuj obciążeniowo z tą samą liczbą instancji aplikacji, które planujesz uruchomić.

pgxpool vs database/sql

Jeśli używasz pgxpool, masz pulę zaprojektowaną pod Postgresa z czytelnymi statystykami i dobrymi domyślnymi zachowaniami. Jeśli używasz database/sql, masz standardowy interfejs działający z wieloma bazami, ale musisz jawnie ustawić parametry puli i zachowanie sterownika.

Praktyczna zasada: jeśli jesteś w pełni na Postgresie i chcesz kontroli, pgxpool jest często prostszy. Jeśli polegasz na bibliotekach oczekujących database/sql, pozostań przy nim, ustaw pulę jawnie i mierz oczekiwania.

Przykład: endpoint listujący zamówienia może działać w 20 ms, ale przy 100 równoczesnych użytkownikach skacze do 2 s. Jeśli logi pokazują 1.9 s oczekiwania na połączenie, tunning zapytań nie pomoże, dopóki pula i łączna liczba połączeń do Postgresa nie będą właściwie dopasowane.

Planowanie zapytań: szybkie odczyty EXPLAIN

Gdy endpoint wydaje się wolny, sprawdź, co Postgres faktycznie robi. Szybkie odczytanie EXPLAIN często wskazuje poprawkę w minutach.

Uruchom to na dokładnym SQL, które wysyła twoje API:

EXPLAIN (ANALYZE, BUFFERS)
SELECT id, status, created_at
FROM orders
WHERE user_id = $1 AND status = $2
ORDER BY created_at DESC
LIMIT 50;

Kilka linii ma największe znaczenie. Spójrz na węzeł najwyższego poziomu (co Postgres wybrał) i sumy na dole (ile to zajęło). Porównaj też estymowane vs rzeczywiste wiersze. Duże rozbieżności zwykle oznaczają, że planner źle zgadł.

Co zwykle znaczą kluczowe linie

Jeśli widzisz Index Scan lub Index Only Scan, Postgres używa indeksu, co zwykle jest dobre. Bitmap Heap Scan może być ok dla średnich dopasowań. Seq Scan oznacza przeszukanie całej tabeli, co jest dopuszczalne tylko, gdy tabela jest mała lub prawie każdy wiersz pasuje.

Typowe czerwone flagi:

  • Seq Scan na dużej tabeli
  • Estymowane wiersze vs rzeczywiste bardzo różne (np. 10 estymowanych vs 10 000 rzeczywistych)
  • Sort zajmujący większość czasu (często z ORDER BY)
  • "Filter:" usuwający wiele wierszy po skanie
  • Wysokie shared read blocks w BUFFERS (dużo danych odczytanych)

Dlaczego plany się mylą (i łatwe poprawki)

Wolne plany zwykle wynikają z kilku wzorców:

  • Brak indeksu dla wzorca WHERE + ORDER BY (np. (user_id, status, created_at))
  • Niedopasowane typy (np. porównywanie kolumny UUID do parametru tekstowego), co może blokować użycie indeksu
  • Funkcje w WHERE (np. WHERE lower(email) = $1), co może wymusić skany, chyba że dodasz indeks wyrażeniowy

Jeśli plan wygląda dziwnie i estymaty są daleko, statystyki często są przestarzałe. Uruchom ANALYZE (lub pozwól autovacuum nadrobić zaległości), żeby Postgres poznał bieżącą liczbę wierszy i rozkład wartości. To ma znaczenie po dużych importach lub gdy nowe endpointy szybko zapisują dużo danych.

Indeksowanie pod zapytania, które faktycznie wykonujesz

Planowanie wydajności przed budową
Użyj trybu planowania, aby zdecydować o filtrach, sortowaniu i paginacji zanim wygenerujesz kod.

Indeksy pomagają tylko wtedy, gdy pasują do tego, jak API pyta o dane. Jeśli tworzysz je na zgadywankę, dostajesz wolniejsze zapisy, większe zajęcie dysku i niewielkie przyspieszenie.

Praktyczny sposób myślenia: indeks to skrót dla konkretnego pytania. Jeśli API zada inne pytanie, Postgres zignoruje skrót.

Buduj indeksy wokół filtrów + sortowania

Jeśli endpoint filtruje po account_id i sortuje po created_at DESC, pojedynczy indeks złożony często bije dwa osobne indeksy. Pomaga Postgresowi znaleźć właściwe wiersze i zwrócić je w odpowiedniej kolejności przy mniejszej pracy.

Zasady praktyczne, które zwykle się sprawdzają:

  • Indeksuj kolumny, po których najczęściej filtrujesz, potem dodaj kolumnę sortującą.
  • Trzymaj indeksy złożone krótkie. Dwie kolumny są powszechne; trzy czasem OK; więcej zwykle podejrzane.
  • Umieść najbardziej selektywny filtr pierwszy (ten, który najbardziej zawęża wyniki).
  • Unikaj oddzielnych indeksów, które są w pełni pokryte przez lepszy indeks złożony.
  • Wol preferuj jeden dobrze dobrany indeks zamiast kilku „może przydatnych”.

Przykład: jeśli twoje API ma GET /orders?status=paid i zawsze pokazuje najnowsze, indeks (status, created_at DESC) dobrze pasuje. Jeśli większość zapytań również filtruje po kliencie, (customer_id, status, created_at) może być lepszy, ale tylko jeśli tak endpoint rzeczywiście działa w produkcji.

Indeksy częściowe dla powszechnych filtrów

Jeśli większość ruchu trafia do wąskiego wycinka wierszy, indeks częściowy może być tańszy i szybszy. Na przykład, jeśli aplikacja głównie czyta rekordy aktywne, indeksując tylko WHERE active = true indeks będzie mniejszy i bardziej prawdopodobne, że zostanie w pamięci.

Aby potwierdzić, że indeks pomaga, wykonaj szybkie kontrole:

  • Uruchom EXPLAIN (lub EXPLAIN ANALYZE w bezpiecznym środowisku) i szukaj indeksowego skanu pasującego do zapytania.
  • Porównaj czasy i przeczytane wiersze z i bez indeksu.
  • Obserwuj „Rows Removed by Filter” — jeśli ciągle wysokie, indeks nie pasuje do filtra.

Usuwaj nieużywane indeksy ostrożnie. Sprawdź statystyki użycia (czy indeks był skanowany). Rzucaj je pojedynczo w oknach niskiego ryzyka i miej plan rollbacku. Nieużywane indeksy nie są nieszkodliwe — spowalniają inserty i aktualizacje przy każdym zapisie.

Wzorce paginacji, które nie zwalniają z czasem

Paginacja to często miejsce, gdzie szybkie API zaczyna się wydawać wolne, nawet gdy baza jest zdrowa. Traktuj paginację jako problem projektowania zapytania, nie szczegół UI.

Dlaczego LIMIT/OFFSET się pogarsza

LIMIT/OFFSET wygląda prosto, ale głębsze strony zwykle kosztują więcej. Postgres dalej musi przejść (i często posortować) wiersze, które pomijasz. Strona 1 może dotykać kilkudziesięciu wierszy. Strona 500 może zmusić bazę do przeskanowania i odrzucenia dziesiątek tysięcy tylko po to, by zwrócić 20 wyników.

Może też tworzyć niestabilne wyniki, gdy wiersze są wstawiane lub usuwane między żądaniami. Użytkownicy mogą zobaczyć duplikaty lub brakujące elementy, bo znaczenie „wiersz 10 000” zmienia się w czasie.

Keyset pagination (cursor) z przykładem „last seen”

Paginacja kluczowa zadaje inne pytanie: „Daj mi następne 20 wierszy po ostatnim, który widziałem.” To trzyma DB przy małym, spójnym wycinku.

Prosta wersja używa rosnącego id:

SELECT id, created_at, title
FROM posts
WHERE id > $1
ORDER BY id
LIMIT 20;

Twoje API zwraca next_cursor równe ostatniemu id na stronie. Następne żądanie używa tej wartości jako $1.

Dla sortowania opartego na czasie użyj stabilnego porządku i rozwiązywania remisów. created_at sam w sobie nie wystarcza, jeśli dwa wiersze mają ten sam znacznik czasu. Użyj złożonego kursora:

WHERE (created_at, id) < ($1, $2)
ORDER BY created_at DESC, id DESC
LIMIT 20;

Kilka zasad zapobiega duplikatom i brakującym wierszom:

  • Zawsze dołącz unikalny tie-breaker w ORDER BY (zwykle id).
  • Trzymaj porządek sortowania identyczny między żądaniami.
  • Uczyń kursor nieczytelnym dla klientów (zakoduj created_at i id razem).
  • Jeśli użytkownicy mogą filtrować, dołącz te same filtry na każdej stronie.
  • Preferuj niemutowalne pola sortowania (czas utworzenia) nad mutowalnymi (status, score) gdy to możliwe.

Kształtowanie JSON: szybsze odpowiedzi przez mniejsze payloady

Uporządkuj odpowiedzi JSON
Utrzymuj odpowiedzi lekkimi przez świadome kształtowanie payloadów i unikaj niezamierzonego SELECT *.

Zadziwiająco częstą przyczyną odczucia wolnego API nie jest baza. To odpowiedź. Duże JSON-y zajmują więcej czasu na budowę, wysyłkę i parsowanie po stronie klienta. Najszybszy zysk to często zwrócenie mniej.

Zacznij od SELECT. Jeśli endpoint potrzebuje tylko id, name i status, poproś o te kolumny i nic więcej. SELECT * cicho staje się cięższe w czasie, gdy tabele zyskują długie teksty, bloby JSON i kolumny audytowe.

Innym częstym spowolnieniem jest N+1 przy budowaniu odpowiedzi: pobierasz listę 50 elementów, a potem uruchamiasz 50 dodatkowych zapytań, by dołączyć powiązane dane. Może przejść w testach, a potem zawalić się przy prawdziwym ruchu. Preferuj jedno zapytanie zwracające to, czego potrzebujesz (ostrożne joiny), lub dwa zapytania, gdzie drugie grupuje po ID.

Kilka sposobów, by utrzymać payloady mniejsze bez łamania klientów:

  • Używaj flagi include= (lub maski fields=), aby odpowiedzi listy były szczupłe, a szczegóły mogły opcjonalnie załadować extras.
  • Ogranicz tablice zagnieżdżone (np. tylko najnowsze 10 zdarzeń) i zapewnij oddzielny endpoint dla pełnej historii.
  • Nie zwracaj surowych kolumn JSON wewnętrznych, jeśli klienci potrzebują tylko kilku kluczy.
  • Używaj krótkich kodów zamiast powtarzania długich etykiet.

Budować JSON w Postgresie czy w Go?

Oba podejścia mogą być szybkie. Wybierz w zależności od tego, co optymalizujesz.

Funkcje JSON Postgresa (jsonb_build_object, json_agg) są przydatne, gdy chcesz mniej rund podróży i przewidywalne kształty z jednego zapytania. Kształtowanie w Go pomaga, gdy potrzebujesz logiki warunkowej, wielokrotnego wykorzystania struktur lub prostszych SQL-i do utrzymania. Jeśli SQL budujący JSON staje się trudny do czytania, trudniej go potem stroić.

Dobra zasada: pozwól Postgresowi filtrować, sortować i agregować. Potem niech Go zajmie się końcową prezentacją.

Jeśli szybko generujesz API (np. z Koder.ai), wczesne dodanie flag include pomaga uniknąć endpointów, które z czasem puchną. Daje też bezpieczny sposób dodawania pól bez obciążania każdej odpowiedzi.

Krok po kroku tuning przed pierwszymi użytkownikami

Nie potrzebujesz ogromnego laboratorium testowego, żeby wykryć większość problemów wydajnościowych. Krótka, powtarzalna procedura ujawnia problemy, które zamieniają się w outage’y, gdy przychodzi prawdziwy ruch, szczególnie gdy punkt startowy to generowany kod, który masz zamiar wypuścić.

Zanim cokolwiek zmienisz, zapisz małą bazę wyjściową:

  • p95 i p99 latencja dla najważniejszych endpointów
  • wskaźnik błędów i timeouty
  • CPU bazy i aktywne połączenia
  • 5 najwolniejszych zapytań według całkowitego czasu (nie tylko pojedynczego najgorszego)

Przejście tuningowe

Zacznij od małych kroków, zmieniaj jedną rzecz naraz i testuj po każdej zmianie.

  1. Uruchom 10–15 minutowy test obciążeniowy wyglądający jak rzeczywiste użycie. Traf te same endpointy, które będą używać pierwsi użytkownicy (login, listy, wyszukiwanie, tworzenie). Posortuj trasy według p95 latencji i całkowitego czasu.

  2. Sprawdź presję na połączenia zanim stuningujesz SQL. Zbyt duża pula przytłoczy Postgresa. Zbyt mała pula tworzy długie oczekiwania. Szukaj rosnącego czasu oczekiwania na zdobycie połączenia i skoków w liczbie połączeń. Dostosuj limity puli i idle najpierw, potem powtórz test.

  3. EXPLAIN najwolniejszych zapytań i napraw największy czerwony alert. Zwykli podejrzani to pełne skany tabel na dużych tabelach, sorty na dużych zestawach wyników i joiny, które eksplodują liczbę wierszy. Wybierz jedno najgorsze zapytanie i spraw, by stało się nudne (czyli przewidywalnie szybkie).

  4. Dodaj lub dostosuj pojedynczy indeks, potem testuj ponownie. Indeksy pomagają, gdy pasują do twojego WHERE i ORDER BY. Nie dodawaj pięciu naraz. Jeśli wolny endpoint to „lista zamówień po user_id sortowana po created_at”, indeks złożony (user_id, created_at) może być różnicą między natychmiast a bolesnym.

  5. Ogranicz odpowiedzi i paginację, potem przetestuj znowu. Jeśli endpoint zwraca 50 wierszy z wielkimi blobami JSON, płacą za to baza, sieć i klient. Zwracaj tylko pola, których UI potrzebuje i preferuj paginację, która nie zwalnia wraz ze wzrostem tabel.

Prowadź prosty changelog: co zmieniono, dlaczego i co ruszyło w p95. Jeśli zmiana nie poprawia bazy, wycofaj ją i idź dalej.

Częste błędy i pułapki do uniknięcia

Dostrajaj endpointy przez czat
Iteruj nad wolnymi endpointami opisując problem i dopracowując zapytania i handlery w czacie.

Większość problemów wydajnościowych w API Go + Postgres jest samoszkodliwa. Dobra wiadomość jest taka, że kilka kontroli wykrywa je przed przyjściem realnego ruchu.

Klasyczna pułapka to traktowanie rozmiaru puli jak pokrętła prędkości. Ustawienie jej „tak wysokiej, jak możliwe” często spowalnia wszystko. Postgres spędza więcej czasu na żonglowaniu sesjami, pamięcią i lockami, a twoja aplikacja zaczyna dostawać timeouty falami. Mniejsza, stabilna pula z przewidywalną współbieżnością zwykle wygrywa.

Inny częsty błąd to „zindeksuj wszystko”. Dodatkowe indeksy mogą pomóc odczytom, ale też spowalniają zapisy i mogą zmieniać plany zapytań w zaskakujący sposób. Jeśli API często insertuje lub aktualizuje, każdy dodatkowy indeks dodaje pracy. Mierz przed i po, i ponownie sprawdzaj plany po dodaniu indeksu.

Dług paginacyjny wkrada się cicho. Offsetowa paginacja wygląda dobrze na początku, potem p95 rośnie, bo DB musi przejść coraz więcej wierszy.

Rozmiar payloadu JSON to kolejny ukryty koszt. Kompresja pomaga z pasmem, ale nie usuwa kosztu budowania, alokowania i parsowania dużych obiektów. Przycinaj pola, unikaj głębokiego zagnieżdżenia i zwracaj tylko to, czego ekran potrzebuje.

Jeśli patrzysz tylko na średni czas odpowiedzi, przegapisz, gdzie zaczyna się ból użytkownika. p95 (a czasem p99) to miejsca, gdzie saturacja puli, oczekiwania na locki i wolne plany pokazują się pierwsze.

Szybka pre-launch checklista:

  • Obserwuj czas oczekiwania puli i liczbę połączeń Postgresa podczas małego testu obciążeniowego.
  • Porównaj średnią vs p95 dla tego samego endpointu.
  • Zweryfikuj, że paginacja nie pogarsza się przy tabeli 10x większej.
  • Sprawdź rozmiary odpowiedzi dla endpointów listujących (bajty mają znaczenie).
  • Powtórz EXPLAIN po dodaniu indeksów lub zmianie filtrów.

Szybka lista kontrolna i kolejne kroki przed uruchomieniem

Przed pojawieniem się rzeczywistych użytkowników chcesz dowodu, że API pozostanie przewidywalne pod obciążeniem. Cel to nie perfekcyjne liczby, a wychwycenie kilku problemów, które powodują timeouty, skoki lub bazę, która przestaje przyjmować pracę.

Uruchom kontrole w stagingu przypominającym produkcję (podobny rozmiar DB, te same indeksy, te same ustawienia puli): zmierz p95 latency dla kluczowych endpointów pod obciążeniem, złap najwolniejsze zapytania według łącznego czasu, obserwuj czas oczekiwania puli, uruchom EXPLAIN (ANALYZE, BUFFERS) najgorszego zapytania, by potwierdzić, że używa oczekiwanego indeksu, i sanity-check rozmiary payloadów na najruchliwszych trasach.

Potem zrób jeden run worst-case, który imituje, jak produkty się psują: zażądaj głęboką stronę, zastosuj najszerszy filtr i potestuj przy zimnym starcie (zrestartuj API i uderz w to samo żądanie jako pierwsze). Jeśli głęboka paginacja pogarsza się z każdą stroną, przełącz na paginację kursorem przed uruchomieniem.

Zapisz domyślne wybory, żeby zespół podejmował spójne decyzje później: limity i timeouty puli, zasady paginacji (maksymalny rozmiar strony, czy offset jest dozwolony, format kursora), zasady zapytań (wybieraj tylko potrzebne kolumny, unikaj SELECT *, ogranicz kosztowne filtry) oraz zasady logowania (próg wolnych zapytań, jak długo trzymać próbki, jak etykietować endpointy).

Jeśli budujesz i eksportujesz usługi Go + Postgres za pomocą Koder.ai, krótka faza planowania przed wdrożeniem pomaga utrzymać filtry, paginację i kształty odpowiedzi świadomie. Gdy zaczniesz stroić indeksy i kształty zapytań, snapshoty i rollback ułatwiają cofnięcie „poprawki”, która pomaga jednemu endpointowi, a szkodzi innym. Jeśli chcesz jedno miejsce do iteracji nad tym workflow, Koder.ai na koder.ai jest zaprojektowane wokół generowania i usprawniania tych serwisów przez czat, a potem eksportu źródeł, gdy jesteś gotowy.

Często zadawane pytania

Jak szybko stwierdzić, czy moje API w Go jest wolne przez Postgres czy przez mój kod?

Zacznij od rozdzielenia czasu oczekiwania na DB od czasu pracy aplikacji.

  • Jeśli baza jest winna, handler spędza większość czasu czekając na zapytanie. CPU Go zwykle pozostaje w normie, podczas gdy żądania kumulują się „w locie”.
  • Jeśli aplikacja jest wolna, zapytania zwracają szybko, ale czas tracisz na budowanie obiektów, dodatkowe zapytania per-wiersz, marshaling dużego JSON lub logowanie. CPU i pamięć Go zwykle rosną wraz z rozmiarem odpowiedzi.

Dodaj proste mierniki rozbijające „oczekiwanie na połączenie” i „wykonanie zapytania”, aby zobaczyć, która strona dominuje.

Jakie metryki powinienem śledzić najpierw przed jakimkolwiek tuningiem?

Użyj małej, powtarzalnej bazy pomiarowej:

  • p95 latency dla kluczowych endpointów (nie średnia)
  • współczynnik błędów (5xx, timeouty, anulowania)
  • czas DB na żądanie (czas oczekiwania na Postgres)

Wybierz jasny cel, np. „p95 poniżej 200 ms przy 50 równoczesnych użytkownikach, błędy < 0.5%”. Zmieniaj tylko jedną rzecz naraz i powtarzaj ten sam miks żądań.

Czy powinienem włączyć logowanie wolnych zapytań w Postgres i jaki próg jest praktyczny?

Włącz logowanie wolnych zapytań z niskim progiem w testach przed uruchomieniem (na przykład 100–200 ms) i loguj pełne zapytanie, by skopiować je do klienta SQL.

Zachowaj to tymczasowo:

  • W produkcji robi się to szybko hałaśliwe.
  • Może dodać narzut, jeśli logujesz zbyt wiele.

Gdy znajdziesz największych winowajców, przejdź do próbkowania lub podnieś próg.

Jakie są dobre początkowe ustawienia puli połączeń dla API w Go na Postgres?

Praktyczny domyśl to mała wielokrotność rdzeni CPU na instancję API, często 5–20 max open connections, z podobną liczbą połączeń bezczynnych i recyklingiem co 30–60 minut.

Dwa typowe tryby awarii:

  • Pula za mała: żądania czekają na połączenie mimo, że czas zapytania w Postgres wygląda dobrze.
  • Pula za duża: Postgres przeciążony wieloma aktywnymi sesjami i opóźnienia stają się nierówne.

Pamiętaj, że pule się mnożą przez instancje (20 połączeń × 10 instancji = 200 połączeń).

Jak potwierdzić, że to pula połączeń jest wąskim gardłem (a nie SQL)?

Podziel czas wywołań DB na dwie części:

  • Czas oczekiwania na połączenie (pool wait)
  • Czas wykonania zapytania (praca Postgresa)

Jeśli większość czasu to oczekiwanie na pulę, dostosuj rozmiar puli, timeouty i liczbę instancji. Jeśli większość to wykonanie zapytania, skup się na EXPLAIN i indeksach.

Potwierdź też, że zawsze zamykasz rows, żeby połączenia wracały do puli.

Na co patrzeć najpierw w EXPLAIN, gdy endpoint jest wolny?

Uruchom EXPLAIN (ANALYZE, BUFFERS) na dokładnym SQL, które wysyła API i zwróć uwagę na:

  • Seq Scan na dużej tabeli
  • Ogromną różnicę między estimated rows vs actual rows
  • Sort dominujący czas (często przy ORDER BY)
  • „Rows Removed by Filter” bardzo wysokie
  • Dużo shared read blocks w BUFFERS (ciężkie odczyty)

Najpierw napraw największy czerwony alert; nie tunuj wszystkiego naraz.

Jak wybrać właściwy indeks dla endpointu listującego z filtrami i sortowaniem?

Indeksy powinny pasować do tego, co faktycznie robi endpoint: filtry + porządek sortowania.

Dobre domyślne podejście:

  • Zbuduj indeks złożony dla typowego wzorca WHERE + ORDER BY.
  • Trzymaj go małym (2 kolumny często, 3 czasem).
  • Umieść najbardziej selektywny filtr pierwszy, potem kolumna sortująca.

Przykład: jeśli filtrujesz po user_id i sortujesz po newest, indeks (user_id, created_at DESC) często rozwiązuje problem p95.

Kiedy warto użyć indeksu częściowego w Postgres?

Użyj indeksu częściowego, gdy większość ruchu trafia w przewidywalny podzbiór wierszy.

Przykład:

  • Dużo odczytów tylko dla active = true
  • Niewiele zapytań dla wierszy nieaktywnych

Indeks częściowy jak ... WHERE active = true pozostaje mniejszy, częściej mieści się w pamięci i zmniejsza narzut zapisu w porównaniu z indeksowaniem wszystkiego.

Potwierdź przez EXPLAIN, że Postgres faktycznie z niego korzysta.

Dlaczego paginacja LIMIT/OFFSET robi się wolniejsza i co zamiast tego użyć?

LIMIT/OFFSET staje się wolniejsze na głębokich stronach, bo Postgres dalej musi przeskoczyć (i często posortować) pomijane wiersze. Strona 1 dotyka kilkudziesięciu wierszy, strona 500 może zmusić bazę do przejrzenia i odrzucenia dziesiątek tysięcy, by zwrócić 20 wyników.

Preferuj paginację kluczową (keyset/cursor):

  • Użyj stabilnego sortu + unikalnego tie-breakera (często id).
  • Trzymaj ORDER BY identyczne między żądaniami.
  • Zakoduj (created_at, id) lub podobne w kursorze.

To utrzymuje koszt każdej strony mniej więcej stały wraz ze wzrostem tabeli.

Moje zapytania DB są szybkie, ale odpowiedzi nadal wolne — czy powinienem przyciąć payloady JSON?

Zazwyczaj tak dla endpointów listujących. Najszybsza odpowiedź to ta, której nie wysyłasz.

Praktyczne zwycięstwa:

  • Selekcjonuj tylko potrzebne kolumny (unikaj SELECT *).
  • Dodaj include= lub fields=, by klienta opcjonalnie załadował ciężkie pola.
  • Ogranicz zagnieżdżone tablice (np. tylko najnowsze 10) i pobieraj resztę przez dedykowany endpoint.
  • Unikaj wzorca N+1 (50 wierszy + 50 dodatkowych zapytań). Używaj joinów lub zapytań grupujących po ID.

Często zmniejszając payload redukujesz CPU Go, presję pamięci i tail latency.

Related posts