[Përkthim] Envoy model i rrjedhave (Envoy threading model)

Перевод статьи: Envoy threading model — https://blog.envoyproxy.io/envoy-threading-model-a8d44b922310

Данная статься показалась мне достаточно интересной, а так как Envoy чаще всего используется как часть «istio» или просто как «ingress controller» kubernetes, следовательно большинство людей не имеют с ним такого же прямого взаимодействия как например с типовыми установками Nginx или Haproxy. Однако если что-то ломается, было бы хорошо понимать как оно устроенно изнутри. Я постарался перевести как можно больше текста на русский в том числе и специальные слова, для тех кому больно на такое смотреть я оставил оригиналы в скобках. Добро пожаловать под кат.

Техническая документация низкого уровня по кодовой базе Envoy в настоящее время довольно скудная. Чтобы исправить это, я планирую сделать серию статей в блоге о различных подсистемах Envoy. Так как это первая статья, пожалуйста, дайте мне знать, что вы думаете и что вам могло быть интересно в следующих статьях.

Один из наиболее распространенных технических вопросов, которые я получаю о Envoy, это запрос на низкоуровневое описание используемой модели потоков (threading model). В этом посте я опишу как Envoy сопоставляет соединения с потоками, а также описание системы локального хранилища потоков (Thread Local Storage), которая используется внутри, чтобы сделать код более параллельным и высокопроизводительным.

Описание потоков (Threading overview)

[Përkthim] Envoy model i rrjedhave (Envoy threading model)

Envoy использует три различных типа потоков:

  • Основной (Main): Этот поток управляет запуском и завершением процесса, всей обработкой XDS (xDiscovery Service) API, включая DNS, проверку работоспособности (health checking), общее управление кластером и процессом работы сервиса (runtime), сбросом статистики, администрирование и общее управление процессами — Linux сигналы, горячий перезапуск (hot restart) и т. д. Все, что происходит в этом потоке, является асинхронным и «неблокирующим». В целом основной поток координирует все критические процессы функциональности, для выполнения которых не требуется большого количества ЦПУ. Это позволяет большую часть кода управления писать так, как если бы он был однопоточным.
  • Рабочий (Worker): По умолчанию Envoy создает рабочий поток(worker thread) для каждого аппаратного потока в системе, это можно контролировать с помощью опции --concurrency. Каждый рабочий поток запускает «неблокирующий» цикл событий (event loop), который отвечает за прослушивание (listening) каждого прослушивателя (listener), на момент написания статьи (29 июля 2017 г.) нет сегментирования (sharding) прослушивателя (listener), прием новых соединений, создание экземпляра стека фильтров для подключения и обработку всех операций ввода-вывода (IO) за время существования соединения. Опять же, это позволяет большую часть кода обработки соединений писать так, как если бы он был однопоточным.
  • Файловый (File flusher): Каждый файл, который пишет Envoy, в основном журналы доступа (access logs), в настоящее время имеет независимый блокирующий поток. Это связано с тем, что запись в файлы кэшированные файловой системой даже при использовании O_NONBLOCK иногда может блокироваться (вздох). Когда рабочим потокам необходимо записать в файл, данные фактически перемещаются в буфер в памяти, где они в конечном итоге сбрасываются через поток file flush. Это одна из областей кода, в которой технически все рабочие потоки (worker threads) могут блокировать (block) одну и ту же блокировку (lock), пытаясь заполнить буфер памяти.

Обработка соединений (Connection handling)

Как обсуждалось вкратце выше, все рабочие потоки прослушивают всех слушателей (listeners) без какого-либо сегментирования. Таким образом, ядро используется для грамотной отправки принятых сокетов в рабочие потоки. Современные ядра в целом очень хороши в этом, они используют такие функции, как повышение приоритета ввода-вывода (IO), чтобы попытаться заполнить поток работой, прежде чем начать использовать другие потоки, которые также прослушивают тот же сокет, а также не использовать циклическую блокировку (Spinlock) для обработки каждого запроса.
Как только соединение принято на рабочем потоке (worker thread), оно никогда не покидает этот поток (thread). Вся дальнейшая обработка соединения полностью обрабатывается в рабочем потоке (worker thread), включая любое поведение пересылки (forwarding behavior).

