[Përkthim] Modeli i filetimit të të dërguarit

Përkthimi i artikullit: Modeli i filetimit të të dërguarit - https://blog.envoyproxy.io/envoy-threading-model-a8d44b922310

Më dukej mjaft interesant ky artikull, dhe meqenëse Envoy përdoret më shpesh si pjesë e "istio" ose thjesht si "kontrolluesi i hyrjes" së kubernetes, shumica e njerëzve nuk kanë të njëjtin ndërveprim të drejtpërdrejtë me të si, për shembull, me tipike Instalimet Nginx ose Haproxy. Megjithatë, nëse diçka prishet, do të ishte mirë të kuptonim se si funksionon nga brenda. U përpoqa të përkthej sa më shumë nga teksti në rusisht, duke përfshirë fjalë të veçanta; për ata që e shohin të dhimbshme ta shikojnë këtë, i lashë origjinalet në kllapa. Mirë se vini në mace.

Dokumentacioni teknik i nivelit të ulët për bazën e kodeve të Envoy aktualisht është mjaft i rrallë. Për të korrigjuar këtë, kam në plan të bëj një sërë postimesh në blog rreth nënsistemeve të ndryshme të Envoy. Meqenëse ky është artikulli i parë, ju lutem më tregoni se çfarë mendoni dhe çfarë mund t'ju interesojë në artikujt e ardhshëm.

Një nga pyetjet teknike më të zakonshme që marr për Envoy është të kërkoj një përshkrim të nivelit të ulët të modelit të filetimit që ai përdor. Në këtë postim, unë do të përshkruaj se si Envoy harton lidhjet me temat, si dhe sistemin e ruajtjes lokale të Thread që përdor brenda për ta bërë kodin më paralel dhe me performancë të lartë.

Vështrim i përgjithshëm i filetimit

[Përkthim] Modeli i filetimit të të dërguarit

I dërguari përdor tre lloje të ndryshme rrymash:

  • Kryesor: Ky thread kontrollon fillimin dhe përfundimin e procesit, të gjithë përpunimin e API-së XDS (xDiscovery Service), duke përfshirë DNS, kontrollin e shëndetit, menaxhimin e përgjithshëm të grupeve dhe kohëzgjatjes, rivendosjen e statistikave, administrimin dhe menaxhimin e përgjithshëm të procesit - sinjalet Linux. rinisja e nxehtë, etj. Gjithçka që ndodh në këtë thread është asinkron dhe "jo bllokues". Në përgjithësi, filli kryesor koordinon të gjitha proceset e funksionalitetit kritik që nuk kërkojnë një sasi të madhe të CPU për të ekzekutuar. Kjo lejon që shumica e kodeve të kontrollit të shkruhet sikur të ishte një filetim i vetëm.
  • Punëtor: Si parazgjedhje, Envoy krijon një fill pune për çdo fije harduerike në sistem, kjo mund të kontrollohet duke përdorur opsionin --concurrency. Çdo fill pune ekzekuton një lak ngjarjesh "jo bllokuese", e cila është përgjegjëse për dëgjimin e secilit dëgjues; në momentin e shkrimit (29 korrik 2017) nuk ka ndarje të dëgjuesit, pranimin e lidhjeve të reja, instancimin e një pirg filtri për lidhjen dhe përpunimin e të gjitha operacioneve hyrëse/dalëse (IO) gjatë jetës së lidhjes. Përsëri, kjo lejon që shumica e kodeve të trajtimit të lidhjes të shkruhet sikur të ishte me një filetim të vetëm.
  • Shpëlarësi i skedarëve: Çdo skedar që shkruan Envoy, kryesisht regjistrat e aksesit, aktualisht ka një fije të pavarur bllokuese. Kjo për faktin se shkrimi në skedarë të ruajtur nga sistemi i skedarëve edhe kur përdoret O_NONBLOCK ndonjëherë mund të bllokohet (psherëtimë). Kur thread-et e punës duhet të shkruajnë në një skedar, të dhënat në fakt zhvendosen në një tampon në memorie ku përfundimisht kalohen përmes fillit turret. Kjo është një zonë e kodit ku teknikisht të gjitha fijet e punëtorëve mund të bllokojnë të njëjtin bllokim ndërsa përpiqen të mbushin një tampon memorie.

