[Translation] Envoy threading model

Cikk fordítása: Envoy szálkezelési modell — https://blog.envoyproxy.io/envoy-threading-model-a8d44b922310

Ez a cikk számomra elég érdekesnek tűnt, és mivel az Envoy-t leggyakrabban az «istio» részének vagy egyszerűen a Kubernetes «ingress controller»-eként használják, ezért a legtöbb ember nem rendelkezik közvetlen tapasztalattal vele, mint például a tipikus Nginx vagy Haproxy telepítésekkel. Azonban, ha valami elromlik, jó lenne tudni, hogyan működik belülről. Igaz próbáltam a lehető legtöbb szöveget oroszról magyarra fordítani, beleértve a speciális szavakat is, a fájdalmasan érző emberek számára az eredeti szavakat zárójelben hagytam. Üdvözöljük a továbbiakban.

Az Envoy kódalap műszaki dokumentációja jelenleg meglehetősen szegényes. Ennek orvoslására egy sor cikket tervezek a blogban az Envoy különböző alrendszereiről. Mivel ez az első cikk, kérem, tudassa velem, mit gondol, és mi lehetett érdekes a következő cikkekben.

Az egyik leggyakoribb műszaki kérdés, amit az Envoy-ról kapok, a használt szálkezelési modell alacsony szintű leírására vonatkozik. Ebben a bejegyzésben leírom, hogyan kapcsolja össze az Envoy a kapcsolatokat a szálakkal, valamint a szálak helyi tárolási (Thread Local Storage) rendszerének leírását, amelyet belsőleg használnak a kód párhuzamosabbá és nagyobb teljesítményűvé tételére.

Szálak leírása (Threading overview)

[Translation] Envoy threading model

Az Envoy három különböző szálat használ:

  • Fő (Main): Ez a szál irányítja a folyamat indítását és leállítását, az összes XDS (xDiscovery Service) API feldolgozását, beleértve a DNS-t, az egészségügyi ellenőrzést, a klaszter általános kezelését és a szolgáltatás működését, a statisztikák alaphelyzetbe állítását, az adminisztrációt és a folyamatok általános kezelését — Linux jelek, forró újraindítás és így tovább. Minden, ami ebben a szálban történik, aszinkron és „nem blokkoló”. Összességében a fő szál koordinálja az összes kritikus funkciót, amelyhez nem szükséges nagy mennyiségű CPU. Ez lehetővé teszi a legtöbb kezelési kód írását, mintha az egy szálas lenne.
  • Munkaszál (Worker): Alapértelmezés szerint az Envoy minden hardverszálhoz (hardware thread) létrehoz egy munkaszálat, amit az alábbi opcióval vezérelhet: --concurrency. Minden munkafolyamat elindít egy „nem blokkoló” eseményciklust (event loop), amely felelős minden hallgató (listener) figyeléséért. A cikk írásának idején (2017. július 29.) nincs a hallgató (listener) szegmentálása (sharding), új kapcsolatok fogadása, a kapcsolathoz tartozó szűrőverem (stack) példányának létrehozása, és az összes bemeneti-kimeneti (IO) művelet feldolgozása a kapcsolat fennállásának ideje alatt. Ismét, ez lehetővé teszi, hogy a kapcsolatok feldolgozásához szükséges kódot úgy írják, mintha az egyszálú lenne.
  • Fájl (File flusher): Minden fájl, amelyet az Envoy ír, főleg a hozzáférési naplók (access logs), jelenleg független blokkoló szálat használ. Ennek az az oka, hogy a fájlokba történő írás, még a fájlrendszer gyorsítótárazása esetén is, néha blokkolhatja az írást (sóhajt). Amikor a munkafolyamatoknak fájlba kell írniuk, az adatok ténylegesen a memóriában lévő gyorsítótárba kerülnek, ahol végül a file flush révén kiürülnek. Ez egy olyan kódterület, ahol technikailag minden munkafolyamat (worker threads) blokkolhatja (block) ugyanazt a zárolást (lock), miközben megpróbálja megtölteni a memória gyorsítótárát. Kapcsolatok kezelése (Connection handling)Mint azt röviden fentebb megvitattuk, minden munkafolyamat figyeli az összes hallgatót (listeners) bármiféle szegmentálás nélkül. Így a magot (kernel) a kapott socketek munkafolyamatokhoz való ésszerű továbbítására használják. A modern magok általában nagyon jók ebben, olyan funkciókat használnak, mint a bemeneti-kimeneti (IO) prioritás-emelés, hogy megpróbálják munkával megtölteni a szálat, mielőtt elkezdenék használni a másik szálakat, amelyek szintén figyelik ugyanazt a socketet, valamint hogy elkerüljék a ciklikus zárolást (Spinlock) az egyes kérések feldolgozása során.