Это имеет несколько важных последствий:

  • Все пулы соединений в Envoy относятся к рабочему потоку. Таким образом, хотя пулы соединений HTTP/2 делают только одно соединение с каждым вышестоящим хостом за раз, если есть четыре рабочих потока, будет четыре соединения HTTP/2 на вышестоящий хост в устойчивом состоянии.
  • Arsyja pse Envoy funksionon në këtë mënyrë është se, duke mbajtur gjithçka në një rrjedhë pune, pothuajse e gjithë kodi mund të shkruhet pa bllokime dhe sikur të ishte një jetë e vetme. Ky dizajn e thjeshton shkrimin e një sasi të madhe kodi dhe është shumë i shkallëzueshëm për një numër pothuajse të pakufizuar rrjedhash pune.
  • Megjithatë, një nga përfundimet kryesore është se nga këndvështrimi i efikasitetit të pëlhurës së memories dhe lidhjeve, është në të vërtetë shumë e rëndësishme të konfigurohet parametri --concurrency. Të kesh më shumë rrjedha pune se sa nevojitet do të çojë në humbje të memories, krijimin e më shumë lidhjeve të papërdorura dhe uljen e shpejtësisë së arrijës në pëlhurën e lidhjeve. Në Lyft, kontenierët tanë të envoy sidecar punojnë me një paralelizëm shumë të ulët, kështu që performanca përputhet pothuajse me shërbimet pranë të cilave ndodhen. Ne e drejtojmë Envoy si një Proxy të Kufirit (edge) vetëm kur paralelizmi është maksimal.

Çfarë do të thotë mënyra pa bllokim (What non-blocking means)

Termi "pa bllokim" është përdorur disa herë në diskutimet mbi mënyrën si funksionojnë rrjedha kryesore dhe ato punuese. E gjithë kodi është shkruar me kushtin që asgjë kurrë nuk bllokohet. Megjithatë, kjo nuk është plotësisht e saktë (çfarë nuk është plotësisht e saktë?).

Envoy përdor disa bllokime të zgjatura procesi:

  • Siç është përmendur, gjatë shkrimit të regjistrave të aksesit, të gjitha rrjedhat pune marrin një bllokim të njëjtë para se të mbushin tamponin e regjistrit në memories. Koha e mbajtjes së bllokimit duhet të jetë shumë e ulët, por është e mundur që kjo bllokim të contestet në paralelizëm të lartë dhe kapacitet të lartë.
  • Envoy përdor një sistem shumë të ndërlikuar për përpunimin e statistikave, i cili është lokal për rrjedhën. Kjo do të jetë një temë e një postimi të veçantë. Megjithatë, do ta përmend shkurtimisht se si pjesë e përpunimit lokal të statistikave të rrjedhës, ndonjëherë është e nevojshme të merret një bllokim për "depon statistikash" për qëllime qendrore. Kjo bllokim nuk duhet kurrë të kërkohet.
  • Rrjedha kryesore herë pas here ka nevojë për koordinim me të gjitha rrjedhat punuese. Kjo bëhet duke "publikuar" nga rrjedha kryesore në rrjedhat punuese, dhe ndonjëherë edhe nga rrjedhat punuese mbrapsht në rrjedhën kryesore. Për të dërguar, nevojitet një bllokim që mesazhi i publikuar të mund të vendoset në radhë për shpërndarjen e mëvonshme. Këto bllokime nuk deberían asnjëherë të kenë konkurrencë të rëndësishme, por ato prapë mund të bllokohen teknikisht.
  • Kur Envoy shkruan regjistrin në rrjedhën standard të gabimeve (standard error), ai merr një bllokim të gjithë procesit. Për përgjithësi, regjistrimi lokal i Envoy konsiderohet i tmerrshëm nga këndvështrimi i performancës, kështu që nuk i kushtohet shumë vëmendje përmirësimit të tij.
  • Ka disa bllokime të tjera të rastit, por asnjë nga ato nuk është kritike për performancën dhe nuk duhet kurrë të contestet.