Trajtimi i lidhjes

Siç u diskutua shkurt më lart, të gjitha temat e punëtorëve dëgjojnë të gjithë dëgjuesit pa asnjë ndarje. Kështu, kerneli përdoret për të dërguar me hijeshi prizat e pranuara te fijet e punëtorëve. Kernelet moderne janë përgjithësisht shumë të mira në këtë, ata përdorin veçori si rritja e prioritetit të hyrjes/daljes (IO) për të tentuar të mbushin një fill me punë përpara se të fillojnë të përdorin threads të tjerë që po dëgjojnë gjithashtu në të njëjtën fole, dhe gjithashtu nuk përdorin round robin mbyllje (Spinlock) për të përpunuar çdo kërkesë.
Pasi një lidhje pranohet në një fill pune, ajo kurrë nuk e lë atë fill. I gjithë përpunimi i mëtejshëm i lidhjes trajtohet tërësisht në fillin e punës, duke përfshirë çdo sjellje përcjellëse.

Kjo ka disa pasoja të rëndësishme:

  • Të gjitha grupet e lidhjeve në Envoy i janë caktuar një filli pune. Pra, megjithëse grupet e lidhjeve HTTP/2 bëjnë vetëm një lidhje me çdo host në rrjedhën e sipërme në të njëjtën kohë, nëse ka katër thread-e punëtore, do të ketë katër lidhje HTTP/2 për host në rrjedhën e sipërme në një gjendje të qëndrueshme.
  • Arsyeja pse Envoy funksionon në këtë mënyrë është se duke mbajtur gjithçka në një fije të vetme punëtore, pothuajse i gjithë kodi mund të shkruhet pa bllokim dhe sikur të ishte me një fije të vetme. Ky dizajn e bën të lehtë shkrimin e shumë kodeve dhe shkallëzohet jashtëzakonisht mirë në një numër pothuajse të pakufizuar të temave të punës.
  • Megjithatë, një nga pikat kryesore është se nga pikëpamja e grupit të memories dhe efikasitetit të lidhjes, në fakt është shumë e rëndësishme të konfiguroni --concurrency. Të kesh më shumë fije punëtore sesa duhet, do të harxhojë kujtesën, do të krijojë më shumë lidhje boshe dhe do të zvogëlojë shkallën e bashkimit të lidhjeve. Në Lyft, kontejnerët e karrigeve anësore të të dërguarit tanë funksionojnë me njëkohshmëri shumë të ulët, në mënyrë që performanca përafërsisht të përputhet me shërbimet që ulen pranë. Ne e drejtojmë Envoy si një përfaqësues të avantazhit vetëm në njëkohshmëri maksimale.

Çfarë do të thotë mosbllokim?

Termi "jo-bllokues" është përdorur disa herë deri më tani kur diskutohet se si funksionojnë fijet kryesore dhe punëtore. I gjithë kodi është shkruar me supozimin se asgjë nuk është bllokuar ndonjëherë. Megjithatë, kjo nuk është plotësisht e vërtetë (çfarë nuk është plotësisht e vërtetë?).

