PRODSuwerenna europejska platforma BaaSOtwórz Panel →

Wydajność · 10 min odczytu

Dlaczego brak garbage collectora zmienia opóźnienie p99

Affane Daylami · Fondateur · 2 lipca 2026

Powrót do bloga

p99 mierzy najwolniejsze żądanie spośród stu — to, które łamie umowę SLA, podczas gdy średnia pozostaje idealna. W przypadku backendu o dużym natężeniu ruchu ta garść powolnych żądań często ma jedną przyczynę: moduł wyrzucania elementów bezużytecznych, który przerywa cały program w celu zwolnienia pamięci. Rdza nie ma pojemnika na śmieci. Oto mechanizm z przestarzałymi źródłami zewnętrznymi, a nie danymi marketingowymi.

Ten tekst w języku angielskim został wygenerowany automatycznie na podstawie francuskiego oryginału i nie był jeszcze recenzowany.
Ta strona została przetłumaczona automatycznie. Wersja angielska jest miarodajna.

Artykuł ten opiera się wyłącznie na przestarzałych źródłach zewnętrznych — nigdy na wymyślonym szyfrze Aurabase. Nie uwzględniono żadnych opóźnień p99 specyficznych dla naszego backendu. Rdzeń aplikacji piszemy w języku Rust, bez modułu zbierającego śmieci: fakt ten można zweryfikować bezpośrednio w kodzie, obszarze roboczym Cargo i usługach axum. Jednakże nie opublikowaliśmy jeszcze powtarzalnej metodologii testów porównawczych p99, aby wykazać to w liczbach. Ten tekst wyjaśnia mechanizm, a nie zmierzony wynik.

Najważniejsze

  • p99 mierzy najwolniejsze zapytanie spośród stu — miejsce, w którym pauza modułu zbierającego elementy bezużyteczne (GC) boli najbardziej, a nie przeciętnie (Dean i Barroso, „The Tail at Scale”, Google, 2013).
  • GC przerywa cały program („zatrzymaj świat”), aby zwolnić nieużywaną pamięć. Rust nie ma GC: pamięć jest zwalniana dokładnie w momencie, gdy wartość wykracza poza zakres, co jest weryfikowane przez kompilator.
  • Discord udokumentował usługę pamięci podręcznej w 2020 r., w której Go uruchamiał cykl usuwania elementów bezużytecznych co najmniej co dwie minuty, przy czym każdy cykl powodował gwałtowny wzrost opóźnień (blog inżynieryjny Discord).
  • Skrócenie przerwy GC wymaga lat inżynierii, nawet w Google: kolektor GB wzrósł z 300–400 ms do 500 µs w latach 2015–2018, nigdy nie osiągając zera (go.dev).
  • Rdzeń backendu Aurabase jest napisany w języku Rust, bez modułu zbierającego elementy bezużyteczne – zweryfikowane w kodzie. Do chwili obecnej nie opublikowano żadnych danych dotyczących opóźnienia Aurabase p99: jest to mechanizm, a nie pomiar.
#
Opóźnienie ogona

Dlaczego p99 nie jest średnie

Przeciętność kryje w sobie to, co najważniejsze. Jeśli 99 żądań na 100 odpowie w ciągu 5 ms, a tylko jedno zajmie 500 ms, średnia pozostaje niska. Jednak u jednego użytkownika na stu czas oczekiwania jest sto razy dłuższy. P99 mierzy dokładnie to żądanie: najwolniejszą setną, tę, która narusza umowę SLA, podczas gdy pulpit nawigacyjny średniego opóźnienia pozostaje zielony.

W Google Jeffrey Dean i Luiz André Barroso sformalizowali ten problem w „The Tail at Scale” (Komunikacja ACM, tom 56, 2013). Ich obserwacja, często cytowana, ponieważ: „Tymczasowe epizody dużych opóźnień, które są nieistotne w systemach średniej wielkości, mogą zdominować ogólną wydajność usług na dużą skalę”. W skrócie: sporadyczne epizody opóźnień, nieistotne na małą skalę, ostatecznie dominują w postrzeganej wydajności systemu rozproszonego.

