[Перевод] Model wątków Envoy (Envoy threading model)

Tłumaczenie artykułu: Model wątków Envoy — https://blog.envoyproxy.io/envoy-threading-model-a8d44b922310

Ten artykuł wydał mi się dość interesujący, a ponieważ Envoy jest najczęściej używany jako część «istio» lub po prostu jako «ingress controller» w Kubernetes, większość ludzi nie ma z nim tak bezpośredniego kontaktu, jak ma to miejsce w przypadku typowych instalacji Nginx lub Haproxy. Jednak jeśli coś się zepsuje, dobrze byłoby rozumieć, jak to działa od środka. Starałem się przetłumaczyć jak najwięcej tekstu na język polski, włącznie ze specjalnymi terminami, dla tych, którzy nie mogą na to patrzeć, pozostawiłem oryginały w nawiasach. Zapraszam pod kat.

Dokumentacja techniczna na niskim poziomie dotycząca kodu źródłowego Envoy jest obecnie dość uboga. Aby to naprawić, planuję stworzyć serię artykułów na blogu o różnych podsystemach Envoy. Ponieważ jest to pierwszy artykuł, proszę daj mi znać, co o tym myślisz i co mogłoby Cię interesować w następnych artykułach.

Jednym z najczęściej zadawanych pytań technicznych, które otrzymuję na temat Envoy, jest prośba o niskopoziomowy opis używanego modelu wątków (threading model). W tym wpisie opiszę, jak Envoy mapuje połączenia do wątków, a także przedstawię opis systemu lokalnego magazynu wątków (Thread Local Storage), który jest używany wewnętrznie, aby uczynić kod bardziej równoległym i wydajnym.

Przegląd wątków (Threading overview)

[Перевод] Model wątków Envoy (Envoy threading model)

Envoy używa trzech różnych typów wątków:

  • Główny (Main): Ten wątek zarządza uruchamianiem i kończeniem procesu, całym przetwarzaniem API XDS (xDiscovery Service), w tym DNS, sprawdzaniem stanu (health checking), ogólnym zarządzaniem klastrem i procesem pracy usługi (runtime), resetowaniem statystyk, administracją i ogólnym zarządzaniem procesami — sygnały Linux, gorący restart (hot restart) itd. Wszystko, co dzieje się w tym wątku, jest asynchroniczne i „nienblokujące”. Ogólnie rzecz biorąc, wątek główny koordynuje wszystkie krytyczne procesy funkcjonalności, do których nie jest potrzebna znacząca ilość CPU. To pozwala na pisanie większości kodu zarządzającego tak, jakby był jednoprocesorowy.
  • Roboczy (Worker): Domyślnie Envoy tworzy wątek roboczy (worker thread) dla każdego wątku sprzętowego w systemie, co można kontrolować za pomocą opcji --concurrencyKażdy wątek roboczy uruchamia "nieblokującą" pętlę zdarzeń (event loop), która odpowiada za nasłuchiwanie każdego nasłuchiwacza (listener); w momencie pisania tego artykułu (29 lipca 2017 r.) nie ma segmentacji (sharding) nasłuchiwacza (listener), przyjmowania nowych połączeń, tworzenia instancji stosu filtrów dla połączeń i przetwarzania wszystkich operacji wejścia-wyjścia (IO) podczas trwania połączenia. To z kolei pozwala pisać większość kodu przetwarzania połączeń tak, jakby był jednowątkowy.
  • Odtwarzacz plików (File flusher): Każdy plik, który zapisuje Envoy, głównie dzienniki dostępu (access logs), w chwili obecnej ma niezależny, blokujący wątek. Jest to związane z tym, że zapis do plików buforowanych przez system plików, nawet przy użyciu O_NONBLOCK może czasami powodować blokadę (sigh). Gdy wątki robocze muszą zapisać do pliku, dane są faktycznie przenoszone do bufora w pamięci, gdzie ostatecznie są zrzucane przez file flush. To jedna z części kodu, w której technicznie wszystkie wątki robocze (worker threads) mogą zablokować (block) tę samą blokadę (lock), próbując napełnić bufor pamięci.

Obsługa połączeń (Connection handling)

Jak wspomniano wcześniej, wszystkie wątki robocze nasłuchują wszystkich nasłuchiwaczy (listeners) bez jakiejkolwiek segmentacji. W związku z tym jądro jest wykorzystywane do sprawnego przesyłania zaakceptowanych gniazd do wątków roboczych. Nowoczesne jądra ogólnie radzą sobie z tym bardzo dobrze; wykorzystują takie funkcje, jak zwiększenie priorytetu wejścia-wyjścia (IO), aby spróbować wypełnić wątek pracą, zanim zaczną korzystać z innych wątków, które również nasłuchują tego samego gniazda, a także unikają użycia blokady cyklicznej (Spinlock) do przetwarzania każdego zapytania.
Gdy połączenie zostaje zaakceptowane w wątku roboczym (worker thread), nigdy nie opuszcza tego wątku (thread). Cała dalsza obróbka połączenia jest w pełni realizowana w wątku roboczym (worker thread), w tym wszelkie zachowania związane z przekazywaniem (forwarding behavior).

