[Переклад] Envoy модель потоків (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)

[Переклад] Envoy модель потоків (Envoy threading model)

Envoy використовує три різні типи потоків:

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

Обробка з'єднань (Connection handling)

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

Це має кілька важливих наслідків:

  • Всі пули сполук Envoy відносяться до робочого потоку. Таким чином, хоча пули з'єднань HTTP/2 роблять тільки одне з'єднання з кожним хостом вищестоящим за раз, якщо є чотири робочих потоку, буде чотири з'єднання HTTP/2 на вищий хост у стійкому стані.
  • Причина, по якій Envoy працює таким чином, полягає в тому, що, зберігаючи все в одному робочому потоці, майже весь код може бути написаний без блокувань і як однопоточний. Цей дизайн спрощує написання великої кількості коду та неймовірно добре масштабується для майже необмеженої кількості робочих потоків.
  • Однак одним з основних висновків є те, що з точки зору ефективності пулу пам'яті та з'єднань насправді дуже важливо налаштувати параметр --concurrency. Наявність більшої кількості робочих потоків, ніж необхідно, призведе до втрати пам'яті, створення більшої кількості недіючих з'єднань та зниження швидкості потрапляння в пул з'єднань. У Lyft наші envoy sidecar контейнери працюють з дуже низьким паралелізмом, так що продуктивність приблизно відповідає службам, поряд з якими вони сидять. Ми запускаємо Envoy як прикордонний проксі-сервер (edge) тільки при максимальному паралелізмі (concurrency).

Що означає не блокуючий режим (What non-blocking means)

Термін «неблокуючий» досі використовувався кілька разів під час обговорення того, як працюють основний та робочий потоки. Весь код написано за умови, що ніщо ніколи не блокується. Однак, це не зовсім правильно (що не зовсім правильно?).

Envoy використовує кілька тривалих блокувань процесу:

  • Як мовилося раніше, під час запису журналів доступу все робочі потоки отримують однакову блокування перед заповненням буфера журналу пам'яті. Час утримання блокування має бути дуже низьким, але можливо, що це блокування оскаржуватиметься при високому паралелізмі та високій пропускній здатності.
  • Envoy використовує дуже складну систему обробки статистики, яка є локальної для потоку. Це буде тема окремої посади. Тим не менш, я коротко згадаю, що як частина локальної обробки статистики потоку іноді потрібно отримати блокування для центрального сховища статистики. Це блокування не повинно будь-коли вимагатися.
  • Основний потік періодично потребує координації з усіма робочими потоками. Це робиться шляхом публікації з основного потоку в робочі потоки, а іноді і з робочих потоків назад в основний потік. Для надсилання потрібно блокувати, щоб опубліковане повідомлення можна було помістити в чергу для подальшої доставки. Ці блокування ніколи не повинні зазнавати серйозного суперництва, але вони все одно можуть технічно блокуватися.
  • Коли Envoy пише журнал у системний потік помилок (standard error), він отримує блокування всього процесу. У цілому нині, локальне ведення журналу Envoy вважається жахливим з погляду продуктивності, тому його поліпшенню приділяється багато уваги.
  • Є кілька інших випадкових блокувань, але жодна з них не є критичною для продуктивності і ніколи не повинна заперечуватися.

Локальне сховище потоку (Thread local storage)

Через спосіб, яким Envoy відокремлює обов'язки основного потоку від обов'язків робочого потоку, існує вимога, що складна обробка може бути виконана в головному потоці, а потім надана кожному робочому потоку з високим ступенем паралелізму. У цьому розділі описано систему Envoy Thread Local Storage (TLS) на високому рівні. У наступному розділі я опишу, як він використовується для керування кластером.
[Переклад] Envoy модель потоків (Envoy threading model)

