Bioyino — rozproszony, skalowalny agregator metryk

Więc zbierasz metryki. Tak jak my. My też zbieramy metryki. Oczywiście, te ważne dla biznesu. Dziś opowiemy o pierwszym ogniwie systemu naszego monitoringu — serwerze agregacji zgodnym z statsd. bioyino, dlaczego go napisaliśmy i dlaczego zrezygnowaliśmy z brubeck.

Bioyino — rozproszony, skalowalny agregator metryk

Z naszych wcześniejszych artykułów (1, 2) można dowiedzieć się, że przez pewien czas zbieraliśmy metryki za pomocą brubeck. Jest napisany w C. Z punktu widzenia kodu — prosty jak korek (co jest ważne, gdy chcesz wnosić wkład) i, co najważniejsze, bez problemu radzi sobie z naszymi wolumenami wynoszącymi 2 miliony metryk na sekundę (MPS) w szczycie. Dokumentacja deklaruje wsparcie dla 4 milionów MPS z gwiazdką. Oznacza to, że uzyskasz podaną liczbę, jeśli poprawnie skonfigurujesz sieć na Linuxie. (Nie wiemy, ile MPS można uzyskać, jeśli sieć zostanie pozostawiona bez zmian). Mimo tych zalet, mieliśmy kilka poważnych zastrzeżeń do brubeck.

Zastrzeżenie 1. Github — twórca projektu — przestał go wspierać: publikować poprawki i poprawki, akceptować nasze i (nie tylko nasze) PR. W ciągu ostatnich kilku miesięcy (gdzieś od lutego-marca 2018) aktywność wzrosła, ale przed tym była prawie 2-letnia całkowita cisza. Ponadto projekt jest rozwijany dla wewnętrznych potrzeb Gihub, co może stanowić poważną przeszkodę dla wprowadzania nowych funkcji.

Zastrzeżenie 2. Dokładność obliczeń. Brubeck zbiera do agregacji tylko 65536 wartości. W naszym przypadku dla niektórych metryk w okresie agregacji (30 s) może przychodzić znacznie więcej wartości (1 527 392 w szczycie). W rezultacie takiego próbkowania wartości maksimum i minimum wydają się bezużyteczne. Na przykład, wygląda to tak:

Bioyino — rozproszony, skalowalny agregator metryk
Jak było

Bioyino — rozproszony, skalowalny agregator metryk
Jak powinno być

Z tej samej przyczyny sumy są ogólnie obliczane niepoprawnie. Dodaj do tego błąd z przepełnieniem 32-bitowego float, który w ogóle wysyła serwer w segfault przy otrzymaniu na pozór nieszkodliwej metryki, i jest w ogóle świetnie. Błąd, nota bene, nie został poprawiony.

I, w końcu, Zastrzeżenie X. W momencie pisania tego artykułu, jesteśmy gotowi zaprezentować ją wszystkim 14 w miarę działającym implementacjom statsd, które udało nam się znaleźć. Wyobraźmy sobie, że pewna infrastruktura rozrosła się na tyle, że przyjmowanie 4 mln MPS już nie wystarcza. Lub może jeszcze się nie rozrosła, ale metryki są dla was już na tyle ważne, że nawet krótkie, 2-3 minutowe przerwy na wykresach mogą stać się krytyczne i wywołać ataki nieodpartych epizodów depresji u menedżerów. Ponieważ leczenie depresji jest wdzięcznym zadaniem, potrzebne są rozwiązania techniczne.

Po pierwsze, odporność na awarie, aby nagły problem na serwerze nie wywołał w biurze psychiatrycznego apokalipsy zombie. Po drugie, skalowalność, aby móc przyjmować więcej niż 4 mln MPS, jednocześnie nie zagłębiając się w stos sieciowy Linuxa i spokojnie rosnąć „w bok” do wymaganych rozmiarów.

