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, а счётчик рестартов потом прячет настоящую причину.
- Сильно разные счётчики рестартов у одинаковых реплик указывают на окружение, а не на код.