← Все статьи

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 нет отката. Не можете доказать, что писатель мёртв, — не делайте.