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

клиент Application Gateway публичный IP зона 1: копии набор масштабирования зона 2: копии набор масштабирования зона 3: копии набор масштабирования PostgreSQL зоно-избыточный резерв переключение за минуты

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

Обязательно

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

Есть два способа дать сервису больше мощности.

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

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

Масштабируемые наборы виртуальных машин

Днём нагрузка растёт, ночью падает, а копии всё время одни и те же: либо простаивают, либо не справляются. Руками поднимать и гасить их — не вариант. В Azure для этого есть Virtual Machine Scale Sets (VMSS): вы описываете шаблон одной машины, а набор сам поддерживает нужное число одинаковых копий. Ключевые свойства:

  • Автомасштабирование — нагрузка перевалила порог по процессору: набор добавил копии; спала — убрал лишние;
  • Автовосстановление — копию, переставшую проходить проверку здоровья, набор может пересоздать. Само это не включается: в наборе заводят политику автоматического восстановления (automatic instance repairs) и указывают, откуда брать признак здоровья — расширение Application Health Extension или проверку балансировщика, назначенную в модели набора. Без этого не пересоздаётся ничего;
  • Распределение по зонам — копии раскидывают по нескольким зонам доступности; выпадет зона — сервис продолжит работать на оставшихся.

Аналог группы автомасштабирования (Auto Scaling Group) в AWS. Для контейнерных нагрузок ту же роль играют AKS и Container Apps с их автомасштабированием.

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

Раз копий несколько, кто-то раздаёт между ними входящие запросы. В Azure два варианта (подробнее — в статье про сеть):

  • Load Balancer — на уровне соединений (L4), быстрый и простой. Берите вариант Standard: дешёвый Basic зон не поддерживает и с 30 сентября 2025 года выведен из эксплуатации, так что резерва по зонам с ним не выйдет;
  • Application Gateway — на уровне HTTP (L7): маршрутизация по путям и хостам, терминация TLS, проверки здоровья. Обычно он и стоит публичной точкой входа, а приложения прячутся во внутренней сети.

Проверки здоровья

Балансировщик не должен слать запросы в мёртвую копию. За это отвечает проверка здоровья (health probe) — балансировщик периодически стучится по адресу вроде GET /health и считает копию живой, только если та отвечает 200. Не отвечает — трафик в неё не идёт. Тут легко склеить два разных механизма: сама проверка только выводит копию из ротации, пересоздаёт её отдельная политика автоматического восстановления набора — если вы её включили.

Практический совет: делайте /health честным. Если он отвечает 200 всегда, даже когда база недоступна, балансировщик будет слать трафик в заведомо сломанную копию. Хорошая проверка отражает, готов ли сервис реально обслуживать запросы.

Две разные проверки и что в них включать

«Честный /health» — совет, у которого есть подвох, и о нём надо сказать прямо.

Проверки бывают двух видов, и они отвечают на разные вопросы. Готовность — «можно ли давать этому экземпляру трафик»: отрицательный ответ убирает его из ротации. Живость — «не завис ли процесс»: отрицательный ответ ведёт к перезапуску или замене экземпляра.

И теперь главное. Если в проверку добавить зависимость — базу, кэш, соседний сервис, — то при мигании этой зависимости все экземпляры разом ответят отрицательно. Для готовности это означает, что сервис исчезнет из ротации целиком, хотя часть запросов он мог бы обслужить; для живости — что платформа начнёт перезапускать все экземпляры одновременно, и короткий сбой базы превратится в получасовую недоступность сервиса.

Рабочее разделение:

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

Практический минимум: два разных адреса (/health/live и /health/ready), а не один на всё. Балансировщик смотрит на готовность, платформа перезапуска — на живость.

Масштабирование вниз: куда деваются запросы

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

Четыре механизма, которые делают его незаметным:

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

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

Корректная остановка приложения. Получив сигнал завершения, приложение перестаёт брать новое и дожидается текущего. Это часть кода, а не настройки платформы, и без неё дренаж бесполезен: платформа даёт время, а процесс умирает мгновенно.

Защита от снятия (scale-in protection). Отдельная пометка на экземпляре, означающая «этот не снимать при уменьшении». Нужна там, где экземпляр выполняет долгую работу, которую нельзя прервать: обработку большой задачи, длинную выгрузку. Приложение ставит пометку на время работы и снимает после — и тогда масштабирование вниз выбирает кого-то другого.

Порядок, который получается: решение снять копию → пометка «не снимать», если она есть, отменяет выбор → вывод из ротации с дренажом → сигнал приложению → завершение → удаление. Проверяется это так же, как всегда: постоянная нагрузка плюс принудительное уменьшение набора, и в отчёте не должно быть ни одной ошибки соединения.

Доступность данных