Backend przetwarzający tysiące żądań na sekundę z pewnością wyśle ​​w tym czy innym momencie żądanie, które spadnie podczas pauzy GC. Na dużą skalę nie jest to przypadek rzadki. To statystyczna pewność.

#
Mechanika GC

Co robi moduł zbierający elementy bezużyteczne i dlaczego wstrzymuje wszystko

Moduł zbierający elementy bezużyteczne (GC) w sposób ciągły śledzi żywe obiekty programu — te, do których wciąż istnieją odniesienia — i zwalnia pamięć obiektów, które stały się niedostępne. To śledzenie nazywa się tracing: GC przechodzi przez wykres referencyjny, zaznacza to, co jest nadal używane, a następnie przegląda resztę.

Problem: przechodzenie przez ten wykres podczas tworzenia przez program nowych odniesień daje niespójne wyniki. Historyczna odpowiedź, wciąż używana w ostateczności przez wiele współczesnych GC, to stop-the-world — cały program zatrzymuje się podczas zaznaczania i skanowania. Im większa sterta, tym dłuższa jest zwykle przerwa: jej czas trwania zależy od rozmiaru bieżących danych, a nie od bieżącego obciążenia.

Większość współczesnych GC stosuje strategię pokoleniową: zakłada, że ​​większość obiektów umiera młodo. Dlatego ostatnie przydziały są często i szybko skanowane w małym obszarze pamięci. Obiekty, które przetrwają kilka cykli, migrują na większy obszar i są skanowane rzadko – ale gdy ten obszar wymaga oczyszczenia, związana z nim pauza rośnie wraz z jego rozmiarem. To właśnie ta „większa” przerwa, a nie małe „drobne” przerwy, dominuje w przypadku usługi o dużym natężeniu ruchu i alokacji.

Informacje

Nowoczesne GC współbieżne i pokoleniowe zmniejszają częstotliwość i czas trwania tych przerw, pracując równolegle z programem. Jednak prawie wszystkie z nich posiadają mechanizm awaryjny „zatrzymaj świat” w przypadkach granicznych, a ograniczenie tego zjawiska wymaga lat inżynierii. W sekcji 04 przedstawiono przykład ilościowy i źródłowy.

#
Prawdziwy przypadek

Discord, 2020: przerwa w GC staje się incydentem produkcyjnym

W lutym 2020 inżynier Jesse Howarth opublikował post, który stał się punktem odniesienia w branży: „Dlaczego Discord przechodzi z Go na Rust” (Blog inżynieryjny Discord). Odpowiednia usługa Read States zarządza stanem przeczytania wiadomości dla milionów użytkowników — dziesiątkami milionów wpisów w pamięci podręcznej — z setkami tysięcy aktualizacji na sekundę.

Diagnoza jest bezpośrednia, cytowana w artykule: „Go wymusi uruchamianie zbierania śmieci co najmniej co 2 minuty”. Innymi słowy, Go uruchamia w tej usłudze cykl usuwania elementów bezużytecznych co najmniej co dwie minuty — a każdy cykl powoduje gwałtowny wzrost opóźnienia widoczny na wykresach zespołu.

Zespół najpierw zmniejszył rozmiar pamięci podręcznej, aby wygładzić skoki. Kompromis pozostał niekorzystny: mniej przerw w GC, ale więcej żądań braku pamięci podręcznej spadających do bazy danych — stąd ogólny spadek p99 w innych miejscach. Podstawową poprawką było przepisanie usługi w Rust, bez modułu zbierającego elementy bezużyteczne do monitorowania.

Post wywołał ożywioną debatę techniczną: ponad 1580 punktów i 642 komentarze w Hacker News tego samego dnia jego publikacji (4 lutego 2020 r.) — znak, że problem wykracza daleko poza sprawę Discord.

