Rolling Update: как развернуть новую версию без остановки сервиса

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

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

Проблема: развёртывание «всё или ничего»

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

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

Решение: заменяем по одному экземпляру

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

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

sequenceDiagram participant LB as Load Balancer participant I1 as Instance 1 participant I2 as Instance 2 participant I3 as Instance 3 LB->>I1: Drain traffic I1-->>LB: Acknowledged I1->>I1: Deploy v2.0 I1->>I1: Health check /ready I1-->>LB: Ready LB->>I1: Resume traffic LB->>I2: Drain traffic I2-->>LB: Acknowledged I2->>I2: Deploy v2.0 I2->>I2: Health check /ready I2-->>LB: Ready LB->>I2: Resume traffic LB->>I3: Drain traffic I3-->>LB: Acknowledged I3->>I3: Deploy v2.0 I3->>I3: Health check /ready I3-->>LB: Ready LB->>I3: Resume traffic
  1. У вас пять серверов, на которых работает версия 1.0 вашего приложения.
  2. Вы выводите один сервер из эксплуатации.
  3. Вы развёртываете версию 2.0 на этом сервере.
  4. Вы проверяете, что новая версия работает корректно.
  5. Вы возвращаете сервер в эксплуатацию.
  6. Вы переходите к следующему серверу и повторяете процесс.

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

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

Почему важны health checks

Rolling update работает только если вы можете определить, что новая версия действительно работает корректно, прежде чем обновлять следующий экземпляр. Здесь на помощь приходят health checks.

Health check — это простой механизм, который проверяет, готов ли экземпляр принимать трафик. Обычно это эндпоинт вроде /health или /ready, который возвращает успешный ответ, когда приложение работает нормально. Ваша система оркестрации — будь то Kubernetes, балансировщик нагрузки или кастомный инструмент развёртывания — проверяет этот эндпоинт перед отправкой трафика на экземпляр.

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

Без health checks вы действуете вслепую. Вы можете обновить все пять серверов, прежде чем заметите, что новая версия падает при запуске. К тому моменту пострадают все пользователи.

Когда rolling updates работают хорошо

Rolling updates идеальны для изменений, обратно совместимых. Это изменения, при которых старая и новая версии могут работать бок о бок без проблем. Примеры:

  • Добавление новых логов
  • Исправление мелкой ошибки, не меняющей форматы данных
  • Изменение цветов интерфейса или текста
  • Добавление нового API-эндпоинта, которым пока никто не пользуется
  • Обновление зависимости, не меняющей поведение

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

Компромиссы

Rolling updates просты и не требуют дополнительной инфраструктуры. Вы используете те же серверы, что уже есть. Не нужно поднимать параллельное окружение или выделять дополнительные мощности. Затраты на инфраструктуру остаются прежними во время обновления.

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

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

Практический чек-лист

Прежде чем внедрять rolling update, убедитесь, что у вас есть следующее:

  • Health check endpoint: ваше приложение должно предоставлять надёжный способ проверки своей работоспособности.
  • Обратная совместимость: новая версия должна работать вместе со старой во время перехода.
  • План отката: знайте, как откатить отдельный экземпляр, если health check провалится.
  • Мониторинг: следите за частотой ошибок и временем ответа во время обновления, чтобы вовремя заметить проблемы.
  • Достаточное количество экземпляров: rolling updates лучше всего работают как минимум с тремя экземплярами. Если их только два, вы теряете половину мощности во время обновления.

Вывод

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

Для небольших обратно совместимых изменений rolling updates часто более чем достаточны. Они просты, экономичны и широко поддерживаются платформами оркестрации контейнеров вроде Kubernetes и облачными инструментами развёртывания. Но для более рискованных изменений — миграций баз данных, изменений протоколов или крупных переработок функциональности — может потребоваться больше контроля. Здесь на помощь приходят стратегии вроде blue-green deployment или canary release. Но это уже тема для отдельной статьи.