← Все статьи

Пять дней на 1/3 при зелёных дашбордах: когда статус CRD врёт

MySQL InnoDB Cluster пять дней работал без HA, а статус ресурса, фаза подов и readiness gate показывали здоровье. Все три сигнала шли от одного зависшего оператора.

В Optimacros на MySQL InnoDB Cluster держится кластер, который обслуживает маркетинговые сайты компании. Он рассчитан на три участника, чтобы пережить потерю одного. Около пяти дней он работал на одном участнике из трёх — без всякой отказоустойчивости, — а все дашборды были зелёными.

Три сигнала, одна ложь

Когда я наконец присмотрелся, три как будто независимых сигнала в один голос говорили «здоров»:

  • кастомный ресурс InnoDBCluster показывал ONLINE и onlineInstances: 3;
  • поды были в Running: у сломанных вторичных узлов оставался живой sidecar-контейнер;
  • readiness gate, который выставляет оператор, был True.

UI кластера показывал этот .status как актуальный, так что любой беглый взгляд видел трёх здоровых участников, хотя двое из них были мертвы.

Почему все три совпали

Они вовсе не были независимыми. Все три писал один и тот же MySQL-оператор, а его reconciler завис. Выдало его поле .status.cluster.lastProbeTime: оно застыло на дате несколько дней назад.

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

Читаем ground truth

Решение — смотреть на сигналы, которые не проходят через оператор:

  • readyReplicas у StatefulSet: kube-state-metrics читает его напрямую из API-сервера;
  • число участников Group Replication из mysqld-exporter;
  • собственное представление MySQL Router о живых участниках, видное в его логах.

Все три сошлись между собой и разошлись с кастомным ресурсом. rollout restart оператора разморозил его, и примерно через 30 секунд статус показал свежее время пробы и реальную картину. Перезапуск оператора безопасен: он только оркестрирует, Group Replication работает сама по себе. Мёртвого участника он, правда, не чинит — это была отдельная пересборка.

Цепочка доверия

Теперь я думаю об этих сигналах как о цепочке от факта к слухам:

containerStatuses[].ready → StatefulSet.status.readyReplicas → pod.status.phase → CustomResource.status

Алерт ставьте на самый левый сигнал, который дёшево собирать, и никогда на самый правый. Мы добавили PrometheusRule на kube_statefulset_status_replicas_ready < 3 в течение пяти минут. Он не пытается обнаружить зависший оператор — из Prometheus это сложно, статус CR не метрика. Он просто перестаёт зависеть от сигнала, который может зависнуть.

Выводы

  • .status, который пишет контроллер, — подсказка, а не факт. Если таймстемп пробы или heartbeat в нём старый, старо и всё остальное.
  • Если несколько «независимых» сигналов здоровья исходят от одного компонента, у вас один сигнал, а не несколько.
  • Алертьте по ground truth, у которого нет общего домена отказа с тем, за чем он следит.
  • То же самое верно для любого ресурса под оператором: баз данных, Vault, сертификатов cert-manager, здоровья приложений Argo CD.