Материал подготовлен в рамках курса «Высоконагруженные системы: архитектура и масштабирование».
Привет, Хабр!
Меня зовут Андрей Бирюков. Я — независимый эксперт в области ИТ и ИБ, преподаю в учебных центрах и пишу статьи и книги.
На просторах сети существует множество серьезных систем, в которых нагрузка может многократно увеличиться буквально за доли секунды. Возьмем к примеру трансляции крупных спортивных событий — таких, где в одну секунду миллионы пользователей могут одновременно нажимать кнопку «Play».
В такие моменты становится понятно: автоскейлинг — это ложь.
Облачные провайдеры обещают нам эластичность при увеличении нагрузки. Но в реальности все получается не совсем так. К тому моменту, как ваш кластер запустит новые поды, база данных уже исчерпает пул соединений, а пользователи увидят ошибку 500.
Для решения этих проблем мы привыкли полагаться на автоскейлинг, но в реальности это реактивный механизм, который срабатывает после того, как нагрузка уже ударила.
И ваша задача заключается в том, чтобы спроектировать систему так, чтобы она смогла выжить в этот промежуток. И сделать это можно, не надеясь на магию облаков.
Почему автоскейлинг не спасает
Давайте представим следующую ситуацию: вы запускаете распродажу лимитированных кроссовок. В 12:00 на сайт заходят 200 тысяч человек. Ваш автоскейлер настроен на увеличение реплик при превышении порога 70% CPU.
Но, что происходит на самом деле:
12:00:00 — нагрузка взлетает. CPU достигает 90%.
12:00:15 — метрики доходят до CloudWatch или другого мониторинга. Срабатывает правило масштабирования.
12:00:30 — начинается запуск новых контейнеров или виртуальных машин.
12:00:45–59 — поды проходят инициализацию. Если это Java‑приложение, добавляется время на прогрев JVM.
12:01:00 — новые экземпляры готовы принимать трафик.
Получается, что в течение целой минуты (а это не так мало!) ваша система работает на пределе. А если у вас тяжёлые SQL‑запросы к PostgreSQL, пул соединений может исчерпаться уже через 5 секунд после начала пика.

В результате мы приходим к не очень утешительному выводу: автоскейлинг не решает проблему мгновенной перегрузки. Да, он решает проблему долгосрочного роста, но для пиковых нагрузок нужны другие механизмы.
Агрессивное отбрасывание нагрузки (Load Shedding)
Пожалуй, самый эффективный способ выжить в пик — это перестать пытаться обработать все запросы. Допустим, ваша система может обслуживать 100 тысяч запросов в секунду, но на нее приходит 120 тысяч. Если вы попытаетесь обслужить всех, база данных ляжет — и ответы не получит никто. Но если вы отбросите 20 тысяч «лишних» запросов — 100 тысяч пользователей получат сервис.
Здесь возникает следующий резонный вопрос: как понять, какие запросы отбрасывать?
Наиболее подходящим способом является классификация трафика по приоритету. Так, в Netflix, который обслуживает миллионы одновременных стримов, трафик делят на категории прямо на уровне шлюза:
Первый уровень — критичный. Логин, воспроизведение видео (в e‑commerce — оформление заказа, блокировка товара на складе). Эти запросы должны выполняться всегда.
Второй уровень — деградируемый. Поиск, рекомендации, профиль пользователя. Их можно отдавать из кеша — даже если данные немного устарели.
Третий уровень — необязательный. Персональные подборки, ленты соцсетей, виджеты «похожие товары». Эти запросы можно отбрасывать без сожаления — пользователь увидит чуть более пустую страницу, но видео запустится или заказ оформится.
Главное правило: если вы заранее не определите, что отключать в пик, система решит за вас — отключив всё.
Как это выглядит в коде
Итак, с теорией проблемы мы разобрались, теперь давайте посмотрим, как это реализуется на практике. Здесь нам на помощь приходят адаптивные лимиты параллелизма.
Например, вы измеряете время ответа базы данных, и как только оно превышает порог (например, 50 миллисекунд), система автоматически перестаёт вызывать необязательные сервисы.
Вот пример логики, близкой к той, что используют в Netflix:
def update_limit(self, rtt, inflight, timeout_observed): # Если время ответа превысило порог или был таймаут if timeout_observed or rtt > self.latency_threshold: # Уменьшаем лимит на 20% — мультипликативное снижение self.current_limit = math.floor(self.current_limit * 0.8) # Если всё хорошо и загрузка близка к лимиту elif inflight * 2 >= self.current_limit: # Увеличиваем лимит на 1 — аддитивное увеличение self.current_limit += 1 # Держим лимит в разумных границах self.current_limit = min(self.max_limit, max(self.min_limit, self.current_limit))
Алгоритм AIMD (Additive Increase, Multiplicative Decrease) — позаимствован из механизмов управления перегрузкой в TCP (тот самый TCP Window).

Система постоянно «прощупывает» свой предел: увеличивает лимит понемногу, пока латентность остаётся низкой, и резко сбрасывает его при первых признаках перегрузки. Таким образом, вы не настраиваете лимиты вручную — система находит их сама.
Адаптивные лимиты на практике
Все в том же Netflix раньше настраивали лимиты параллелизма вручную, На основе результатов нагрузочного тестирования. Но система постоянно модифицируется, добавляются новые фичи, меняется топология, срабатывает автоскейлинг. Естественно, в таком динамическом мире, статические лимиты быстро устаревают.
В компании внедрили адаптивные лимиты, которые основываются на законе Литтла из теории массового обслуживания:
Где L — это количество запросов в системе (параллелизм), λ — интенсивность поступления, W — время обработки.
Соответственно, время определяется следующим образом:

