NotReady шесть часов, а нода всё это время была жива
Все сигналы говорили, что нода мертва. На деле она задыхалась от нехватки памяти, а очевидный фикс — удалить ноду и принудительно отцепить том — был в одной команде от порчи базы.
В Gambchamp, на продуктовой платформе в AWS EKS, нода с базой данных шесть часов висела в NotReady. kubectl describe node по всем условиям писал Kubelet stopped posting node status. Член StatefulSet’а ClickHouse застрял в Terminating на этой ноде с всё ещё подключённым томом, а его пара ушла в 96 перезапусков.
Всё говорило «машины больше нет». И учебное лечение столь же очевидно: kubectl delete node, чтобы освободить застрявший под, дать тому принудительно отцепиться, дать StatefulSet’у перепланироваться. Эта команда была в одном нажатии от уничтожения данных.
Сначала fencing
Перед любым force-detach нужно доказать, что старый писатель действительно мёртв. Поэтому вместо действий я сделал fencing — только чтение:
- EC2-инстанс был в состоянии
running; - обе проверки статуса, системная и инстанса, —
ok; - агент SSM перестал отвечать примерно через три минуты после kubelet.
Работающий инстанс с зелёными проверками — это не мёртвая машина. Настоящей подсказкой было то, что два независимых демона замолчали почти одновременно: демоны не падают синхронно, а вот голодают — синхронно.
Голодает, а не умерла
CloudWatch показывал CPU ровно 55 % на всём окне со всплесками до 99,9 %. На ноде с 2 vCPU 55 % — это одно ядро, загруженное наглухо. Так выглядит непрерывный reclaim памяти.
Причина — одна строчка арифметики:
container limits.memory (4Gi) > node allocatable (3.07Gi)
Лимит cgroup выше того, что нода реально может отдать, никогда не вызовет OOM-kill на уровне cgroup. Контейнер растёт, пока не кончится память всей машины, а поскольку swap нет, ядро проводит время в прямом reclaim. kubelet и агент SSM не получают CPU и не могут отправить heartbeat. NotReady было утверждением про kubelet, а не про машину — и база на ней, скорее всего, всё ещё писала в свой том.
Отцепить том от живого писателя — верный способ получить испорченный диск. reclaim=Retain сохраняет диск, но не его целостность.
Что было дальше
Ничего — намеренно. Fencing ответил на вопрос «безопасен ли force-detach?» словом нет, и я не сделал ни одной мутации. Через какое-то время системный OOM killer ядра наконец выбрал процесс ClickHouse, память вернулась, kubelet снова начал отправлять статус, и нода вернулась в Ready.
Такое восстановление — удача, а не фикс. Настоящий фикс — сделать так, чтобы лимит помещался: limits.memory ниже allocatable ноды, тогда cgroup убьёт один контейнер, а не ядро утопит всю ноду. И правильно подобрать пул, который просто был мал для того, что на нём крутилось. Поднять лимит ещё выше — значит сделать хуже.
Выводы
NotReadyописывает kubelet, а не машину. Перед любым разрушительным шагом — fencing: состояние инстанса, проверки статуса, пинг агента в user space, CPU за окно инцидента.runningплюсstatus checks okзначит лишь, что гипервизор достаёт до ядра. Смотрите вместе с CPU и heartbeat агента.- Два демона, замолчавшие вместе, — это голодание по ресурсам, а не сетевая изоляция.
- Сравнивайте лимиты памяти с allocatable ноды, а не с capacity. Лимит, который не помещается, ноду не защитит.
- У
delete nodeс force-detach нет отката. Не можете доказать, что писатель мёртв, — не делайте.