Amint a kapcsolat létrejön egy munkafolyamatban (worker thread), az soha nem hagyja el ezt a szálat (thread). Minden további kapcsolatfeldolgozás teljesen a munkafolyamatban (worker thread) történik, beleértve a bármilyen továbbítási (forwarding) viselkedést is.

Ennek számos fontos következménye van:
Miután a kapcsolatot elfogadták a munkaszálon (worker thread), az sosem hagyja el ezt a szálat (thread). A kapcsolat további feldolgozása teljes mértékben a munkaszálon (worker thread) történik, beleértve bármilyen átkonfigurálási (forwarding behavior) viselkedést.

Ennek számos fontos következménye van:

  • Minden kapcsolati medence az Envoy-ban a munkafolyamathoz tartozik. Így, bár a HTTP/2 kapcsolati medencék csak egyetlen kapcsolatot létesítenek minden feljebb lévő gazdagéppel, ha négy munkafolyamat van, akkor négy HTTP/2 kapcsolat lesz a feljebb lévő gazdagéphez stabil állapotban.
  • Az ok, amiért az Envoy így működik, az, hogy minden egy munkafolyamatban tartva szinte az összes kódot zárak nélkül lehet írni, mintha egy szálú lenne. Ez a tervezés megkönnyíti a nagyszámú kód írását és hihetetlenül jól skálázódik szinte korlátlan számú munkafolyamat számára.
  • Mindazonáltal az egyik alapvető megállapítás, hogy hatékonyság szempontjából a memória- és kapcsolati medencének valóban nagyon fontos a paraméter beállítása. --concurrencyTöbb munkafolyamat megléte, mint amennyi szükséges, memóriaf wasted is, több tétlen kapcsolat létrehozásához és a kapcsolat medencébe való bejutás sebességének csökkenéséhez vezet. A Lyft esetében az envoy sidecar konténereink nagyon alacsony párhuzamossággal működnek, így a teljesítmény körülbelül megegyezik az azok mellett ülő szolgáltatásokkal. Az Envoy-t csak maximális párhuzamosság (concurrency) esetén indítjuk el határ menti proxy-ként (edge).

Mit jelent a nem blokkoló mód

A "nem blokkoló" kifejezést eddig többször használták a fő- és munkafolyamatok működésének megvitatásakor. Az összes kódot az alapján írták, hogy semmi sem blokkolódik soha. Azonban ez nem teljesen igaz (miért nem teljesen igaz?).