Копии приложения размножить легко — они одинаковые. С базой сложнее: данные должны быть согласованы. Здесь помогают управляемые базы: у Azure Database for PostgreSQL можно включить зоно-избыточную высокую доступность (zone-redundant HA) — резервный сервер в другой зоне и автопереключение при отказе. Это снимает самую страшную единую точку отказа — базу — без ручной возни. Для приложения при этом меняется одно: адрес подключения остаётся прежним, но при отказе основного узла соединения рвутся на десятки секунд, и пул должен уметь переподключиться, а не упасть.

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

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

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

Типичные ошибки новичков:

  • держат состояние в памяти копии — теряют при масштабировании и пересоздании;
  • всё в одной зоне — авария зоны уносит весь сервис;
  • фиктивный /health, который всегда 200 — трафик идёт в сломанные копии;
  • забывают про базу — приложение продублировано, а база в одном экземпляре осталась точкой отказа.
Дополнительно: при первом чтении можно пропустить

Глубже: числа, без которых масштабирование обманываетрасширенное

Две группы цифр, которые меняют ожидания от автомасштабирования и от доступности.

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

Практический вывод тот же, что и в любом облаке: автомасштабирование спасает от роста в течение дня и не спасает от всплеска за тридцать секунд. Пики, которые известны заранее, закрывают расписанием; неизвестные — запасом, то есть целью по загрузке 50–60 %, а не 85 %.

Что обещает соглашение об уровне услуги. Цифры у Azure привязаны к тому, как развёрнуто приложение:

СхемаОбещанная доступность
Одна машина с диском на быстрых носителях99,9 % (около 43 минут простоя в месяц)
Две и более машин в группе доступности (один дата-центр, разные стойки)99,95 %
Две и более машин в разных зонах доступности99,99 % (около 4 минут в месяц)

Разница между первой и последней строкой — это разница в порядок по допустимому простою, и стоит она ровно одного решения при создании: разнести копии по зонам. Отдельно стоит помнить, что обещание относится к инфраструктуре: если ваше приложение падает само, никакая схема размещения этого не покроет.

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

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

az vm list-usage --location westeurope -o table | head -20

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

Глубже: прогрев: копия получает трафик раньше, чем готоварасширенное

Свежая копия отвечает на запрос проверки раньше, чем прогрела пул соединений, кэши и компилятор. Внешне всё правильно — проверка зелёная, трафик пошёл, — а первые запросы получают таймауты или ответ в разы медленнее обычного.

Три настройки, которые это закрывают:

  • Задержка до первой пробы. Проверка начинается не сразу после запуска, а через заданное время — достаточно, чтобы приложение успело поднять зависимости.
  • Прогрев экземпляра. У набора машин есть время, в течение которого новая копия считается прогревающейся: её метрики не участвуют в решениях о масштабировании, и трафик на неё подаётся не сразу.
  • Плавный ввод у балансировщика. Часть балансировщиков умеет подавать на новую копию долю трафика, увеличивая её постепенно. Это лучший вариант, если он доступен: копия прогревается на реальных запросах, но не на всех сразу.

И то, что можно сделать в приложении: прогрев при старте. Перед тем как ответить «готов», приложение само открывает соединения к базе, выполняет один запрос, заполняет критичные кэши. Тогда «готов» означает готов, и никакие настройки платформы не нужны.

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

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

Липкие сессии (у балансировщика Azure — привязка по адресу клиента, у App Service — специальная cookie) заставляют запросы одного пользователя приходить на ту же копию. Это работает и это временный обход, а не решение. Что он ломает:

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

Поэтому липкость включают с одной целью: дожить до того, как состояние вынесут, и с записанным сроком. А выносят в том же порядке, что везде: сессии — во внешний кэш или в токен у клиента, файлы — в хранилище объектов, общие счётчики и блокировки — в общий кэш, долгую работу — в очередь.

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

Коротко

  • Растут вширь копиями без состояния: вертикальный рост упирается в самую большую машину и остаётся одной точкой отказа.
  • Virtual Machine Scale Sets добавляют и убирают копии по нагрузке, пересоздают упавшие по политике автоматического восстановления и раскладывают по зонам.
  • Метрика усредняется за минуты, машина стартует десятки секунд, приложение дольше: автомасштабирование спасает от роста в течение дня, а известные пики закрывают расписанием.
  • Одна машина обещает 99,9 %, две в группе доступности 99,95 %, две в разных зонах 99,99 %: порядок разницы в простое стоит одного решения при создании.
  • Квота ядер на регион молча останавливает набор ниже его максимума: сверяют до нагрузочного испытания и поднимают заранее.
  • Load Balancer уровня соединений только Standard, Basic снят с эксплуатации; Application Gateway на HTTP стоит публичной точкой входа.
  • Живость без внешних зависимостей, готовность только с тем, без чего сервис бесполезен, два разных адреса; прогрев через задержку первой пробы, время прогрева экземпляра и плавный ввод трафика.
  • Снятие копии делают незаметным пауза между шагами, дренаж соединений, корректная остановка в коде и защита от снятия для долгой работы.
  • База остаётся точкой отказа, пока не включена зоно-избыточная высокая доступность: резерв не читается, переключение не мгновенно, соединения рвутся; липкие сессии только как временный обход с датой.

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