[Tłumaczenie] Model gwintowania Envoy

Tłumaczenie artykułu: Model gwintowania Envoy - https://blog.envoyproxy.io/envoy-threading-model-a8d44b922310

Uznałem ten artykuł za całkiem interesujący, a ponieważ Envoy jest najczęściej używany jako część „istio” lub po prostu jako „kontroler wejścia” kubernetesa, większość ludzi nie ma z nim takiej samej bezpośredniej interakcji, jak na przykład z typowymi Instalacje Nginx lub Haproxy. Jeśli jednak coś się zepsuje, dobrze byłoby zrozumieć, jak to działa od środka. Starałem się przetłumaczyć jak najwięcej tekstu na język rosyjski, łącznie ze słowami specjalnymi, dla tych, którym przykro się na to patrzeć, oryginały zostawiłem w nawiasach. Witamy w kocie.

Niskopoziomowa dokumentacja techniczna bazy kodu Envoy jest obecnie dość skąpa. Aby temu zaradzić, planuję napisać serię wpisów na blogu na temat różnych podsystemów Envoy. Ponieważ jest to pierwszy artykuł, daj mi znać, co myślisz i czym możesz być zainteresowany w przyszłych artykułach.

Jedno z najczęstszych pytań technicznych, które otrzymuję na temat Envoy, dotyczy prośby o niskopoziomowy opis używanego modelu wątków. W tym poście opiszę, w jaki sposób Envoy odwzorowuje połączenia na wątki, a także system Thread Local Storage, którego używa wewnętrznie, aby uczynić kod bardziej równoległym i wydajnym.

Przegląd gwintowania

[Tłumaczenie] Model gwintowania Envoy

Envoy wykorzystuje trzy różne typy strumieni:

  • Główny: Ten wątek kontroluje uruchamianie i kończenie procesów, całe przetwarzanie interfejsu API XDS (xDiscovery Service), w tym DNS, sprawdzanie stanu, ogólne zarządzanie klastrami i środowiskiem wykonawczym, resetowanie statystyk, administrację i ogólne zarządzanie procesami - sygnały Linuksa, gorący restart itp. Wszystko, co dzieje się w tym wątku jest asynchroniczne i „nieblokujące”. Ogólnie rzecz biorąc, główny wątek koordynuje wszystkie krytyczne procesy funkcjonalne, które nie wymagają dużej ilości procesora do działania. Dzięki temu większość kodu sterującego można zapisać tak, jakby był jednowątkowy.
  • Pracownik: Domyślnie Envoy tworzy wątek roboczy dla każdego wątku sprzętowego w systemie, można to kontrolować za pomocą opcji --concurrency. Każdy wątek roboczy uruchamia „nieblokującą” pętlę zdarzeń, która jest odpowiedzialna za nasłuchiwanie każdego słuchacza; w chwili pisania tego tekstu (29 lipca 2017 r.) nie ma fragmentowania słuchacza, akceptowania nowych połączeń, tworzenia instancji stosu filtrów dla połączenia i przetwarzanie wszystkich operacji wejścia/wyjścia (IO) w trakcie trwania połączenia. Ponownie pozwala to na napisanie większości kodu obsługi połączeń tak, jakby był jednowątkowy.
  • Opróżnianie plików: Każdy plik zapisywany przez Envoy, głównie logi dostępowe, posiada obecnie niezależny wątek blokujący. Wynika to z faktu, że zapisywanie do plików buforowanych przez system plików nawet podczas używania O_NONBLOCK czasami może zostać zablokowany (westchnienie). Kiedy wątki robocze muszą zapisywać do pliku, dane są faktycznie przenoszone do bufora w pamięci, gdzie ostatecznie są opróżniane przez wątek opróżnienie pliku. Jest to jeden obszar kodu, w którym technicznie wszystkie wątki robocze mogą blokować tę samą blokadę podczas próby zapełnienia bufora pamięci.

Obsługa połączenia

Jak pokrótce omówiono powyżej, wszystkie wątki robocze nasłuchują wszystkich słuchaczy bez żadnego fragmentowania. W ten sposób jądro służy do bezpiecznego wysyłania zaakceptowanych gniazd do wątków roboczych. Nowoczesne jądra są w tym na ogół bardzo dobre, używają funkcji takich jak zwiększanie priorytetów wejścia/wyjścia (IO), aby spróbować wypełnić wątek pracą, zanim zaczną używać innych wątków, które również nasłuchują na tym samym gnieździe, a także nie korzystają z działania okrężnego blokowanie (Spinlock) w celu przetworzenia każdego żądania.
Gdy połączenie zostanie zaakceptowane w wątku roboczym, nigdy nie opuszcza ono tego wątku. Całe dalsze przetwarzanie połączenia jest obsługiwane całkowicie w wątku roboczym, łącznie z wszelkimi zachowaniami przekazywania.

