Cikk fordítása:
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](/wp-content/uploads/2019/04/c5028463db1eb5b07ccb1a3af1f64a03.jpeg)
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 flushré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](/wp-content/uploads/2019/04/a47af1e8eb1f55a4d7609b3ff3ee9ce1.jpeg)
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](/wp-content/uploads/2019/04/238f5718729e1f12f8f0d6507f16abe8.jpeg)
A klaszter szálainak kezelése a következő összetevőket és lépéseket tartalmazza:
- 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.
- 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.
- 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.
- Minden munkafolyamat folyamatosan végrehajt egy eseményfeldolgozó ciklust.
- 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.
- A következő pihenőidő alatt a munkafolyamat frissíti a snapshotot a különített TLS-helyen.
- 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
