[Translation] Envoy threading model

Pagsasalin ng artikulo: Envoy threading model - https://blog.envoyproxy.io/envoy-threading-model-a8d44b922310

Natagpuan ko na ang artikulong ito ay medyo kawili-wili, at dahil ang Envoy ay kadalasang ginagamit bilang bahagi ng "istio" o simpleng "ingress controller" ng mga kubernetes, karamihan sa mga tao ay walang direktang pakikipag-ugnayan dito gaya ng, halimbawa, sa karaniwang Mga pag-install ng Nginx o Haproxy. Gayunpaman, kung may masira, makabubuting maunawaan kung paano ito gumagana mula sa loob. Sinubukan kong isalin ang pinakamaraming teksto sa Russian hangga't maaari, kabilang ang mga espesyal na salita; para sa mga nakakasakit na tingnan ito, iniwan ko ang mga orihinal sa panaklong. Maligayang pagdating sa pusa.

Ang mababang antas na teknikal na dokumentasyon para sa Envoy codebase ay kasalukuyang medyo kalat. Upang malunasan ito, plano kong gumawa ng isang serye ng mga post sa blog tungkol sa iba't ibang mga subsystem ng Envoy. Dahil ito ang unang artikulo, mangyaring ipaalam sa akin kung ano ang iniisip mo at kung ano ang maaaring maging interesado ka sa mga artikulo sa hinaharap.

Isa sa mga pinakakaraniwang teknikal na tanong na natatanggap ko tungkol sa Envoy ay humihingi ng mababang antas na paglalarawan ng threading model na ginagamit nito. Sa post na ito, ilalarawan ko kung paano imamapa ng Envoy ang mga koneksyon sa mga thread, gayundin ang sistema ng Thread Local Storage na ginagamit nito sa loob upang gawing mas parallel at high-performance ang code.

Pangkalahatang-ideya ng threading

[Translation] Envoy threading model

Gumagamit ang Envoy ng tatlong magkakaibang uri ng stream:

  • Pangunahing: Kinokontrol ng thread na ito ang proseso ng pagsisimula at pagwawakas, lahat ng pagpoproseso ng XDS (xDiscovery Service) API, kabilang ang DNS, pagsusuri sa kalusugan, pangkalahatang cluster at pamamahala ng runtime, pag-reset ng mga istatistika, pangangasiwa at pamamahala sa pangkalahatang proseso - mga signal ng Linux. mainit na pag-restart, atbp. Lahat ng iyon ang nangyayari sa thread na ito ay asynchronous at "non-blocking". Sa pangkalahatan, ang pangunahing thread ay nag-coordinate ng lahat ng mga kritikal na proseso ng pag-andar na hindi nangangailangan ng malaking halaga ng CPU upang tumakbo. Nagbibigay-daan ito sa karamihan ng control code na maisulat na parang single threaded.
  • Manggagawa: Bilang default, lumilikha si Envoy ng thread ng manggagawa para sa bawat thread ng hardware sa system, makokontrol ito gamit ang opsyon --concurrency. Ang bawat thread ng manggagawa ay nagpapatakbo ng "hindi humaharang" na loop ng kaganapan, na responsable para sa pakikinig sa bawat tagapakinig; sa oras ng pagsulat (Hulyo 29, 2017) walang sharding ng tagapakinig, pagtanggap ng mga bagong koneksyon, paggawa ng isang filter stack para sa ang koneksyon, at pagpoproseso ng lahat ng input/output (IO) na mga operasyon sa buong buhay ng koneksyon. Muli, pinapayagan nito ang karamihan sa mga code sa paghawak ng koneksyon na maisulat na parang single threaded ito.
  • Flusher ng file: Ang bawat file na isinusulat ng Envoy, pangunahin ang pag-access sa mga log, ay kasalukuyang mayroong independiyenteng blocking thread. Ito ay dahil sa ang katunayan na ang pagsulat sa mga file na naka-cache ng file system kahit na ginagamit O_NONBLOCK maaaring minsan ay naharang (sigh). Kapag ang mga thread ng manggagawa ay kailangang sumulat sa isang file, ang data ay aktwal na inililipat sa isang buffer sa memorya kung saan ito ay tuluyang na-flush sa thread flush ng file. Ito ay isang lugar ng code kung saan ang lahat ng mga thread ng manggagawa ay maaaring harangan ang parehong lock habang sinusubukang punan ang isang memory buffer.

