14 апр. 2025 г.·7 мин чтения

Как развертывать без простоя, когда нет платформенной команды

Развертывать без простоя проще, если использовать балансировщик, честные проверки готовности и медленные сдвиги трафика. Узнайте простой процесс релиза, который под силу небольшим командам.

Как развертывать без простоя, когда нет платформенной команды

Почему релизы приводят к простоям

Большинство инцидентов при релизах происходят из-за тайминга, а не из-за плохого кода. Старая версия прекращает обслуживать запросы на несколько секунд раньше, а новая ещё не готова. Этот разрыв достаточен, чтобы пользователи получили ошибки.

Небольшие команды часто сталкиваются с этим. Один человек запускает скрипт деплоя, процесс перезапускается, а трафик при этом продолжает поступать. Если серверу нужно 20–30 секунд на запуск, каждый запрос в этом окне может упасть.

Медленный старт усугубляет проблему. Многие приложения говорят "я поднялся" раньше, чем действительно готовы. Веб-сервер слушает порт, но приложение всё ещё загружает конфиг, прогревает кеши, открывает соединения с базой или запускает воркеры. Ранние запросы приходят, приложение задыхается, и пользователи видят случайные 500, хотя само изменение было небольшим.

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

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

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

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

Минимальная рабочая конфигурация

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

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

Вам не нужна большая платформа для этого. Реверс-прокси или облачный балансировщик, два процесса или контейнера приложения и один понятный скрипт деплоя — этого достаточно для многих команд. Держите настройку скучной. Сложная логика раскатки обычно приносит больше проблем, чем решает.

Небольшая рабочая конфигурация включает:

  • две копии приложения, работающие постоянно
  • один балансировщик нагрузки перед ними
  • проверку готовности перед любым сдвигом трафика
  • быстрый путь отката к предыдущей версии

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

Откат должен быть коротким и повторяемым. Держите предыдущую версию доступной, пока новая не выдержит реальный трафик какое-то время. Если растёт число ошибок или запросы замедляются, верните трафик назад и остановите релиз. Вы хотите, чтобы откат занимал минуты, а не долгую сессию отладки в проде.

Для одного API или веб-приложения многим командам и этого достаточно. Сначала заведите эти четыре элемента — дальше всё становится гораздо проще.

Пишите честные проверки здоровья

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

Многие команды используют один эндпоинт /health для всего. Это часто вводит в заблуждение. Разделите проверки на две задачи. Liveness говорит, что процесс ещё жив и не завис. Readiness говорит балансировщику, должна ли эта инстанс получать трафик.

Readiness должна показывать неготовность, пока выполняются работы старта. Если приложение загружает конфиг, прогревает кеши, открывает соединения с базой, подтягивает секреты или выполняет задачи при старте — оно не готово. Быстрый 200 OK от наполовину запущенного приложения выглядит хорошо в логах, но пользователи платят за это ошибками.

Полезная проверка готовности обычно подтверждает несколько базовых фактов. Приложение закончилo стартап. Оно может достучаться до базы или очереди, если запросы зависят от них. Оно загрузило нужные конфиги и секреты. У него достаточно свободных ресурсов, чтобы принять трафик.

Делайте liveness намного проще. Если привязать liveness к базе данных, короткий обрыв в базе может заставить оркестратор перезапустить здоровые копии без причин. Это превратит маленькую проблему в большую.

Представьте небольшой API, который стартует 12 секунд, потому что загружает модели и открывает пул соединений PostgreSQL. В эти 12 секунд liveness может возвращать успех, а readiness — нет. Процесс жив, но ему не стоит принимать пользовательский трафик.

Одна деталь часто пропускается: тестируйте проверки здоровья под нормальным трафиком, а не только в стейджинге. Эндпоинт, который проходит в тихой тестовой среде, может врать в проде. Таймауты, медленные запросы, пустые пулы воркеров и застрявшие фоновые задачи часто проявляются только при реальном трафике.

Относитесь к проверкам здоровья как к боевому коду. Они должны быть строгими, честными и скучными.

Используйте балансировщик как ворота трафика

Балансировщик даёт одно место для контроля трафика. Это убирает много риска, когда вы деплоите с небольшой командой.

Направляйте пользователей только на копии приложения, которые прошли реальные проверки. Если копия начинает падать, балансировщик должен быстро исключить её из ротации. Нельзя позволить сломанному процессу оставаться в ротации ещё минуту, пока пользователи набирают ошибки.

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

Несколько настроек важнее остальных. Проверяйте каждую копию каждые 5–10 секунд. Помечайте её как нездоровую после 2–3 неудачных проверок. Используйте короткие таймауты запросов, чтобы зависшие копии быстро выпадали. Дренируйте соединения 30–60 секунд перед остановкой.

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

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

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

