[Překlad] Envoy threading model

Překlad článku: Envoy threading model – https://blog.envoyproxy.io/envoy-threading-model-a8d44b922310

Tento článek mi přišel docela zajímavý, a protože Envoy se nejčastěji používá jako součást „istio“ nebo prostě jako „kontrolor vstupu“ kubernetes, většina lidí s ním nemá stejnou přímou interakci jako například s typickým Instalace Nginx nebo Haproxy. Pokud se však něco rozbije, bylo by dobré pochopit, jak to funguje zevnitř. Snažil jsem se přeložit co nejvíce textu do ruštiny, včetně speciálních slov, pro ty, kterým je bolestné se na to dívat, jsem originály nechal v závorce. Vítejte u kočky.

Nízkoúrovňová technická dokumentace pro kódovou základnu Envoy je v současnosti poměrně řídká. Abych to napravil, plánuji napsat sérii blogových příspěvků o různých subsystémech Envoy. Jelikož se jedná o první článek, dejte mi prosím vědět, co si myslíte a co by vás mohlo zajímat v dalších článcích.

Jednou z nejčastějších technických otázek, které dostávám ohledně Envoy, je požadavek na nízkoúrovňový popis modelu závitování, který používá. V tomto příspěvku popíšu, jak Envoy mapuje připojení k vláknům a také systém Thread Local Storage, který interně používá, aby byl kód paralelnější a výkonnější.

Přehled závitování

[Překlad] Envoy threading model

Envoy používá tři různé typy streamů:

  • Hlavní: Toto vlákno řídí spouštění a ukončování procesů, veškeré zpracování XDS (xDiscovery Service) API, včetně DNS, kontroly stavu, obecné správy clusteru a runtime, resetování statistik, administraci a obecnou správu procesů – signály Linuxu. Hot restart atd. Vše, co děje v tomto vlákně je asynchronní a "neblokující". Obecně platí, že hlavní vlákno koordinuje všechny kritické funkční procesy, které ke svému běhu nevyžadují velké množství CPU. To umožňuje zapsat většinu řídicího kódu, jako by byl jednovláknový.
  • Pracovník: Ve výchozím nastavení Envoy vytvoří pracovní vlákno pro každé hardwarové vlákno v systému, což lze ovládat pomocí této volby --concurrency. Každé pracovní vlákno spouští „neblokující“ smyčku událostí, která je zodpovědná za naslouchání každému posluchači; v době psaní článku (29. července 2017) nedochází k shardování posluchače, přijímání nových připojení, vytváření instancí zásobníku filtrů pro připojení a zpracování všech vstupně/výstupních (IO) operací během životnosti připojení. Opět to umožňuje většinu kódu pro zpracování připojení zapsat, jako by byl jednovláknový.
  • Proplachovač souborů: Každý soubor, který Envoy zapisuje, zejména protokoly přístupu, má aktuálně nezávislé blokovací vlákno. To je způsobeno tím, že zápis do souborů uložených v mezipaměti souborového systému i při použití O_NONBLOCK může se někdy zablokovat (povzdech). Když pracovní vlákna potřebují zapisovat do souboru, data se ve skutečnosti přesunou do vyrovnávací paměti v paměti, kde se nakonec vyprázdní skrz vlákno. vyprázdnění souboru. Toto je jedna oblast kódu, kde mohou technicky všechna pracovní vlákna blokovat stejný zámek při pokusu o zaplnění vyrovnávací paměti.

Manipulace s připojením

Jak bylo stručně uvedeno výše, všechna pracovní vlákna naslouchají všem posluchačům bez jakéhokoli stříhání. Jádro se tedy používá k bezproblémovému odesílání akceptovaných soketů do pracovních vláken. Moderní jádra jsou v tomto obecně velmi dobrá, používají funkce, jako je zvýšení priority vstupu/výstupu (IO), aby se pokusila zaplnit vlákno prací, než začnou používat jiná vlákna, která také naslouchají na stejném soketu, a také nepoužívají kruhové ovládání. zamykání (Spinlock) pro zpracování každého požadavku.
Jakmile je připojení přijato v pracovním vláknu, nikdy toto vlákno neopustí. Veškeré další zpracování připojení je řešeno výhradně v pracovním vláknu, včetně případného přeposílání.