Paghawak ng koneksyon

Gaya ng napag-usapan sa itaas, lahat ng mga thread ng manggagawa ay nakikinig sa lahat ng mga tagapakinig nang walang anumang sharding. Kaya, ang kernel ay ginagamit upang maipadala ang mga tinatanggap na socket sa mga thread ng manggagawa. Ang mga modernong kernel sa pangkalahatan ay napakahusay dito, gumagamit sila ng mga feature tulad ng input/output (IO) priority boosting upang subukang punan ang isang thread ng trabaho bago sila magsimulang gumamit ng iba pang mga thread na nakikinig din sa parehong socket, at hindi rin gumagamit ng round robin pag-lock (Spinlock) upang iproseso ang bawat kahilingan.
Kapag natanggap na ang isang koneksyon sa thread ng manggagawa, hindi na ito umaalis sa thread na iyon. Ang lahat ng karagdagang pagproseso ng koneksyon ay ganap na pinangangasiwaan sa thread ng manggagawa, kabilang ang anumang pag-uugali sa pagpapasa.

Ito ay may ilang mahahalagang kahihinatnan:

  • Ang lahat ng mga pool ng koneksyon sa Envoy ay itinalaga sa isang thread ng manggagawa. Kaya, kahit na ang HTTP/2 connection pool ay gumagawa lamang ng isang koneksyon sa bawat upstream host sa isang pagkakataon, kung mayroong apat na worker thread, magkakaroon ng apat na HTTP/2 na koneksyon sa bawat upstream host sa isang steady state.
  • Ang dahilan kung bakit gumagana ang Envoy sa ganitong paraan ay na sa pamamagitan ng pagpapanatili ng lahat sa isang solong thread ng manggagawa, halos lahat ng code ay maaaring isulat nang walang pagharang at parang ito ay isang sinulid. Pinapadali ng disenyong ito ang pagsulat ng maraming code at mga kaliskis nang napakahusay sa halos walang limitasyong bilang ng mga thread ng manggagawa.
  • Gayunpaman, ang isa sa mga pangunahing takeaway ay na mula sa isang memory pool at pananaw sa kahusayan ng koneksyon, talagang napakahalagang i-configure ang --concurrency. Ang pagkakaroon ng mas maraming thread ng manggagawa kaysa sa kinakailangan ay mag-aaksaya ng memorya, lilikha ng mas maraming idle na koneksyon, at bawasan ang rate ng pagsasama-sama ng koneksyon. Sa Lyft, tumatakbo ang aming mga envoy sidecar container nang napakababa ng concurrency para halos tumugma ang performance sa mga serbisyong katabi nila. Pinapatakbo namin ang Envoy bilang isang edge proxy lamang sa maximum concurrency.

Ano ang ibig sabihin ng non-blocking?

Ang terminong "hindi pagharang" ay ginamit nang ilang beses sa ngayon kapag tinatalakay kung paano gumagana ang mga thread ng pangunahing at manggagawa. Ang lahat ng code ay nakasulat sa pag-aakalang walang na-block. Gayunpaman, hindi ito ganap na totoo (ano ang hindi ganap na totoo?).