Эта одна перемена значительно упрощает развертывание без простоя. Балансировщик может убрать трафик с проблемных копий прежде, чем большинство пользователей заметит что-то не так.

Заменяйте по одной копии за раз

Принесите ваш следующий релиз
Короткая консультация поможет найти уязвимости до дня релиза.

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

Допустим, у вас три инстанса API за балансировщиком. Запустите четвёртую с новой версией или замените одну старую на новую. Остальные две оставьте как есть.

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

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

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

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

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

Медленно сдвигайте трафик

Медленная раскатка лучше смелой. Не отправляйте всех пользователей на новую версию сразу, если не хотите угадывать в напряжении.

Простой шаблон хорошо работает для многих команд. Начните с 5% трафика на новую версию, потом 25%, затем 50%. Если всё в норме — отправьте оставшийся трафик.

Пауза между шагами важна так же, как и проценты. Дайте каждому шагу достаточно времени, чтобы показать поведение реальных пользователей, а не только шум старта. На загруженном сервисе 5 минут может хватить. На более тихом — понадобится 15 минут или больше.

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

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

Держите старую версию запущенной на протяжении всей раскатки. Это даёт безопасное место для трафика, если новая версия начнёт падать через 10 минут, а не в первую минуту. Многие преждевременно выключают старую копию и превращают маленькую проблему в простой.

Простой пример помогает представить это. Допустим, у вас API с четырьмя инстансами за балансировщиком. Добавьте одну новую инстанс и оставьте три старые. Это почти даёт сдвиг в 25% без какого-либо специнструмента. Если всё хорошо — замените вторую старую, потом третью.

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

Простой пример для одного API

Проверьте следующее развертывание
Получите второе мнение по проверкам готовности, шагам отката и сдвигам трафика.

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

Команда стартует версию 2 в пустом слоте. Пока она поднимается, две старые копии продолжают обслуживать запросы, так что пользователи не ощутят пустоты. Балансировщик держит новую копию вне ротации, пока её проверка готовности не докажет больше, чем "процесс запущен".

Для одного API хорошая проверка готовности может быть простой. Она должна подтвердить, что приложение завершило старт, умеет отвечать на обычные запросы и может достучаться до всего необходимого при каждом запросе, например до базы или кеша. Если новая копия всё ещё прогревается или не проходит проверку миграций, балансировщик должен держать её в стороне.

Когда новая копия проходит, команда даёт ей небольшую долю трафика, часто 5–10%. Этого достаточно, чтобы поймать реальные проблемы, не переводя всех пользователей на новую версию сразу. В течение следующих 10 минут наблюдают за уровнем ошибок, латентностью, количеством перезапусков и всплесками CPU/памяти.

Если показатели в норме — команда заменяет одну старую копию на новую и повторяет те же проверки. После ещё одного тихого окна заменяют последнюю и возвращают веса трафика в обычное состояние.

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

Вот и весь шаблон: держите старые копии в обслуживании, впускайте новую копию только после реальных проверок и сдвигайте трафик малыми шагами, которые можно быстро отменить.

Ошибки, которые ломают раскатку

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

Одна распространённая ошибка — считать проверку здоровья доказательством работоспособности. Многие проверки подтверждают только, что процесс запустился и отвечает на один эндпоинт. Этого недостаточно. Приложение может возвращать 200 на /health, но при этом не иметь доступа к базе, не читать очередь или не обслуживать трафик достаточно быстро.

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

Сессии ломают раскатки чаще, чем команды ожидают. Если каждая копия хранит сессии в памяти, пользователи будут пересекаться между версиями и выходить из системы. Постепенные сдвиги трафика становятся хаотичными, потому что пользователь может попасть на одну копию при логине и на другую при следующем запросе. Общая сессия в Redis или базе, подписанные куки или статeless-аутентификация избегают этой ловушки.

Поведение при остановке ломает множество кросс-тестированных релизов. Старой версии приходит сигнал остановки, она сразу закрывается и роняет активные запросы. Пользователи видят случайные ошибки, хотя новая версия здорова. Дайте старой копии время на дренаж. Сначала остановите новый трафик, пусть текущие запросы завершатся, и только потом завершайте процесс.

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

Короткая репетиция помогает. Запустите новую версию за балансировщиком и посмотрите на реальную готовность, а не только на старт процесса. Держите старую и новую версии совместимыми с базой в течение одного окна релиза. Сливайте трафик перед остановкой и измеряйте, сколько реально занимают запросы. Отрепетируйте откат на маленьком релизе, даже если он кажется безобидным.