Ma to kilka ważnych konsekwencji:

  • Wszystkie pule połączeń w Envoy są przypisane do wątku roboczego. Zatem chociaż pule połączeń HTTP/2 nawiązują w danym momencie tylko jedno połączenie z każdym hostem nadrzędnym, jeśli istnieją cztery wątki robocze, w stanie stabilnym będą cztery połączenia HTTP/2 na host nadrzędny.
  • Powodem, dla którego Envoy działa w ten sposób, jest to, że utrzymując wszystko w jednym wątku roboczym, prawie cały kod można zapisać bez blokowania i tak, jakby był jednowątkowy. Ten projekt ułatwia pisanie dużej ilości kodu i niezwykle dobrze skaluje się do niemal nieograniczonej liczby wątków roboczych.
  • Jednakże jednym z głównych wniosków jest to, że z punktu widzenia puli pamięci i wydajności połączenia bardzo ważne jest skonfigurowanie --concurrency. Posiadanie większej liczby wątków roboczych niż to konieczne spowoduje marnowanie pamięci, utworzenie większej liczby bezczynnych połączeń i zmniejszenie szybkości łączenia połączeń. W Lyft nasze kontenery boczne typu envoy działają z bardzo niską współbieżnością, dzięki czemu wydajność jest mniej więcej równa usługom, obok których się znajdują. Uruchamiamy Envoy jako brzegowy serwer proxy tylko przy maksymalnej współbieżności.

Co oznacza brak blokowania?

Termin „nieblokujący” był dotychczas używany kilka razy przy omawianiu działania wątku głównego i roboczego. Cały kod jest pisany przy założeniu, że nic nie jest nigdy blokowane. Nie jest to jednak do końca prawdą (co nie jest do końca prawdą?).

Envoy używa kilku długich blokad procesowych:

  • Jak już wspomniano, podczas zapisywania dzienników dostępu wszystkie wątki robocze uzyskują tę samą blokadę przed zapełnieniem bufora dziennika w pamięci. Czas utrzymywania blokady powinien być bardzo krótki, ale możliwe jest zakwestionowanie blokady przy dużej współbieżności i dużej przepustowości.
  • Envoy używa bardzo złożonego systemu do obsługi statystyk lokalnych dla wątku. Będzie to tematem osobnego wpisu. Wspomnę jednak krótko, że w ramach lokalnego przetwarzania statystyk wątków czasami konieczne jest wykupienie blokady na centralnym „magazynu statystyk”. To blokowanie nigdy nie powinno być wymagane.
  • Główny wątek musi okresowo koordynować działanie ze wszystkimi wątkami roboczymi. Odbywa się to poprzez „publikowanie” z wątku głównego do wątków roboczych, a czasami z wątków roboczych z powrotem do wątku głównego. Wysyłanie wymaga blokady, aby opublikowana wiadomość mogła zostać umieszczona w kolejce do późniejszego dostarczenia. Nigdy nie należy poważnie kwestionować tych blokad, ale z technicznego punktu widzenia nadal można je zablokować.
  • Gdy Envoy zapisuje dziennik w strumieniu błędów systemowych (błąd standardowy), nabywa blokadę całego procesu. Ogólnie rzecz biorąc, lokalne logowanie Envoya jest uważane za okropne z punktu widzenia wydajności, więc nie poświęcono zbyt wiele uwagi jego ulepszaniu.
  • Istnieje kilka innych losowych blokad, ale żadna z nich nie jest krytyczna dla wydajności i nigdy nie należy jej kwestionować.

Lokalna pamięć wątków

Ze względu na sposób, w jaki Envoy oddziela obowiązki głównego wątku od obowiązków wątku roboczego, istnieje wymaganie, aby złożone przetwarzanie mogło być wykonywane w głównym wątku, a następnie dostarczane do każdego wątku roboczego w wysoce współbieżny sposób. W tej sekcji opisano ogólny magazyn wątków Envoy Thread Local Storage (TLS). W następnej sekcji opiszę, jak jest używany do zarządzania klastrem.
[Tłumaczenie] Model gwintowania Envoy