Ma to kilka ważnych konsekwencji:

  • Wszystkie pule połączeń w Envoy odnoszą się do wątku roboczego. Tak więc, chociaż pule połączeń HTTP/2 nawiązują tylko jedno połączenie z każdym nadrzędnym hostem na raz, jeśli jest cztery wątki robocze, w stanie równowagi będą cztery połączenia HTTP/2 z nadrzędnym hostem.
  • Powód, dla którego Envoy działa w ten sposób, polega na tym, że zachowując wszystko w jednym wątku roboczym, niemal cały kod może być napisany w sposób bezblokujący i wyglądać na jednowątkowy. Ten projekt upraszcza pisanie dużej ilości kodu i niezwykle dobrze skalowuje się dla prawie nieograniczonej liczby wątków roboczych.
  • Jednak jednym z głównych wniosków jest to, że z punktu widzenia wydajności puli pamięci i połączeń, naprawdę ważne jest skonfigurowanie parametru --concurrency. Posiadanie większej liczby wątków roboczych niż to konieczne prowadzi do marnotrawienia pamięci, tworzenia większej ilości bezczynnych połączeń i obniżenia szybkości wchodzenia do puli połączeń. W Lyft nasze kontenery envoy sidecar działają z bardzo niskim poziomem równoległości, więc wydajność odpowiada mniej więcej usługom, obok których się znajdują. Uruchamiamy Envoy jako serwer proxy brzegowy tylko przy maksymalnej równoległości.

Co oznacza tryb bezblokujący

Termin ‚bezblokujący’ był używany kilka razy podczas omawiania, jak działają wątek główny i wątki robocze. Cały kod jest pisany pod warunkiem, że nic nigdy nie jest blokowane. Niezależnie od tego, to nie do końca prawda.

Envoy wykorzystuje kilka długotrwałych blokad procesu:

  • Jak już wspomniano, podczas nagrywania dzienników dostępu wszystkie wątki robocze otrzymują tę samą blokadę przed zapełnieniem bufora dziennika w pamięci. Czas utrzymywania blokady powinien być bardzo niski, ale może się zdarzyć, że ta blokada będzie kwestionowana przy wysokiej równoległości i dużej przepustowości.
  • Envoy używa bardzo zaawansowanego systemu do przetwarzania statystyk, który jest lokalny dla strumienia. To będzie temat osobnego wpisu. Niemniej jednak, krótko wspomnę, że w ramach lokalnego przetwarzania statystyk strumienia czasami konieczne jest uzyskanie blokady dla centralnego „magazynu statystyk”. Ta blokada nie powinna być nigdy wymagana.
  • Główny strumień okresowo potrzebuje koordynacji ze wszystkimi strumieniami roboczymi. Dokonuje się tego poprzez „publikację” z głównego strumienia do strumieni roboczych, a czasami także z strumieni roboczych z powrotem do głównego strumienia. Do wysyłania wymagana jest blokada, aby opublikowana wiadomość mogła zostać umieszczona w kolejce do późniejszej dostawy. Te blokady nigdy nie powinny być przedmiotem poważnej konkurencji, ale technicznie mogą one nadal być blokowane.
  • Kiedy Envoy zapisuje dziennik w systemowym strumieniu błędów (standard error), uzyskuje blokadę całego procesu. Ogólnie rzecz biorąc, lokalne logowanie Envoy jest uznawane za kiepskie pod względem wydajności, więc mało uwagi poświęca się jego poprawie.
  • Istnieje kilka innych sporadycznych blokad, ale żadna z nich nie jest krytyczna dla wydajności i nigdy nie powinna być kwestionowana.

Lokalne przechowywanie strumienia (Thread local storage)

Z powodu sposobu, w jaki Envoy rozdziela obowiązki głównego strumienia od obowiązków strumienia roboczego, istnieje wymóg, że złożone przetwarzanie może być realizowane w głównym strumieniu, a następnie udostępniane każdemu strumieniowi roboczemu z dużym stopniem równoległości. W tej sekcji opisano system Envoy Thread Local Storage (TLS) na wysokim poziomie. W następnej sekcji opiszę, jak jest używany do zarządzania klastrem.
[Перевод] Model wątków Envoy (Envoy threading model)