Gumagamit ang Envoy ng ilang mahabang proseso ng lock:

  • Gaya ng tinalakay, kapag nagsusulat ng mga log ng pag-access, lahat ng mga thread ng manggagawa ay nakakakuha ng parehong lock bago mapunan ang in-memory log buffer. Ang oras ng paghawak ng lock ay dapat na napakababa, ngunit posible para sa lock na labanan sa mataas na concurrency at mataas na throughput.
  • Gumagamit ang Envoy ng napakakomplikadong sistema para pangasiwaan ang mga istatistika na lokal sa thread. Ito ang magiging paksa ng isang hiwalay na post. Gayunpaman, maikling babanggitin ko na bilang bahagi ng lokal na pagproseso ng mga istatistika ng thread, kung minsan ay kinakailangan na kumuha ng lock sa isang sentral na "stats store". Ang pag-lock na ito ay hindi dapat kailanganin.
  • Ang pangunahing thread ay pana-panahong kailangang makipag-ugnayan sa lahat ng mga thread ng manggagawa. Ginagawa ito sa pamamagitan ng "pag-publish" mula sa pangunahing thread patungo sa mga thread ng manggagawa, at kung minsan mula sa mga thread ng manggagawa pabalik sa pangunahing thread. Nangangailangan ng lock ang pagpapadala upang ang nai-publish na mensahe ay mai-queue para sa paghahatid sa ibang pagkakataon. Ang mga kandado na ito ay hindi dapat seryosong labanan, ngunit maaari pa rin silang mai-block sa teknikal.
  • Kapag nagsusulat si Envoy ng isang log sa stream ng error ng system (karaniwang error), nakakakuha ito ng lock sa buong proseso. Sa pangkalahatan, ang lokal na pag-log ng Envoy ay itinuturing na kahila-hilakbot mula sa isang pananaw sa pagganap, kaya hindi gaanong nabigyan ng pansin ang pagpapabuti nito.
  • Mayroong ilang iba pang mga random na lock, ngunit wala sa mga ito ang kritikal sa pagganap at hindi kailanman dapat hamunin.

Lokal na imbakan ng thread

Dahil sa paraan ng paghihiwalay ng Envoy sa mga responsibilidad ng pangunahing thread mula sa mga responsibilidad ng thread ng manggagawa, mayroong isang kinakailangan na ang kumplikadong pagproseso ay maaaring gawin sa pangunahing thread at pagkatapos ay ibigay sa bawat thread ng manggagawa sa isang lubos na kasabay na paraan. Inilalarawan ng seksyong ito ang Envoy Thread Local Storage (TLS) sa isang mataas na antas. Sa susunod na seksyon ay ilalarawan ko kung paano ito ginagamit upang pamahalaan ang isang kumpol.
[Translation] Envoy threading model

Tulad ng inilarawan na, pinangangasiwaan ng pangunahing thread ang halos lahat ng pagpapaandar ng pamamahala at kontrol ng eroplano sa proseso ng Envoy. Ang control plane ay medyo overloaded dito, ngunit kapag tiningnan mo ito sa loob ng proseso ng Envoy mismo at ihambing ito sa pagpapasa na ginagawa ng mga thread ng manggagawa, ito ay may katuturan. Ang pangkalahatang tuntunin ay ang pangunahing proseso ng thread ay gumagana, at pagkatapos ay kailangan nitong i-update ang bawat thread ng manggagawa ayon sa resulta ng gawaing iyon. sa kasong ito, ang thread ng manggagawa ay hindi kailangang kumuha ng lock sa bawat pag-access.

Ang sistema ng TLS (Thread local storage) ng Envoy ay gumagana tulad ng sumusunod:

  • Ang code na tumatakbo sa pangunahing thread ay maaaring maglaan ng TLS slot para sa buong proseso. Kahit na ito ay abstracted, sa pagsasanay ito ay isang index sa isang vector, na nagbibigay ng O(1) access.
  • Ang pangunahing thread ay maaaring mag-install ng arbitrary na data sa puwang nito. Kapag ito ay tapos na, ang data ay nai-publish sa bawat thread ng manggagawa bilang isang normal na kaganapan ng loop ng kaganapan.
  • Maaaring magbasa ang mga thread ng manggagawa mula sa kanilang TLS slot at makuha ang anumang thread-local na data na available doon.

Bagama't ito ay isang napaka-simple at hindi kapani-paniwalang makapangyarihang paradigm, ito ay halos kapareho sa konsepto ng pagharang ng RCU(Read-Copy-Update). Sa pangkalahatan, ang mga thread ng manggagawa ay hindi kailanman nakakakita ng anumang pagbabago sa data sa mga TLS slot habang tumatakbo ang trabaho. Ang pagbabago ay nangyayari lamang sa panahon ng pahinga sa pagitan ng mga kaganapan sa trabaho.