Ponieważ mieliśmy zapas w zakresie skalowalności, postanowiliśmy zacząć od odporności na awarie. „O! Odporność na awarie! To proste, to potrafimy”, pomyśleliśmy i uruchomiliśmy 2 serwery, uruchamiając na każdym kopię brubeck. W tym celu musieliśmy skopiować ruch z metrykami na oba serwery i nawet napisać do tego małe narzędzie. Problemu odporności na awarie w ten sposób rozwiązaliśmy, ale… nie zbyt dobrze. Na początku wszystko wydawało się w porządku: każdy brubeck zbiera swoją wersję agregacji, zapisuje dane w Graphite co 30 sekund, nadpisując stary interwał (to dzieje się po stronie Graphite). Jeśli nagle jeden serwer zawiedzie, zawsze mamy drugi z własną kopią zaggregowanych danych. Jednak pojawia się problem: gdy serwer zawodzi, na wykresach pojawia się „ząb”. Jest to związane z tym, że 30-sekundowe interwały w brubeck nie są zsynchronizowane, i w momencie awarii jeden z nich nie jest nadpisywany. W momencie uruchamiania drugiego serwera dzieje się to samo. Całkiem znośne, ale chcemy lepiej! Problem skalowalności również się nie zniknął. Wszystkie metryki wciąż „lecą” na pojedynczy serwer, więc jesteśmy ograniczeni tymi samymi 2-4 mln MPS, w zależności od wydajności sieci.