#
Przejdź do historii

Trzy lata inżynierii w Google, aby skrócić pauzę z 400 ms do 500 µs

Moduł zbierający elementy bezużyteczne Go ilustruje skalę wysiłku wymaganego do okiełznania pauzy w GC — nawet przy zasobach dedykowanego zespołu Google. Rick Hudson, kierownik techniczny Go GC, udokumentował tę historię w dwóch oficjalnych postach na blogu Go.

Przed sierpniem 2015 r300-400 msHistoryczny kolekcjoner Go, przed przeprojektowaniem
Sierpień 2015 · Przejdź do wersji 1.530-40 msPierwszy konkurencyjny kolekcjoner, ustawiony cel < 10 ms
2016 · Przejdź do wersji 1.6< 10 ms (utrzymany poziom SLO)Początkowy cel osiągnięty w produkcji
Marzec 2017 · Przejdź do wersji 1.8poniżej milisekundyUsuwanie skanowania stosu zatrzymującego świat
Sierpień 2017 · Przejdź do wersji 1.9100-200 µs (znacznik)Nowy nieformalny punkt odniesienia wspomniany przez zespół
2018 · Ogłoszono SLO500 µs na cyklCel usługi sformalizowany przez Ricka Hudsona

Źródło: „Getting to Go: The Journey of Go’s Garbage Collector”, go.dev, 12 lipca 2018 r.; i „Go GC: priorytetem są małe opóźnienia i prostota”, go.dev, 31 sierpnia 2015 r.

Trzy lata oddanej pracy zmniejszyły typową przerwę tysiąckrotnie. Ale przerwa nigdy nie zniknęła: jest to cel serwisowy (SLO), a nie absolutna gwarancja zera. Z założenia śledzenie GC musi od czasu do czasu przechodzić przez wykres żywych obiektów. Jedyną regulowaną zmienną jest częstotliwość i czas trwania tej podróży – a nie jej istnienie.

Ten wybór priorytetu nie jest neutralny. Go koncentruje się przede wszystkim na usługach sieciowych i backendach internetowych, gdzie kilkusetmilisekundowa pauza bezpośrednio psuje wrażenia użytkownika — stąd ogromny wysiłek włożony w opóźnienia, a nie surową przepustowość GC. Inne zarządzane środowiska wykonawcze odziedziczyły różne kompromisy, ukształtowane na podstawie ich historycznych przypadków użycia, zanim zdecydowały się na wprowadzenie własnych modułów zbierających o niskiej pauzie. Wspólny punkt pozostaje ten sam: wszystkie zaczynają się od śledzenia GC, a zatem od mechanizmu pauzy, który należy zminimalizować – nigdy nie można go wyeliminować przez konstrukcję.

#
Mechanika rdzy

Dlaczego Rust nie ma tego problemu w konstrukcji

Rdza nie zmniejsza przerw w GC: eliminuje mechanizm, który je powoduje. Kompilator śledzi po kompilacji, kto jest właścicielem każdej wartości pamięci — jest toownership. Kiedy właściciel wartości wykracza poza zakres, Rust automatycznie wstawia wywołanie zwalniające tę pamięć w tym samym miejscu kodu binarnego. Mechanizm ten nazywa się RAII (pozyskiwanie zasobów to inicjalizacja): wydanie jest deterministyczne i nie jest planowane przez moduł zbierający elementy bezużyteczne działający w tle.

Alexandru Nedelcu, autor bloga technicznego rozpoznawalnego w ekosystemie Scala/Rust, podsumowuje kompromis w niedawnym artykule: „Kompromis, jaki oferuje Rust, polega na łatwości użytkowania, preferując wydajność z przewidywalnym opóźnieniem i bezpieczeństwem” (alexn.org, 21 lipca 2026 r.). Rust zamienia część prostoty pisania na przewidywalne opóźnienia.