Ginagamit ito ng Envoy sa dalawang magkaibang paraan:

  • Sa pamamagitan ng pag-iimbak ng iba't ibang data sa bawat thread ng manggagawa, maaaring ma-access ang data nang walang anumang pagharang.
  • Sa pamamagitan ng pagpapanatili ng nakabahaging pointer sa pandaigdigang data sa read-only na mode sa bawat thread ng manggagawa. Kaya, ang bawat thread ng manggagawa ay may bilang ng data reference na hindi maaaring bawasan habang tumatakbo ang trabaho. Kapag huminahon na ang lahat ng manggagawa at nag-upload ng bagong nakabahaging data, masisira ang lumang data. Ito ay kapareho ng RCU.

Cluster update threading

Sa seksyong ito, ilalarawan ko kung paano ginagamit ang TLS (Thread local storage) para pamahalaan ang isang cluster. Kasama sa pamamahala ng cluster ang xDS API at/o pagpoproseso ng DNS, pati na rin ang pagsusuri sa kalusugan.
[Translation] Envoy threading model

Kasama sa pamamahala ng daloy ng cluster ang mga sumusunod na bahagi at hakbang:

  1. Ang Cluster Manager ay isang bahagi sa loob ng Envoy na namamahala sa lahat ng kilalang cluster upstream, ang Cluster Discovery Service (CDS) API, ang Secret Discovery Service (SDS) at Endpoint Discovery Service (EDS) API, DNS, at mga aktibong external na pagsusuri. pagsusuri sa kalusugan. Responsable ito sa paglikha ng isang "kalaunan pare-pareho" na view ng bawat upstream cluster, na kinabibilangan ng mga natuklasang host pati na rin ang katayuan sa kalusugan.
  2. Ang health checker ay nagsasagawa ng aktibong pagsusuri sa kalusugan at nag-uulat ng mga pagbabago sa katayuan ng kalusugan sa cluster manager.
  3. Ginagawa ang CDS (Cluster Discovery Service) / SDS (Secret Discovery Service) / EDS (Endpoint Discovery Service) / DNS para matukoy ang cluster membership. Ibinabalik ang pagbabago ng estado sa cluster manager.
  4. Ang bawat thread ng manggagawa ay patuloy na nagsasagawa ng loop ng kaganapan.
  5. Kapag natukoy ng cluster manager na nagbago ang estado para sa isang cluster, gagawa ito ng bagong read-only na snapshot ng estado ng cluster at ipapadala ito sa bawat thread ng manggagawa.
  6. Sa susunod na tahimik na panahon, ia-update ng thread ng manggagawa ang snapshot sa inilaan na TLS slot.
  7. Sa panahon ng isang kaganapan sa I/O na dapat na matukoy ang host na mag-load ng balanse, ang load balancer ay hihiling ng TLS (Thread local storage) slot upang makakuha ng impormasyon tungkol sa host. Hindi ito nangangailangan ng mga kandado. Tandaan din na ang TLS ay maaari ring mag-trigger ng mga kaganapan sa pag-update upang ang mga balanse ng pag-load at iba pang mga bahagi ay maaaring muling kalkulahin ang mga cache, istruktura ng data, atbp. Ito ay lampas sa saklaw ng post na ito, ngunit ginagamit sa iba't ibang lugar sa code.

Gamit ang pamamaraan sa itaas, maaaring iproseso ng Envoy ang bawat kahilingan nang walang anumang pagharang (maliban sa inilarawan dati). Bukod sa pagiging kumplikado ng TLS code mismo, karamihan sa code ay hindi kailangang maunawaan kung paano gumagana ang multithreading at maaaring isulat na single-threaded. Ginagawa nitong mas madaling isulat ang karamihan sa code bilang karagdagan sa mahusay na pagganap.

Iba pang mga subsystem na gumagamit ng TLS

Ang TLS (Thread local storage) at RCU (Read Copy Update) ay malawakang ginagamit sa Envoy.