Depoja lokale e rrjedhës (Thread local storage)

Për shkak të mënyrës se si Envoy ndan detyrat e rrjedhës kryesore nga ato të rrjedhës punuese, ekziston një kërkesë që përpunimi i ndërlikuar të mund të kryhet në rrjedhën kryesore dhe pastaj të sigurohet çdo rrjedhë pune me një nivel të lartë paralelizmi. Në këtë seksion është përshkruar sistemi Thread Local Storage (TLS) të Envoy në një nivel të lartë. Në seksionin e ardhshëm do ta përshkruaj si përdoret për menaxhimin e klasterit.
[Përkthim] Envoy model i rrjedhave (Envoy threading model)

Siç është përshkruar tashmë, rrjedha kryesore përpunon pothuajse të gjitha funksionet e menaxhimit dhe funksionalitetin e planeve të menaxhimit (control plane) në procesin e Envoy. Plani i menaxhimit këtu është disi i ngarkuar, por nëse e shohim atë brenda vetë procesit të Envoy dhe e krahasojmë me përcjelljen që bëjnë rrjedhat punuese, kjo duket e arsyeshme. Si një rregull, procesi i rrjedhës kryesore kryen një punë të caktuar dhe pastaj i nevojitet të përditësojë çdo rrjedhë pune sipas rezultatit të kësaj pune, duke siguruar që rrjedha punuese nuk ka nevojë të vendosë një bllokim në çdo qasje.

Sistemi TLS (Thread local storage) të Envoy funksionon si më poshtë:

  • Kodi që ekzekutohet në rrjedhën kryesore mund të alokojë një slot TLS për të gjithë procesin. Megjithëse kjo është e abstraktuar, në praktikë është një indeks në një vektor, duke siguruar qasje O(1).
  • Rrjedha kryesore mund të vendosë të dhëna të rastësishme në slotin e saj. Kur kjo bëhet, të dhënat publikohen në çdo rrjedhë pune si një ngjarje e zakonshme e ciklit të ngjarjeve.
  • Rrjedhat punuese mund të lexojnë nga sloti i tyre TLS dhe të nxjerrin të dhëna të çdo lokalizimi të rrjedhës që janë të disponueshme atje.

Megjithëse kjo është një paradigëm shumë e thjeshtë dhe jashtëzakonisht e fuqishme, ajo është shumë e ngjashme me konceptin e bllokimit RCU (Read-Copy-Update). Në thelb, rrjedhat e punës kurrë nuk shohin ndonjë ndryshim të të dhënave në vendet TLS gjatë kryerjes së punëve. Ndryshimi ndodh vetëm në periudhën e qetësisë midis ngjarjeve të punës.

Envoy e përdor këtë në dy mënyra të ndryshme:

  • Duke ruajtur të dhëna të ndryshme në çdo rrjedh pune, qasje në këto të dhëna arrihet pa asnjë bllokim.
  • Duke ruajtur një tregues të përbashkët për të dhënat globale në modin 'vetëm për lexim' në çdo rrjedh pune. Në këtë mënyrë, çdo rrjedh pune ka një numër referencash për të dhënat që nuk mund të zvogëlohet gjatë kryerjes së punës. 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.

Rrjedha e përditësimit të klasterit (Cluster update threading)

