← Все статьи

Liveness-проба, привязанная к чужой сети: 330 рестартов, которые ничего не чинили

Две одинаковые реплики cloudflared — 330 и 13 рестартов. Liveness проверяла туннель до edge, поэтому каждая икота аплинка превращалась в SIGKILL.

В моей домашней лаборатории у двух реплик одного Deployment cloudflared — один образ, один конфиг — была совершенно разная история: у одной 330 рестартов, у другой 13. Exit code 137, reason Error, не OOMKilled.

Асимметрия — это подсказка

Разница 25:1 между одинаковыми репликами сразу исключает образ и конфиг. То, что их убивает, зависит от ноды или сетевого пути, а не от кода. А 137 с reason: Error — это SIGKILL от kubelet, на практике — упавшая проба.

Что на самом деле спрашивала проба

Liveness опрашивала /ready. Я решил, что это значит «процесс готов», и вместо дальнейших догадок спросил сам эндпоинт через port-forward:

/ready -> {"status":200,"readyConnections":4}

У cloudflared /ready сообщает о состоянии соединений туннеля до edge Cloudflare, а не о том, жив ли процесс. Поэтому при каждой икоте внешней сети соединения падали, /ready проваливался, и kubelet убивал совершенно здоровый процесс. Логи говорили об этом прямо:

failed to accept QUIC stream: timeout: no recent network activity

Рестарт пода не чинит чужой WAN. Окончательное доказательство, что причина всегда была снаружи: рестарты прекратились сами в тот день, когда починили домашний аплинк, без каких-либо изменений в Deployment.

Исправление

  • Liveness теперь опрашивает /metrics — он отвечает, пока сам процесс обслуживает HTTP.
  • Readiness осталась на /ready: там «поднят ли туннель» — ровно правильный вопрос. Неготовый под перестаёт получать трафик, но его не убивают.
  • timeoutSeconds поднял с 1 до 5. Одна секунда — агрессивно для процесса, занятого QUIC-вводом-выводом.

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

И сознательно не стал совмещать с этим просроченный апгрейд cloudflared, чтобы у возможной регрессии была одна очевидная причина.

У воркеров самого приложения правильный шаблон уже был, его и стоит копировать: /health всегда отдаёт 200 и несёт состояние Redis как поле, а /ready отдаёт 503, когда Redis недоступен.

Выводы

  • Liveness отвечает на один вопрос: «процесс завис, и поможет ли рестарт?»
  • Если ответ зависит от сети, базы или третьей стороны, это место для readiness или алерта, но не для liveness.
  • Liveness на внешнюю зависимость превращает чужую аварию в твой CrashLoop, а счётчик рестартов потом прячет настоящую причину.
  • Сильно разные счётчики рестартов у одинаковых реплик указывают на окружение, а не на код.