Scroll to top

DevKucher;

Cinnamon: використання столітніх технологій для створення потужного механізму скидання навантаження

Картинка для Cinnamon: використання столітніх технологій для створення потужного механізму скидання навантаження

Це переклад статті Uber Cinnamon: Using Century Old Tech to Build a Mean Load Shedder

Вступ

Uber має тисячі мікросервісів, які обслуговують приблизно 130 млн щомісячних користувачів — пасажирів, клієнтів Uber Eats і партнерів-водіїв. Трафік від цих користувачів призводить до мільйонів запитів за секунду (тобто RPS), агрегованих між нашими сервісами, як безпосередньо з онлайн-трафіку (наприклад, користувач натискає кнопку в застосунку), так і опосередковано з офлайн-трафіку (наприклад, batch-завдання, що виконує агрегацію по поїздках). За такої складності неминуче виникають ситуації, коли один або кілька сервісів перенавантажуються через сплески трафіку (наприклад, надто агресивне batch-завдання) або через уповільнення (наприклад, база даних починає працювати повільно).

Для пом’якшення таких ситуацій критично важливо реалізувати graceful degradation — поступову деградацію, за якої ми відкидаємо запити з нижчим пріоритетом (наприклад, batch job) раніше, ніж будь-які запити з високим пріоритетом (наприклад, онлайн-запити клієнтів). Наша мета — забезпечити якісний користувацький досвід навіть тоді, коли окремі частини бекенду мають проблеми з навантаженням. З огляду на масштаб сервісної інфраструктури Uber, будь-яке рішення для graceful degradation має бути автоматичним і не вимагати конфігурації, оскільки навіть одна конфігураційна змінна на сервіс швидко перетвориться на значні інженерні витрати, враховуючи кількість сервісів та частоту застарівання таких параметрів.

Для вирішення цих двох проблем ми створили бібліотеку для відсікання навантаження (load shedder) — Cinnamon. Вона побудована на основі досвіду попереднього механізму відсікання навантаження — QALM, який забезпечував graceful degradation. Окрім цього, Cinnamon динамічно та безперервно регулює пропускну здатність сервісу (використовуючи модифікований алгоритм TCP-Vegas — відносно «молоду» ідею 1994 року) і, відповідно, динамічно налаштовує обсяг відсікання навантаження залежно від вхідних запитів (застосовуючи підхід PID-регулятора, що бере початок у XVII столітті). Порівняно з попередніми підходами, Cinnamon здатен обробляти значно більші перенавантаження, зберігаючи прийнятні затримки (P50 latency зростає на 50% при перенавантаженні 300%). Він є дуже ефективним — зазвичай додає лише 1 мікросекунду на запит. Ну і звісно, Cinnamon не потребує конфігурації та активно впроваджується в сервісах Uber.

Цей блог-пост — перший у серії з трьох матеріалів про Cinnamon. Серія буде цікавою для всіх — від читачів без попереднього досвіду з динамічними load shedder’ами до експертів у цій галузі. У цьому матеріалі ми надамо огляд Cinnamon і порівняємо його з QALM на основі CoDel. У наступних двох частинах ми детально розглянемо, як визначається рівень відмов за допомогою PID-регулятора та як автоматично регулюється пропускна здатність за допомогою модифікованого алгоритму TCP Vegas.


Передумови

Перш ніж перейти до архітектури та принципів роботи Cinnamon, коротко опишемо, як працює QALM. Якщо ви вже читали блог-пост про QALM, можете пропустити цей розділ.

QALM використовує алгоритм Controlled Delay (CoDel) для виявлення перевантаження і, у разі його настання, відкидає запити з нижчим пріоритетом (якщо такі є). По суті, CoDel (а отже й QALM) моніторить чергу вхідних запитів і час перебування кожного запиту в черзі. Якщо всі запити затримуються більш ніж, наприклад, на 5 мс протягом вікна 100 мс, CoDel починає відсікати запити. У випадку QALM це означає відхилення запитів з нижчим пріоритетом.

У будь-який момент часу QALM обмежує кількість запитів, що виконуються паралельно, що фактично визначає пропускну здатність (capacity). Оскільки QALM реалізовано на Go, це обмеження впроваджується шляхом поміщення всіх запитів у канал (channel), з якого набір фонових goroutine обробляє їх до встановленої межі. Для використання QALM необхідно налаштувати пріоритети викликів і максимальну кількість одночасно оброблюваних запитів.

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

Image

Рисунок 1: Діаграма двох сервісів, де Service 1 отримує онлайн-трафік від користувачів і від тестової системи. В обох випадках він викликає Service 2 для отримання інформації.

У наведеному прикладі, якщо ми налаштовуємо пріоритети в Service 2, можемо встановити для запитів від Service 1 високий пріоритет (оскільки він обслуговує онлайн-трафік мобільних застосунків). Однак через те, що Service 1 також обслуговує тестові системи, ці запити будуть помилково класифіковані в Service 2 як високопріоритетні.

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

Тому ми розпочали розробку Cinnamon, щоб усунути ці проблеми, а саме:

  1. Краще поширення пріоритетів
  2. Відсутність необхідності конфігурації
  3. Висока продуктивність

Архітектура

Cinnamon реалізований як RPC middleware, який легко інтегрується в Go-сервіси Uber. Кожен запит на краю системи (edge) маркується пріоритетом, який передається вниз по стеку викликів, щоб усі сервіси, що обробляють запит, знали його важливість для користувача. Це показано на діаграмі нижче:

Image

Рисунок 2: Діаграма інтеграції Cinnamon у сервісну mesh-архітектуру Uber.