W tym samym artykule podsumowano, dlaczego nowoczesne GC nie zawsze wystarczą: "Nowoczesne GC próbują wykonywać swoją pracę stopniowo i jednocześnie, bez wpływu na program. Ale ich możliwości są ograniczone, cofając się do cyklu GC zatrzymującego świat, który zamraża cały program, wpływając w ten sposób na opóźnienia".

Oto mechanizm w około dziesięciu wierszach — ogólny przykład, a nie wyciąg z kodu Aurabase:

example.rsrust
struct Connection {
    id: u32,
}

impl Drop for Connection {
    fn drop(&mut self) {
        println!("connexion {} fermée", self.id);
    }
}

fn handle_request() {
    let conn = Connection { id: 42 };
    // ...przetwarza żądanie...
} // conn wykracza tutaj poza zakres: wykonuje się drop().
   // dokładnie w tym momencie, sprawdzanym przez kompilator —
   // nie poprzez cykl zbierania śmieci.

Ważny niuans: nie wszystko jest bezpłatne. Typy zliczające odniesienia (Rc, Arc) dodają niewielki koszt do każdego klonu i wydania. Koszt ten ma charakter lokalny i deterministyczny. Nigdy nie ma przerwy, która zawiesza cały program podczas przeglądania sterty pamięci.

Przydatne wyjaśnienie dotyczące asynchronicznego backendu: asynchroniczne środowisko wykonawcze Rust (tokio, używane przez wszystkie usługi Aurabase) nie ma nic wspólnego z modułem zbierającym elementy bezużyteczne. Planuje wspólne zadania w puli wątków, ale nigdy nie wykonuje iteracji po wykresie obiektu na żywo, aby zwolnić pamięć. Zamieszanie jest powszechne w ekosystemach, w których asynchroniczne środowisko wykonawcze i GC są zarządzane przez tę samą maszynę wirtualną.

#
Implikacje architektoniczne

Co to zmienia w przypadku backendu o dużym natężeniu ruchu

Na backendie, który obsługuje tysiące jednoczesnych żądań, brak GC usuwa zmienną z równania p99. Nie trzeba już zmieniać rozmiaru sterty pamięci, dostosowywać generacji modułu zbierającego ani monitorować cyklu, który może nastąpić w najgorszym momencie. Opóźnienie pojedynczego żądania zależy od jego własnej pracy, a nie od jakiegoś nieprzewidywalnego zdarzenia globalnego w innym miejscu programu.

Rdzeń backendu Aurabase stosuje tę zasadę: wszystkie usługi (aura-gateway, aura-auth, aura-db, aura-realtime, aura-storage, aura-functions, aura-ai…) są napisane w języku Rust i zorganizowane w jednym obszarze roboczym Cargo. Można to sprawdzić bezpośrednio w repozytorium:

Cargo.tomltoml
[workspace]
resolver = "2"

members = [
    "services/aura-gateway",
    "services/aura-auth",
    "services/aura-db",
    # ...9 innych usług + wspólne biblioteki
]

[workspace.dependencies]
axum = { version = "0.8", features = ["ws", "multipart", "macros"] }

Aktualny ekstrakt z Cargo.toml, workspace edycja 2021, resolwer v2 — zweryfikowany w repozytorium Aurabase.

Czego ten fakt nie dowodzi na tym etapie: wartość opóźnienia p99 zmierzona dla Aurabase. Nie opublikowaliśmy jeszcze powtarzalnej metodologii testów porównawczych dla naszego własnego backendu — jest to praca w toku, a nie wynik dostępny dzisiaj. Brak modułu zbierającego śmieci jest mechanizmem zweryfikowanym w kodzie. Samo to nie stanowi dowodu na zmierzone opóźnienie p99. Należy pamiętać o tym rozróżnieniu w obliczu wszelkich argumentów marketingowych na ten temat, w tym naszych — zobacz nasze porównanie techniczne Aurabase vs Supabase, aby poznać szczegóły architektury.

