[Překlad] Envoy model vláken (Envoy threading model)

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

Tento článek se mi zdál poměrně zajímavý. Vzhledem k tomu, že se Envoy nejčastěji používá jako součást „istio“ nebo jednoduše jako „ingress controller“ pro Kubernetes, většina lidí nemá s ním přímou interakci jako například s běžnými instalacemi Nginx nebo Haproxy. Přesto by bylo dobré rozumět tomu, jak funguje zevnitř, pokud nastanou problémy. Snažil jsem se přeložit co nejvíce textu do češtiny a u některých speciálních slov jsem ponechal originál v závorkách pro ty, kdo to nemají rádi. Vítejte pod odkazem.

Dokumentace na nízké úrovni k základnímu kódu Envoy je v současnosti poměrně řídká. Abych to napravil, plánuju vytvořit sérii článků na blogu o různých subsystémech Envoy. Jako první článek, prosím, dejte mi vědět, co si o něm myslíte a co by vás mohlo zajímat v dalších článcích.

Jedna z nejčastějších technických otázek, které dostávám ohledně Envoy, je žádost o nízkoúrovňový popis používaného modelu vláken (threading model). V tomto příspěvku popíšu, jak Envoy mapuje spojení na vlákna a také popis systému místního úložiště vláken (Thread Local Storage), který se používá uvnitř pro zajištění paralelismu a vysokého výkonu kódu.

Přehled vláken (Threading overview)

[Překlad] Envoy model vláken (Envoy threading model)

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

  • Hlavní (Main): Toto vlákno řídí spuštění a ukončení procesu, veškeré zpracování API XDS (xDiscovery Service), včetně DNS, kontroly funkčnosti, celkového řízení klastru a běhu služby (runtime), resetování statistik, správy a celkového řízení procesů — signály Linuxu, horký restart (hot restart) atd. Vše, co se v tomto vlákně děje, je asynchronní a „neblokující“. Hlavní vlákno v podstatě koordinuje všechny kritické procesy funkčnosti, pro jejichž vykonávání není nutné velké množství CPU. To umožňuje psát většinu řídicího kódu tak, jako by byl jednovláknový.
  • Pracovní (Worker): Ve výchozím nastavení Envoy vytváří pracovní vlákno (worker thread) pro každý fyzický procesor v systému, což lze řídit pomocí volby --concurrency. Každý pracovní vlákno spouští „neblokující“ smyčku událostí (event loop), která zodpovídá za naslouchání (listening) každému naslouchateli (listener). K datu napsání článku (29. července 2017) neexistuje segmentace (sharding) naslouchatele (listener), příjem nových připojení, vytvoření instance zásobníku filtrů pro připojení a zpracování všech operací vstupu a výstupu (IO) během existence připojení. Opět to umožňuje psát většinu kódu pro zpracování připojení, jako by byl jednovláknový.
  • Souborový (File flusher): Každý soubor, který Envoy zapisuje, především přístupové protokoly (access logs), má v současnosti nezávislý blokující vlákno. Důvodem je, že zápis do souborů cachovaných souborovým systémem i při použití O_NONBLOCK často může být blokován (sigh). Když pracovní vlákna potřebují zapsat do souboru, data se fakticky přesunou do paměťového bufferu, odkud jsou nakonec vyprázdněna skrze file flush. To je jedna z oblastí kódu, kde technicky mohou všechna pracovní vlákna (worker threads) blokovat (block) stejný zámek (lock), když se pokoušejí naplnit buffer paměti.

Zpracování připojení (Connection handling)