To má několik důležitých důsledků:

  • Všechny fondy připojení v Envoy jsou přiřazeny k pracovnímu vláknu. Ačkoli tedy fondy připojení HTTP/2 vytvářejí pouze jedno připojení ke každému hostiteli proti proudu, pokud existují čtyři pracovní vlákna, budou v ustáleném stavu na každý hostitel proti proudu čtyři připojení HTTP/2.
  • Důvod, proč Envoy funguje tímto způsobem, je ten, že tím, že je vše udržováno na jediném pracovním vlákně, lze téměř veškerý kód psát bez blokování a jako by byl jednovláknový. Tento design usnadňuje psaní velkého množství kódu a neuvěřitelně dobře se škáluje pro téměř neomezený počet pracovních vláken.
  • Jedním z hlavních poznatků však je, že z hlediska paměťového fondu a efektivity připojení je ve skutečnosti velmi důležité nakonfigurovat --concurrency. Více pracovních vláken, než je nutné, způsobí plýtvání pamětí, vytvoří více nečinných připojení a sníží rychlost sdružování připojení. V Lyftu jezdí naše kontejnery postranních vozíků s velmi nízkou souběhem, takže výkon zhruba odpovídá službám, u kterých sedí. Spouštíme Envoy jako okrajový proxy pouze při maximální souběžnosti.

Co znamená neblokování?

Pojem "neblokování" byl zatím použit několikrát, když se diskutovalo o tom, jak fungují hlavní a pracovní vlákna. Veškerý kód je napsán za předpokladu, že nikdy není nic blokováno. Není to však tak úplně pravda (co není tak úplně pravda?).

Envoy používá několik zámků dlouhého procesu:

  • Jak bylo uvedeno, při zápisu protokolů přístupu získají všechna pracovní vlákna stejný zámek, než se zaplní vyrovnávací paměť protokolu v paměti. Doba držení zámku by měla být velmi krátká, ale je možné, že zámek bude napaden při vysoké souběžnosti a vysoké propustnosti.
  • Envoy používá velmi složitý systém pro zpracování statistik, které jsou lokální pro vlákno. Toto bude téma samostatného příspěvku. Krátce se však zmíním o tom, že v rámci lokálního zpracování statistik vláken je někdy nutné pořídit zámek na centrálním „úložišti statistik“. Toto zamykání by nikdy nemělo být vyžadováno.
  • Hlavní vlákno se pravidelně potřebuje koordinovat se všemi pracovními vlákny. To se provádí „publikováním“ z hlavního vlákna do pracovních vláken a někdy z pracovních vláken zpět do hlavního vlákna. Odeslání vyžaduje zámek, aby mohla být publikovaná zpráva zařazena do fronty pro pozdější doručení. Tyto zámky by nikdy neměly být vážně napadeny, ale stále mohou být technicky blokovány.
  • Když Envoy zapíše protokol do proudu systémových chyb (standardní chyba), získá zámek na celý proces. Obecně je místní protokolování Envoy považováno za hrozné z hlediska výkonu, takže jeho vylepšení nebyla věnována velká pozornost.
  • Existuje několik dalších náhodných zámků, ale žádný z nich není kritický pro výkon a nikdy by neměl být zpochybněn.

Podproces místní úložiště

Vzhledem k tomu, jak Envoy odděluje odpovědnosti hlavního vlákna od odpovědností pracovního vlákna, existuje požadavek, aby bylo možné provést komplexní zpracování v hlavním vláknu a poté ho poskytnout každému pracovnímu vláknu vysoce souběžným způsobem. Tato část popisuje Envoy Thread Local Storage (TLS) na vysoké úrovni. V další části popíšu, jak se používá ke správě clusteru.
[Překlad] Envoy threading model

