К рабочему сервису почти всегда два требования: он должен выдерживать рост нагрузки (пользователей стало в десять раз больше — сайт не лёг) и не падать при сбоях (сломался один сервер — клиенты этого не заметили). В AWS обе задачи решаются одной связкой: несколько одинаковых копий сервиса стоят за балансировщиком, разбросаны по разным датацентрам и автоматически добавляются и убираются по нагрузке.

Звучит как много новых слов — разберём их по одному, с аналогиями и примерами. После этой статьи вы будете понимать, из чего собирается сервис, который и пики держит, и сбои переживает.

клиент балансировщик один адрес зона a: копии 1 и 2 ASG держит число зона b: копии 3 и 4 ASG держит число зона c: копии 5 и 6 ASG держит число копия не прошла проверку ASG гасит её поднимает новую

Клиент знает один адрес балансировщика, за ним копии, разнесённые по зонам, и Auto Scaling Group, которая держит их число по нагрузке. Внизу вторая половина работы: копию, не прошедшую проверку, группа заменяет, а не только выводит из ротации.

Обязательно

Вертикальное и горизонтальное масштабирование

Когда нагрузка растёт, расширяться можно двумя способами.

Вертикальное масштабирование — взять сервер помощнее: больше процессорных ядер, больше памяти. Аналогия: в кафе не справляется один повар — наняли повара поопытнее, который готовит вдвое быстрее. Просто, но есть три минуса. Первый — у любого сервера есть потолок: мощнее самого мощного варианта уже не взять. Второй — чтобы пересесть на машину побольше, сервис обычно надо перезапустить (а это пауза в работе). Третий, и главный, — сервер всё равно один. Сломался он — лёг весь сервис. Это называют единой точкой отказа (single point of failure): одно звено, поломка которого валит всю систему.

Горизонтальное масштабирование — добавить ещё серверов и распределить нагрузку между ними. Аналогия: вместо одного супер-повара поставили на кухню пятерых обычных, и они делят заказы. Сложнее в устройстве, зато масштабируется почти без предела (нужно больше — добавили ещё пару), и поломка одного сервера не роняет сервис — остальные продолжают работать.

AWS-путь — горизонтальный. Но у него есть условие: сервис должен быть stateless (без состояния). Это значит, что сервер не хранит важные данные у себя в памяти между запросами. Почему это важно: если копий пять и запросы пользователя случайно попадают то на одну, то на другую, ни одна из них не должна «помнить» что-то, чего нет у соседей. Поэтому состояние выносят наружу — в базу данных или кеш. Тогда копии становятся взаимозаменяемыми, и их число можно менять как угодно.

Auto Scaling Groups

Ночью копии сервиса простаивают, днём их не хватает, а угадать нужное число руками не выходит. Эту работу отдают Auto Scaling Group (ASG) — «менеджеру копий» вашего сервиса. Вы задаёте ему три числа:

  • минимум — сколько копий держать всегда (например, 2);
  • максимум — выше скольких не подниматься (например, 10), чтобы не разориться;
  • желаемое — сколько работает прямо сейчас.

Дальше ASG сам меняет желаемое число по нагрузке. Самый ходовой способ — target tracking (отслеживание цели): вы говорите «держи среднюю загрузку процессора около 50%», и ASG сам добавляет копии, когда нагрузка лезет вверх, и убирает, когда спадает. В конфигурации это одно число:

{ "TargetTrackingConfiguration": {
    "PredefinedMetricSpecification": { "PredefinedMetricType": "ASGAverageCPUUtilization" },
    "TargetValue": 50.0 } }

Цель можно задавать не только по процессору — например по ALBRequestCountPerTarget (среднее число запросов на одну копию). Кроме него есть два способа попроще: step scaling («нагрузка выше такой-то — добавь столько-то копий») и scheduled scaling («каждый будний день с девяти утра держи пять копий»). И есть более умный режим — predictive scaling (прогнозное масштабирование): он разбирает историю за последние две недели (чтобы начать, ему хватит суток данных), строит прогноз на двое суток вперёд и поднимает копии заранее, к ожидаемому пику (скажем, к утреннему наплыву), а не постфактум.

Второй, не менее важный, талант ASG — самовосстановление. Он постоянно проверяет состояние копий. Копия перестала отвечать (не прошла health check) — ASG её гасит и поднимает свежую на замену. Так нужное число живых копий поддерживается без вашего участия, даже ночью.

