06 lip 2025·7 min czytania

Profilowanie pamięci w Go przy skokach ruchu: przewodnik po pprof

Profilowanie pamięci w Go pomaga radzić sobie z nagłymi skokami ruchu. Praktyczny przewodnik po pprof, jak znaleźć gorące punkty alokacji w JSON, skanowaniu DB i middleware.

Profilowanie pamięci w Go przy skokach ruchu: przewodnik po pprof

Co robią nagłe skoki ruchu z pamięcią usługi w Go

„Skok pamięci” w produkcji rzadko oznacza jedną prostą liczbę. Możesz zobaczyć RSS (pamięć procesu) szybko rosnące, podczas gdy sterta Go ledwie się porusza, albo sterta rośnie i spada w ostrych falach, gdy GC działa. Jednocześnie opóźnienia często się pogarszają, bo runtime więcej czasu poświęca na sprzątanie.

Typowe wzorce w metrykach:

  • RSS rośnie szybciej niż oczekiwano i czasem nie spada całkowicie po skoku
  • Użycie sterty (heap in-use) rośnie, potem spada w ostrych cyklach, gdy GC działa częściej
  • Tempo alokacji skacze (bajty alokowane na sekundę)
  • Czas pauzy GC i czas CPU używany przez GC rosną, nawet jeśli pojedyncze pauzy są krótkie
  • Opóźnienia żądań rosną, a ogonowe opóźnienia stają się hałaśliwe

Skoki ruchu powiększają alokacje na zapytanie, bo „małe” marnotrawstwo skalowane liniowo z obciążeniem robi różnicę. Jeśli jedno żądanie alokuje dodatkowe 50 KB (tymczasowe bufory JSON, obiekty na skan wiersza, dane kontekstu middleware), to przy 2000 żądań na sekundę dokarmiasz alokator około 100 MB na sekundę. Go potrafi wiele znieść, ale GC wciąż musi prześledzić i zwolnić te krótkotrwałe obiekty. Gdy alokacja przewyższa sprzątanie, cel sterty rośnie, RSS podąża za nim i możesz trafić w limity pamięci.

Objawy są znajome: OOM od orkiestratora, nagłe skoki latencji, więcej czasu spędzanego w GC i serwis, który wygląda na „zajęty”, nawet gdy CPU nie jest wykorzystane. Możesz też dostać thrash GC: serwis działa dalej, ale ciągle alokuje i kolekcjonuje, więc przepustowość spada właśnie wtedy, gdy najbardziej jej potrzeba.

pprof pomaga szybko odpowiedzieć na jedno pytanie: które ścieżki kodu alokują najwięcej i czy te alokacje są konieczne? Profil sterty pokazuje, co jest zatrzymane teraz. Widoki skupione na alokacjach (np. alloc_space) pokazują, co jest tworzone i odrzucane.

Czego pprof nie zrobi: nie wyjaśni każdego bajta RSS. RSS zawiera więcej niż sterta Go (stacki, metadane runtime, mapowania OS, alokacje cgo, fragmentację). pprof najlepiej wskazuje gorące miejsca alokacji w kodzie Go, a nie daje dokładnej sumy pamięci kontenera.

Bezpieczne ustawienie pprof (krok po kroku)

pprof najłatwiej użyć jako endpointów HTTP, ale te endpointy mogą ujawniać dużo o twojej usłudze. Traktuj je jak funkcję administracyjną, nie publiczne API.

1) Dodaj endpointy pprof

W Go najprostsze ustawienie to uruchomić pprof na osobnym serwerze administracyjnym. To trzyma trasy profilowania z dala od głównego routera i middleware.

package main

import (
	"log"
	"net/http"
	_ "net/http/pprof"
)

func main() {
	go func() {
		// Admin only: bind to localhost
		log.Println(http.ListenAndServe("127.0.0.1:6060", nil))
	}()

	// Your main server starts here...
	// http.ListenAndServe(":8080", appHandler)
	select {}
}