Як було описано, основний потік обробляє практично всі функції управління (management) і функціональність площини управління (control plane) у процесі Envoy. Площина управління тут трохи перевантажена, але якщо розглядати її в рамках самого процесу Envoy і порівнювати з пересиланням, яке виконують робочі потоки, це є доцільним. За загальним правилом процес основного потоку виконує деяку роботу, а потім йому необхідно оновлювати кожен робочий потік відповідно до результату цієї роботи. при цьому робочому потоку не потрібно встановлювати блокування при кожному доступі.

Система TLS (Thread local storage) Envoy працює так:

  • Код, що виконується в основному потоці, може виділити TLS слот для всього процесу. Хоча це абстраговано, на практиці це індекс у векторі, що забезпечує доступ O(1).
  • Основний потік може встановлювати довільні дані у свій слот. Коли це зроблено, дані публікуються у кожному робочому потоці як звичайна подія циклу подій.
  • Робочі потоки можуть читати зі свого слота TLS та витягувати будь-які локальні дані потоків, доступні там.

Хоча це дуже проста і надзвичайно потужна парадигма, яка дуже схожа на концепцію блокування RCU (Read-Copy-Update). По суті робочі потоки ніколи не бачать будь-яких змін даних у слотах TLS під час виконання роботи. Зміна відбувається лише у період спокою між робочими подіями.

Envoy використовує це двома різними способами:

  • Зберігаючи різні дані на кожному робочому потоці, доступ до цих даних здійснюється без блокування.
  • Зберігаючи загальний покажчик на глобальні дані у режимі «лише читання» кожному робочому потоці. Таким чином, кожен робочий потік має лічильник посилань на дані, які не можуть бути зменшені під час виконання роботи. Тільки коли всі працівники заспокояться та завантажать нові загальні дані, старі дані будуть знищені. Це ідентично RCU.

Потік оновлення кластера (Cluster update threading)

У цьому розділі я опишу, як TLS (Thread local storage) використовується управління кластером. Управління кластером включає обробку API xDS та/або DNS, а також перевірку працездатності (health checking).
[Переклад] Envoy модель потоків (Envoy threading model)

Управління потоками кластера включає наступні компоненти та етапи:

  1. Менеджер кластера - це компонент усередині Envoy, який управляє всіма відомими апстрімами (upstream) кластера, API-інтерфейсом CDS (Cluster Discovery Service), API-інтерфейсами SDS (Secret Discovery Service) та EDS (Endpoint Discovery Service), DNS та активними зовнішніми перевірками працездатності (health checking). Він відповідає за створення в кінцевому підсумку узгодженого (eventually consistent) подання кожного апстріму (upstream) кластера, який включає виявлені хости, а також стан працездатності (health status).
  2. Засіб перевірки працездатності (health checker) виконує активну перевірку працездатності та повідомляє про зміни стану працездатності диспетчеру кластера.
  3. CDS (Cluster Discovery Service) / SDS (Secret Discovery Service) / EDS (Endpoint Discovery Service) / DNS виконуються для визначення належності до кластера. Зміна стану повертається менеджерові кластера.
  4. Кожен робочий потік виконує цикл обробки подій.
  5. Коли менеджер кластера визначає, що стан кластера змінився, він створює новий знімок стану кластера, доступний тільки для читання, і відправляє його в кожен робочий потік.
  6. Протягом наступного періоду спокою робочий потік оновить знімок виділеному слоті TLS.
  7. Під час події введення-виводу, що має визначити хост для балансування навантаження, балансувальник навантаження буде запитувати слот TLS (Thread local storage) для отримання інформації про хост. Для цього не потрібне блокування. Зверніть увагу також, що TLS може також ініціювати події під час оновлення, тому підсистеми балансування навантаження та інші компоненти можуть перераховувати кеші, структури даних тощо. Це виходить за рамки цього посту, але використовується у різних місцях коду.

Використовуючи вищеописану процедуру, Envoy може обробляти кожен запит без блокування (крім описаних раніше). Крім складності самого коду TLS, більшій частині коду не потрібно розуміти, як працює багатопоточність, і він може бути написаний в однопоточному режимі. Це полегшує написання більшої частини коду на додаток до чудової продуктивності.