Jak już wspomniano, główny strumień obsługuje praktycznie wszystkie funkcje zarządzania i funkcjonalności płaszczyzny zarządzania w procesie Envoy. Płaszczyzna zarządzania jest tutaj nieco przeciążona, ale jeśli spojrzeć na nią w kontekście samego procesu Envoy i porównać z przekazywaniem, które wykonują strumienie robocze, wydaje się to mieć sens. Zasadniczo proces głównego strumienia wykonuje pewną pracę, a następnie musi aktualizować każdy strumień roboczy zgodnie z wynikiem tej pracy. W tym przypadku wątek roboczy nie musi ustawiać blokady przy każdym dostępie..

System TLS (Thread local storage) Envoy działa w następujący sposób:

  • Kod wykonujący się w głównym wątku może przydzielić slot TLS dla całego procesu. Chociaż jest to abstrahowane, w praktyce jest to indeks w wektorze, zapewniający dostęp O(1).
  • Główny wątek może ustawiać dowolne dane w swoim slocie. Kiedy to jest zrobione, dane są publikowane w każdym wątku roboczym jako zwykłe zdarzenie pętli zdarzeń.
  • Wątki robocze mogą odczytywać ze swojego slotu TLS i wyciągać wszelkie lokalne dane wątku, które są tam dostępne.

Chociaż jest to bardzo prosta i niezwykle potężna paradigma, bardzo podobna do koncepcji blokady RCU (Read-Copy-Update). W zasadzie, wątki robocze nigdy nie widzą żadnych zmian danych w slotach TLS podczas wykonywania pracy. Zmiana zachodzi tylko w okresie spoczynku między zdarzeniami roboczymi.

Envoy wykorzystuje to na dwa różne sposoby:

  • Przechowując różne dane w każdym wątku roboczym, dostęp do tych danych odbywa się bez jakiejkolwiek blokady.
  • Przechowując wspólny wskaźnik do globalnych danych w trybie «tylko do odczytu» w każdym wątku roboczym. W ten sposób każdy wątek roboczy ma licznik odniesień do danych, który nie może być zmniejszany podczas wykonywania pracy. Tylko gdy wszyscy pracownicy się uspokoją i załadują nowe wspólne dane, stare dane zostaną zniszczone. Jest to identyczne z RCU.

Wątek aktualizacji klastra (Cluster update threading)

W tej sekcji opiszę, jak TLS (Thread local storage) jest używane do zarządzania klastrem. Zarządzanie klastrem obejmuje przetwarzanie API xDS i/lub DNS, a także sprawdzanie stanu (health checking).
[Перевод] Model wątków Envoy (Envoy threading model)

Zarządzanie wątkami klastra obejmuje następujące elementy i etapy:

  1. Menadżer klastra to komponent wewnątrz Envoy, który zarządza wszystkimi znanymi upstreamami klastra, interfejsem API CDS (Cluster Discovery Service), interfejsami API SDS (Secret Discovery Service) oraz EDS (Endpoint Discovery Service), DNS i aktywnymi zewnętrznymi sprawdzeniami stanu (health checking). Odpowiada on za stworzenie 'w końcu spójnego' (eventually consistent) widoku każdego upstreamu klastra, który obejmuje wykryte hosty oraz stan zdrowia (health status).
  2. Narzędzie do weryfikacji działania (health checker) aktywnie sprawdza stan działania i informuje o zmianach stanu menedżera klastra.
  3. CDS (Cluster Discovery Service) / SDS (Secret Discovery Service) / EDS (Endpoint Discovery Service) / DNS są wykorzystywane do określenia przynależności do klastra. Zmiana stanu jest zwracana do menedżera klastra.
  4. Każdy wątek roboczy nieustannie wykonuje cykl przetwarzania zdarzeń.
  5. Gdy menedżer klastra stwierdzi, że stan klastra uległ zmianie, tworzy nowy, dostępny tylko do odczytu obraz stanu klastra i przesyła go do każdego wątku roboczego.
  6. W następnym okresie bezczynności wątek roboczy zaktualizuje obraz w przydzielonym slocie TLS.
  7. W trakcie zdarzenia wejścia-wyjścia, które ma określić host do równoważenia obciążenia, równoważnik obciążenia poprosi slot TLS (Thread local storage) o informacje o hoście. Nie wymaga to blokad. Należy również zauważyć, że TLS może inicjować zdarzenia podczas aktualizacji, aby podsystemy równoważenia obciążenia i inne komponenty mogły przeliczać pamięci podręczne, struktury danych itp. To wykracza poza ramy tego postu, ale jest używane w różnych miejscach kodu.