Если вы сделаете только одну вещь до следующего деплоя — потренируйте совместное завершение и откат. Именно на этом пункте многие «безопасные» раскатки и проваливаются.

Быстрые проверки до и после релиза

Уменьшить стресс от деплоев
Постройте процесс релиза, который команда сможет запустить в 2 утра без догадок.

Большинство провалившихся раскаток ломаются не на первом запросе. Они ломаются через пять минут, когда стартуют фоновые задачи, прогреваются кеши или появляется медленный эндпоинт.

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

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

Обычной страницы статуса недостаточно. Прогоните один-два медленных эндпоинта, которые делают реальную работу, например логин, поиск, генерацию отчёта или запись в базу. Эти пути часто ломаются раньше, чем эндпоинт здоровья.

Перед сдвигом трафика задайте четыре прямых вопроса:

  • Могут ли старые и новые копии обрабатывать одни и те же запросы и данные одновременно?
  • Дренирует ли балансировщик одну копию аккуратно?
  • Остаются ли медленные эндпоинты здоровыми при лёгком трафике?
  • Готов ли откат, с последней версией всё ещё доступной?

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

Держите одного человека на duty по откату, пока трафик не устаканится. Не дробьте эту задачу по команде. Один человек должен смотреть графики, логи и быстро решать, остаётся версия или уходит. Малые команды обычно лучше работают, когда зоны ответственности чёткие.

Что делать дальше

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

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

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

  1. Запустить одну новую копию.
  2. Дождаться прохождения проверки готовности.
  3. Дать ей небольшую долю трафика.
  4. Наблюдать за ошибками, задержкой и логами несколько минут.
  5. Вернуть трафик, если что-то идёт не так.

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

Если текущая настройка кажется хрупкой, сторонняя помощь может сэкономить время. Хороший консультант быстро заметит слабые проверки готовности, небезопасные дефолты балансировщика или шаги CI/CD, которые заменяют все копии сразу.

Это также то, чем занимается Oleg Sotnikov через oleg.is с стартапами и малыми компаниями. Если нужна вторая пара глаз по дизайну раскатки, легкой инфра или практическому ревью Fractional CTO, короткий аудит поможет укрепить уязвимые места до следующего релиза.

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

Часто задаваемые вопросы

Нужен ли Kubernetes, чтобы развертывать без простоя?

Нет. Большинству небольших команд достаточно двух копий приложения, одного балансировщика нагрузки и правдивой проверки готовности. Если можно добавить третью копию, постепенные раскатки становятся проще, но большая платформа не нужна, чтобы начать.

В чем разница между liveness и readiness?

Liveness отвечает: «процесс ещё жив?» Readiness отвечает: «может ли эта копия сейчас обслуживать реальный трафик?» Делайте liveness простой, а readiness строгой — пусть балансировщик дождётся окончания задач старта, установленных соединений и инициализации.

Сколько копий приложения нужно минимум?

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

Как понять, что новая версия действительно готова?

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

Какой самый быстрый план отката?

Держите старую версию запущенной во время релиза. Если растут ошибки или задержки, сразу возвращайте трафик на старые копии и останавливайте раскатку. Откат остаётся быстрым только если предыдущая версия всё ещё в балансировщике.

Как обращаться с миграциями базы данных во время раскатки?

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

Вызывают ли sticky-сессии проблемы при раскатке?

Часто создают проблемы. Если сессии лежат в памяти каждой копии, пользователи будут «прыгать» между версиями и терять состояние. Перенесите сессии в Redis или базу, используйте подписанные куки или статeless-аутентификацию, чтобы трафик мог свободно перемещаться.

Сколько ждать между сдвигами трафика?

На загруженном сервисе 5 минут на шаг часто достаточно, чтобы увидеть реальное поведение. На тихом приложении лучше ждать 15 минут или больше. Наблюдайте за ошибками, временем ответа и перезапусками, а не только за логами старта.

Как корректно завершать старые копии, не теряя запросы?

Сначала прекратите принимать новый трафик, затем дайте активным запросам завершиться, прежде чем процесс завершится. Окно дренажа 30–60 секунд подходит для многих веб-приложений; увеличьте его, если запросы длительные. Если убить процесс мгновенно — пользователи увидят оборванные ответы.

Что следует проверить перед началом релиза?

Проверьте, что старые и новые версии могут одновременно использовать базу, хранилище сессий и очередь в течение окна релиза. Убедитесь, что балансировщик аккуратно сливает одну копию и прогоните один-два медленных эндпоинта, например логин или запись в базу. Назначьте одного человека наблюдать метрики и принять решение об откате.