Az Envoy több hosszú távú folyamatzárat használ:

  • Ahogy már említettem, a hozzáférési naplók írásakor minden munkafolyamat ugyanazon zárlatot kapja a napló puffer memóriába való kitöltése előtt. A zár megtartási idejének nagyon alacsonynak kell lennie, de lehetséges, hogy ezt a zárat nagy párhuzamosság és nagy áteresztőképesség mellett megkérdőjelezik.
  • Az Envoy egy rendkívül összetett rendszert használ a statisztikák feldolgozására, amely a folyamra vonatkozik. Ez egy külön bejegyzés témája lesz. Mindazonáltal röviden megemlítem, hogy a folyamat statisztikáinak helyi feldolgozása során néha szükség lehet az központi "statisztikai tároló" zárolására. Ezt a zárolást soha nem szabadna megkövetelni.
  • A főfolyam időnként szükségét érzi, hogy koordinálja az összes munkafolyamatot. Ezt a főfolyamból a munkafolyamatokba, néha pedig a munkafolyamatokból vissza a főfolyamba való "közzététellel" végzik. Az üzenetek elküldéséhez zárolás szükséges ahhoz, hogy a közzétett üzenetet sorba lehessen állítani a későbbi kézbesítéshez. Ezeket a zárolásokat soha nem szabadna komoly versenyhelyzetnek kitenni, de technikailag előfordulhat, hogy mégis zárolódhatnak.
  • Amikor az Envoy naplózza a rendszerszintű hibaáramba (standard error), akkor a folyamatot teljes egészében zárolni kell. Összességében az Envoy helyi naplózása teljesítményi szempontból elég rossz, ezért nem sok figyelmet szentelnek a javítására.
  • Vannak néhány egyéb véletlenszerű zárolás, de egyik sem kritikus a teljesítmény szempontjából, és soha nem szabadna vitatni őket.

Folyam helyi tárolás (Thread local storage)

Az Envoy módjából adódóan, hogy elválasztja a főfolyam és a munkafolyamatok feladatait, szükség van arra, hogy a bonyolult feldolgozást el lehessen végezni a főfolyamban, majd ezt nagy párhuzamossággal adják át minden munkafolyamatnak. Ebben a szakaszban a rendszer Envoy Thread Local Storage (TLS) magas szintű leírását találja. A következő szakaszban azt fogom leírni, hogyan használják a klaszter kezelésére.
[Translation] Envoy threading model

Ahogy már említettem, a főfolyam gyakorlatilag az összes kezelés (management) és irányítási sík (control plane) funkcióját kezeli az Envoy folyamat során. Az irányítási sík itt kissé túlterhelt, de ha figyelembe vesszük a folyamat harmadik részét, és összehasonlítjuk a munkafolyamatok által végzett továbbítással, akkor ez értelmesnek tűnik. Általános szabály, hogy a főfolyamat elvégez egy adott munkát, majd frissítenie kell minden munkafolyamatot ennek az eredménynek megfelelően. ebben az esetben a munkafolyamnak nem szükséges zárolnia az erőforrást minden egyes hozzáféréskor.

A TLS (szálak helyi tárolása) Envoy a következőképpen működik:

  • A főszálon futó kód egy TLS slotot oszthat ki az egész folyamat számára. Bár ez el van abstraktálva, a gyakorlatban ez egy index a vektorra, amely O(1) hozzáférést biztosít.
  • A főszál tetszőleges adatokat állíthat be a slotjában. Amikor ez megtörténik, az adatok minden munkafolyam számára nyilvános eseményként közzétételre kerülnek.
  • A munkafolyamok olvashatják a saját TLS slotjukból, és kinyerhetik a rendelkezésre álló helyi száladatokat.

Bár ez egy nagyon egyszerű és rendkívül hatékony paradigma, amely nagyon hasonlít az RCU (Read-Copy-Update) zárolási koncepciójára. A lényeg, hogy a munkafolyamok sosem látnak adatváltozásokat a TLS slotokban a munka végrehajtása közben. A változás csak a munkájuk között, pihenő időszakban történik.

Az Envoy ezt két különböző módon használja:

  • Azáltal, hogy minden munkafolyamon különböző adatokat tárol, ezekhez az adatokhoz való hozzáférés zárolás nélkül történik.
  • Azáltal, hogy minden munkafolyamon egy közös pointert tart a globális adatokhoz „csak olvasásra”. Így minden munkafolyamnak van egy referencia számlálója az adatokhoz, amely munka közben nem csökkenthető. Csak akkor, amikor minden munkás megnyugszik és új globális adatokat töltenek be, a régi adatok kerülnek megsemmisítésre. Ez az RCU-val azonos.