I dërguari përdor disa bllokime të gjata të procesit:

  • Siç u diskutua, kur shkruani regjistrat e aksesit, të gjitha thread-et e punës fitojnë të njëjtin bllokim përpara se të mbushet buferi i regjistrit në memorie. Koha e mbajtjes së bravës duhet të jetë shumë e ulët, por është e mundur që bllokimi të kontestohet në njëkohshmëri të lartë dhe xhiro të lartë.
  • I dërguari përdor një sistem shumë kompleks për të trajtuar statistikat që janë lokale të fillit. Kjo do të jetë tema e një postimi të veçantë. Sidoqoftë, do të përmend shkurtimisht se si pjesë e përpunimit të statistikave të fijeve në nivel lokal, ndonjëherë është e nevojshme të sigurohet një bllokues në një "dyqan statistikash" qendrore. Ky mbyllje nuk duhet të kërkohet kurrë.
  • Fillimi kryesor duhet të koordinohet periodikisht me të gjitha fijet e punëtorëve. Kjo bëhet duke "publikuar" nga filli kryesor në fillin e punëtorit, dhe nganjëherë nga fillesat e punëtorit përsëri në fillin kryesor. Dërgimi kërkon një bllokim në mënyrë që mesazhi i publikuar të mund të vendoset në radhë për dërgim të mëvonshëm. Këto bravë nuk duhet të kundërshtohen kurrë seriozisht, por ato ende teknikisht mund të bllokohen.
  • Kur Envoy shkruan një regjistër në rrjedhën e gabimeve të sistemit (gabim standard), ai merr një bllokim në të gjithë procesin. Në përgjithësi, prerjet lokale të Envoy konsiderohet e tmerrshme nga pikëpamja e performancës, kështu që nuk i është kushtuar shumë vëmendje përmirësimit të saj.
  • Ka disa bllokime të tjera të rastësishme, por asnjëra prej tyre nuk është kritike për performancën dhe nuk duhet të sfidohet kurrë.

Transferoni memorien lokale

Për shkak të mënyrës sesi Envoy ndan përgjegjësitë e fillit kryesor nga përgjegjësitë e fillit të punës, ekziston një kërkesë që përpunimi kompleks të mund të bëhet në fillin kryesor dhe më pas t'i jepet çdo filli pune në një mënyrë shumë të njëkohshme. Ky seksion përshkruan Envoy Thread Local Storage (TLS) në një nivel të lartë. Në pjesën tjetër do të përshkruaj se si përdoret për të menaxhuar një grup.
[Përkthim] Modeli i filetimit të të dërguarit

Siç është përshkruar tashmë, filli kryesor trajton pothuajse të gjithë funksionalitetin e planit të menaxhimit dhe kontrollit në procesin e Dërguarit. Plani i kontrollit është pak i mbingarkuar këtu, por kur e shikon brenda vetë procesit të Envoy dhe e krahason me përcjelljen që bëjnë fijet e punëtorëve, ka kuptim. Rregulli i përgjithshëm është që procesi i fillit kryesor kryen disa punë dhe më pas duhet të përditësojë çdo fill punëtor sipas rezultatit të asaj pune. në këtë rast, filli i punës nuk ka nevojë të marrë një bllokim në çdo akses.

Sistemi i Envoy's TLS (Thread local storage) funksionon si më poshtë:

  • Kodi që funksionon në fillin kryesor mund të ndajë një vend të caktuar TLS për të gjithë procesin. Edhe pse kjo është e abstraguar, në praktikë është një indeks në një vektor, duke siguruar akses O(1).
  • Fillimi kryesor mund të instalojë të dhëna arbitrare në slotin e tij. Kur kjo të bëhet, të dhënat publikohen në çdo fill pune si një ngjarje normale e ciklit të ngjarjes.
  • Fijet e punës mund të lexojnë nga foleja e tyre TLS dhe të marrin çdo të dhënë vendore të temave të disponueshme atje.

Edhe pse është një paradigmë shumë e thjeshtë dhe tepër e fuqishme, ajo është shumë e ngjashme me konceptin e bllokimit të RCU (Lexo-Kopjo-Përditëso). Në thelb, temat e punëtorëve nuk shohin kurrë ndonjë ndryshim të të dhënave në foletë TLS ndërsa puna po funksionon. Ndryshimi ndodh vetëm gjatë periudhës së pushimit ndërmjet ngjarjeve të punës.

I dërguari e përdor këtë në dy mënyra të ndryshme:

  • Duke ruajtur të dhëna të ndryshme në çdo thread punëtor, të dhënat mund të aksesohen pa asnjë bllokim.
  • Duke mbajtur një tregues të përbashkët për të dhënat globale në modalitetin vetëm për lexim në çdo thread të punës. Kështu, çdo fill pune ka një numër referimi të të dhënave që nuk mund të zvogëlohet ndërsa puna është duke u ekzekutuar. Vetëm kur të gjithë punëtorët të qetësohen dhe të ngarkojnë të dhëna të reja të përbashkëta, të dhënat e vjetra do të shkatërrohen. Kjo është identike me RCU.