Në këtë seksion, unë do të përshkruaj si TLS (Thread local storage) përdoret për menaxhimin e klasterit. Menaxhimi i klasterit përfshin trajtimin e API xDS dhe/ose DNS, si dhe kontrollin e shëndetit (health checking).
[Përkthim] Envoy model i rrjedhave (Envoy threading model)

Menaxhimi i rrjedhave të klasterit përfshin komponentët dhe fazat e mëposhtme:

  1. Menaxheri i klasterit është një komponent brenda Envoy që menaxhon të gjithë upstream-et e njohura të klasterit, API-in CDS (Cluster Discovery Service), API-të SDS (Secret Discovery Service) dhe EDS (Endpoint Discovery Service), DNS dhe kontrollet e jashtme aktive të shëndetit. Ai është përgjegjës për krijimin e një pamjeje 'përfundimisht të konsoliduar' (eventually consistent) për çdo upstream të klasterit, që përfshin hostet e zbuluar dhe statusin e shëndetit.
  2. Mjeti i kontrollit të shëndetit (health checker) kryen kontrolle aktive të shëndetit dhe raporton për ndryshimet në statusin e shëndetit të menaxherit të klasterit.
  3. CDS (Cluster Discovery Service) / SDS (Secret Discovery Service) / EDS (Endpoint Discovery Service) / DNS janë të zhvilluara për të përcaktuar përkatësinë në klaster. Ndryshimi në statusin kthehet te menaxheri i klasterit.
  4. Çdo rrjedh pune vazhdimisht kryen një cikël përpunimi ngjarjesh.
  5. Kur menaxheri i klasterit përcakton se statusi për klasterin ka ndryshuar, ai krijon një pamje të re të statusit të klasterit, të disponueshme vetëm për lexim, dhe e dërgon atë në çdo rrjedh pune.
  6. Gjatë periudhës së ardhshme të qetësisë, rrjedha e punës do të përditësojë pamjen në vendin e caktuar TLS.
  7. Gjatë një ngjarjeje input-output, e cila duhet të përcaktojë hostin për balansimin e ngarkesës, balancuesi i ngarkesës do të kërkojë vendin TLS (Thread local storage) për të marrë informacion mbi hostin. Për këtë nuk kërkohen bllokime. Vini re gjithashtu se TLS mund të nxisë ngjarje gjatë përditësimit, kështu që nën-sistemët për balansimin e ngarkesës dhe komponentët e tjerë mund të ripozojnë cache-t, strukturat e të dhënave, etj. Kjo kalon përtej këtij postimi, por përdoret në vende të ndryshme të kodit.

Duke përdorur procedurën e përshkruar më sipër, Envoy mund të trajtojë çdo kërkesë pa asnjë bllokim (përveç atyre të përmendura më parë). Përveç kompleksitetit të kodit të TLS, pjesa më e madhe e kodit nuk ka nevojë të kuptojë se si funksionon shumë-rrjedhësia, dhe ai mund të shkruhet në një mod do marrëveshje. Kjo lehtëson shkruan shumicën e kodeve në përveç performancës superiore.

Nën-sistemët e tjera që përdorin TLS (Other subsystems that make use of TLS)

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

Shembuj përdorimi:

  • Mekanizmi i ndërrimit të funksionalitetit gjatë ekzekutimit: Lista aktuale e funksionalitetit të përfshirë llogaritet në rrjedhën kryesore. Pastaj, çdo rrjedh pune i jepet një pamje vetëm për lexim duke përdorur semantikën RCU.
  • Zëvendësimi i tabelave të ruterit: për tabelat e ruterit të ofruara nga RDS (Route Discovery Service), tabelat e ruterit krijohen në rrjedhën kryesore. Një pamje vetëm për lexim më pas do të sigurohet çdo rrjedhe pune duke përdorur semantikën RCU (Read Copy Update). Kjo e bën ndërrimin e tabelave të ruterit efektivisht atomar.
  • Kashimi i headerave HTTP: Siç del, llogaritja e headerit HTTP për secilën kërkesë (në kryerjen e ~25K+ RPS për bërthamë) është mjaft e shtrenjtë. Envoy llogarit në mënyrë qendrore headerin përafërsisht çdo gjysmë sekonde dhe e siguron atë për çdo punëtor përmes TLS dhe RCU.