Jeśli trochę pomyślisz o problemie i jednocześnie wykopiesz trochę śniegu, może przyjść ci do głowy oczywisty pomysł: potrzebny jest statsd, który potrafi działać w trybie rozproszonym. To znaczy taki, w którym zrealizowana jest synchronizacja między węzłami pod względem czasu i metryk. „Oczywiście takie rozwiązanie pewnie już istnieje” – powiedzieliśmy i zaczęliśmy googlować... I nic nie znaleźliśmy. Przeszukując dokumentację różnych statsd (https://github.com/etsy/statsd/wiki#server-implementations na dzień 11.12.2017), nie znaleźliśmy nic. Widać, ani programiści, ani użytkownicy tych rozwiązań nie mieli do czynienia z TAKĄ liczbą metryk, inaczej na pewno coś by wymyślili.

I wtedy przypomnieliśmy sobie o „zabawkowym” statsd – bioyino, które napisaliśmy na hackathonie dla zabawy (nazwa projektu została wygenerowana przez skrypt przed rozpoczęciem hackathonu) i zrozumieliśmy, że pilnie potrzebujemy własnego statsd. Po co?

  • ponieważ na świecie jest zbyt mało klonów statsd,
  • ponieważ można zapewnić pożądaną lub zbliżoną do pożądanej odporność na awarie i skalowalność (w tym zsynchronizować agregowane metryki między serwerami i rozwiązać problem konfliktów przy przesyłaniu),
  • ponieważ można liczyć metryki dokładniej, niż robi to brubeck,
  • ponieważ możemy sami zbierać bardziej szczegółową statystykę, której brubeck niemal w ogóle nam nie dostarczał,
  • ponieważ pojawiła się szansa na zaprogramowanie własnej aplikacji wysokowydajnej rozproszonej, która nie będzie dokładnie powtarzać architektury innej takiej samej wysokowydajnej...

Na czym pisać? Oczywiście na Rust. Dlaczego?

  • ponieważ już istniał prototyp rozwiązania,
  • ponieważ autor artykułu w tym czasie już znał Rust i chciał napisać coś na nim do produkcji z możliwością publikacji jako open-source,
  • ponieważ języki z GC nam nie odpowiadają ze względu na naturę otrzymywanego ruchu (praktycznie w czasie rzeczywistym) i przerwy GC są praktycznie niedopuszczalne,
  • ponieważ potrzebna jest maksymalna wydajność, porównywalna z C,
  • ponieważ Rust zapewnia nam bezpieczną współbieżność, a zaczynając pisać to w C/C++, mielibyśmy jeszcze więcej problemów z bezpieczeństwem, przepełnieniami bufora, wyścigami i innymi strasznymi słowami.

Był także argument przeciwko Rust. Firma nie miała doświadczenia w tworzeniu projektów w Rust, a teraz również nie planujemy go używać w głównym projekcie. Dlatego istniały poważne obawy, że nic z tego nie wyjdzie, ale zdecydowaliśmy się zaryzykować i spróbowaliśmy.

Czas mijał...

W końcu, po kilku nieudanych próbach, pierwsza działająca wersja była gotowa. Co z tego powstało? Powstało coś takiego.

Bioyino — rozproszony, skalowalny agregator metryk

Każdy węzeł otrzymuje swój własny zestaw metryk i gromadzi je u siebie, przy czym nie agreguje metryk dla tych typów, dla których do ostatecznej agregacji potrzebny jest ich pełny zestaw. Węzły są ze sobą połączone niejakim protokołem rozproszonego blokowania (distributed lock), który pozwala wybrać wśród nich ten jeden jedyny (tutaj płakaliśmy), który zasługuje na wysłanie metryk Wielkiemu. W tej chwili problem ten rozwiązują środki Consul, ale w przyszłości ambicje autora sięgają do własnego implementacji Raft, gdzie tym zasłużonym oczywiście będzie węzeł lidera konsensusu. Oprócz konsensusu, węzły dość często (domyślnie raz na sekundę) wysyłają swoim sąsiadom te części wstępnie agregowanych metryk, które udało się zebrać w tej sekundzie. Oznacza to, że skalowalność i odporność na awarie pozostają — każdy z węzłów wciąż przechowuje pełny zestaw metryk, ale metryki te są już wysyłane w postaci agregowanej, przez TCP i z kodowaniem w protokole binarnym, więc koszty duplikacji w porównaniu z UDP są znacznie niższe. Pomimo dość dużej ilości przychodzących metryk, kumulacja wymaga bardzo mało pamięci i jeszcze mniej CPU. Dla naszych dobrze kompresowalnych metryk to zaledwie kilka dziesiątków megabajtów danych. Dodatkowym atutem jest brak zbędnych nadpisów danych w Graphite, jak miało to miejsce w przypadku burbecka.

Pakiety UDP z metrykami są rozdzielane między węzłami na urządzeniu sieciowym za pomocą prostego Round Robin. Oczywiście, sprzęt sieciowy nie analizuje zawartości pakietów, a zatem może obsługiwać znacznie więcej niż 4M pakietów na sekundę, nie wspominając już o metrykach, o których nie ma pojęcia. Biorąc pod uwagę, że metryki docierają nie pojedynczo w każdym pakiecie, nie przewidujemy problemów z wydajnością w tym miejscu. W przypadku awarii serwera urządzenie sieciowe szybko (w ciągu 1-2 sekund) wykrywa ten fakt i wyklucza uszkodzony serwer z rotacji. W rezultacie tego, pasywne (tzn. nienależące do liderów) węzły można włączać i wyłączać praktycznie bez zauważalnych spadków na wykresach. Maksimum, co tracimy, to część metryk, które wpłynęły w ciągu ostatniej sekundy. Nagle utracony/wyłączony/zamieniony lider wciąż pokaże nieznaczną anomalię (30-sekundowy interwał pozostaje nadal zdezynchronizowany), ale przy istnieniu połączeń między węzłami można zminimalizować te problemy, na przykład, wysyłając pakiety synchronizujące.

Trochę o wewnętrznej budowie. Aplikacja oczywiście jest wielowątkowa, ale architektura wątków różni się od tej używanej w brubeck. Wątki w brubeck są jednorodne — każdy z nich odpowiada jednocześnie za zbieranie informacji i agregację. W bioyino wątki robocze (workers) są podzielone na dwie grupy: odpowiedzialne za sieć i odpowiedzialne za agregację. Takie podział umożliwia elastyczniejsze zarządzanie aplikacją w zależności od rodzaju metryk: tam, gdzie potrzebna jest intensywna agregacja, można dodać agregatorów, a tam, gdzie jest dużo ruchu sieciowego — zwiększyć liczbę wątków sieciowych. Obecnie na naszych serwerach pracujemy z 8 sieciowymi i 4 agregującymi wątkami.

Część obliczeniowa (odpowiedzialna za agregację) jest dość nudna. Bufory wypełnione przez wątki sieciowe są rozdzielane między wątki obliczeniowe, gdzie są następnie przetwarzane i agregowane. Na żądanie metryki są przekazywane do wysyłki do innych węzłów. Wszystko to, w tym przesyłanie danych między węzłami i współpraca z Consul, odbywa się asynchronicznie, działa na frameworku tokio.

O wiele więcej problemów podczas rozwoju sprawiła część sieciowa odpowiedzialna za odbieranie metryk. Głównym celem wydzielenia strumieni sieciowych w oddzielne encje było dążenie do zmniejszenia czasu, który strumień poświęca nie na odczyt danych z gniazda. Opcje z wykorzystaniem asynchronicznego UDP i zwykłego recvmsg szybko odpadły: pierwsza zużywa zbyt dużo CPU w przestrzeni użytkownika na przetwarzanie zdarzeń, druga — zbyt wiele przełączeń kontekstu. Dlatego obecnie używane jest recvmmsg z dużymi buforami (a bufory, panowie oficerowie, to nie jest coś, co można lekceważyć!). Wsparcie dla zwykłego UDP pozostawiono dla przypadków o niskim obciążeniu, kiedy recvmmsg nie jest konieczne. W trybie multimessage udaje się osiągnąć najważniejsze: zdecydowana większość czasu strumień sieciowy porządkuje kolejkę OS — odczytuje dane z gniazda i przenosi je do bufora w przestrzeni użytkownika, tylko sporadycznie przełączając się, aby oddać wypełniony bufer agregatorom. Kolejka w gnieździe praktycznie się nie kumuluje, a liczba odrzuconych pakietów prawie się nie zwiększa.

Uwaga

W ustawieniach domyślnych rozmiar bufora jest ustawiony na dość dużą wartość. Jeżeli zdecydujesz się wypróbować serwer samodzielnie, możesz się spotkać z tym, że po wysłaniu małej ilości metryk, nie dotrą one do Graphite, pozostając w buforze strumienia sieciowego. Aby pracować z małą ilością metryk, należy ustawić w konfiguracji wartości bufsize i task-queue-size na mniejsze.

Na koniec — trochę wykresów dla miłośników wykresów.

Statystyka liczby przychodzących metryk na każdym serwerze: ponad 2 mln MPS.

Bioyino — rozproszony, skalowalny agregator metryk

Wyłączenie jednego z węzłów i przekierowanie przychodzących metryk.

Bioyino — rozproszony, skalowalny agregator metryk

Statystyka dotycząca wychodzących metryk: zawsze wysyła tylko jeden węzeł — raidboss.

Bioyino — rozproszony, skalowalny agregator metryk

Statystyka pracy każdego węzła z uwzględnieniem błędów w różnych modułach systemu.

Bioyino — rozproszony, skalowalny agregator metryk

Szczegółowe informacje o przychodzących metrykach (nazwy metryk ukryte).

Bioyino — rozproszony, skalowalny agregator metryk

Co zamierzamy dalej robić z tym wszystkim? Oczywiście pisać kod, na pewno! Projekt początkowo planowany był jako open-source i tak pozostanie przez całe swoje życie. W najbliższych planach — przejście na własną wersję Raft, zmiana protokołu peer na bardziej przenośny, dodanie dodatkowej wewnętrznej statystyki, nowych typów metryk, naprawa błędów i inne usprawnienia.

Oczywiście, zachęcamy wszystkich chętnych do pomocy w rozwoju projektu: twórzcie PR, Issues, postaramy się odpowiadać, poprawiać itd.

Na tym kończymy, jak to się mówi, that's all folks, kupujcie nasze słonie!

Odtwarzaj wideo


Źródło: habr.com
Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster