Считаем операции, а не пользователей
Десять тысяч пользователей в базе не подсказывают, сколько нужно серверов. Большинство может заходить раз в месяц. А один клиент с массовым экспортом способен положить сервис. Для оценки нагрузки нужны запросы в секунду, работа на один запрос, размеры данных и характер пиков.
Разделяем быстрые чтения, записи, тяжёлые вычисления и фоновые джобы. У каждого класса свой бюджет задержки и лимит параллельной работы. Если смешать всё в одну среднюю цифру, дешёвые чтения спрячут медленные отчёты, а нагрузочный тест даст слишком оптимистичный результат.
Есть полезная прикидка: в стабильном потоке 200 RPS при среднем времени запроса 0,2 секунды означают около 40 запросов в системе одновременно. При двух секундах это уже около 400. Речь о средних величинах в устойчивом режиме. Эта формула не вычисляет p99 и не означает, что нам автоматически нужны 400 соединений с БД.
Для модели берём конкретный час работы продукта. Допустим, клиенты открывают список заказов, иногда сохраняют изменение и редко скачивают большой отчёт. Считаем частоту каждой операции отдельно. Отчёт может занимать один процент запросов, но большую часть CPU или дискового чтения. Поэтому доля запросов и доля затрат не совпадают.
Пики тоже бывают разными. Одновременный вход после рассылки отличается от непрерывного роста в течение дня. В первом случае важны холодный кэш и быстрый набор параллельных запросов, во втором устойчивая пропускная способность. Для каждого сценария нужен тест, похожий на его форму, а не только одна максимальная цифра RPS.
Фиксируем бюджет задержки по пути. Если продукт обещает ответ за секунду, нельзя каждому из пяти последовательных вызовов отдать по секунде. Часть времени уйдёт на сеть, сериализацию и ожидание. Такой бюджет помогает понять, какая зависимость уже съедает обещание, даже если каждый сервис отдельно считает себя быстрым.
Разбираем размер работы. Запрос списка на десять объектов и экспорт за год могут попасть в один эндпоинт, но требуют разных ресурсов. Лимит параллельности без ограничения размера входа даст ложное ощущение контроля. Для тяжёлых операций задаём границы выборки, время выполнения и правила фоновой обработки.
Наконец, учитываем рост данных. Тест на пустой базе не описывает работу через год. Индексы, большие аккаунты и распределение популярных объектов меняют стоимость запроса. В модели записываем объём и перекосы, которые использовали. Тогда результаты можно сравнить позже, а не спорить, почему тот же RPS внезапно перестал помещаться.
Как найти узкое место сервиса
Запрос может считать 30 мс, а затем 900 мс ждать свободное соединение с БД. CPU будет свободен, хотя пользователю медленно. Новые инстансы добавят желающих занять пул, но база от этого быстрее не станет. Чтобы увидеть такое место, разделяем выполнение и ожидание: очередь, пул соединений, блокировки, вызов апстрима.
На тесте постепенно поднимаем нагрузку и смотрим, где throughput перестаёт расти. Если завершённых запросов больше не становится, а незавершённых всё больше, мы уже копим долг. Длинная очередь некоторое время скрывает отказ, зато увеличивает latency, расход памяти и работу для клиентов, которые давно закрыли страницу.
Рабочий лимит выбираем ниже точки, где начинает разваливаться время ответа. Запас нужен для обычных скачков и выпадения части ресурсов. Универсальное правило "держать CPU ниже 70%" здесь мало помогает: предел зависит от характера нагрузки и того, где запросы ждут.
Самый полезный первый график часто связывает входящий поток, успешные завершения и число операций в работе. По нему видно, когда сервис перестаёт догонять вход. Если смотреть только ошибки, начало перегрузки останется незаметным: клиенты ещё ждут, а ресурсы уже заняты будущими таймаутами.
Трейс одного медленного запроса подскажет место ожидания, но не обязательно объяснит всю нагрузку. Проверяем, насколько этот путь типичен и сколько запросов задерживается так же. Иначе можно потратить день на редкий хвост, пропустив массовое ожидание пула. Трейсы полезно сверять с агрегированными метриками.
Время в пуле отделяем от времени SQL. Если запрос к базе занимает 15 мс, а соединение ожидается 500 мс, оптимизация самого SQL не гарантирует нужного эффекта. Причиной может быть слишком много параллельной работы, соединение, которое не возвращают, или транзакция с внешним вызовом.
При эксперименте меняем одну переменную. Например, ограничиваем параллельные отчёты, оставляя ресурсы прежними, и повторяем ту же нагрузку. Если интерактивные запросы ускорились, получили связь между конкуренцией и задержкой. Это намного полезнее одновременной замены сервера, пула и нескольких параметров без ясного вывода.
После исправления снова измеряем всю цепочку. Узкое место обычно перемещается: раньше тормозил CPU, теперь БД или сетевой лимит апстрима. Это ожидаемо. Ошибка начинается, когда первый удачный результат превращается в постоянное правило "теперь просто добавляем инстансы" без проверки нового предела.
Что каждый инстанс добавляет под капотом
Допустим, процесс держит пул до 20 соединений. Четыре инстанса могут открыть 80 соединений, двадцать уже 400. Если БД выдерживает меньше, автоскейлинг приложения упрётся в её лимит. Бюджет считаем на весь сервис, оставляя место другим приложениям, джобам и административным подключениям.
Ещё один стоппер для горизонтального скейлинга: локальное состояние. Загруженный файл остаётся на диске одного инстанса. Сессия в памяти теряется после переключения. Джоба по расписанию может стартовать в каждом процессе. До подключения новых реплик нужно решить, где хранится общее состояние и кто владеет выполнением фоновой работы.
Пул не нужно автоматически увеличивать вместе с количеством запросов. Больше соединений иногда означает больше конкурирующих операций внутри базы и худшее время ответа. Ищем диапазон, в котором БД завершает полезную работу, а приложение не создаёт лишнее давление. Для разных классов работы могут понадобиться разные лимиты.
Для файлов выбираем общее хранилище или явно ограниченный локальный сценарий. Для сессий определяем, как любой инстанс проверит пользователя. Для периодических задач нужен способ избежать случайного параллельного выполнения или сделать повторы безопасными. Привязка к одному инстансу может быть временным решением, но его отказ тогда становится отдельным сценарием.
Балансировщик распределяет запросы, но не обязательно работу. Один клиент может отправлять дорогие запросы, другие дешёвые. Некоторые соединения живут долго, некоторые инстансы медленнее прогреваются. Проверяем фактическое распределение задержек и загрузки, чтобы средний график не скрывал один перегруженный процесс.
Выключение инстанса тоже часть скейлинга. Сначала прекращаем отдавать ему новую работу, затем даём текущим операциям завершиться в установленный срок. Для задач, которые не успели закончиться, нужны правила возврата в очередь или сверки результата. Иначе уменьшение мощности или обычный деплой будут регулярно терять работу.
Считаем стоимость нового инстанса целиком. Он создаёт соединения, собирает метрики, хранит кэш и может повторно читать одни данные при прогреве. На старте несколько новых процессов иногда дают всплеск, который перегружает зависимость. Плавный допуск к трафику и общий бюджет помогают не превратить расширение в дополнительную аварию.
Не даём перегрузке разойтись по системе
Ограничиваем количество одновременных операций, длину очереди и время ожидания. Когда сервис уже перегружен, быстрый отказ части запросов сохраняет ресурс для остальных. Можно временно не строить тяжёлые отчёты, чтобы продолжать оформлять заказы. Приоритеты должны отражать важность операций для продукта.
Ретраи без общего бюджета легко усиливают аварию. Если на трёх уровнях каждый вызов делает до трёх попыток, один запрос может превратиться в 27 обращений к последней зависимости. Поэтому выбираем, кто отвечает за ретрай, ограничиваем попытки и используем backoff с jitter, чтобы клиенты не повторяли запросы одновременно.
Таймаут означает, что мы не дождались ответа. Операция при этом могла завершиться. Для повторяемой записи нужен стабильный ключ операции и защита от дубликатов, связанная с изменением данных. Множество ключей в памяти одного процесса не спасёт после рестарта. Важно согласованно сохранить и бизнес-результат, и возможность вернуть его при повторе.
Лимиты выбираем в месте, где ещё дешево отказаться. Если очередь уже заняла память и каждый запрос открыл транзакцию, поздний отказ не вернёт всё потраченное. Проверка размера входа, ограничение конкурентной работы и admission control должны стоять до самой дорогой части пути.
Backpressure означает, что следующий участок сообщает о нехватке ресурса, а предыдущий снижает давление. Если производитель продолжает писать быстрее обработки, даже надёжная очередь лишь откладывает проблему. Для неё нужен максимальный возраст задачи и решение, что делать с устаревшей работой.
Ретрай имеет смысл, когда повтор способен изменить результат. Повторять неверный запрос с теми же данными бесполезно. Повторять временный сетевой сбой иногда полезно, если операция допускает повтор и остаётся время. Бюджет ограничиваем общей длительностью пользовательского запроса, а не отдельным таймаутом каждой попытки.
Случайный разброс задержки нужен именно против синхронного возвращения клиентов. Если всем сказать ждать одну секунду, через секунду они снова придут вместе. Но jitter не создаёт дополнительную мощность. При длительной перегрузке всё равно требуется ограничить вход, уменьшить работу или добавить ресурс в правильном месте.
Отмена тоже заслуживает внимания. Клиент закрыл страницу, но сервер продолжает строить отчёт и вызывает апстримы. Где возможно, передаём отмену дальше и освобождаем ресурс. Для операции, которая уже меняет данные, отмена клиентского ожидания не должна оставлять неизвестное бизнес-состояние. Её завершение и получение результата разбираем отдельно.
Автоскейлинг не мгновенный
Инстанс надо запустить, прогреть и допустить к трафику. Короткий всплеск может закончиться раньше. Для известного события, например старта продаж, мощность лучше подготовить заранее. Для неожиданного пика нужны лимиты и понятный ответ при перегрузке, иначе сервис упадёт до появления новых ресурсов.
Сигнал скейлинга выбираем под работу. Воркеры очереди могут ждать БД и почти не тратить CPU. Для них полезнее скорость поступления и завершения задач, а также возраст самой старой. Тысяча лёгких задач и тысяча минутных отчётов требуют разной мощности. Но воркеров нельзя добавлять бесконечно: ниже по цепочке тоже есть лимиты.
Для автоскейлинга измеряем задержку реакции целиком: сбор метрики, решение контроллера, запуск процесса, прогрев и готовность принимать запросы. Короткое время старта контейнера не означает короткое время появления полезной мощности. Сервис с тяжёлым прогревом может потребовать постоянный запас.
На очереди оцениваем не только backlog, но и срок завершения. Если вход закончился, можно прикинуть время разбора как число задач, делённое на текущую скорость завершения. При продолжающемся входе важна разница скоростей. Для неодинаковых задач считаем классы отдельно, иначе маленькие задачи дадут ложный прогноз тяжёлой партии.
Рост и уменьшение мощности требуют разной осторожности. Быстро добавить ресурс бывает полезно, быстро убрать его после короткого спада может вызвать новый пик ожидания. Порог, задержка и минимальное число процессов должны учитывать форму нагрузки, стоимость прогрева и допустимое время восстановления.
Проверяем верхнюю границу заранее. Квоты, количество адресов, лимит соединений и мощность БД ограничат расширение раньше желаемого числа процессов. Если контроллер упёрся в предел, команда должна увидеть это как отдельное состояние. Сам факт включённого автоскейлинга не даёт гарантии, что запас доступен.
Полезно тестировать отказ сигнала. Метрика может пропасть, задержаться или начать считать другое после релиза. Определяем, что контроллер делает в этом случае и как ограничивается ошибочное масштабирование. Автоматизация снижает ручную работу только тогда, когда её поведение при плохих данных тоже понятно.
Проверяем нагрузку вместе с отказом
На тесте воспроизводим размеры данных, смесь операций и поведение клиентов после ошибки. Затем убираем инстанс или замедляем апстрим. Если система держит пик только в идеальных условиях, такой capacity plan не оставляет запаса на отказ.
После изменения сравниваем успешные операции в секунду, хвост latency, стоимость и восстановление. Фиксируем условия теста и найденную границу. Следующий шаг может оказаться оптимизацией запроса или настройкой пула, а не ещё одним инстансом. Результат скейлинга виден в полезной работе сервиса, а не в количестве контейнеров.
Нагрузочный тест должен отправлять запросы с нужным темпом, даже когда ответы замедляются. Если каждый виртуальный клиент ждёт завершения перед следующим запросом, сервис сам снизит вход за счёт ожидания. Можно получить красивый отчёт именно потому, что генератор перестал воспроизводить настоящий пик.
Проверяем пределы генератора и сети отдельно. Если нагрузочная машина исчерпала CPU, измеренный потолок относится к ней. Если тест использует один аккаунт и один набор ключей, он может создать нехарактерную конкуренцию или слишком тёплый кэш. Условия должны быть воспроизводимыми и объяснимыми.
Сценарий отказа задаём конкретно: один инстанс недоступен, апстрим отвечает в пять раз медленнее, соединение иногда обрывается. Затем смотрим, какие операции сохранились и сколько времени занял возврат. Формулировка "сервис выдерживает сбои" без списка таких условий не имеет проверяемого смысла.
Стоимость считаем на успешную полезную операцию. Два раза больше ресурсов ради пяти процентов throughput могут быть допустимы для короткого пика, но не для постоянной работы. В расчёт добавляем зависимости и операционное сопровождение. Так скейлинг остаётся решением задачи, а не соревнованием по числу серверов.
Результат теста превращаем в рабочие ограничения: безопасный диапазон нагрузки, нужный запас, условия автоскейлинга и действия при выходе за предел. Проверяем их после существенного изменения данных или пути запроса. Измеренная мощность имеет дату и условия; она не становится свойством системы навсегда.
Разбираем пик оформления заказов целиком
Предположим, перед акцией сервис держит обычную нагрузку, а на пике хвост ответа растёт. Трейсы показывают короткий SQL и долгое ожидание пула. База уже занята параллельными отчётами. Здесь добавление инстансов без нового бюджета соединений увеличит конкуренцию. Первый эксперимент ограничивает отчёты и сравнивает интерактивный путь на той же нагрузке.
Если оформление ускорилось, разделяем лимиты важного пути и фоновой работы. Для входа задаём контролируемый отказ, для очереди максимальный возраст, для ретраев общий бюджет. Проверяем, что клиент не создаёт второй заказ после потерянного ответа. Часть ресурса сохраняем на отказ инстанса, а известный пик встречаем заранее прогретой мощностью.
На следующем тесте воспроизводим нужный темп независимо от замедления ответов, используем крупные аккаунты и холодный старт. Затем замедляем апстрим и убираем процесс. Смотрим полезные завершения, хвост latency и время возврата. Если отказы стали быстрыми, но успешных заказов меньше, это не улучшение результата.
Полученную границу переносим в эксплуатацию вместе с условиями: набор операций, данные, доступные зависимости и запас. Автоскейлинг работает только до общего лимита, его достижение видно команде. Следующий рост оцениваем по новому узкому месту. Так план масштабирования становится последовательностью проверяемых изменений, а не обещанием бесконечного расширения контейнеров.