Jak bylo stručně diskutováno výše, všechna pracovní vlákna naslouchají všem naslouchatelům (listeners) bez jakékoli segmentace. Takto je jádro využíváno k efektivnímu odesílání přijatých soketů do pracovních vláken. Moderní jádra jsou v tomto ohledu velmi dobrá, používají funkce jako zvyšování priority vstupu a výstupu (IO), aby se pokusila naplnit vlákno prací, než začnou používat další vlákna, která také naslouchají stejnému soketu, a také se vyhýbají cyklickému blokování (Spinlock) při zpracování každé žádosti.
Jakmile je připojení přijato na pracovním vlákně (worker thread), nikdy toto vlákno (thread) neopustí. Veškeré další zpracování připojení se plně provádí v pracovním vlákně (worker thread), včetně jakéhokoli chování přeposílání (forwarding behavior).

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

  • Všechny spojovací pooly v Envoy patří do pracovního vlákna. Takže, i když HTTP/2 spojovací pooly dělají pouze jedno spojení s každým nadřazeným hostitelem najednou, pokud existují čtyři pracovní vlákna, bude ve stabilním stavu čtyři HTTP/2 spojení na nadřazený hostitel.
  • Důvod, proč Envoy funguje tímto způsobem, spočívá v tom, že uchováváním všeho v jednom pracovním vlákně může být většina kódu napsána bez blokování a více méně jako jednobarevná. Tento design usnadňuje psaní velkého množství kódu a neuvěřitelně dobře se škáluje na téměř neomezený počet pracovních vláken.
  • Jedním z hlavních závěrů je však to, že z hlediska efektivity poolu paměti a spojení je ve skutečnosti velmi důležité nakonfigurovat parametr --concurrency. Mít více pracovních vláken, než je potřeba, povede k plýtvání pamětí, vytváření většího počtu nečinných spojení a zpomalení přístupu do spojovacího poolu. V Lyft našich envoy sidecar kontejnerech funguje s velmi nízkým paralelismem, takže výkon je přibližně shodný se službami, vedle kterých sedí. Spouštíme Envoy jako hraniční proxy server pouze při maximálním paralelismu.

Co znamená neblokující režim

Termín „neblokující“ byl doposud několikrát použit při diskusi o tom, jak fungují hlavní a pracovní vlákna. Celý kód je napsán s předpokladem, že nic nikdy neblokuje. Není to však úplně pravda.

Envoy používá několik dlouhých blokací procesu:

  • Jak již bylo řečeno, při zapisování přístupových logů dostávají všechna pracovní vlákna stejnou blokaci před zaplněním vyrovnávací paměti logu v paměti. Délka zadržení blokace by měla být velmi nízká, ale je možné, že tato blokace bude zpochybňována při vysokém paralelismu a vysoké propustnosti.
  • Envoy používá velmi složitý systém pro zpracování statistik, který je lokální pro tok. Tohle bude téma samostatného příspěvku. Přesto krátce zmíním, že jako součást lokálního zpracování statistik toku je někdy potřeba získat zámek pro centrální „úložnu statistik“. Tento zámek by nikdy neměl být potřebný.
  • Hlavní tok potřebuje občas koordinaci se všemi pracovními toky. To se dělá prostřednictvím „publikování“ z hlavního toku do pracovních toků a někdy i z pracovních toků zpět do hlavního toku. Pro odesílání je potřeba zámek, aby se publikovaná zpráva mohla umístit do fronty pro následnou doručení. Tyto zámky by nikdy neměly být vystaveny vážné konkurenci, ale stále mohou být technicky zablokovány.
  • Když Envoy zapisuje záznam do systémového toku chyb (standard error), získává zámek celého procesu. Obecně se lokální zapisování deníku Envoy považuje za hrozné z hlediska výkonu, proto se jeho zlepšení nevěnuje příliš pozornosti.
  • 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.

Lokální úložiště vlákna (Thread local storage)

Vzhledem k tomu, jakým způsobem Envoy odděluje povinnosti hlavního vlákna od povinností pracovního vlákna, existuje požadavek, že složité zpracování může být provedeno v hlavním vláknu a poté předáno každému pracovnímu vláknu s vysokým stupněm paralelismu. V této části je popsán systém Envoy Thread Local Storage (TLS) na vysoké úrovni. V následující části popíšu, jak se používá pro správu clusteru.
[Překlad] Envoy model vláken (Envoy threading model)