Тут есть тонкость, о которую спотыкается всякий, кто собрал схему по инструкции. Приложение, которое стартует минуту, успеет не ответить на первые проверки — и попадёт в круг «подняли, не ответило, убили, подняли снова». Чтобы этого не было, есть health check grace period: льготное время после запуска копии, в течение которого неудачная проверка ещё не приводит к замене. В консоли оно по умолчанию 300 секунд, а вот при создании через командную строку — ноль, то есть выключено.

Масштабирование не мгновенно

Самое важное, чего не видно из настроек: между «нагрузка выросла» и «новая копия обслуживает запросы» проходят минуты. Складывается это время из четырёх частей.

Метрика должна дойти. Обычная метрика загрузки собирается раз в пять минут; подробный режим — раз в минуту. То есть решение о масштабировании принимается с запозданием на одну-две точки, и это уже минута-две.

Копию надо запустить. Запуск виртуальной машины — десятки секунд до состояния «операционная система загрузилась». Дальше скачивание образа контейнера, если он большой, — ещё десятки секунд.

Приложение должно стартовать. Тяжёлый процесс с прогревом и загрузкой зависимостей поднимается 15–40 секунд, иногда дольше; лёгкий — за секунды.

Проверка работоспособности должна пройти. Балансировщик считает копию живой после нескольких успешных проверок подряд (по умолчанию нужно пять успешных с интервалом 30 секунд — то есть до двух с половиной минут, если интервалы не сокращать).

Итог для типичного сервиса — от трёх до семи минут от всплеска до готовой копии. Отсюда главный практический вывод: автомасштабирование не спасает от резкого всплеска. Оно спасает от роста нагрузки в течение дня, от ночного снижения и от медленного тренда, но пик, который наступает за тридцать секунд (рассылка, реклама, начало распродажи), придётся встречать уже поднятыми копиями.

Что с этим делают:

  • Запас на постоянно. Минимум ASG держат не «сколько нужно сейчас», а с запасом на время реакции: цель по загрузке ставят 50–60 %, а не 90 %.
  • Расписание вперёд. Если пик предсказуем (утро рабочего дня, начало акции), поднимают копии по расписанию заранее — это тот самый scheduled scaling.
  • Прогнозное масштабирование из раздела выше поднимает копии до пика по истории — но только для повторяющихся пиков.
  • Прогрев. Приложение, которое после старта первые секунды медленное (компиляция, кэши), отдают в ротацию не сразу: у целевой группы есть плавный ввод (slow start), а у ASG — прогрев экземпляра (instance warmup), который ещё и не даёт масштабированию считать непрогретые копии.
  • Быстрее стартовать. Заранее собранный образ со всем нужным вместо установки при запуске, заранее прогретый кэш классов или готовый снимок процесса, маленький образ контейнера — всё это сокращает самую длинную часть.

И проверка, которая ставит всё на место: включите нагрузку и посмотрите по графику, через сколько после её роста появилась новая копия. Это число и есть время реакции вашего сервиса — с ним и проектируют запас.

Балансировщики нагрузки: ALB и NLB

Копий теперь несколько — но клиент знает один адрес. Кто-то должен встречать каждый запрос и решать, какой копии его отдать. Это и есть балансировщик нагрузки (load balancer): единая входная дверь, за которой прячется пул копий. Он раскидывает запросы между ними поровну и — важная деталь — не шлёт трафик в нездоровые копии.