Fillimi i përditësimit të grupit

Në këtë seksion, unë do të përshkruaj se si përdoret TLS (magazinimi lokal i Thread) për të menaxhuar një grup. Menaxhimi i grupeve përfshin përpunimin xDS API dhe/ose DNS, si dhe kontrollin e shëndetit.
[Përkthim] Modeli i filetimit të të dërguarit

Menaxhimi i rrjedhës së grupimeve përfshin komponentët dhe hapat e mëposhtëm:

  1. Menaxheri i grupeve është një komponent brenda Envoy që menaxhon të gjitha grupet e njohura në rrjedhën e sipërme, API-në e Shërbimit të Zbulimit të Grupeve (CDS), API-të e Shërbimit të Zbulimit Sekret (SDS) dhe Shërbimit të Zbulimit të Endpoint (EDS), DNS-të dhe kontrollet e jashtme aktive. Ai është përgjegjës për krijimin e një pamjeje "përfundimisht të qëndrueshme" të çdo grupi në rrjedhën e sipërme, i cili përfshin hostet e zbuluar si dhe gjendjen shëndetësore.
  2. Kontrolluesi shëndetësor kryen një kontroll aktiv shëndetësor dhe raporton ndryshimet e gjendjes shëndetësore te menaxheri i grupit.
  3. CDS (Cluster Discovery Service) / SDS (Secret Discovery Service) / EDS (Endpoint Discovery Service) / DNS kryhen për të përcaktuar anëtarësimin në grup. Ndryshimi i gjendjes i kthehet menaxherit të grupit.
  4. Çdo thread punëtor ekzekuton vazhdimisht një lak ngjarjesh.
  5. Kur menaxheri i grupit përcakton se gjendja e një grupi ka ndryshuar, ai krijon një pamje të re vetëm për lexim të gjendjes së grupit dhe e dërgon atë në çdo fill pune.
  6. Gjatë periudhës së ardhshme të qetë, filli i punëtorit do të përditësojë fotografinë në slotin e caktuar TLS.
  7. Gjatë një ngjarjeje I/O që supozohet të përcaktojë balancën e hostit për të ngarkuar, balancuesi i ngarkesës do të kërkojë një slot TLS (Thread local storage) për të marrë informacion rreth hostit. Kjo nuk kërkon bravë. Vini re gjithashtu se TLS gjithashtu mund të aktivizojë ngjarjet e përditësimit në mënyrë që balancuesit e ngarkesës dhe komponentët e tjerë të mund të rillogaritin cache, strukturat e të dhënave, etj. Kjo është përtej qëllimit të këtij postimi, por përdoret në vende të ndryshme në kod.

Duke përdorur procedurën e mësipërme, Envoy mund të përpunojë çdo kërkesë pa asnjë bllokim (përveç siç përshkruhet më parë). Përveç kompleksitetit të vetë kodit TLS, shumica e kodit nuk ka nevojë të kuptojë se si funksionon multithreading dhe mund të shkruhet me një fije të vetme. Kjo e bën shumicën e kodit më të lehtë për t'u shkruar përveç performancës superiore.

Nënsisteme të tjera që përdorin TLS

TLS (Thread local storage) dhe RCU (Read Copy Update) përdoren gjerësisht në Envoy.