Jeśli nie możesz otworzyć drugiego portu, możesz zamontować trasy pprof w głównym serwerze, ale łatwiej je przez przypadek ujawnić. Osobny port admina to bezpieczny domyślny wybór.

2) Zabezpiecz przed wdrożeniem

Zacznij od kontroli trudnych do pomylenia. Bindując do localhost, endpointy nie są osiągalne z internetu, chyba że ktoś dodatkowo wystawi ten port.

Szybka lista kontrolna:

  • Uruchom pprof na porcie administracyjnym, nie na głównym portcie użytkownika
  • Binduj do 127.0.0.1 (lub prywatnego interfejsu) w produkcji
  • Dodaj allowlistę na krawędzi sieci (VPN, bastion lub wewnętrzna podsieć)
  • Wymagaj uwierzytelnienia, jeśli krawędź może je egzekwować (basic auth lub token)
  • Zweryfikuj, że możesz pobrać profile, których będziesz używać: heap, allocs, goroutine

3) Zbuduj i wdrażaj ostrożnie

Utrzymaj zmianę małą: dodaj pprof, wypchnij ją i potwierdź, że jest osiągalna tylko stamtąd, skąd oczekujesz. Jeśli masz staging, przetestuj tam najpierw, symulując obciążenie i przechwytując profil sterty i allocs.

Dla produkcji wdrażaj stopniowo (jedna instancja lub mały wycinek ruchu). Jeśli pprof jest źle skonfigurowany, zasięg szkód pozostanie mały, dopóki naprawiasz.

Przechwyć właściwe profile podczas skoku

Podczas skoku pojedynczy snapshot rzadko wystarcza. Zrób małą linię czasu: kilka minut przed skokiem (baseline), w trakcie skoku (impact) i kilka minut po (recovery). Łatwiej wtedy oddzielić rzeczywiste zmiany alokacji od normalnego rozruchu.

Jeśli potrafisz odtworzyć skok przy kontrolowanym obciążeniu, dopasuj go jak najbardziej do produkcji: miks żądań, rozmiary payloadów i współbieżność. Skok z małych żądań zachowuje się inaczej niż skok z dużymi odpowiedziami JSON.

Weź zarówno profil sterty, jak i profil skupiony na alokacjach. Odpowiadają na różne pytania:

  • Heap (inuse) pokazuje, co jest żywe i trzyma pamięć teraz
  • Alokacje (alloc_space lub alloc_objects) pokazują, co jest intensywnie tworzone, nawet jeśli szybko się zwalnia

Praktyczny wzorzec przechwytywania: złap jeden profil sterty, potem profil alokacji, potem powtórz 30–60 sekund później. Dwa punkty w trakcie skoku pomagają zobaczyć, czy podejrzana ścieżka jest stabilna, czy przyspiesza.

# examples: adjust host/port and timing to your setup
curl -o heap_during.pprof "http://127.0.0.1:6060/debug/pprof/heap"
curl -o allocs_30s.pprof "http://127.0.0.1:6060/debug/pprof/allocs?seconds=30"

Wraz z plikami pprof zapisz kilka statystyk runtime, żeby wyjaśnić, co robił GC w tym samym czasie. Rozmiar sterty, liczba GC i czas pauz zazwyczaj wystarczą. Nawet krótka linijka logu przy każdym zrzucie pomaga powiązać „alokacje wzrosły” z „GC zaczął działać non-stop”.

Prowadź notatki incydentu: wersja builda (commit/tag), wersja Go, ważne flagi, zmiany konfiguracji i jaki ruch się działy (endpointy, tenanty, rozmiary payloadów). Te szczegóły często mają znaczenie, gdy porównujesz profile i odkrywasz, że miks żądań się zmienił.

Jak czytać profile sterty i alokacji

Profil sterty odpowiada na różne pytania w zależności od widoku.