Mga halimbawa ng paggamit:

  • Mekanismo para sa pagbabago ng pag-andar sa panahon ng pagpapatupad: Ang kasalukuyang listahan ng pinaganang functionality ay kinakalkula sa pangunahing thread. Ang bawat thread ng manggagawa ay bibigyan ng read-only na snapshot gamit ang RCU semantics.
  • Pinapalitan ang mga talahanayan ng ruta: Para sa mga talahanayan ng ruta na ibinigay ng RDS (Route Discovery Service), ang mga talahanayan ng ruta ay nilikha sa pangunahing thread. Ang read-only na snapshot ay kasunod na ibibigay sa bawat thread ng manggagawa gamit ang RCU (Read Copy Update) semantics. Ginagawa nitong atomically efficient ang pagbabago ng mga talahanayan ng ruta.
  • HTTP header caching: Sa lumalabas, ang pagkalkula ng HTTP header para sa bawat kahilingan (habang tumatakbo ~25K+ RPS bawat core) ay medyo mahal. Ang Envoy ay sentral na kino-compute ang header humigit-kumulang bawat kalahating segundo at ibinibigay ito sa bawat manggagawa sa pamamagitan ng TLS at RCU.

Mayroong iba pang mga kaso, ngunit ang mga nakaraang halimbawa ay dapat magbigay ng isang mahusay na pag-unawa sa kung para saan ginagamit ang TLS.

Mga kilalang pitfalls sa pagganap

Bagama't mahusay ang pagganap ng Envoy sa pangkalahatan, may ilang kapansin-pansing lugar na nangangailangan ng pansin kapag ginamit ito nang may napakataas na concurrency at throughput:

  • Gaya ng inilarawan sa artikulong ito, sa kasalukuyan ang lahat ng thread ng manggagawa ay nakakakuha ng lock kapag sumusulat sa access log memory buffer. Sa mataas na concurrency at mataas na throughput, kakailanganin mong i-batch ang mga access log para sa bawat thread ng manggagawa sa gastos ng out-of-order na paghahatid kapag sumulat sa huling file. Bilang kahalili, maaari kang lumikha ng isang hiwalay na log ng pag-access para sa bawat thread ng manggagawa.
  • Bagama't ang mga istatistika ay lubos na na-optimize, sa napakataas na concurrency at throughput ay malamang na magkakaroon ng atomic na pagtatalo sa mga indibidwal na istatistika. Ang solusyon sa problemang ito ay mga counter sa bawat thread ng manggagawa na may panaka-nakang pag-reset ng mga central counter. Tatalakayin ito sa susunod na post.
  • Ang kasalukuyang arkitektura ay hindi gagana nang maayos kung ang Envoy ay i-deploy sa isang senaryo kung saan kakaunti ang mga koneksyon na nangangailangan ng makabuluhang mapagkukunan sa pagproseso. Walang garantiya na ang mga koneksyon ay pantay na maipamahagi sa mga thread ng manggagawa. Malutas ito sa pamamagitan ng pagpapatupad ng pagbabalanse ng koneksyon ng manggagawa, na magbibigay-daan sa pagpapalitan ng mga koneksyon sa pagitan ng mga thread ng manggagawa.

Konklusyon

Ang threading model ng Envoy ay idinisenyo upang magbigay ng kadalian ng programming at napakalaking parallelism sa kapinsalaan ng potensyal na aksaya ng memorya at mga koneksyon kung hindi na-configure nang tama. Binibigyang-daan ito ng modelong ito na gumanap nang napakahusay sa napakataas na bilang ng thread at throughput.
Gaya ng maikling nabanggit ko sa Twitter, ang disenyo ay maaari ding tumakbo sa ibabaw ng isang buong user-mode networking stack tulad ng DPDK (Data Plane Development Kit), na maaaring magresulta sa mga conventional server na humahawak ng milyun-milyong kahilingan sa bawat segundo na may ganap na pagproseso ng L7. Ito ay magiging lubhang kawili-wili upang makita kung ano ang itatayo sa susunod na ilang taon.
Isang huling mabilis na komento: Maraming beses na akong tinanong kung bakit pinili namin ang C++ para sa Envoy. Ang dahilan ay nananatiling ito pa rin ang malawak na ginagamit na pang-industriyang grado na wika kung saan ang arkitektura na inilarawan sa post na ito ay maaaring itayo. Ang C++ ay tiyak na hindi angkop para sa lahat o kahit na maraming mga proyekto, ngunit para sa ilang mga kaso ng paggamit ito pa rin ang tanging tool upang magawa ang trabaho.

Mga link sa code

Mga link sa mga file na may mga interface at pagpapatupad ng header na tinalakay sa post na ito:

Pinagmulan: www.habr.com

Magdagdag ng komento