Shembuj të përdorimit:

  • Mekanizmi për ndryshimin e funksionalitetit gjatë ekzekutimit: Lista aktuale e funksioneve të aktivizuara llogaritet në temën kryesore. Çdo filli pune i jepet më pas një fotografi vetëm për lexim duke përdorur semantikën RCU.
  • Zëvendësimi i tabelave të rrugës: Për tabelat e itinerarit të ofruara nga RDS (Shërbimi i zbulimit të rrugës), tabelat e itinerarit krijohen në fillin kryesor. Pamja e çastit vetëm për lexim do t'i jepet më pas çdo filli pune duke përdorur semantikën RCU (Read Copy Update). Kjo e bën ndryshimin e tabelave të rrugëve në mënyrë atomike efikase.
  • Ruajtja në memorie e kokës së HTTP: Siç rezulton, llogaritja e kokës së HTTP për secilën kërkesë (ndërsa ekzekutoni ~ 25K+ RPS për bërthamë) është mjaft e shtrenjtë. Envoy llogarit në mënyrë qendrore kokën përafërsisht çdo gjysmë sekonde dhe ia ofron çdo punonjësi nëpërmjet TLS dhe RCU.

Ka raste të tjera, por shembujt e mëparshëm duhet të ofrojnë një kuptim të mirë të asaj për çfarë përdoret TLS.

Grackat e njohura të performancës

Ndërsa Envoy performon mjaft mirë në përgjithësi, ka disa fusha të dukshme që kërkojnë vëmendje kur përdoret me njëkohshmëri dhe xhiro shumë të lartë:

  • Siç përshkruhet në këtë artikull, aktualisht të gjitha temat e punës fitojnë një bllokim kur shkruajnë në buferin e kujtesës së regjistrit të aksesit. Me njëkohshmëri të lartë dhe xhiro të lartë, do t'ju duhet të grumbulloni regjistrat e aksesit për çdo thread punëtor në kurriz të dorëzimit jashtë rendit kur shkruani në skedarin përfundimtar. Përndryshe, ju mund të krijoni një regjistër të veçantë aksesi për çdo fill pune.
  • Edhe pse statistikat janë shumë të optimizuara, në njëkohshmëri dhe xhiro shumë të lartë ka të ngjarë të ketë grindje atomike mbi statistikat individuale. Zgjidhja e këtij problemi është numëruesit për fije punëtore me rivendosje periodike të numëruesve qendrorë. Kjo do të diskutohet në një postim të mëvonshëm.
  • Arkitektura aktuale nuk do të funksionojë mirë nëse Envoy vendoset në një skenar ku ka shumë pak lidhje që kërkojnë burime të konsiderueshme përpunimi. Nuk ka asnjë garanci që lidhjet do të shpërndahen në mënyrë të barabartë midis fijeve të punëtorëve. Kjo mund të zgjidhet duke zbatuar balancimin e lidhjes së punëtorëve, i cili do të lejojë shkëmbimin e lidhjeve midis fijeve të punëtorëve.

konkluzioni

Modeli i filetimit të Envoy është krijuar për të ofruar lehtësi programimi dhe paralelizëm masiv në kurriz të memories dhe lidhjeve potencialisht të kota nëse nuk konfigurohen siç duhet. Ky model e lejon atë të performojë shumë mirë në numërimin dhe xhiros shumë të lartë të fijeve.
Siç e përmenda shkurtimisht në Twitter, dizajni mund të funksionojë gjithashtu në krye të një rrjeti të plotë të modalitetit të përdoruesit, siç është DPDK (Data Plane Development Kit), i cili mund të rezultojë në serverët konvencionalë që trajtojnë miliona kërkesa në sekondë me përpunim të plotë L7. Do të jetë shumë interesante të shihet se çfarë do të ndërtohet në vitet e ardhshme.
Një koment i fundit i shpejtë: Më kanë pyetur shumë herë pse zgjodhëm C++ për Envoy. Arsyeja mbetet se ajo është ende e vetmja gjuhë e përdorur gjerësisht e klasës industriale në të cilën mund të ndërtohet arkitektura e përshkruar në këtë postim. C++ definitivisht nuk është i përshtatshëm për të gjitha apo edhe shumë projekte, por për raste të caktuara përdorimi është ende i vetmi mjet për të kryer punën.

Lidhje me kodin

Lidhje me skedarët me ndërfaqe dhe zbatime të kokës të diskutuara në këtë postim:

Burimi: www.habr.com

Shto një koment