Inuse space pokazuje, co jest nadal w pamięci w momencie zrzutu. Użyj go do wykrywania wycieków, długożyjących cache'ów lub przypadków, gdy żądania zostawiają obiekty.

Alloc space (całkowite alokacje) pokazuje, co było alokowane w czasie, nawet jeśli zostało szybko zwolnione. Użyj go, gdy skoki powodują dużo pracy GC, skoki latencji lub OOM-y z churnu.

Próbkowanie ma znaczenie. Go nie zapisuje każdej alokacji. Próbkuje alokacje (sterowane przez runtime.MemProfileRate), więc małe, częste alokacje mogą być niedostatecznie reprezentowane, a liczby są estymacjami. Największe winowajczynie i tak zwykle się wyróżniają, szczególnie w warunkach skokowych. Szukaj trendów i głównych kontrybutorów, nie idealnego rozliczenia.

Najbardziej użyteczne widoki pprof:

  • top: szybki przegląd, kto dominuje w inuse lub alloc (sprawdź zarówno flat, jak i cumulative)
  • list : źródła alokacji na poziomie linii wewnątrz gorącej funkcji
  • graph: ścieżki wywołań, które wyjaśniają, jak tam dotarłeś

Diffy są praktyczne. Porównaj profil baseline (normalny ruch) z profilem ze skoku, by uwypuklić, co się zmieniło, zamiast gonić za szumem tła.

Zwaliduj ustalenia małą zmianą przed dużym refaktorem:

  • Reużyj bufora (lub dodaj mały sync.Pool) w gorącej ścieżce
  • Ogranicz tworzenie obiektów na żądanie (np. unikaj budowania tymczasowych map dla JSON)
  • Re-profiluj pod tym samym obciążeniem i potwierdź, że diff się zmniejszył tam, gdzie oczekiwałeś

Jeśli liczby ruszają we właściwą stronę, znalazłeś rzeczywistą przyczynę, nie tylko przerażający raport.

Znajdowanie gorących punktów alokacji w kodzie JSON

Preferuj typowane modele odpowiedzi
Przekształć endpoint podatny na skoki w typowany, spójny model odpowiedzi za pomocą wizualnych schematów.
Buduj teraz

Podczas skoków praca związana z JSON może stać się dużym rachunkiem pamięciowym, bo odbywa się przy każdym żądaniu. Gorące miejsca JSON często objawiają się wieloma małymi alokacjami, które mocniej obciążają GC.

Czerwone flagi w pprof

Jeśli widok sterty lub alokacji wskazuje encoding/json, przyjrzyj się temu, co do niego podajesz. Wzorce, które zwykle zwiększają alokacje:

  • Używanie map[string]any (lub []any) dla odpowiedzi zamiast typowanych struktur
  • Marshalowanie tego samego obiektu wielokrotnie (np. do logów i do odpowiedzi)
  • Pretty printing z json.MarshalIndent w produkcji
  • Budowanie JSON przez tymczasowe stringi (fmt.Sprintf, konkatenacja) przed marshalingiem
  • Konwertowanie dużego []byte na string (lub odwrotnie) tylko po to, by dopasować API

json.Marshal zawsze alokuje nowy []byte dla całego wyjścia. json.NewEncoder(w).Encode(v) zwykle unika tego jednego dużego bufora, bo zapisuje do io.Writer, ale nadal może alokować wewnętrznie, szczególnie jeśli v pełne jest any, map lub struktur z wieloma wskaźnikami.

Szybkie poprawki i eksperymenty

Zacznij od typowanych struktur dla kształtu odpowiedzi. Zmniejszają one pracę refleksji i unikają boxingowania pól do interface{}.

Potem usuń zbędne tymczasowe obiekty na zapytanie: reużyj bytes.Buffer przez sync.Pool (ostrożnie), nie wstawiaj indentacji w produkcji i nie re-marshaluj tylko dla logów.

Małe eksperymenty potwierdzające, że to JSON jest winny:

  • Zastąp map[string]any strukturą dla jednego gorącego endpointu i porównaj profile
  • Przełącz z Marshal na Encoder, zapisujący bezpośrednio do response
  • Usuń MarshalIndent lub formatowanie debugowe i re-profiluj pod tym samym obciążeniem
  • Pomiń kodowanie JSON dla niezmienionych buforowanych odpowiedzi i zmierz spadek

Najważniejsze miejsca alokacji przy skanowaniu zapytań do bazy

Gdy pamięć skacze podczas obciążenia, odczyty z bazy często są niespodzianką. Łatwo skupić się na czasie wykonania SQL, ale krok skanowania może alokować dużo na wiersz, szczególnie gdy skanujesz do elastycznych typów.

Typowi winowajcy:

  • Skanowanie do interface{} (lub map[string]any) i pozwalanie driverowi decydować o typach
  • Konwertowanie []byte na string dla każdego pola
  • Używanie nullable wrapperów (sql.NullString, sql.NullInt64) w dużych zestawach wyników
  • Pobieranie dużych pól tekstowych/blobów, których nie zawsze potrzebujesz

Jeden wzorzec, który potajemnie pali pamięć, to skanowanie danych wiersza do zmiennych tymczasowych, a potem kopiowanie do właściwej struktury (albo budowanie mapy na wiersz). Jeśli możesz skanować bezpośrednio do struktury z konkretnymi polami, unikasz dodatkowych alokacji i kontroli typów.

Rozmiar partii i paginacja zmieniają „kształt” pamięci. Pobieranie 10 000 wierszy do slice alokuje pamięć na wzrost slice i każdy wiersz, wszystko naraz. Jeśli handler potrzebuje tylko strony, wymuś stronicowanie w zapytaniu i utrzymuj rozmiar strony stabilny. Jeśli musisz przetworzyć dużo wierszy, streamuj je i agreguj małe podsumowania zamiast przechowywać każdy wiersz.

Duże pola tekstowe wymagają uwagi. Wiele driverów zwraca tekst jako []byte. Konwersja tego do string kopiuje dane, więc robienie tego dla każdego wiersza może eksplodować alokacje. Jeśli wartość jest potrzebna tylko czasami, opóźnij konwersję lub skanuj mniej kolumn dla danego endpointu.

Aby potwierdzić, czy driver czy twój kod robi większość alokacji, sprawdź, co dominuje w profilach:

  • Jeśli ramki wskazują na twój kod mapujący, skup się na celach skanowania i konwersjach
  • Jeśli ramki wskazują do database/sql lub drivera, zmniejsz liczbę wierszy i kolumn najpierw, potem rozważ opcje specyficzne dla drivera
  • Sprawdź zarówno alloc_space, jak i alloc_objects; wiele drobnych alokacji może być gorsze niż kilka dużych

Przykład: endpoint „list orders” skanuje SELECT * do []map[string]any. Podczas skoku każde żądanie buduje tysiące małych map i stringów. Zmiana zapytania na wybieranie tylko potrzebnych kolumn i skanowanie do []Order{ID int64, Status string, TotalCents int64} często natychmiast obniża alokacje. Ta sama zasada działa, jeśli profilujesz wygenerowany backend z AppMaster: gorący punkt zwykle leży w tym, jak kształtujesz i skanujesz dane wynikowe, a nie w samej bazie.

Wzorce middleware, które cicho alokują na zapytanie

Wdróż tam, gdzie trzeba
Wdróż tam, gdzie potrzebujesz — w chmurze lub self-hosted, aby dopasować limity pamięci i dostęp do profilowania.
Zacznij budować

Middleware wydaje się tanie, bo to „tylko wrapper”, ale działa przy każdym żądaniu. Podczas skoku małe alokacje na żądanie sumują się i pokazują jako rosnące tempo alokacji.

Middleware logujące to częste źródło: formatowanie stringów, budowanie map pól lub kopiowanie nagłówków dla ładniejszego outputu. Helpery generujące request ID mogą alokować przy każdym żądaniu, konwertując ID na string i przyczepiając go do kontekstu. Nawet context.WithValue może alokować, jeśli na każde żądanie zapisujesz nowe obiekty (lub nowe stringi).

Kompresja i obsługa ciała to kolejny częsty winowajca. Jeśli middleware czyta całe body, by „zerknąć” lub zweryfikować, możesz mieć duży bufor na żądanie. Middleware gzip może alokować dużo, jeśli tworzy nowych readerów i writerów każdorazowo zamiast reużywać bufory.

Warstwy auth i sesji są podobne. Jeśli każde żądanie parsuje tokeny, dekoduje base64 ciasteczka lub ładuje blob sesji do świeżych struktur, dostajesz stały churn, nawet gdy praca handlera jest lekka.

Tracing i metryki mogą alokować więcej, niż się spodziewasz, gdy etykiety są budowane dynamicznie. Konkatenowanie nazw tras, user-agentów lub identyfikatorów tenantów w nowe stringi na żądanie to klasyczny ukryty koszt.

Wzorce, które często pokazują się jako „śmierć przez tysiąc cięć”:

  • Budowanie logów fmt.Sprintf i nowe map[string]any na żądanie
  • Kopiowanie nagłówków do nowych map lub slice dla logów lub podpisywania
  • Alokowanie nowych gzip bufferów i readerów/writerów zamiast pooling
  • Tworzenie etykiet metryk o wysokiej kardynalności (wiele unikalnych stringów)
  • Zapisywanie nowych struktur w context na każde żądanie

Aby odizolować koszt middleware, porównaj dwa profile: jeden z pełnym łańcuchem włączonym i jeden z middleware tymczasowo wyłączonym lub zastąpionym no-opem. Prosty test to endpoint health, który powinien być niemal bez alokacji. Jeśli /health alokuje dużo podczas skoku, handler nie jest problemem.

Jeśli budujesz backendy Go generowane przez AppMaster, ta sama zasada ma zastosowanie: trzymaj funkcje przekrojowe (logowanie, auth, tracing) mierzalne i traktuj alokacje na żądanie jako budżet, który możesz audytować.

Poprawki, które zwykle szybko się zwracają

Dodawaj moduły bez dodatkowego klejenia
Dodawaj moduły auth i payments bez łączenia kruchego stosu middleware.
Rozpocznij

Mając widoki heap i allocs z pprof, priorytetyzuj zmiany, które zmniejszają alokacje na zapytanie. Cel nie polega na genialnych sztuczkach, a na sprawieniu, by gorąca ścieżka tworzyła mniej krótkotrwałych obiektów pod obciążeniem.

Zacznij od bezpiecznych, nudnych zwycięstw

Jeśli rozmiary są przewidywalne, prealokuj. Jeśli endpoint zwykle zwraca około 200 elementów, stwórz slice z pojemnością 200, żeby nie rósł i nie kopiował się kilka razy.

Unikaj budowania stringów w gorących ścieżkach. fmt.Sprintf jest wygodny, ale często alokuje. Dla logów preferuj pola strukturalne i reużywaj małego bufora tam, gdzie ma to sens.

Jeśli generujesz duże odpowiedzi JSON, rozważ ich strumieniowanie zamiast budowania jednego ogromnego []byte lub string w pamięci. Typowy wzorzec skoku: przychodzi żądanie, czytasz duże body, budujesz dużą odpowiedź, pamięć skacze, aż GC nadgoni.

Szybkie zmiany, które zwykle widać wyraźnie w profilach przed/po:

  • Prealokuj slice i mapy, kiedy znasz zakres rozmiarów
  • Zastąp ciężkie formatowanie fmt tańszymi alternatywami w obsłudze żądań
  • Streamuj duże odpowiedzi JSON (koduj bezpośrednio do response writer)
  • Użyj sync.Pool dla powtarzalnych, jednorodnych obiektów (bufory, encodery) i zawsze je resetuj/przywracaj
  • Ustal limity żądań (rozmiar body, rozmiar payloadu, page size), by ograniczyć najgorsze przypadki

Używaj sync.Pool ostrożnie

sync.Pool pomaga, gdy wielokrotnie alokujesz ten sam typ, jak bytes.Buffer na żądanie. Może też zaszkodzić, jeśli poolujesz obiekty o nieprzewidywalnych rozmiarach lub zapominasz je zresetować — wtedy duże tablice pozostają żywe.

Mierz przed i po użyciu tego samego obciążenia:

  • Zrób profil allocs w oknie skoku
  • Wprowadź jedną zmianę na raz
  • Uruchom ten sam miks żądań i porównaj całkowite allocs/op
  • Obserwuj ogonowe opóźnienia, nie tylko pamięć

Jeśli tworzysz backendy Go wygenerowane przez AppMaster, te poprawki nadal mają zastosowanie do kodu wokół handlerów, integracji i middleware. Tam zwykle chowają się alokacje wywołane skokami.

Typowe błędy w użyciu pprof i fałszywe alarmy

Najszybszy sposób na zmarnowanie dnia to optymalizowanie niewłaściwej rzeczy. Jeśli serwis jest wolny, zacznij od CPU. Jeśli jest zabijany przez OOM, zacznij od sterty. Jeśli przetrwa, ale GC działa bez przerwy, patrz na tempo alokacji i zachowanie GC.

Inną pułapką jest patrzenie tylko na „top” i myślenie, że to koniec. „Top” ukrywa kontekst. Zawsze sprawdzaj stosy wywołań (lub flame graph), żeby zobaczyć, kto wywołał alokator. Poprawka często leży o jedną lub dwie ramki wyżej niż gorąca funkcja.

Obserwuj też mylenie inuse z churnem. Żądanie może alokować 5 MB krótkotrwałych obiektów, wywołać dodatkowy GC i skończyć z tylko 200 KB inuse. Patrząc wyłącznie na inuse, przegapisz churn. Patrząc tylko na całkowite alokacje, możesz optymalizować coś, co nigdy nie pozostaje resident i nie stanowi ryzyka OOM.

Szybkie sanity checki przed zmianą kodu:

  • Potwierdź właściwy widok: heap inuse dla retencji, alloc_space/alloc_objects dla churnu
  • Porównuj stosy, nie tylko nazwy funkcji (encoding/json często jest objawem)
  • Reprodukuj ruch realistycznie: te same endpointy, rozmiary payloadów, nagłówki, współbieżność
  • Zrób baseline i profil ze skoku, potem zrób diff

Nierealistyczne testy obciążeniowe powodują fałszywe alarmy. Jeśli twój test wysyła małe ciała JSON, a produkcja wysyła 200 KB payloady, będziesz optymalizować złą ścieżkę. Jeśli twój test zwraca jeden wiersz bazy, nigdy nie zobaczysz zachowania skanowania przy 500 wierszach.

Nie gonić szumu. Jeśli funkcja pojawia się tylko w profilu ze skoku (nie w baseline), to mocny trop. Jeśli pojawia się w obu na tym samym poziomie, może to być normalna praca tła.

Przykładowy realistyczny incydent

Utrzymaj czystość kodu podczas iteracji
Regeneruj czysty kod Go, gdy wymagania się zmieniają, bez pozostawiania starych pamięciożernych ścieżek.
Generuj kod