Dzięki opisanej powyżej procedurze Envoy może przetwarzać każde żądanie bez jakichkolwiek blokad (poza wcześniej opisanymi). Poza złożonością samego kodu TLS, większość kodu nie musi rozumieć, jak działa wielowątkowość, i może być napisana w trybie jednowątkowym. Ułatwia to pisanie większości kodu oprócz doskonałej wydajności.

Inne podsystemy, które wykorzystują TLS

TLS (Thread local storage) oraz RCU (Read Copy Update) są szeroko stosowane w Envoy.

Przykłady użycia:

  • Mechanizm zmiany funkcjonalności w czasie wykonywania: Aktualna lista włączonych funkcji jest obliczana w głównym wątku. Następnie każdemu wątkowi roboczemu przydzielany jest obraz tylko do odczytu z wykorzystaniem semantyki RCU.
  • Zamiana tabel trasowania: dla tabel routingu dostarczanych przez RDS (Route Discovery Service), tabele routingu są tworzone w głównym wątku. W dalszej kolejności każdemu wątkowi roboczemu zostanie udostępniony migawka tylko do odczytu z wykorzystaniem semantyki RCU (Read Copy Update). Umożliwia to atomowe i efektywne zmiany w tabelach routingu.
  • Buforowanie nagłówków HTTP: Jak się okazuje, obliczanie nagłówka HTTP dla każdego żądania (przy przetwarzaniu ~25K+ RPS na rdzeń) jest dość kosztowne. Envoy centralnie oblicza nagłówek mniej więcej co pół sekundy i udostępnia go każdemu pracownikowi za pośrednictwem TLS i RCU.

Są także inne przypadki, ale wcześniejsze przykłady powinny zapewnić dobrą podstawę zrozumienia, do czego służy TLS.

Znane pułapki wydajności (Known performance pitfalls)

Chociaż w ogólności Envoy działa wystarczająco dobrze, istnieje kilka znanych obszarów, które wymagają uwagi, gdy jest używany z bardzo dużym współbieżnością i przepustowością:

  • Jak już opisano w tym artykule, obecnie wszystkie wątki robocze blokują się podczas zapisu do pamięci podręcznej dziennika dostępu. Przy dużym współbieżności i wysokiej przepustowości konieczne będzie pakowanie logów dostępu dla każdego wątku roboczego kosztem nieuporządkowanej dostawy podczas zapisu do ostatecznego pliku. Alternatywnie, można tworzyć oddzielny dziennik dostępu dla każdego wątku roboczego.
  • Chociaż statystyki są bardzo zoptymalizowane, przy bardzo dużym współbieżności i przepustowości prawdopodobnie wystąpi atomowa konkurencja na indywidualnych statystykach. Rozwiązaniem tego problemu będą liczniki na jeden wątek roboczy z okresowym resetowaniem liczników centralnych. To będzie omówione w kolejnej części.
  • Obecna architektura nie będzie dobrze działać, jeśli Envoy zostanie wdrożony w scenariuszu, w którym jest bardzo mało połączeń, wymagających znacznych zasobów do przetwarzania. Nie ma gwarancji, że połączenia będą równomiernie rozłożone między wątki robocze. Można to rozwiązać poprzez wdrożenie równoważenia połączeń roboczych, w ramach którego zostanie wprowadzona możliwość wymiany połączeń między wątkami roboczymi.

Wnioski (Conclusion)

Modela strumieni Envoy została zaprojektowana, aby zapewnić prostotę programowania i masowy pararelizm, co może prowadzić do potencjalnie nieefektywnego wykorzystania pamięci i połączeń, jeśli nie są one odpowiednio skonfigurowane. Model ten umożliwia bardzo dobre działanie przy bardzo dużej liczbie wątków i przepustowości.
Jak wspomniałem krótko na Twitterze, projekt może również działać na pełnoprawnym stosie sieciowym w trybie użytkownika, takim jak DPDK (Data Plane Development Kit), co może sprawić, że zwykłe serwery będą w stanie przetwarzać miliony zapytań na sekundę przy pełnej obróbce L7. Będzie bardzo interesujące zobaczyć, co zostanie zbudowane w ciągu najbliższych kilku lat.
Jedna ostatnia szybka uwaga: wielokrotnie zadawano mi pytanie, dlaczego wybraliśmy C ++ dla Envoy. Powód nadal polega na tym, że jest to wciąż jeden z nielicznych szeroko stosowanych języków na poziomie przemysłowym, w którym można zbudować architekturę opisaną w tym poście. C ++ zdecydowanie nie nadaje się dla wszystkich lub nawet dla wielu projektów, ale dla niektórych zastosowań jest to wciąż jedyne narzędzie do wykonania pracy.

Linki do kodu

Linki do plików z interfejsami i implementacją nagłówków, omawianymi w tym poście:

Ź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