A klaszterfrissítési szál (Cluster update threading)

Ebben a részben leírom, hogyan használják a TLS-t (szálak helyi tárolása) a klaszter kezelésére. A klaszter kezelése magában foglalja az API xDS és/vagy DNS feldolgozását, valamint az egészségügyi ellenőrzést.
[Translation] Envoy threading model

A klaszter szálainak kezelése a következő összetevőket és lépéseket tartalmazza:

  1. A klaszter menedzser az Envoy belső komponense, amely kezeli az összes ismert upstream-et a klaszterben, a CDS (Cluster Discovery Service), az SDS (Secret Discovery Service) és az EDS (Endpoint Discovery Service) API-it, DNS-t és az aktív külső egészségügyi ellenőrzéseket. Felelős az „időben konzisztens” (eventually consistent) nézet létrehozásáért minden upstream-ről, beleértve a felfedezett hosztokat és az egészségi állapotot.
  2. Az egészségellenőrző (health checker) aktívan ellenőrzi a működést, és a működési állapot változásairól tájékoztatja a klaszterkezelőt.
  3. A CDS (Cluster Discovery Service) / SDS (Secret Discovery Service) / EDS (Endpoint Discovery Service) / DNS szolgáltatások arra szolgálnak, hogy meghatározzák a klaszterhez való tartozást. Az állapotváltozás a klaszterkezelőhöz kerül vissza.
  4. Minden munkafolyamat folyamatosan végrehajt egy eseményfeldolgozó ciklust.
  5. Amikor a klaszterkezelő érzékeli, hogy a klaszter állapota megváltozott, létrehoz egy új, csak olvasható állapots snapshotot, és azt elküldi minden munkafolyamatnak.
  6. A következő pihenőidő alatt a munkafolyamat frissíti a snapshotot a különített TLS-helyen.
  7. Beviteli/kimeneti esemény során, amely a terheléselosztás szempontjából a gazdagép meghatározását hivatott segíteni, a terheléselosztó megkéri a TLS (Thread local storage) helyet a gazdagépről szóló információszerzéshez. Ehhez nem szükséges zárakat használni. Vegye figyelembe, hogy a TLS eseményeket is kezdeményezhet frissítésekkor, így a terheléselosztó alrendszerek és más komponensek újraszámíthatják a cache-eket, adatszerkezeteket stb. Ez felülmúlja ennek a bejegyzésnek a kereteit, de sok helyen használják a kódban.

A fent leírt eljárási módszert alkalmazva az Envoy képes minden egyes kérést blokkolás nélkül kezelni (kivéve a már említett eseteket). A TLS kód bonyolultságán kívül a kód túlnyomó része nem igényli a párhuzamos működés működésének ismeretét, és egyszálú módban írható. Ez megkönnyíti a kód nagy részének megírását, elősegítve a kiváló teljesítményt.

A TLS-t használó egyéb alrendszerek

A TLS (Thread local storage) és az RCU (Read Copy Update) széles körben elterjedt az Envoy-ban.

Felhasználási példák:

  • Funkcionalitás végrehajtás ideje alatt történő módosításának mechanizmusa: A jelenlegi engedélyezett funkciók listája a fő szálban számítódik ki. Ezután minden munkafolyamat számára csak olvasható snapshotot biztosítunk RCU szemantikával.
  • Útvonal-táblák cseréje: az RDS (Route Discovery Service) által biztosított útvonal táblázatok esetében az útvonal táblázatok a fő szálban jönnek létre. Később egy csak olvasható pillanatkép kerül rendelkezésre bocsátásra minden munkaszál számára az RCU (Read Copy Update) szemantikáját használva. Ez az útvonal táblázatok változtatását atomikusan hatékonnyá teszi.
  • HTTP fejlécek gyorsítótárazása: Kiderül, hogy az HTTP fejlécek kiszámítása minden egyes kéréshez (kb. ~25K+ RPS/szál) meglehetősen költséges. Az Envoy központilag számítja ki a fejlécet körülbelül fél másodpercenként, és biztosítja azt minden dolgozónak TLS-en és RCU-n keresztül.