Ka edhe raste të tjera, por shembujt e mëparshëm duhet t'i japin një kuptim të mirë se për çfarë përdoret TLS.

Mundësitë e njohura të performancës (Known performance pitfalls)

Megjithëse në përgjithësi Envoy punon mjaft mirë, ka disa zona të njohura që kërkojnë vëmendje kur përdoret me paralelizëm dhe kapacitet të lartë:

  • Siç u përmend në këtë artikull, aktualisht të gjitha rrjedhët e punës bllokohen gjatë shkruarjes në memorjen e tamponit të regjistrave të aksesit. Me një paralelizëm të lartë dhe kapacitet të lartë, do të nevojitet paketimi i regjistrave të aksesit për secilën rrjedhë pune shkaku i dorëzimit të pa renditur gjatë shkruarjes në skedarin përfundimtar. Si alternativë, mund të krijoni një regjistër akses të veçantë për secilën rrjedhë pune.
  • Megjithëse statistikat janë optimizuar shumë, me një paralelizëm dhe kapacitet shumë të lartë, ka gjasa të ketë konkurrencë atomike mbi statistikat individuale. Zgjidhja për këtë problem është numëruesit për një rrjedhë pune me rivendosjen periodike të numëruesve qendrorë. Ky do të diskutohet në një postim të ardhshëm.
  • Arkitektura ekzistuese nuk do të funksionojë mirë nëse Envoy është i vendosur në një skenar në të cilin ka shumë pak lidhje që kërkojnë burime të konsiderueshme për përpunim. Nuk ka garanci që lidhjet do të shpërndahen në mënyrë të barabartë mes rrjedhave të punës. Kjo mund të zgjidhet duke zbatuar balancimin e lidhjeve të punës, ku do të mundësohet ndarja e lidhjeve mes rrjedhave të punës.

Përfundimi (Conclusion)

Modeli i rrjedhave të Envoy është projektuar për të siguruar thjeshtësi programimi dhe paralelizëm masiv, duke pasur parasysh përdorimin e mundshëm të tepruar të memorjes dhe lidhjeve nëse ato nuk konfigurohen siç duhet. Ky model i lejon të funksionojë shumë mirë me numra të lartë të rrjedhave dhe kapaciteti.
Siç e përmenda shkurtimisht në Twitter, dizajni gjithashtu mund të punojë mbi një grumbull të plotë rrjetesh në modalitetin e përdoruesit, siç është DPDK (Data Plane Development Kit), që mund të çojë në përpunimin e miliona kërkesave në sekondë nga serverët e zakonshëm me përpunimin e plotë L7. Do të jetë shumë interesante të shohim se çfarë do të ndërtohet në vitet në vijim.
Një koment i fundit i shpejtë: kam qenë pyetur disa herë pse zgjodhëm C++ për Envoy. Arsyeja është se ky është ende gjuha e vetme e përhapur në industrinë e nivelit të lartë të cilën mund të ndërtojmë arkitekturën e përshkruar në këtë post. C++ nuk është për të gjithë ose madje as për shumë projekte, por për raste të caktuara përdorimi, ky është ende mjeti i vetëm për ta bërë punën.

Lidhjet ndaj kodit (Links to code)

Lidhjet ndaj skedarëve me ndërfaqet dhe implementimin e titujve të diskutuar në këtë post:

Burimi: habr.com

Bleni hostim të besueshëm për faqe me mbrojtje nga DDoS, serverë VPS VDS 🔥 Bleni hostim të besueshëm për faqe me mbrojtje nga DDoS, serverë VPS VDS | ProHoster