W poniedziałek rano wychodzi promocja i twój API Go zaczyna dostawać 8x normalnego ruchu. Pierwszy objaw to nie crash. RSS rośnie, GC robi się bardziej aktywny, a p95 latencji skacze. Najgorętszy endpoint to GET /api/orders, bo aplikacja mobilna odświeża go przy każdym otwarciu ekranu.

Robisz dwa snapshoty: jeden z cichego momentu (baseline) i jeden w trakcie skoku. Zrób ten sam typ profilu sterty w obu przypadkach, by porównanie było uczciwe.

Przebieg działań, który działa w praktyce:

  • Weź baseline heap profile i zanotuj aktualne RPS, RSS i p95 latency
  • W trakcie skoku weź drugi heap profile oraz profil alokacji w tym samym oknie 1–2 minut
  • Porównaj top allocatorów między nimi i skup się na tym, co najbardziej urosło
  • Przejdź od największej funkcji do jej callerów aż trafisz na ścieżkę handlera
  • Zrób jedną małą zmianę, deployuj na jedną instancję i ponownie profiluj

W tym przypadku profil ze skoku pokazał, że większość nowych alokacji pochodziła z kodu JSON. Handler budował map[string]any dla wierszy, a potem wywoływał json.Marshal na slice map. Każde żądanie tworzyło dużo krótkotrwałych stringów i wartości interface{}. Najmniejsza bezpieczna poprawka to przestać budować mapy. Skanuj wiersze bazy bezpośrednio do typowanej struktury i koduj tę slice. Nic więcej się nie zmieniło: te same pola, ten sam kształt odpowiedzi, te same statusy. Po wdrożeniu zmiany na jednej instancji alokacje na ścieżce JSON spadły, czas GC zmalał, a latencja się ustabilizowała.

Dopiero potem wdrażasz stopniowo, obserwując pamięć, GC i wskaźniki błędów. Jeśli korzystasz z platformy no-code jak AppMaster, to przypomnienie, by trzymać modele odpowiedzi typowane i spójne — to pomaga unikać ukrytych kosztów alokacji.

Kolejne kroki, by zapobiec następnemu skokowi pamięci

Gdy ustabilizujesz skok, spraw, by następny był nudny. Traktuj profilowanie jak powtarzalne ćwiczenie.

Napisz krótki runbook dla zespołu, który można uruchomić, gdy są zmęczeni. Powinien mówić, co zbierać, kiedy to robić i jak porównać do znanego dobrego baseline. Bądź praktyczny: dokładne komendy, gdzie trafiają profile i co oznacza „normalnie” dla twoich top allocatorów.

Dodaj lekkie monitorowanie presji alokacji zanim uderzy OOM: rozmiar sterty, cykle GC na sekundę i bajty alokowane na żądanie. Wykrywanie „alokacje na żądanie w górę o 30% tydzień do tygodnia” często jest użyteczniejsze niż czekanie na twardy alarm pamięci.

Przenieś kontrole wcześniej: krótki test obciążeniowy w CI na reprezentatywnym endpointzie. Małe zmiany w odpowiedzi mogą podwoić alokacje, jeśli wywołają dodatkowe kopie, i lepiej to znaleźć zanim zrobi to produkcja.

Jeśli prowadzisz wygenerowany backend Go, eksportuj źródło i profiluj je tak samo. Wygenerowany kod to nadal kod Go, a pprof wskaże realne funkcje i linie.

Jeśli wymagania często się zmieniają, AppMaster (appmaster.io) może być praktycznym sposobem na przebudowę i regenerację czystych backendów Go w miarę rozwoju aplikacji, a potem profilowanie wyeksportowanego kodu pod realistycznym obciążeniem przed wdrożeniem.

FAQ

Why does a sudden traffic spike cause memory to jump even if my code didn’t change?

Skok zwykle zwiększa tempo alokacji bardziej, niż się spodziewasz. Nawet drobne tymczasowe obiekty na zapytanie mnożą się liniowo z RPS, co zmusza GC do częstszego działania i może podnieść RSS, nawet jeśli żywa sterta nie jest duża.

