Skip to content
← Wszystkie wpisy
4 min czytania Dawid Skłodowski

Ruby 2.2: inkrementalny GC i GC symboli

Ruby 2.2.0 przybywa w Boże Narodzenie z dwoma usprawnieniami garbage collectora ważnymi dla realnych aplikacji — inkrementalnym GC skracającym pauzy i GC symboli, który wreszcie zamyka długotrwały wyciek pamięci i wektor ataku DoS.

Punktualnie, Ruby 2.2.0 przybyło w Boże Narodzenie. Tam, gdzie 2.1 dało nam generacyjny collector, 2.2 kontynuuje motyw czynienia runtime lepiej zachowującym się dla długo działających aplikacji serwerowych, które większość z nas faktycznie wysyła. Dwie nagłówkowe zmiany dotyczą garbage collection i obie naprawiają problemy, na które mogłeś trafić, nie wiedząc do końca, czym były. Oto co jest w pudełku.

Inkrementalny GC: krótsze pauzy

Zeszłoroczny generacyjny collector (RGenGC) zredukował, ile pracy wykonuje garbage collector, skanując młode obiekty znacznie częściej niż stare. Ale wciąż czaił się problem: sporadyczny duży GC — ten badający całą stertę — był zdarzeniem „stop the world”. Wszystko się zatrzymuje, gdy on działa, a na dużej stercie ta pauza potrafi być na tyle długa, by ją zauważyć: skok opóźnień w żądaniu webowym, zacięcie w czymkolwiek interaktywnym.

Ruby 2.2 wprowadza inkrementalny GC, który atakuje pauzę wprost. Zamiast wykonywać całą fazę oznaczania w jednym nieprzerywalnym zrywie, dzieli tę pracę na małe przyrosty przeplatane z działaniem twojego programu. Całkowita praca jest mniej więcej taka sama, ale rozłożona, więc najgorsza pojedyncza pauza dramatycznie się kurczy.

# możesz obserwować zachowanie collectora:
GC.stat(:major_gc_count)   # ile pełnych zbierań zostało wykonanych
GC.stat(:minor_gc_count)   # ile tanich zbierań tylko-młodych

Dla aplikacji webowej to najbardziej bezpośrednio odczuwalne usprawnienie w wydaniu. Metryką, która liczy się na produkcji, nie jest średnie opóźnienie, lecz ogon — p99, sporadyczne wolne żądanie, które produkuje długa pauza GC — a inkrementalny GC ten ogon przyciąga. Aktualizujesz się, twój wykres opóźnień robi się trochę płaski na górze, a nie napisałeś kodu, by to się stało. To ta sama filozofia, co RGenGC: zachowaj identyczną poprawność, uczyń runtime łaskawszym dla serwerów.

GC symboli: zamknięcie realnego wycieku pamięci

To poprawka rozwiązująca faktyczny, wykorzystywalny problem, i warto zrozumieć, dlaczego miał znaczenie. W każdym Ruby przed 2.2 symbole nigdy nie były zbierane przez garbage collector. Gdy raz utworzyłeś symbol, żył przez całe życie procesu. Dla symboli, które piszesz jako literały w kodzie (:name, :status), to w porządku — jest ich ustalona liczba. Niebezpieczeństwem były dynamicznie tworzone symbole:

# każdy odrębny łańcuch tutaj staje się trwałym symbolem — przed 2.2 to wyciek
params.each { |key, _| do_something(key.to_sym) }
"user_#{id}".to_sym            # zupełnie nowy nieśmiertelny symbol na każde id

Każda ścieżka kodu zamieniająca niezaufane, zmienne dane wejściowe w symbole była wolnym, nieograniczonym wyciekiem pamięci — a gorzej, wektorem ataku DoS. Atakujący, który mógł skłonić twoją aplikację do wywołania .to_sym na kontrolowanych przez siebie łańcuchach (klasykiem było mass-assignment albo parsowanie danych w klucze symbolowe), mógł popchnąć proces do zużywania pamięci bez limitu, aż padnie. To była prawdziwa, wielokrotnie wykorzystywana klasa podatności, a standardową radą był odruch obronny: nigdy nie wołaj to_sym na danych użytkownika; preferuj klucze łańcuchowe dla wszystkiego zewnętrznego.

Ruby 2.2 czyni dynamicznie tworzone symbole zbieralnymi przez GC. Symbol, do którego nic już nie wskazuje, może teraz być odzyskany jak każdy inny obiekt, co zamyka wyciek i rozbraja DoS. Długotrwałe ostrzeżenie „symbole są na zawsze” jest wreszcie przestarzałe dla symboli dynamicznych, a całą kategorię ostrożnych obejściówek można rozluźnić. (Wciąż powinieneś być rozważny przy zamianie nieograniczonych danych zewnętrznych w symbole, ale to już nie tykająca bomba pamięciowa.)

Mniejsze dodatki

Kilka cichszych usprawnień dopełnia wydanie:

  • Kernel#itself to maleńki, naprawdę użyteczny dodatek: po prostu zwraca odbiorcę. Czyta się niezgrabnie opisany, a oczywiście słusznie w użyciu — grupowanie po własnej wartości elementu staje się array.group_by(&:itself) zamiast hałaśliwego bloku { |x| x }.
  • Lepsze zarządzanie stertą. Strojenie GC wokół tego, jak sterta rośnie i jest przycinana, zostało ulepszone, więc długo działające procesy zarządzają pamięcią z większą gracją — mniej przypadków procesu trzymającego się szczytowego poziomu pamięci, której już nie potrzebuje.
  • Optymalizacja zamrożonych łańcuchów trwa. Praca ku przyszłości frozen-string-literal posuwa się naprzód, wzmacniając nawyk .freeze-owania stałych łańcuchowych na gorących ścieżkach.
  • Porządki. Część długo deprecjonowanych metod zostaje usunięta, a fragmenty biblioteki standardowej przycięte — zwykłe sprzątanie utrzymujące spójność języka.

Czy aktualizować?

Tak, a aspekt bezpieczeństwa czyni to czymś więcej niż zwykłym „miło mieć”. Inkrementalny GC to darmowa poprawa opóźnień dla każdej aplikacji serwerowej — dokładnie ten rodzaj zmiany, którą wdrażasz przez podbicie numeru wersji i obserwowanie, jak opóźnienia ogona się poprawiają. Ale GC symboli to ten, na który trzeba zadziałać świadomie: zamyka prawdziwą dziurę wyciek-pamięci-i-DoS, która kształtowała obronne pisanie w Ruby latami. Jeśli uruchamiasz cokolwiek przetwarzającego niezaufane dane — czyli dowolną aplikację webową — 2.2 usuwa ostrą krawędź, którą starannie obchodziłeś.

Wzorzec w 2.1 i teraz 2.2 jest niewątpliwy i mile widziany: zespół rdzenia metodycznie hartuje Ruby pod produkcję. Szybsze, generacyjne, inkrementalne zbieranie; zamknięty wektor DoS; łagodniejsze zachowanie pamięci przy długim czasie działania. Nic z tego nie zmienia, jak piszesz Ruby na co dzień, a wszystko to czyni Ruby, które już napisałeś, bezpieczniejszym i gładszym w uruchamianiu. Stosownie praktyczny prezent gwiazdkowy.