Когда зелёный пайплайн не означает здоровый деплой

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

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

Что произошло? Пайплайн сказал, что всё в порядке.

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

Что на самом деле говорит health-сигнал

Health-сигнал — это то, как ваше приложение сообщает о своём состоянии. Самая распространённая форма — health-эндпоинт: специальный URL, который ваш пайплайн, балансировщик нагрузки или команда эксплуатации могут вызвать, чтобы проверить, работает ли приложение. Вы наверняка видели эндпоинты с именами /health или /healthz. При вызове здоровое приложение возвращает HTTP 200. Нездоровое — 500 или другой код ошибки.

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

Readiness vs Liveness: два разных вопроса

Существует два различных вида health-проверок, и они отвечают на разные вопросы.

Следующая блок-схема иллюстрирует разницу между liveness и readiness проверками и то, как они влияют на решения при деплое:

flowchart TD A[Health Check Request] --> B{Is the app process running?} B -->|No| C[Liveness fails: Restart container] B -->|Yes| D{Is the app ready to serve traffic?} D -->|No| E[Readiness fails: Remove from load balancer] D -->|Yes| F[Readiness passes: Send traffic] C --> G[Automatic recovery attempt] E --> H[Wait and retry readiness] F --> I[App serves users normally] H --> D G --> B

Readiness сообщает системе, готово ли приложение принимать трафик. Когда приложение только запустилось, ему может потребоваться несколько секунд на подключение к базе данных, загрузку кэша или прогрев соединений. В это время приложение должно сообщать о себе как о «не готовом». Балансировщик нагрузки или оркестратор тогда будут удерживать запросы до тех пор, пока приложение не сообщит о готовности. Без правильной readiness-проверки пользователи могут попасть на полуинициализированное приложение, которое возвращает ошибки или таймауты.

Liveness сообщает системе, живо ли приложение и обрабатывает ли оно запросы. Если приложение попало в deadlock, исчерпало память или перешло в состояние, в котором не может выполнять никакую работу, liveness-проверка это обнаружит. В контейнерных средах, таких как Kubernetes, неудачная liveness-проверка обычно запускает автоматический перезапуск.

Обе проверки служат разным целям в вашем пайплайне деплоя. Когда вы выполняете деплой, пайплайн обычно сначала вызывает readiness-проверку. Если новая версия не становится готовой в течение разумного времени, пайплайн может сразу принять решение об откате. Это гораздо лучше, чем позволить недоделанной версии обслуживать пользователей. Liveness-проверки, с другой стороны, более полезны после завершения деплоя. Они поддерживают приложение в здоровом состоянии с течением времени.

Вот практический пример YAML, который настраивает оба пробника для деплоя в Kubernetes:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  template:
    spec:
      containers:
      - name: app
        image: my-app:1.0.0
        ports:
        - containerPort: 8080
        readinessProbe:
          httpGet:
            path: /ready
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 10
          failureThreshold: 3
        livenessProbe:
          httpGet:
            path: /live
            port: 8080
          initialDelaySeconds: 15
          periodSeconds: 20
          failureThreshold: 3

В этом примере readiness-пробник проверяет /ready каждые 10 секунд после задержки в 5 секунд. Если он трижды не срабатывает, Kubernetes прекращает направлять трафик на под. Liveness-пробник проверяет /live реже и перезапускает контейнер, если проверка не удалась.

Что на самом деле должна проверять хорошая health-проверка

Health-проверка, которая просто возвращает 200, ничего не проверяя — это украшение, а не сигнал. Осмысленная health-проверка должна тестировать компоненты, важные для работы вашего приложения:

  • Связь с базой данных: может ли приложение достичь базы данных и выполнить простой запрос?
  • Критически важные внешние API: доступны ли сервисы, от которых зависит ваше приложение?
  • Внутренние ресурсы: здоров ли пул потоков? Укладывается ли использование памяти в лимиты?

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

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

Health-сигналы в прогрессивных стратегиях деплоя

Health-сигналы становятся ещё более важными, когда вы используете стратегии деплоя, такие как canary или blue-green. Представьте, что вы выкатываете новую версию на небольшую подгруппу пользователей. Ваш пайплайн может мониторить health-эндпоинт этой новой версии. Если health-проверка начинает показывать ошибки или время ответа увеличивается, пайплайн может автоматически переключить трафик обратно на старую версию.

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

Что происходит, когда health-проверки отсутствуют или слабы

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

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

Краткий практический чек-лист

Если вы настраиваете health-проверки для своего приложения, вот короткий список того, что стоит проверить:

  • Проверяет ли ваш health-эндпоинт что-то осмысленное или просто возвращает 200?
  • Есть ли у вас отдельные readiness и liveness проверки с разной глубиной?
  • Использует ли ваш пайплайн readiness-проверку для решения, продолжать деплой или откатывать?
  • Достаточно ли легковесны ваши health-проверки, чтобы не ухудшать производительность при частом вызове?
  • Используют ли ваши прогрессивные стратегии деплоя health-сигналы для раннего обнаружения проблем?

Настоящая проверка деплоя

Зелёный пайплайн — это не доказательство того, что ваше приложение работает. Это доказательство того, что ваш процесс сборки и деплоя прошёл без ошибок. Настоящая проверка наступает после деплоя, когда трафик попадает на новую версию. Health-сигналы — это мост между «деплой завершён» и «приложение действительно корректно обслуживает пользователей».

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