На діаграмі запит із застосунку надходить до Edge-системи, яка пересилає його до Service 1. У процесі обробки Service 1 звертається до бази даних і до Service 2. Endpoint на edge має анотацію пріоритету, яка поширюється до всіх downstream-залежностей через Jaeger. Завдяки цьому всі сервіси в ланцюгу викликів знають критичність запиту для користувачів.

Завдяки анотації та поширенню пріоритетів Cinnamon може автономно працювати на рівні кожного екземпляра сервісу, зберігаючи глобальну узгодженість щодо того, які запити обслуговувати, а які відхиляти під час перевантаження.


Пріоритети

Cinnamon використовує пріоритет, прикріплений до запиту, або встановлює значення за замовчуванням залежно від сервісу-викликача. Пріоритет складається з двох компонентів: tier (рівень) і cohort (когорта). Рівень визначає важливість запиту. В Uber використовується 6 рівнів — від 0 до 5, де 0 — найвищий пріоритет, а 5 — найнижчий.

Когорти використовуються для додаткової сегментації запитів у межах одного рівня, щоб відсікати однакову підмножину запитів у різних сервісах. Підхід натхненний методом WeChat: запити поділяються на 128 когорт на основі користувача. Таким чином, якщо сервіс має відсікти 5% запитів рівня 1, це будуть одні й ті самі 5% користувачів. Для цього застосовується простий механізм шардингу з хешуванням і часовими параметрами, що дозволяє автоматично зміщувати користувачів між когортами, щоб ті самі користувачі не відсікалися завжди першими. Найвищий пріоритет когорти — 0, найнижчий — 127.

З 6 рівнями та 128 когортами кожен ми отримуємо 768 різних пріоритетів, що дозволяє реалізувати дуже ефективну пріоритетну чергу з окремими бакетами для кожного пріоритету.


Архітектура бібліотеки

Нижче показано життєвий цикл запиту в Cinnamon:

Image

Рисунок 3: Проходження запиту через Cinnamon перед передачею до бізнес-логіки.

Етапи обробки:

  1. Priority: перевірка або встановлення пріоритету.
  2. Rejector: визначення, чи слід відхилити запит.
  3. Priority Queue: додавання прийнятого запиту до пріоритетної черги.
  4. Scheduler: контроль максимальної кількості паралельно оброблюваних запитів (inflight limit).
  5. Timeout: якщо запит занадто довго перебуває в черзі — повертається помилка rate limiting (зазвичай таймаут черги становить 33% від загального таймауту запиту).

Cinnamon також має дві фонові goroutine: «PID controller» і «auto-tuner», які безперервно коригують поріг відхилення та inflight-ліміт.


Rejector

Компонент rejector:

a) визначає, чи перевантажений endpoint; b) якщо перевантажений — відсікає певний відсоток запитів для мінімізації черги.

Endpoint вважається перевантаженим, якщо черга не спорожнювалася, наприклад, протягом 10 секунд (підхід натхненний Facebook).

У разі перевантаження використовується PID-регулятор, який на основі співвідношення вхідного та вихідного потоку запитів визначає частку запитів для відсікання. PID швидко знаходить стабільний рівень і повільно зменшує частку відмов після спорожнення черги, запобігаючи осциляціям.


Scheduler

Прийняті запити надходять до пріоритетної черги, з якої scheduler передає їх до бізнес-логіки, дотримуючись inflight-ліміту.

Фонова goroutine auto-tuner моніторить затримки та використовує модифікований алгоритм TCP Vegas для регулювання inflight-ліміту:

  • зростання latency → зменшення ліміту;
  • стабільний latency або знижується → збільшення ліміту.

Експериментальні результати

Для перевірки Cinnamon ми провели серію експериментів і порівняли його з QALM.

Ми оцінювали:

  1. захист сервісу;
  2. захист основних потоків (core flows) через пріоритетне відсікання низькопріоритетних запитів.

Метрики: throughput, goodput, P50 та P99 latency.

Тестовий endpoint імітує реальне навантаження (виділення пам’яті, CPU-сортування, time.Sleep). Базовий latency ≈120 мс без навантаження. Сервіс має 2 інстанси по ~650 RPS кожен (разом ~1300 RPS).


Goodput

Image

Рисунок 4: Goodput без load shedder, з Cinnamon та QALM.

При 200–1000 RPS всі запити проходять. На 1500 RPS обидва механізми активуються. На 2000+ RPS Cinnamon утримує goodput близько до максимальної пропускної здатності. QALM на 3000 RPS знижує goodput до ~40% від capacity.


Затримки

Image

Рисунок 5: P50 latency для tier 1.

Image

Рисунок 6: P99 latency для tier 1.

При перенавантаженні QALM швидко наближається до таймауту 1с, тоді як Cinnamon утримує latency <500 мс (P50 ≈180 мс). PID-регулятор забезпечує стабільний рівень відмов без різких коливань.

Image

Рисунок 7: Порівняння стабільності throughput і latency при 4000 RPS.


Взаємодія PID та Auto-Tuner

Image

Рисунок 8: Взаємодія inflight-ліміту та rejector при 1500 і 2000 RPS.

Auto-tuner коригує inflight-ліміт, а rejector підтримує мінімальну довжину черги.


Підсумок

Cinnamon — це високоефективний load shedder, який застосовує класичні алгоритмічні підходи для динамічного визначення порогів відмов і оцінки пропускної здатності сервісів. Він усуває проблеми QALM (та інших CoDel-базованих рішень), оскільки:

  1. Швидко знаходить стабільний рівень відмов
  2. Автоматично регулює пропускну здатність
  3. Не потребує конфігурації
  4. Має мінімальні накладні витрати

Під час проєктування Cinnamon ми надихалися рішеннями та ідеями з різних джерел: Jaeger, WeChat, Facebook, Netflix та Amazon.