Vannak más esetek is, de a korábbi példák jó alapot adnak arra, hogy mire használják a TLS-t.

Ismert teljesítménybeli csapdák

Bár az Envoy összességében elég jól működik, van néhány ismert terület, amely figyelmet igényel, amikor nagyon magas párhuzamossággal és áteresztőképességgel használják:

  • Ahogy ezt a cikkben már leírtuk, jelenleg minden munkaszál zárolódik, amikor ír a hozzáférési napló memória gyorsítótárába. Magas párhuzamosság és nagy áteresztőképesség mellett szükség lehet a hozzáférési naplók csomagolására minden munkaszál esetében a végső fájlba való írás során. Alternatívaként minden munkaszálhoz külön hozzáférési naplót is létrehozhatunk.
  • Bár a statisztikák nagyon optimalizáltak, nagyon magas párhuzamosság és áteresztőképesség mellett valószínű, hogy atomikus verseny fog kialakulni az egyes statisztikákon. E probléma megoldása egyetlen munkaszálra vonatkozó számlálók alkalmazása központosított számlálók időszakos nullázásával. Ezt a következő bejegyzésben fogjuk megvitatni.
  • A meglévő architektúra nem fog jól működni, ha az Envoy-t olyan forgatókönyvben telepítik, ahol nagyon kevés kapcsolat van, amelyek jelentős erőforrásokat igényelnek a feldolgozáshoz. Nincs garancia arra, hogy a kapcsolatok egyenletesen oszlanak meg a munkaszálak között. Ezt meg lehet oldani a munkakapcsolatok kiegyensúlyozásának implementálásával, amely lehetővé teszi a kapcsolatok cseréjét a munkaszálak között.

Következtetés

Az Envoy szálmodell annak érdekében lett kifejlesztve, hogy megkönnyítse a programozást és a tömeges párhuzamosságot, ami potenciálisan pazarló memória- és csatlakozás-használathoz vezethet, ha nem állítják be megfelelően. Ez a modell lehetővé teszi számára, hogy rendkívül jól működjön nagyon magas szál- és átbocsátási sebesség mellett.
Ahogyan már röviden említettem a Twitteren, a dizájn felülfelvérhet egy teljes funkcionalitású hálózati veremben felhasználói módban, mint például a DPDK (Data Plane Development Kit), ami lehetővé teheti, hogy a hagyományos szerverek másodpercenként millió kérést kezeljenek, teljes L7 feldolgozással. Nagyon izgalmas látni, hogy mi épül az elkövetkező években.
Egy utolsó gyors megjegyzés: sokan többször megkérdezték tőlem, miért választottuk a C++-t az Envoyhoz. Az ok továbbra is az, hogy ez még mindig az egyetlen széles körben elterjedt ipari szintű nyelv, amelyre a posztban leírt architektúra felépíthető. A C++ egyáltalán nem alkalmas minden vagy akár sok projekt számára, de bizonyos használati esetekben még mindig az egyetlen eszköz a munka elvégzésére.

Kódra mutató hivatkozások

A posztban tárgyalt interfészeket és fejléceket tartalmazó fájlokra mutató hivatkozások:

Forrás: habr.com

Megbízható hosting vásárlása weboldalak számára, DDoS védelemmel, VPS VDS szerverek. 🔥 Megbízható webtárhely vásárlása DDoS védelemmel, VPS VDS szerverek | ProHoster