То есть, если время обработки растёт, это означает, что в системе образуется очередь. В результате адаптивный лимит сокращается, и новые запросы начинают отвергаться до того, как очередь разрастётся до критических размеров.
Реальные цифры
Посмотрим небольшой пример с реальными цифрами. В одном из экспериментов с базой данных CockroachDB сравнивали поведение системы с различными настройками и получили следующие значения:
Без ограничений: медианная латентность — 3.18 секунды, 99-й перцентиль — 24.8 секунды.
С контролем приёма (admission control): латентность упала до 0.19 секунды и 0.98 секунды соответственно.
С контролем приёма и лимитами CPU: латентность упала до 0.019 секунды и 0.037 секунды.
Как мы видим, разница — на порядки, и это без увеличения числа машин или других аппаратных ресурсов. Просто система перестала пытаться делать невозможное и начала отвергать лишние запросы.
Почему очередь — ваш враг
Самое простое, что можно сделать в подобной ситуации, это поставить запросы в очередь (привет Кафке).
Пусть они подождут, пока освободятся ресурсы. Но по факту это ошибка, потому что очередь — это замедленная смерть системы, и для того, чтобы это понять, давайте посмотрим, что происходит, когда очередь растёт.
Прежде всего, увеличивается время ответа, клиенты начинают отваливаться по таймауту. Как следствие, они выполняют повторные запросы, которые тоже попадают в очередь. Ну а сервер(ы) вынужден тратить свои ресурсы на обработку запросов, ответы на которые уже никто не ждёт. В итоге система перегружается ещё сильнее и падает.
При этом фиксированные таймауты только усугубляют проблему, так как в перегрузке они срабатывают постоянно, создавая лавину повторных запросов.
Поэтому правильная стратегия — отвергать запросы сразу, как только система начинает задыхаться. Пусть клиент получит ошибку и попробует позже, чем система упадёт полностью. При этом на многих веб‑ресурсах заглушка об ошибке содержит таймер, по истечении которого страница обновится автоматически.
Если пользователь попробует самостоятельно обновиться, таймер еще больше увеличится. В результате мы снижаем количество пользовательских запросов, разгружая наш ресурс.
Как внедрить это в своей системе
Теперь давайте рассмотрим подробно те шаги, которые вы можете предпринять для реализации данного метода.
Шаг 1. Определите приоритет
Для начала побеседуйте с бизнесом и ответьте на вопрос:
«Если у нас будет на 30% больше трафика, чем мы можем обработать, какие функции мы отключим первыми?»
Запишите это в виде политики.
Например:
«Рекомендации отключаются при превышении 70% лимита CPU, поиск деградирует до кеша при 85%, оформление заказа — всегда в приоритете».
Шаг 2. Внедрите адаптивные лимиты на уровне приложения
На этом шаге лучше всего воспользоваться готовыми инструментами, например реализациями библиотеки Netflix на Java, Python, Go и других языках. Их необходимо подключить к слою работы вашей системы с базой данных или внешними API.
Шаг 3. Установите жёсткие таймауты
Для каждого внешнего вызова установите таймаут в 1.2–1.3 раза от 99.9-го перцентиля латентности в нормальных условиях. Если вы не знаете этот показатель — начните его измерять с помощью нагрузочных тестов.
Шаг 4. Убедитесь, что вы умеете «плохо выглядеть»
Проводите game day, то есть искусственно создавайте перегрузку и проверяйте, как ведёт себя система. Только так вы узнаете, работает ли ваша стратегия отбрасывания нагрузки до того, как наступит реальный кризис.
Хотите проверить, насколько хорошо вы ориентируетесь в проектировании высоконагруженных систем? Пройдите бесплатный вступительный тест — он поможет оценить текущий уровень знаний и понять, какие темы стоит подтянуть.
Итог. Надейтесь на лучшее, готовьтесь к худшему
Подведем краткий итог сегодняшней статьи.
Автоскейлинг — полезный инструмент, но важно понимать, что он не заменяет устойчивость системы. Он только даёт больше ресурсов после того, как нагрузка уже ударила.
Поэтому, чтобы пережить пик, ваша система должна уметь отбрасывать неважные запросы, адаптируясь к текущей загрузке без ручной настройки, и при этом не создавать длинных очередей, которые убивают систему медленно и мучительно.
Если вы не определите заранее, как будете деградировать в пик — система сделает это за вас и, скорее всего, самым болезненным способом.

Когда система работает под высокой нагрузкой, проблемы часто начинаются с незаметных деталей: один долгий запрос удерживает блокировку, транзакции начинают ждать друг друга, а производительность падает вместе с ростом числа пользователей.
Чтобы находить такие узкие места и проектировать устойчивые решения, важно понимать, как устроены механизмы блокировок и высокой доступности PostgreSQL.
На открытых уроках разберём практические подходы к работе:
8 сентября в 20:00. «Борьба с блокировками в PostgreSQL: как достичь высокой параллельности при большой нагрузке». Записаться
23 сентября в 20:00. «Узнайте, как использовать Patroni для управления высокодоступными кластерами PostgreSQL». Записаться
Все бесплатные уроки сентября собрали в дайджесте.