В AWS семейство балансировщиков называется ELB (Elastic Load Balancing). Для большинства задач выбирают один из двух:

  • ALB (Application Load Balancer) работает на уровне 7 — уровне приложения (HTTP/HTTPS). Он понимает, что внутри запроса: умеет направлять /api/* на одни копии, а /images/* на другие, разбирать имя домена, расшифровывать HTTPS (терминировать TLS). Для веб-сайтов и API это основной выбор.
  • NLB (Network Load Balancer) работает на уровне 4 — транспортном (TCP/UDP). Он не вникает в содержимое, просто очень быстро прокидывает соединения. Берёт огромные нагрузки с минимальной задержкой. Ещё одно его отличие: в каждой зоне у NLB постоянный IP-адрес, и к нему можно привязать свой Elastic IP — удобно, когда партнёр пускает вас к себе только по списку адресов. У ALB адреса меняются, и работать с ним можно только по имени. Нужен NLB для не-HTTP-протоколов и экстремального трафика.

Ещё одна деталь, на которой легко обжечься: балансировщик сначала делит трафик между зонами и только потом — между копиями внутри зоны. Разровнять нагрузку сразу по всем копиям умеет режим cross-zone load balancing: у ALB он включён всегда, у NLB по умолчанию выключен. Если копий в зонах поровну, разницы не видно. А если в одной зоне две копии, а в другой восемь, то при выключенном режиме первые две получат вчетверо больше запросов каждая — и лягут первыми.

Есть и более узкие типы — Gateway Load Balancer (для пропуска трафика через сетевые экраны и системы защиты) и устаревший Classic Load Balancer (новые проекты на нём не строят). Для старта достаточно запомнить пару ALB / NLB: HTTP — ALB, всё прочее и предельные нагрузки — NLB.

По схеме балансировщик ставят в публичные подсети — туда, куда есть доступ из интернета, — а сами копии сервиса прячут в приватные. Снаружи виден только балансировщик; серверы наружу не торчат, и достучаться до них напрямую нельзя.

Плавный вывод копии из работы

Добавление копий — половина темы. Вторая половина — убрать копию так, чтобы пользователь не заметил, и она нужна чаще: она работает при каждом выкате, при каждом сокращении по нагрузке и при замене нездоровой копии.

Задержка снятия с регистрации (deregistration delay). Когда копию убирают из целевой группы, балансировщик перестаёт отправлять на неё новые запросы, но уже начатые продолжает обслуживать — в течение заданного времени, по умолчанию 300 секунд. Копия при этом ещё жива. Это и есть штатный дренаж: значение подбирают по самому долгому запросу (обычно достаточно 30–60 секунд, а 300 только растягивает выкат).

Сигнал приложению. После снятия с регистрации операционная система отправляет процессу просьбу завершиться (SIGTERM), и приложение обязано её обработать: перестать брать новое, дождаться текущего, закрыть соединения. В большинстве фреймворков это одна настройка мягкой остановки; разбор целиком — в статье про корректную остановку (Java, Go, Node, Python).

Хуки жизненного цикла (lifecycle hooks). Если перед удалением копии нужно что-то сделать — выгрузить журналы, снять метрики, дождаться завершения фоновой задачи, — ASG умеет приостановить удаление и подождать сигнала от вас. Копия переходит в состояние Terminating:Wait, ваш скрипт или функция делает работу и говорит «продолжай». Тот же механизм есть и на запуске (Pending:Wait) — для прогрева или регистрации во внешних системах.

Порядок, который получается в итоге: ASG решил убрать копию → хук на удаление (если есть) → снятие с регистрации в целевой группе с задержкой → SIGTERM приложению → завершение → выключение машины. Если из этой цепочки выпадает любое звено, пользователи видят обрывы — и чаще всего выпадает последнее: приложение не обрабатывает сигнал и умирает вместе с текущими запросами.

Прилипшие сессии: почему это тупик

Первое, что придумывает человек, у которого приложение хранит состояние в памяти: «пусть пользователь всегда попадает на ту же копию». Балансировщик это умеет — включается прилипание (stickiness), и он выдаёт пользователю cookie, по которому потом узнаёт нужную копию.

Работает. И вот чем это заканчивается.

Копия исчезает (выкат, масштабирование вниз, сбой) — вместе с ней исчезает состояние всех прилипших к ней пользователей: корзины, шаги оформления, авторизованные сессии. Не «часть запросов не удалась», а «эти люди начали заново».

Нагрузка перестаёт распределяться ровно: добавили копии, а старые пользователи остались на прежних, и новая копия стоит пустой до следующего наплыва.

Выкат становится заметным: замена каждой копии — это потеря сессий её пользователей, то есть «обновление всегда по ночам».

Отсюда вывод: прилипание — это обход симптома, а лечить надо причину. Оно оправдано ровно в двух случаях: как временная мера, пока состояние выносят наружу, и для соединений, которые по природе привязаны к копии (WebSocket), где оно всё равно работает.

Несколько зон доступности

Горизонтальное масштабирование спасает от поломки одного сервера. Но что, если откажет целый датацентр — пожар, авария электросети? Если все ваши копии стояли там, не поможет и ASG.

Здесь в игру входят зоны доступности (Availability Zone, AZ). AZ — это отдельный, физически изолированный датацентр AWS. В одном регионе (например, во Франкфурте) таких зон обычно три и больше, они стоят в разных местах и не зависят друг от друга. Если раскидать копии ASG по нескольким зонам за одним балансировщиком, отказ целой зоны перестаёт быть катастрофой: балансировщик просто перестаёт слать туда трафик, а копии в живых зонах его подхватывают. Это и называют multi-AZ — развёртывание в нескольких зонах.

Health checks: кто решает, что копия живая

Связывает всю конструкцию механизм health checks (проверок здоровья). И балансировщик, и ASG регулярно стучатся в каждую копию: «ты живой, готов принимать запросы?». Не ответила как надо — балансировщик перестаёт слать ей трафик, а ASG поднимает замену. Тонкость, на которой спотыкаются новички: проверка должна быть осмысленной. Ответ «порт открыт» ещё не значит, что сервис готов работать — он мог запуститься, но ещё не подключиться к базе. Поэтому делают отдельную ручку готовности (readiness), которая отвечает «ок» только когда сервис реально может обслуживать запросы. Ровно та же идея используется в Kubernetes с его liveness- и readiness-пробами.

Две разные проверки работоспособности

У ASG и у балансировщика свои проверки, и путаница между ними даёт узнаваемый инцидент: «копия помечена нездоровой, трафик на неё не идёт, но её никто не заменяет».

Проверка балансировщика (ELB health check) — это ваш эндпоинт: балансировщик обращается к нему и решает, отправлять ли трафик. Не прошла — копию убирают из ротации.

Проверка ASG по состоянию машины (EC2 status check) — это проверка самой виртуальной машины на уровне гипервизора: жива ли машина, доступна ли сеть. Приложение внутри может быть насмерть зависшим — с точки зрения этой проверки всё в порядке.

А теперь главное: по умолчанию ASG смотрит только на вторую проверку. То есть зависшее приложение будет числиться исправным, и замены не произойдёт никогда. Лечится одной настройкой — тип проверки у группы меняют на ELB (или ELB вместе с EC2):

aws autoscaling update-auto-scaling-group --auto-scaling-group-name orders \
  --health-check-type ELB --health-check-grace-period 120

Теперь неудачная проверка балансировщика приводит к замене копии, а не только к выводу из ротации. Вместе с этим обязательно проверяют льготный период (health-check-grace-period): он должен покрывать время старта приложения, иначе получится круг «подняли — не успело ответить — убили».

Для ECS и EKS то же разделение существует в своём виде: проверка целевой группы решает про трафик, а проверка контейнера или пробы Kubernetes — про перезапуск. Вывод одинаковый: убедитесь, что кто-то заменяет копию, а не только выводит её из ротации.

Где это применяется

Эта связка — копии за балансировщиком, разнесённые по зонам, под управлением Auto Scaling — лежит в основе почти любого боевого сервиса в AWS: интернет-магазинов, API мобильных приложений, корпоративных порталов. Её собирают руками в консоли при первом знакомстве, а в реальных проектах описывают кодом — через Terraform или CloudFormation, чтобы вся конструкция воспроизводилась и проверялась как обычный код.

Типичные ошибки тех, кто только начинает:

  • Сервис не stateless. Сессии и загруженные файлы хранят в памяти копии — и при втором запросе на другую копию пользователя «выкидывает». Лечится выносом состояния в базу/кеш и в объектное хранилище.
  • Все копии в одной зоне. Multi-AZ забыли включить — отказ одного датацентра валит весь сервис, хотя копий было несколько.
  • Бессмысленный health check. Проверяют «порт открыт» вместо реальной готовности — балансировщик шлёт трафик в копию, которая ещё не подключилась к базе, и пользователи ловят ошибки.
  • Максимум ASG задран до небес. При всплеске нагрузки (или ошибке в коде, бесконечном цикле) копии плодятся десятками — и приходит большой счёт.
Дополнительно: при первом чтении можно пропустить

Глубже: куда выносить состояниерасширенное

«Вынести состояние наружу» — правильный, но слишком общий совет. Разложим по видам.

Сессии пользователей. Два рабочих ответа. Первый — общий кэш (Redis как сервис): копии читают и пишут сессию туда, копия стала безразличной. Второй, обычно лучший, — никакой серверной сессии вовсе: состояние входа едет в подписанном токене у клиента, а сервер только проверяет подпись. Тогда нет ни общего хранилища, ни его отказа; разбор — в статье про схемы аутентификации.

Загруженные файлы. В объектное хранилище, а не на диск копии и не в базу. Файл на локальном диске исчезнет вместе с копией и не будет виден остальным; файл в базе превращает базу в файловый сервер и раздувает резервные копии. В хранилище объектов приложение кладёт файл и хранит у себя только ссылку.

Кэш вычислений. Локальный кэш в памяти — это нормально и полезно, если он не обязателен: копия без прогретого кэша работает медленнее, но правильно. Всё, что обязано быть одинаковым у всех копий (счётчики, блокировки, ограничение частоты), уходит в общий кэш.

Фоновые задачи и расписание. Задача по расписанию, запущенная на всех копиях одновременно, выполняется столько раз, сколько копий. Ответ — либо блокировка на общем хранилище (ShedLock и аналоги), либо вынесение расписания наружу: планировщик облака запускает одну задачу или отправляет сообщение в очередь.

Долгие операции. Не в памяти копии, а в таблице заданий или очереди — тогда потеря копии означает задержку, а не потерю работы. Это то же, о чём говорит статья про дренаж HTTP (Java, Go, Node, Python).

Проверка, что состояние действительно вынесено, простая и неприятная: погасите одну копию прямо во время работы и посмотрите, что стало с пользователями. Если кто-то заметил — состояние ещё внутри.

Глубже: квоты: потолок не только в бюджетерасширенное

Максимум у ASG — это ваше решение, но выше него есть потолок, который ставит сам аккаунт. Про него узнают в самый неудобный момент: нагрузка выросла, группа пытается поднять копии, и в её событиях появляется отказ на квоте.

Что чаще всего упирается:

  • Число ядер запущенных машин — квота считается не в машинах, а в vCPU по семействам типов, на регион. Значения по умолчанию у нового аккаунта скромные, и «поднять двадцать машин по восемь ядер» в них не влезает.
  • Публичные адреса (Elastic IP) — по умолчанию пять на регион. Схема с адресом на каждую машину упирается в это сразу.
  • Целевые группы и правила балансировщика, число задач ECS, число функций — у каждого сервиса свои пределы.
  • Адреса внутри подсети. Это не квота, а арифметика: подсеть /24 — это 256 адресов минус пять зарезервированных, и на кластер Kubernetes с сетевым плагином, который выдаёт адрес каждому поду, этого хватает быстрее, чем кажется.

Что с этим делают заранее, а не в инциденте: смотрят действующие значения в консоли квот (Service Quotas), запрашивают повышение до того, как оно понадобится (заявка обрабатывается от минут до суток), ставят оповещение на приближение к пределу, и планируют диапазоны подсетей с запасом на рост — переразметить сеть под работающей нагрузкой намного дороже, чем взять /20 сразу.

Коротко

  • Расти вширь, а не вверх: несколько копий за балансировщиком, и это работает только для сервиса без состояния в памяти.
  • Auto Scaling Group держит число копий между минимумом и максимумом по цели (target tracking по загрузке или по запросам на копию) и заменяет копии, не прошедшие проверку.
  • Между всплеском и готовой копией проходит от трёх до семи минут: метрика, запуск, старт приложения, проверки; от резкого пика спасают запас по минимуму, расписание и прогрев, а не автомасштабирование.
  • ALB разбирает HTTP и маршрутизирует по пути и домену, NLB прокидывает TCP с минимальной задержкой; балансировщик стоит в публичной подсети, копии в приватных, cross-zone у NLB включают руками.
  • Копию убирают через задержку снятия с регистрации, SIGTERM приложению и хуки жизненного цикла: выпадет любое звено, и пользователи получат обрывы.
  • Прилипшие сессии это обход симптома: состояние выносят в общий кэш или подписанный токен, файлы в объектное хранилище, расписание под общую блокировку, долгие операции в очередь.
  • Копии разносят по зонам доступности, а проверка здоровья проверяет готовность, а не открытый порт.
  • У ASG и балансировщика разные проверки: по умолчанию группа смотрит только на состояние машины, и зависшее приложение никто не заменит, пока тип проверки не сменили на ELB с льготным периодом на старт.
  • Квоты аккаунта на ядра, публичные адреса и адреса в подсети ставят потолок выше максимума ASG: их смотрят и поднимают заранее.

Что почитать дальше