Когда откат делает только хуже (и что делать вместо этого)

Вы только что выкатили новую версию. Пайплайн зелёный. Health-чеки проходят. CPU и память в норме. Но телефон начинает разрываться от сообщений пользователей. Функция, которая работала вчера, сегодня сломана. Данные, которые пишутся в базу, выглядят неверно. А логи ошибок? Пусто.

В этот момент большинство команд думает: «Надо просто откатиться». Звучит просто. Заменить новую версию старой. Проблема решена. Но на практике откат может превратить управляемую проблему в катастрофу. Старая версия может не понять данные, которые уже записала новая. Схема базы данных могла измениться. Или сам откат занимает так много времени, что за время перехода страдает ещё больше пользователей.

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

Настоящий сигнал к откату

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

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

Решение об откате обычно приходит из комбинации сигналов:

  • Health-чеки начинают падать
  • Частота ошибок резко возрастает
  • Отчёты пользователей описывают поведение, не соответствующее ожиданиям
  • Бизнес-метрики отклоняются от нормальных паттернов

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

Установите чёткое окно наблюдения перед каждым деплоем. Решите заранее: если мы не видим проблем в первые 15 минут, считаем релиз стабильным. Если видим проблемы в этом окне — откатываемся немедленно. Это убирает колебания, которые делают плохую ситуацию ещё хуже.

Почему stateless и stateful — это не одно и то же

Простота отката почти полностью зависит от того, хранит ли ваше приложение состояние.

Для stateless-приложений откат тривиален. Вы перенаправляете трафик обратно на старую версию. Никаких данных для восстановления. Никаких схем для согласования. Старая версия продолжает работу с того же места, потому что она никогда не зависела от состояния новой версии. Вот почему stateless-сервисы часто являются первыми кандидатами для агрессивных стратегий отката.

Для stateful-приложений откат — это совсем другая игра. Представьте, что ваша новая версия записала 10 000 записей в базу с новым полем, о котором старая версия ничего не знает. Когда вы откатываете приложение, старая версия пытается прочитать эти записи. Она падает, потому что формат данных не соответствует тому, что она ожидает. Или хуже: новая версия изменила структуру таблицы базы данных. Теперь старое приложение даже не может запуститься, потому что схема несовместима.

Вот ловушка: откат приложения без отката данных. Если ваш деплой изменил схему базы данных или записал данные в новом формате, отката кода недостаточно. Вам нужно либо:

  • Восстановить базу данных до состояния до деплоя
  • Написать миграции, которые откатывают изменения схемы
  • Смириться с тем, что часть данных будет потеряна или повреждена

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

Три стратегии, которые действительно работают

Разные ситуации требуют разных подходов к откату. Вот три, которые команды используют на практике.

Следующее дерево решений поможет выбрать стратегию:

flowchart TD A[Обнаружена ошибка] --> B{Stateful?} B -->|Нет| C[Откат или переключение трафика] B -->|Да| D{Схема данных изменена?} D -->|Да| E{Можно быстро исправить forward fix?} E -->|Да| F[Forward fix] E -->|Нет| G[Принять и исправить] D -->|Нет| H{Логическая ошибка или проблема производительности?} H -->|Логическая ошибка| I{Можно быстро исправить forward fix?} I -->|Да| J[Forward fix] I -->|Нет| K[Переключение трафика или откат] H -->|Проблема производительности| L[Переключение трафика или откат]

Forward Fix (Исправление вперёд)

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

Forward fix хорошо работает, когда:

  • Ошибка изолирована и может быть быстро исправлена
  • Ваш пайплайн может доставить новую версию за минуты
  • Данные, записанные сломанной версией, восстанавливаемы или могут быть мигрированы

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

Traffic Shift (Переключение трафика)

Если вы используете canary или blue-green деплой, откат так же прост, как перенаправление трафика обратно на старую версию. Старая версия всё ещё работает и готова принимать трафик. Нет необходимости ждать процесса развёртывания. Нет переходного периода, когда одни пользователи попадают на старую версию, а другие — на новую.

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

Вот конкретный пример использования Kubernetes для переключения трафика обратно на старую версию при canary-деплое:

# Проверить текущее распределение трафика (предполагается сервис с двумя селекторами)
kubectl get virtualservice my-app -o jsonpath='{.spec.http[0].route[*].weight}'

# Перенаправить 100% трафика на старую версию (v1)
kubectl patch virtualservice my-app --type='json' -p='[
  {"op": "replace", "path": "/spec/http/0/route/0/weight", "value": 100},
  {"op": "replace", "path": "/spec/http/0/route/1/weight", "value": 0}
]'

# Проверить изменения
kubectl get virtualservice my-app -o yaml | grep -A5 "route:"

Этот подход предполагает, что у вас есть service mesh или ingress controller (например, Istio или Traefik), поддерживающий взвешенную маршрутизацию. Для более простых конфигураций того же эффекта можно добиться, обновив селектор сервиса так, чтобы он указывал исключительно на поды старой версии.

Accept and Patch (Принять и исправить)

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

Этот подход работает, когда:

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

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

Практический чек-лист перед следующим деплоем

Перед деплоем обсудите с командой следующие вопросы:

  • Какие сигналы запустят решение об откате? (health, ошибки, отчёты пользователей, бизнес-метрики)
  • Как долго мы будем наблюдать перед принятием решения? (5 минут, 15 минут, 30 минут)
  • Меняет ли этот деплой схему базы данных или формат данных?
  • Если да, есть ли у нас протестированный план обратной миграции или восстановления?
  • Старая версия всё ещё работает и готова принимать трафик?
  • Можем ли мы исправить вперёд быстрее, чем откатиться?

Запишите ответы. Поделитесь с командой. Время планировать откат — до деплоя, а не во время инцидента.

Вывод

Откат — это не универсальная страховочная сеть. Для stateless-приложений это надёжный запасной выход. Для stateful-приложений это может быть ловушкой, которая делает только хуже. Решение об откате зависит от того, сколько данных было затронуто, как быстро вы можете исправить вперёд и может ли старая версия всё ещё работать с текущим состоянием вашей системы.

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