Jak již bylo popsáno, hlavní vlákno zpracovává prakticky všechny funkce správy a řídicí roviny v procesu Envoy. Řídicí rovina je zde trochu přetížená, ale když se na ni podíváte v rámci samotného Envoy procesu a porovnáte to s přeposíláním, které dělají pracovní vlákna, dává to smysl. Obecným pravidlem je, že proces hlavního vlákna vykonává nějakou práci a poté musí aktualizovat každé pracovní vlákno podle výsledku této práce. v tomto případě pracovní vlákno nemusí získat zámek pro každý přístup.

Envoy's TLS (místní úložiště vláken) funguje následovně:

  • Kód spuštěný v hlavním vláknu může celému procesu přidělit slot TLS. Ačkoli je to abstrahováno, v praxi se jedná o index do vektoru, který poskytuje přístup O(1).
  • Hlavní vlákno může do svého slotu instalovat libovolná data. Když se tak stane, data se publikují do každého pracovního vlákna jako běžná událost smyčky událostí.
  • Pracovní vlákna mohou číst ze svého slotu TLS a načítat všechna místní data vlákna, která jsou tam k dispozici.

Přestože se jedná o velmi jednoduché a neuvěřitelně silné paradigma, je velmi podobné konceptu blokování RCU (Read-Copy-Update). Pracovní vlákna v podstatě nikdy nezaznamenají žádné změny dat ve slotech TLS, když je práce spuštěna. Ke změně dochází pouze v době odpočinku mezi pracovními akcemi.

Envoy to používá dvěma různými způsoby:

  • Uložením různých dat v každém pracovním vláknu lze k datům přistupovat bez jakéhokoli blokování.
  • Udržováním sdíleného ukazatele na globální data v režimu pouze pro čtení v každém pracovním vláknu. Každé pracovní vlákno má tedy počet datových odkazů, který nelze snížit, když je práce spuštěna. Teprve až se všichni pracovníci uklidní a nahrají nová sdílená data, budou stará data zničena. Toto je totožné s RCU.

Podprocesy aktualizace klastru

V této části popíšu, jak se TLS (Thread local storage) používá ke správě clusteru. Správa clusteru zahrnuje zpracování xDS API a/nebo DNS a také kontrolu stavu.
[Překlad] Envoy threading model

Správa toku clusteru zahrnuje následující součásti a kroky:

  1. Cluster Manager je komponenta v rámci Envoy, která spravuje všechny známé upstreamy clusteru, Cluster Discovery Service (CDS) API, Secret Discovery Service (SDS) a Endpoint Discovery Service (EDS) API, DNS a aktivní externí kontroly. Je zodpovědný za vytvoření „nakonec konzistentního“ pohledu na každý upstream cluster, který zahrnuje objevené hostitele i zdravotní stav.
  2. Kontrola stavu provádí aktivní kontrolu stavu a hlásí změny stavu správci klastru.
  3. K určení členství v clusteru se provádějí CDS (Cluster Discovery Service) / SDS (Secret Discovery Service) / EDS (Endpoint Discovery Service) / DNS. Změna stavu je vrácena správci clusteru.
  4. Každé pracovní vlákno nepřetržitě provádí smyčku událostí.
  5. Když správce klastru zjistí, že se stav klastru změnil, vytvoří nový snímek stavu klastru pouze pro čtení a odešle ho každému pracovnímu vláknu.
  6. Během dalšího období klidu pracovní vlákno aktualizuje snímek v přiděleném slotu TLS.
  7. Během I/O události, která má určit hostitele pro vyvážení zátěže, si nástroj pro vyrovnávání zátěže vyžádá slot TLS (místní úložiště vláken), aby získal informace o hostiteli. To nevyžaduje zámky. Všimněte si také, že TLS může také spouštět události aktualizace, takže nástroje pro vyrovnávání zatížení a další komponenty mohou přepočítat mezipaměti, datové struktury atd. To je nad rámec tohoto příspěvku, ale používá se na různých místech v kódu.

Pomocí výše uvedeného postupu může Envoy zpracovat každý požadavek bez jakéhokoli blokování (kromě výše popsaného). Kromě složitosti samotného kódu TLS většina kódu nemusí rozumět tomu, jak funguje vícevláknové zpracování, a lze jej psát jednovláknově. Kromě vynikajícího výkonu se díky tomu většina kódu snadněji píše.

