Два способи прийняти більше навантаження
Вітаємо
Коли сервіс починає не витримувати навантаження, оператор стикається з вибором. Зробити наявну машину більшою (більше CPU, більше RAM, швидші диски). Або додати більше машин, кожна з яких виконує ту саму роботу.
Перший шлях називається вертикальним масштабуванням (scale up). Другий — горизонтальним масштабуванням (scale out).
Цей урок пояснює, чому майже кожна сучасна веб-архітектура обирає горизонтальне масштабування та яка властивість навантаження робить цей вибір можливим. Відповідь ховається в одному слові: стан.
Наприкінці ви зрозумієте:
- Криві вартості вертикального та горизонтального масштабування та де кожен із них доречний
- Що означають на практиці терміни «з станом» (stateful) та «без стану» (stateless), і чому один із них легко масштабується за низькою вартістю
- Математичні розрахунки для визначення розміру флоту реплік за очікуваного та пікового навантаження
- Правило запасу потужності (headroom), яке запобігає колапсу шару за точкою «коліна черги» (queueing knee)
- Де має зберігатися стан (він ніколи не зникає) і як вивести його зі шарів, які потребують масштабування
Чому горизонтальне масштабування перемагає після певного порогу
Вертикальне масштабування: одна більша «коробка»
Переваги: простота. Змін у коді не потрібно. Узгодження не потрібне. Той самий процес тепер має більше CPU.
Недоліки: стеля. Найбільша комерційно доступна віртуальна машина має скінченну кількість RAM та ядер. Вище за це, жодні гроші не куплять більше ресурсів. Вартість зростає надлінійно після точки оптимуму пропозиції постачальника. Збій цієї однієї машини виводить із ладу весь сервіс.
Горизонтальне масштабування: багато менших машин
Переваги: немає стелі (до вашої готовності платити за машини та координувати їх). Потужність лінійно зростає з кількістю реплік, передбачувано. Збій однієї репліки знімає 1/N потужності, а не 100%.
Недоліки: вимагає, щоб навантаження це підтримувало. Деякі навантаження (одна велика база даних, станова ігрова серверна частина, що утримує активні сесії) чинять опір горизонтальному масштабуванню. Координація та розподіл навантаження стають операційними питаннями.
Точка перетину: будь-який продуктивний сервіс, який має вижити після збою будь-якої окремої машини, повинен працювати щонайменше на двох машинах. Щойно ви приймаєте дві, ви вже обрали горизонтальне масштабування. Звідси питання не «чи варто?», а «наскільки дешево ми можемо додати наступну репліку?».
Ключовий фактор: навантаження, яке не зберігає стан за запитом на самій машині. Тоді будь-яка репліка може відповісти на будь-який запит, а додавання репліки збільшує потужність без координації.
Становий та безстановий на практиці
Стан ніколи не зникає, він лише переміщується
Компонент зі станом: зберігає інформацію, втрата якої змінить поведінку системи. Наприклад, база даних, що зберігає облікові записи користувачів. Кеш, що зберігає сесійні токени. Робочий процес, що прив'язує тривале потокове з'єднання до конкретного користувача.
Безстановий компонент: не зберігає жодної інформації, втрата якої мала б значення. Наприклад, веб-шар, який читає запит, виконує запит до бази даних та записує відповідь. Кожен запит є самодостатнім; шар нічого не запам'ятовує між запитами.
Ключовий висновок: стан ніколи не зникає з системи. Він переміщується в шар, призначений для його зберігання (база даних, кластер Redis, сховище об'єктів). Тоді шари, що обробляють трафік, можуть бути безстановими, а безстанові шари масштабуються горизонтально, оскільки будь-яка репліка може відповісти на будь-який запит.
Практичний тест: якщо ви випадково завершите роботу одного процесу в цьому шарі та перезапустите його, чи відчує якийсь користувач хибну відповідь або втрату сесії? Якщо так, то компонент має стан. Якщо ні, то стану немає.
Приклади
- Веб-процес Python, який читає запити, виконує запити до Postgres та повертає JSON: безстановий. Стан зберігається в Postgres.
- Веб-процес Python, який зберігає кошики покупок користувачів у локальній пам'яті: зі станом. Завершення роботи процесу призведе до втрати кошиків.
- Сервер WebSocket, який утримує відкриті з'єднання з користувачами чату: статичний у сенсі з'єднань. Завершення процесу розриває з'єднання; клієнти повинні перепідключитися. Часто такі системи все ще масштабуються горизонтально за умови ретельного підходу (липкі сесії, консистентне хешування).
- Кеш Redis перед базою Postgres: статичний щодо вмісту кешу, але це прийнятно, якщо промахи кешу є допустимими. Збій репліки означає промах кешу, а не втрату даних.
Проєктування для горизонтального масштабування = виведення стану з шару, який потребує масштабування.
Аудит підозрілого шару
Команда запускає API рекомендацій на 6 серверах віртуальних машин (VM) за зворотним проксі. Додаток: читає ID користувача з запиту, отримують недавню активність користувача з Postgres, запускає алгоритм оцінювання, повертає список рекомендованих елементів. Дві нестандартні поведінки:
- Додаток зберігає кеш «недавнієї активності користувача» у пам'яті процесу, який заповнюється при першому запиті для користувача та повторно використовується при наступних запитах.
- Додаток використовує липкі сесії: коли користувач потрапляє на VM #3, усі його наступні запити надходять на VM #3 (проксі налаштовано на липкий роутинг за допомогою cookie).
Формула реплік
Найпростіша формула пропускної здатності
Щойно шар стає безстановим, його розмірування стає арифметичним. Вам потрібно достатня кількість реплік, щоб навантаження в стані рівноваги надходило та відходило з однаковою швидкістю, з запасом для пікових навантажень.
Формула:
репліки = ⌈ (пікове_навантаження × коефіцієнт_піку) / потужність_однієї_репліки ⌉ + запас
Де:
- пікове_навантаження: максимальне стійке число запитів за секунду, яке ви очікуєте в нормальному режимі роботи
- коефіцієнт_піку: множник, що покриває короткочасні сплески вище пікового значення (зазвичай 1,5x–2x для передбачуваного трафіку, 3x або більше для вірусного / непередбачуваного)
- потужність_однієї_репліки: кількість запитів за секунду, які одна репліка обробляє з прийнятною затримкою та використанням ресурсів (зазвичай вимірюється при 70% навантаженні CPU, а не при насиченні)
- запас: додаткові репліки, щоб несправність кількох реплік не призвела до збою шару (зазвичай 1–2 репліки для малих флотів, 10–20% для великих)
Приклад розрахунку: бекенд обробляє 100 запитів/с при завантаженні CPU 70% на один репліку. Пікове навантаження становить 600 запитів/с. Очікуються періодичні стрибки навантаження в 2 рази. Необхідно забезпечити стійкість до виходу з ладу 2 реплік.
replicas = ⌈ (600 × 2) / 100 ⌉ + 2 = 12 + 2 = 14 реплік
Правило 80%
Пропускна здатність на один репліку — це не точка насичення. Вимірюйте пропускну здатність при завантаженні CPU 70–80%, ніколи не на 100%.
Після досягнення приблизно 80% використання криві чергування різко зростають: черга, яка оброблялася за 10 мс при завантаженні 60%, обробляється за 80 мс при завантаженні 90%. Спочатку ламається не пропускна здатність, а затримка. (У супутньому уроці geometry_of_stateless_horizontal_scaling ця крива виводиться математично.)
Автоскейлінг проти статичного провізіонування
Статичний підхід: провізіонуйте ресурси під пікове навантаження з запасом на стрибки та приймайте витрати на роботу з низьким використанням поза піковими часами.
Автоскейлінг: контролер додає та прибирає репліки на основі спостережуваного використання, цільової затримки або глибини черги.
Попередження щодо автоскейлінгу: час холодного запуску має значення. Якщо новому репліку потрібно 2 хвилини для завантаження, автоскейлінг не зможе відреагувати на 30-секундний пік навантаження. Дозрілий автоскейлінг підтримує теплу пул попередньо провізіонованих реплік трохи нижче порогу масштабування вгору.
Розрахуйте розмір флоту для нового сервісу
Ваша команда планує запустити API метаданих відео. Бенчмарки показують, що один реплік обробляє 250 зап/с при 70% завантаженні CPU та p99 затримці 50 мс. Маркетинг прогнозує пікове навантаження 4 000 зап/с у години пік. Заплановане промо-заходження може спричинити короткочасний стрибок до 3x від пікового значення. Ви хочете, щоб сервіс витримував 3 одночасні збої репліків, не перевищуючи 80% завантаження на решті.
Холодний старт, повільне згасання та інші реальні межі
Реальні флоти мають реальні межі
Формула припускає, що репліки з’являються миттєво, одразу приймають трафік та одразу знімають трафік. Жоден із цих фактів не виконується в продакшені.
Холодний старт: новій репліці потрібно завантажити ОС, запустити процес, завантажити конфігурацію, прогрівити кеші та пройти перевірки здоров’я. Це може зайняти від 5 секунд (перезапуск контейнера) до 5 хвилин (повне завантаження VM + завантаження образу). Автомасштабування не може реагувати на сплески, коротші за цю затримку.
Повільне згасання: репліці, яку прибирають із пулу, потрібен час, щоб завершити поточні запити перед термінуванням. Інакше користувачі бачать обрізані відповіді. Зворотні проксі-сервери підтримують згасання (перестати приймати нові запити, завершити активні), але це займає від секунд до хвилин.
Теплий пул: виробничі флоти утримують пул попередньо провізіонуваних, але простоячих реплік, готових прийняти трафік за сигналом. Це обмін невеликих постійних витрат на швидку реакцію на пікові навантаження.
Дренаж з'єднань проти миттєвого вбивання: важливе значення має коректне завершення роботи. SIGTERM, який ініціює дренаж, займає більше часу, ніж SIGKILL, але не порушує запити користувачів.
Вікно перевірки здоров'я: репліка, яка щойно запустилася, може пройти свою першу перевірку здоров'я до того, як пул з'єднань із базою даних стане «теплим»; тоді проксі надсилає реальний трафік, і перші десяток запитів виконуються повільно. Налаштуйте перевірки здоров'я так, щоб вони тестували реальний шлях обробки, а не лише життєздатність процесу.
Повзання липкості: навіть номінально безстанові шари з часом набувають липкості (кеші CDN, кеші резолверів DNS, пули з'єднань). Будьте підозріли до «ідентичних реплік», які поведінка яких все ж відрізняється.
Теплий пул чи реактивне автоскалування?
Ваш API метаданих відео (той самий, що й у попередньому запитанні, розрахований на 51 репліку для стабільного піку + сплеску) зазнає 30-секундного сплеску до 5 разів нормального навантаження щоразу, коли завантажують нове вірусне відео. Автоскалування в даний час потребує 90 секунд, щоб додати нову репліку з нуля (завантаження образу + розігрів). Під час 90-секундного проміжку затримки різко зростають, і деякі запити завершуються з таймаутом.
Проектуйте безстанове рівне за обмежень
Синтез
Ви дізналися, чому горизонтальне масштабування перемагає після досягнення невеликого порогу, що означає стан на практиці, як розмістити флот за очікуваного та пікового навантаження, а також де горизонтальне масштабування дає збій на межі.
Застосуйте всі чотири.
Спроєктуйте бекенд-шар для feed.example.com, API соціальної стрічки. Обмеження: продуктивність на репліку 200 запитів/с при 70% навантаженні CPU; очікуване пікове навантаження 1500 запитів/с; коефіцієнт стрибка 2.5x (рідкісні вірусні новини); виживання при одночасному виході з ладу 2 реплік; час холодного запуску 60 секунд; сплески можуть тривати 45 секунд; бюджет дозволяє певний запас ідleness, але не постійне провізіонування в 2.5 рази.
Куди веде цей курс далі
Куди веде цей курс
Зараз у вас є робоча ментальна модель безстанового шару: чому він масштабується, як його розміряти, що ламається на його межах та куди має переміститися стан, коли ви виводите його за межі шару, який потребує зростання.
Наступний урок у цьому курсі (cs_distsys_ingress_egress_separation) розглядає більш тонку проблему: навіть ідеально розмірений безстановий шар може зламатися несподіваним чином, якщо вхідний та вихідний трафік використовують один і той самий мережевий шлях. Класичний приклад стосується проксі, який намагається з’єднатися сам із собою; рішення полягає у розділенні одного шару на два з різними відповідальностями.
Супутній урок: geometry_of_stateless_horizontal_scaling виводить криву черги, застосування закону Літтла до флоту реплік та геометричний сенс «коліна» при 80% завантаженості.
Молодець. Далі.