Prawidłowy pomiar p99 wymaga własnej dyscypliny: reprezentatywnych warunków obciążenia, percentyli obliczonych w wystarczająco szerokim oknie przesuwnym oraz środowiska testowego zbliżonego do produkcyjnego. Publikowanie danych liczbowych bez tej metodologii jest jak publikowanie danych marketingowych. Właśnie tego nie chcemy robić w tym artykule.

#
Niuans

Czego brak GC nie rozwiązuje

No-GC nie jest magiczną różdżką

Usunięcie modułu zbierającego elementy bezużyteczne eliminuje tylko jedno źródło opóźnień końcowych — nie wszystkie. Backend Rusta może nadal pokazywać obniżony poziom p99 z powodu oczekiwania sieci, przesycenia puli połączeń Postgres, spornej blokady bazy danych, słabo zindeksowanego zapytania SQL lub powolnego wywołania API innej firmy. Mechanizm opisany w tym artykule usuwa przyczynę strukturalną. Nie zapewnia odporności na innych.

Na przykład w Aurabase każda usługa komunikuje się z Postgres poprzez pulę połączeń (sqlx), a z innymi usługami poprzez NATS JetStream. Niewymiarowa pula, wolno zużywająca się subskrypcja NATS lub zapytanie SQL bez odpowiedniego indeksu powodują własny skok opóźnień — niezależnie od braku modułu zbierającego elementy bezużyteczne.

Praktyczny wniosek: brak GC jest dobrym powodem architektonicznym, aby wybrać backend Rusta dla systemu obsługującego p99. Nie jest to samo w sobie gwarancją opóźnienia – ani w Aurabase, ani gdzie indziej. Metoda, która ma znaczenie, pozostaje ta sama: zmierz, opublikuj metodologię, a następnie popraw to, co ujawnią pomiary. Jeśli przeprowadzasz migrację z backendu za pomocą GC, nasz przewodnik migracji Supabase do Aurabase szczegółowo opisuje, co się zmienia, a co pozostaje bez zmian.

#
Często zadawane pytania

Często zadawane pytania: moduł zbierający elementy bezużyteczne i opóźnienie p99

Czy brak modułu zbierającego elementy bezużyteczne gwarantuje niski poziom p99?+
Nie. Usuwa strukturalne źródło nieprzewidywalnych przerw, ale inne czynniki — sieć, pula połączeń, blokady baz danych — również wpływają na opóźnienie końcowe. Zobacz sekcję „Czego nie naprawia żaden GC” powyżej.
Dlaczego Discord po prostu nie dostroił swojego GC Go inaczej?+
Zespół wypróbował kilka poprawek, w tym zmniejszenie rozmiaru pamięci podręcznej, udokumentowanych w poście z 2020 roku. Kompromis pozostał niekorzystny: mniej przerw w GC w porównaniu z większą liczbą chybień w pamięci podręcznej. Przepisanie w Rust raczej usunęło kompromis niż go przeniosło.
Czy nowoczesne GC, takie jak Go, rozwiązały problem?+
Znacząco go zmniejszyli — GC Go wzrósł z 300–400 ms do 500 mikrosekund w latach 2015–2018 (go.dev) — ale nie wyeliminowali. Śledzenie GC z założenia utrzymuje mechanizm pauzy dla przypadków brzegowych.
Czy Aurabase opublikowało test porównawczy opóźnień p99?+
Jeszcze nie. Faktem zweryfikowanym w kodzie jest brak modułu zbierającego elementy bezużyteczne (Rust, obszar roboczy Cargo, usługi axum). Zmierzona wartość opóźnienia p99 i jej powtarzalna metodologia muszą zostać opublikowane – wolimy brak danych od danych niepochodzących ze źródeł.

GOTOWY DO WDROŻENIA?

Twój backend w pięć minut.

Karta kredytowa nie jest wymagana · 500 MB za darmo · 50 000 MAU