Jak již bylo zmíněno, hlavní tok zpracovává prakticky všechny funkce správy a funkcionalitu řídící roviny v procesu Envoy. Řídící rovina je zde mírně zatížena, ale pokud ji posuzujeme v rámci samotného procesu Envoy a porovnáme ji s přeposíláním, které vykonávají pracovní toky, zdá se to rozumné. Obecně platí, že hlavní proces vykonává nějakou práci a poté je třeba aktualizovat každé pracovní vlákno podle výsledku této práce. Přitom není nutné, aby pracovní vlákno nastavovalo zámek při každém přístupu..

Systém TLS (Thread local storage) Envoy funguje následujícím způsobem:

  • Kód, který běží v hlavním vláknu, může vyhranit slot TLS pro celý proces. I když je to zjednodušené, v praxi je to index vektorové struktury, který zajišťuje přístup O(1).
  • Hlavní vlákno může do svého slotu nastavit libovolná data. Jakmile je to provedeno, jsou data publikována v každém pracovním vlákně jako běžná událost v cyklu událostí.
  • Pracovní vlákna mohou číst ze svého slotu TLS a získávat jakákoliv místní data, která jsou tam dostupná.

I když je to velmi jednoduchá a neuvěřitelně silná paradigma, velmi podobná konceptu zámků RCU (Read-Copy-Update). V podstatě pracovní vlákna nikdy nevidí žádné změny dat ve slotech TLS během vykonávání práce. Změna se provádí pouze v době klidu mezi pracovními událostmi.

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

  • Uchováváním různých dat v každém pracovním vlákně, přístup k těmto datům se realizuje bez jakékoliv zámky.
  • Uchováváním společného ukazatele na globální data v režimu „pouze pro čtení“ v každém pracovním vlákně. Tímto způsobem každé pracovní vlákno má čítač odkazů na data, který nemůže být snížen během vykonávání práce. Pouze když se všechny pracovní jednotky utiší a načtou nová sdílená data, stará data budou zničena. To je identické s RCU.

Vlákno aktualizace clustera (Cluster update threading)

V této sekci popíšu, jak se TLS (Thread local storage) používá pro řízení clusteru. Řízení clusteru zahrnuje zpracování API xDS a/nebo DNS, stejně jako kontrolu zdravotního stavu (health checking).
[Překlad] Envoy model vláken (Envoy threading model)

Správa clusterových vláken zahrnuje následující komponenty a fáze:

  1. Správce clusteru je komponenta uvnitř Envoy, která spravuje všechny známé upstreamy clusteru, API rozhraní CDS (Cluster Discovery Service), API rozhraní SDS (Secret Discovery Service) a EDS (Endpoint Discovery Service), DNS a aktivní externí kontroly zdravotního stavu (health checking). Je odpovědný za vytvoření „konečně konzistentního“ (eventually consistent) obrazu každého upstreamu clusteru, který zahrnuje objevené hostitele a také stav zdravotního stavu (health status).
  2. Nástroj pro kontrolu funkčnosti (health checker) provádí aktivní kontrolu stavu a informuje správce clusteru o změnách stavu.
  3. CDS (Cluster Discovery Service) / SDS (Secret Discovery Service) / EDS (Endpoint Discovery Service) / DNS jsou prováděny za účelem určení příslušnosti k clustru. Změna stavu se vrací manažerovi clusteru.
  4. Každý pracovní vlákno neustále provádí cyklus zpracování událostí.
  5. Když manažer clusteru zjistí, že se stav clustru změnil, vytvoří nový snímek stavu clustru, který je pouze pro čtení, a odešle jej každému pracovnímu vláknu.
  6. Během následující doby klidu pracovní vlákno aktualizuje snímek ve vyhrazeném slotu TLS.
  7. Během vstupně-výstupní události, která má určit hostitele pro vyvažování zátěže, vyvažovač zátěže požádá slot TLS (Thread local storage) o informace o hostiteli. K tomu nejsou potřebné zámky. Upozorňujeme také, že TLS může také iniciovat události při aktualizaci, takže subsystémy vyvažování zátěže a další komponenty mohou přepočítávat cache, datové struktury atd. To přesahuje rámec tohoto příspěvku, ale používá se na různých místech v kódu.