Інші підсистеми, що використовують TLS (Other subsystems that make use of TLS)

TLS (Thread local storage) та RCU (Read Copy Update) широко використовуються в Envoy.

Приклади використання:

  • Механізм зміни функціональності у процесі виконання: Поточний список увімкненого функціоналу обчислюється в основному потоці. Потім кожному робочому потоку надається знімок для читання з використанням семантики RCU.
  • Заміна таблиць маршрутів: для таблиць маршрутів, що надаються RDS (Route Discovery Service), таблиці маршрутів створюються в основному потоці. Знімок тільки для читання надалі буде наданий кожному робочому потоку з використанням семантики RCU (Read Copy Update). Це робить зміну таблиць маршрутів атомарно ефективним.
  • Кешування заголовків HTTP: Як з'ясовується, обчислення заголовка HTTP для кожного запиту (при виконанні ~25K+ RPS на ядро) є досить дорогим. Envoy централізовано обчислює заголовок приблизно кожні півсекунди та надає його кожному працівнику через TLS та RCU.

Є й інші випадки, але попередні приклади мають забезпечити хороше розуміння того, навіщо використовується TLS.

Відомі підводні камені продуктивності (Known performance pitfalls)

Хоча Envoy працює досить добре, є кілька відомих областей, які вимагають уваги, коли він використовується з дуже високим паралелізмом і пропускною здатністю:

  • Як уже описано в цій статті, в даний час всі робочі потоки отримують блокування під час запису в буфер пам'яті журналу доступу. При високому паралелізмі та високій пропускній здатності потрібно виконати пакетування журналів доступу для кожного робочого потоку за рахунок невпорядкованої доставки під час запису в кінцевий файл. Як альтернативу, можна створювати окремий журнал доступу кожного робочого потоку.
  • Хоча статистика дуже оптимізована, при дуже високому паралелізмі і пропускної спроможності, ймовірно, буде атомарна конкуренція на індивідуальній статистиці. Вирішення цієї проблеми - лічильники на один робочий потік з періодичним скиданням центральних лічильників. Це буде обговорюватися на наступному посту.
  • Існуюча архітектура не буде працювати добре, якщо Envoy розгорнуто в сценарії, в якому дуже мало з'єднань, що вимагають значних ресурсів для обробки. Немає гарантії, що зв'язки будуть рівномірно розподілені між робочими потоками. Це може бути вирішено шляхом реалізації балансування робочих з'єднань, коли буде реалізована можливість обміну з'єднаннями між робочими потоками.

Висновок (Conclusion)

Модель потоків Envoy розроблена для забезпечення простоти програмування та масового паралелізму за рахунок потенційно марнотратного використання пам'яті та з'єднань, якщо вони не налаштовані правильно. Ця модель дозволяє йому дуже добре працювати при дуже високій кількості потоків та пропускної спроможності.
Як я коротко згадав у Твіттері, дизайн також може працювати поверх повнофункціонального мережевого стека в режимі користувача, такого як DPDK (Data Plane Development Kit), що може призвести до того, що звичайні сервери будуть обробляти мільйони запитів за секунду при повній обробці L7. Буде дуже цікаво подивитися, що буде збудовано у найближчі кілька років.
Один останній швидкий коментар: мене багато разів питали, чому ми вибрали C++ для Envoy. Причина, як і раніше, полягає в тому, що це все ще єдина широко поширена мова промислового рівня, якою можна побудувати архітектуру, описану в цьому пості. C++ виразно не підходить для всіх або навіть для багатьох проектів, але для певних випадків використання це все ще єдиний інструмент для виконання роботи (to get the job done).

Посилання на код (Links to code)

Посилання на файли з інтерфейсами та реалізацією заголовків, що обговорюються в цьому пості:

Джерело: habr.com

Додати коментар або відгук