Další subsystémy, které využívají TLS

V Envoy jsou široce používány TLS (Thread local storage) a RCU (Read Copy Update).

Příklady použití:

  • Mechanismus pro změnu funkčnosti během provádění: Aktuální seznam povolených funkcí se vypočítává v hlavním vláknu. Každému pracovnímu vláknu je pak poskytnut snímek pouze pro čtení pomocí sémantiky RCU.
  • Výměna trasových tabulek: U směrovacích tabulek poskytovaných službou RDS (Route Discovery Service) jsou směrovací tabulky vytvořeny v hlavním vláknu. Snímek pouze pro čtení bude následně poskytnut každému pracovnímu vláknu pomocí sémantiky RCU (Read Copy Update). Díky tomu je změna směrovacích tabulek atomicky efektivní.
  • Ukládání hlaviček HTTP do mezipaměti: Jak se ukazuje, výpočet HTTP hlavičky pro každý požadavek (při běhu ~25K+ RPS na jádro) je poměrně drahý. Envoy centrálně počítá hlavičku přibližně každou půl sekundu a poskytuje ji každému pracovníkovi prostřednictvím TLS a RCU.

Existují i ​​jiné případy, ale předchozí příklady by měly dobře porozumět tomu, k čemu se TLS používá.

Známá výkonnostní úskalí

Zatímco Envoy funguje celkově docela dobře, existuje několik pozoruhodných oblastí, které vyžadují pozornost, když je používán s velmi vysokou souběžností a propustností:

  • Jak je popsáno v tomto článku, všechna pracovní vlákna aktuálně získávají zámek při zápisu do vyrovnávací paměti protokolu přístupu. Při vysoké souběžnosti a vysoké propustnosti budete muset dávkovat přístupové protokoly pro každé pracovní vlákno na úkor doručení mimo pořadí při zápisu do konečného souboru. Případně můžete vytvořit samostatný protokol přístupu pro každé pracovní vlákno.
  • Ačkoli jsou statistiky vysoce optimalizované, při velmi vysoké souběžnosti a propustnosti pravděpodobně dojde k atomickému sporu o jednotlivé statistiky. Řešením tohoto problému jsou čítače na pracovní vlákno s periodickým nulováním centrálních čítačů. O tom bude řeč v následujícím příspěvku.
  • Současná architektura nebude dobře fungovat, pokud bude Envoy nasazen ve scénáři, kde existuje velmi málo připojení, která vyžadují značné prostředky na zpracování. Neexistuje žádná záruka, že připojení budou rovnoměrně rozdělena mezi pracovní vlákna. To lze vyřešit implementací vyvažování pracovních připojení, které umožní výměnu připojení mezi pracovními vlákny.

Závěr

Model vláken Envoy je navržen tak, aby poskytoval snadné programování a masivní paralelismus na úkor potenciálně plýtvání pamětí a připojeními, pokud nejsou správně nakonfigurovány. Tento model mu umožňuje pracovat velmi dobře při velmi vysokém počtu vláken a propustnosti.
Jak jsem stručně zmínil na Twitteru, návrh může také běžet na úplném síťovém zásobníku v uživatelském režimu, jako je DPDK (Data Plane Development Kit), což může vést k tomu, že konvenční servery zpracovávají miliony požadavků za sekundu s plným zpracováním L7. Bude velmi zajímavé sledovat, co se v příštích letech postaví.
Poslední rychlá poznámka: Mnohokrát jsem byl dotázán, proč jsme si pro Envoy vybrali C++. Důvodem zůstává, že je to stále jediný široce používaný průmyslový jazyk, ve kterém lze budovat architekturu popsanou v tomto příspěvku. C++ se rozhodně nehodí pro všechny nebo dokonce pro mnoho projektů, ale pro určité případy použití je to stále jediný nástroj, jak dokončit práci.

Odkazy na kód

Odkazy na soubory s rozhraními a implementacemi záhlaví probírané v tomto příspěvku:

Zdroj: www.habr.com

Přidat komentář