Pomocí výše uvedené procedury může Envoy zpracovávat každý požadavek bez jakýchkoli zámků (kromě výše uvedených). Kromě složitosti samotného kódu TLS většina kódu nemusí chápat, jak funguje vícerozměrnost, a může být napsána v jednovláknovém režimu. To usnadňuje psaní většiny kódu a zajišťuje výborný výkon.

Další subsystémy využívající TLS

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

Příklady použití:

  • Mechanismus změny funkcionality za běhu: Aktuální seznam zapnutých funkcí se vypočítává v hlavním vlákně. Každému pracovnímu vláknu je pak poskytnut snímek pouze pro čtení pomocí RCU semantiky.
  • Nahrazení tabulek směrování: Pro tabulky tras poskytovaných RDS (Route Discovery Service) se tabulky tras vytvářejí v hlavním vlákně. Snapshot jen pro čtení bude později poskytnut každému pracovnímu vláknu pomocí semantiky RCU (Read Copy Update). To činí změny tabulek tras atomově efektivními.
  • Caching HTTP hlaviček: Jak se ukazuje, výpočet HTTP hlavičky pro každý požadavek (při provádění ~25K+ RPS na jádro) je poměrně náročný. Envoy centrálně vypočítává hlavičku přibližně každých půl sekundy a poskytuje ji každému pracovníku přes TLS a RCU.

Existují i další případy, ale předchozí příklady by měly zajistit dobré porozumění tomu, k čemu se TLS používá.

Známé úskalí výkonu

Ačkoli Envoy obecně funguje dobře, existuje několik známých oblastí, které vyžadují pozornost, když se používá s velmi vysokým paralelismem a propustností:

  • Jak bylo uvedeno v tomto článku, v současnosti všechny pracovní vlákna získávají zámek při zápisu do paměťového bufferu protokolu přístupu. Při vysokém paralelismu a vysoké propustnosti bude nutné provádět balíčkování protokolu přístupu pro každé pracovní vlákno na úkor nepořádné dodávky při zápisu do konečného souboru. Jako alternativu lze vytvářet samostatný protokol přístupu pro každé pracovní vlákno.
  • I když jsou statistiky velmi silně optimalizovány, při velmi vysokém paralelismu a propustnosti pravděpodobně dojde k atomové konkurenci na jednotlivých statistikách. Řešením tohoto problému jsou počítadla pro jedno pracovní vlákno s periodickým resetováním centrálních počítadel. To bude probíráno v dalším příspěvku.
  • Současná architektura nebude dobře fungovat, pokud je Envoy nasazen ve scénáři, kde je velmi málo připojení, které vyžadují značné zdroje pro zpracování. Není zaručeno, že spojení budou rovnoměrně rozdělena mezi pracovní vlákna. To může být vyřešeno implementací vyvažování pracovních spojení, kde bude umožněn přenos spojení mezi pracovními vlákny.

Závěr

Modela vláken Envoy byla navržena pro zajištění jednoduchosti programování a masivního paralelismu s potenciálně zbytečným použitím paměti a spojení, pokud nejsou správně nakonfigurována. Tato modela mu umožňuje velmi dobře fungovat při velmi vysokém počtu vláken a propustnosti.
Jak jsem stručně zmínil na Twitteru, design může také fungovat nad plně funkčním síťovým stackem v uživatelském módu, jako je DPDK (Data Plane Development Kit), což může umožnit obvyklým serverům zpracovávat miliony požadavků za sekundu při plném zpracování L7. Bude velmi zajímavé vidět, co se vybuduje v následujících letech.
Jedna poslední rychlá poznámka: mnohokrát se mě ptali, proč jsme zvolili C++ pro Envoy. Důvod zůstává ten, že je to stále jediný široce používaný jazyk na průmyslové úrovni, na kterém lze postavit architekturu, jak je popsána v tomto příspěvku. C++ určitě není vhodné pro všechny nebo dokonce pro mnoho projektů, ale pro určité případy použití je to stále jediný nástroj, který splňuje úkol.

Odkazy na kód

Odkazy na soubory s rozhraními a implementací hlaviček, na které se v tomto příspěvku odkazuje:

Zdroj: habr.com

Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery 🔥 Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery | ProHoster