Why does RSS grow while the Go heap looks stable?

Metryki sterty pokazują pamięć zarządzaną przez Go, ale RSS zawiera więcej: stosy goroutine, metadane runtime, mapowania OS, fragmentację i wszelkie alokacje poza stertą (w tym niektóre użycia cgo). Podczas skoków to normalne, że RSS i sterta poruszają się inaczej — użyj pprof, by wskazać gorące miejsca alokacji, zamiast próbować „dopasować” RSS dokładnie.

Should I look at heap or alloc profiles first during a spike?

Zacznij od profilu sterty, gdy podejrzewasz retencję (coś zostaje w pamięci), a od profilu skupionego na alokacjach (np. allocs/alloc_space), gdy podejrzewasz churn (dużo krótkotrwałych obiektów). Podczas skoków ruchu churn często jest głównym problemem, bo napędza CPU GC i ogonowe opóźnienia.

What’s the safest way to expose pprof in production?

Najprostsze i najbezpieczniejsze ustawienie to uruchomić pprof na osobnym serwerze admina związanym z 127.0.0.1 i udostępniać go tylko przez dostęp wewnętrzny. Traktuj pprof jak interfejs administracyjny — ujawnia wewnętrzne szczegóły serwisu.

How many profiles should I capture, and when?

Zrób krótki timeline: jeden profil kilka minut przed skokiem (baseline), jeden w trakcie skoku (impact) i jeden po (recovery). To ułatwia rozróżnienie rzeczywistych zmian alokacji od normalnej aktywności rozruchowej.

What’s the difference between inuse and alloc_space in pprof?

Użyj inuse, by znaleźć to, co faktycznie jest zatrzymane w chwili zrzutu, i alloc_space (lub alloc_objects), by znaleźć to, co jest intensywnie tworzone. Częsty błąd to patrzenie tylko na inuse i przeoczenie churnu, który powoduje thrash GC.

What are the quickest ways to cut JSON-related allocations?

Gdy encoding/json dominuje w alokacjach, zwykle to kształt danych jest winny, nie biblioteka sama w sobie. Zastąpienie map[string]any typowanymi strukturami, unikanie json.MarshalIndent i nie budowanie JSON-a przez tymczasowe stringi często natychmiast zmniejsza alokacje.

Why can database query scanning blow up memory during spikes?

Skanowanie wierszy do elastycznych typów jak interface{} czy map[string]any, konwertowanie []byte na string dla wielu pól i pobieranie zbyt wielu wierszy/kolumn może alokować dużo na zapytanie. Wybieraj tylko potrzebne kolumny, stronicuj wyniki i skanuj bezpośrednio do konkretnych pól struktury — to częste, skuteczne poprawki.

What middleware patterns commonly cause “death by a thousand cuts” allocations?

Middleware działa przy każdym żądaniu, więc małe alokacje pod obciążeniem rosną szybko. Tworzenie nowych stringów do logów, generowanie wysokorozróżnialnych etykiet do śledzenia, generowanie ID żądania, tworzenie gzip readerów/writerów na żądanie i zapisywanie nowych obiektów w context mogą się sumować do stałego churnu alokacji.

Can I use this pprof workflow on Go backends generated by AppMaster?

Tak — ta metoda profilowania działa dla dowolnego kodu Go, wygenerowanego lub pisanego ręcznie. Jeśli wyeksportujesz wygenerowany backend (AppMaster), możesz uruchomić pprof, znaleźć ścieżki alokacji i dostosować modele, handlery i logikę przekrojową, by zmniejszyć alokacje przed kolejnym skokiem.

Łatwy do uruchomienia
Stworzyć coś niesamowitego

Eksperymentuj z AppMaster z darmowym planem.
Kiedy będziesz gotowy, możesz wybrać odpowiednią subskrypcję.

Rozpocznij