Jak już opisano, główny wątek obsługuje praktycznie całą funkcjonalność płaszczyzny zarządzania i kontroli w procesie Envoy. Płaszczyzna sterowania jest tutaj nieco przeciążona, ale jeśli spojrzysz na nią w samym procesie Envoy i porównasz ją z przekazywaniem wykonywanym przez wątki robocze, ma to sens. Ogólna zasada jest taka, że ​​proces głównego wątku wykonuje pewną pracę, a następnie musi zaktualizować każdy wątek roboczy zgodnie z wynikiem tej pracy. w tym przypadku wątek roboczy nie musi nabywać blokady przy każdym dostępie.

System TLS (lokalny magazyn wątków) firmy Envoy działa w następujący sposób:

  • Kod działający w głównym wątku może przydzielić miejsce TLS dla całego procesu. Chociaż jest to abstrakcyjne, w praktyce jest to indeks wektora zapewniający dostęp O (1).
  • Główny wątek może instalować dowolne dane w swoim gnieździe. Po wykonaniu tej czynności dane są publikowane w każdym wątku roboczym jako normalne zdarzenie pętli zdarzeń.
  • Wątki robocze mogą czytać ze swojego gniazda TLS i pobierać wszelkie dostępne tam dane lokalne wątku.

Chociaż jest to bardzo prosty i niezwykle potężny paradygmat, jest bardzo podobny do koncepcji blokowania RCU (Read-Copy-Update). Zasadniczo wątki robocze nigdy nie widzą żadnych zmian danych w gniazdach TLS podczas pracy. Zmiana następuje tylko w okresie odpoczynku pomiędzy wydarzeniami w pracy.

Envoy używa tego na dwa różne sposoby:

  • Dzięki przechowywaniu różnych danych w każdym wątku roboczym można uzyskać do nich dostęp bez żadnego blokowania.
  • Utrzymując wspólny wskaźnik do danych globalnych w trybie tylko do odczytu w każdym wątku roboczym. Dlatego każdy wątek roboczy ma liczbę odwołań do danych, której nie można zmniejszyć podczas wykonywania pracy. Stare dane zostaną zniszczone dopiero wtedy, gdy wszyscy pracownicy uspokoją się i prześlą nowe udostępnione dane. Jest to identyczne z RCU.

Wątkowanie aktualizacji klastra

W tej sekcji opiszę, w jaki sposób TLS (lokalny magazyn wątków) jest używany do zarządzania klastrem. Zarządzanie klastrem obejmuje przetwarzanie xDS API i/lub DNS, a także sprawdzanie stanu.
[Tłumaczenie] Model gwintowania Envoy

Zarządzanie przepływem klastra obejmuje następujące komponenty i kroki:

  1. Menedżer klastrów to komponent rozwiązania Envoy, który zarządza wszystkimi znanymi strumieniami nadrzędnymi klastra, interfejsami API usługi Cluster Discovery Service (CDS), interfejsami API Secret Discovery Service (SDS) i Endpoint Discovery Service (EDS), systemem DNS i aktywnymi kontrolami zewnętrznymi. Jest odpowiedzialny za utworzenie „ostatecznie spójnego” widoku każdego klastra nadrzędnego, który obejmuje wykryte hosty oraz stan zdrowia.
  2. Moduł sprawdzania kondycji przeprowadza aktywną kontrolę kondycji i zgłasza zmiany stanu kondycji menedżerowi klastra.
  3. CDS (Cluster Discovery Service) / SDS (Secret Discovery Service) / EDS (Endpoint Discovery Service) / DNS są wykonywane w celu określenia przynależności do klastra. Zmiana stanu jest zwracana do menedżera klastra.
  4. Każdy wątek roboczy w sposób ciągły wykonuje pętlę zdarzeń.
  5. Gdy menedżer klastra ustali, że stan klastra uległ zmianie, tworzy nową migawkę stanu klastra w trybie tylko do odczytu i wysyła ją do każdego wątku roboczego.
  6. Podczas następnego spokojnego okresu wątek roboczy zaktualizuje migawkę w przydzielonym gnieździe TLS.
  7. Podczas zdarzenia we/wy, które ma określić hosta do równoważenia obciążenia, moduł równoważenia obciążenia zażąda gniazda TLS (lokalnego magazynu wątków) w celu uzyskania informacji o hoście. Nie wymaga to zamków. Należy również pamiętać, że TLS może również wyzwalać zdarzenia aktualizacji, dzięki czemu moduły równoważenia obciążenia i inne komponenty mogą ponownie obliczać pamięci podręczne, struktury danych itp. Wykracza to poza zakres tego posta, ale jest używane w różnych miejscach kodu.

Korzystając z powyższej procedury, Envoy może obsłużyć każde żądanie bez żadnego blokowania (z wyjątkiem przypadków opisanych wcześniej). Oprócz złożoności samego kodu TLS, większość kodu nie musi rozumieć, jak działa wielowątkowość i można go zapisać w trybie jednowątkowym. Dzięki temu większość kodu jest łatwiejsza do napisania, a także zapewnia doskonałą wydajność.

Inne podsystemy korzystające z protokołu TLS

TLS (lokalny magazyn wątków) i RCU (aktualizacja odczytu kopii) są szeroko stosowane w Envoy.

Przykłady zastosowania:

  • Mechanizm zmiany funkcjonalności w trakcie realizacji: Aktualna lista włączonych funkcjonalności jest obliczana w głównym wątku. Każdy wątek roboczy otrzymuje następnie migawkę tylko do odczytu przy użyciu semantyki RCU.
  • Wymiana tabel tras: W przypadku tablic tras udostępnianych przez usługę RDS (Route Discovery Service) tablice tras są tworzone w głównym wątku. Migawka tylko do odczytu zostanie następnie udostępniona każdemu wątkowi roboczemu przy użyciu semantyki RCU (Read Copy Update). To sprawia, że ​​zmiana tabel tras jest atomowo wydajna.
  • Buforowanie nagłówka HTTP: Jak się okazuje, obliczenie nagłówka HTTP dla każdego żądania (przy uruchomieniu ~25 tys. 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.

Istnieją inne przypadki, ale poprzednie przykłady powinny dobrze zrozumieć, do czego używany jest TLS.

Znane pułapki wydajnościowe

Chociaż Envoy ogólnie działa całkiem nieźle, istnieje kilka godnych uwagi obszarów, które wymagają uwagi, gdy jest używany z bardzo dużą współbieżnością i przepustowością:

  • Jak opisano w tym artykule, obecnie wszystkie wątki robocze uzyskują blokadę podczas zapisu do bufora pamięci dziennika dostępu. Przy dużej współbieżności i dużej przepustowości konieczne będzie wsadowe dzienniki dostępu dla każdego wątku roboczego kosztem dostarczania poza kolejnością podczas zapisywania do pliku końcowego. Alternatywnie możesz utworzyć oddzielny dziennik dostępu dla każdego wątku roboczego.
  • Chociaż statystyki są wysoce zoptymalizowane, przy bardzo dużej współbieżności i przepustowości prawdopodobnie będzie istniała atomowa rywalizacja o poszczególne statystyki. Rozwiązaniem tego problemu są liczniki na wątek roboczy z okresowym resetowaniem liczników centralnych. Zostanie to omówione w kolejnym poście.
  • Obecna architektura nie będzie dobrze działać, jeśli Envoy zostanie wdrożony w scenariuszu, w którym istnieje bardzo niewiele połączeń wymagających znacznych zasobów obliczeniowych. Nie ma gwarancji, że połączenia będą równomiernie rozdzielone pomiędzy wątkami roboczymi. Można to rozwiązać, wdrażając równoważenie połączeń roboczych, co umożliwi wymianę połączeń między wątkami roboczymi.

Wniosek

Model wątków Envoy został zaprojektowany tak, aby zapewnić łatwość programowania i ogromną równoległość kosztem potencjalnie marnotrawionej pamięci i połączeń, jeśli nie jest poprawnie skonfigurowany. Model ten pozwala mu działać bardzo dobrze przy bardzo dużej liczbie wątków i przepustowości.
Jak krótko wspomniałem na Twitterze, projekt może również działać na pełnym stosie sieciowym w trybie użytkownika, takim jak DPDK (Data Plane Development Kit), co może skutkować obsługą milionów żądań na sekundę przez konwencjonalne serwery przy pełnym przetwarzaniu L7. Bardzo interesujące będzie zobaczyć, co zostanie zbudowane w ciągu najbliższych kilku lat.
Ostatni krótki komentarz: wiele razy pytano mnie, dlaczego wybraliśmy C++ dla Envoy. Powodem pozostaje to, że jest to nadal jedyny powszechnie używany język klasy przemysłowej, w którym można zbudować architekturę opisaną w tym poście. C++ zdecydowanie nie nadaje się do wszystkich lub nawet wielu projektów, ale w niektórych przypadkach użycia jest nadal jedynym narzędziem do wykonania zadania.

Linki do kodu

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

Źródło: www.habr.com

Dodaj komentarz