Два канала наблюдения и их границы
kube-state-metrics читает Kubernetes API и публикует нормализованные метрики о желаемом и наблюдаемом состоянии объектов. Он удобен для массового обнаружения Pod и контейнеров, динамики restart count, причин ожидания или завершения и связей с владельцами. Но это не журнал kubelet, не поток событий и не источник stdout приложения; набор метрик также зависит от версии экспортера и разрешённых ресурсов. Прямой collection через Kubernetes API даёт актуальные поля Pod status, containerStatuses, lastState, условия, ownerReferences, Events и ревизии ReplicaSet. Он богаче контекстом, зато требует пагинации, контроля нагрузки, обработки исчезающих объектов и аккуратного RBAC. В Zabbix я использую kube-state-metrics для устойчивых временных рядов и LLD, а API-запрос — для подтверждения состояния и атрибутов инцидента. Если источники расходятся, сравниваю время выборки и UID объекта: имя Pod могло уже перейти к новому экземпляру. Ни один канал по отдельности не доказывает корневую причину завершения процесса.
Карточка сбоя: рестарты, причина и код выхода
Сначала я фиксирую namespace, Pod UID, имя контейнера и владельца, затем читаю restartCount, текущее state.reason, lastState.terminated.reason и exitCode. Рост restart count важен как изменение на временной шкале, а не как вечное число: рестарты могут относиться к прошлому эпизоду. Текущий reason Waiting/CrashLoopBackOff описывает ожидание следующего запуска, тогда как last termination reason сообщает, чем закончилась предыдущая попытка. Exit code 0 способен означать, что основной процесс штатно выполнил короткую задачу, хотя контроллер ожидает долговечный сервис; ненулевой код требует сопоставления с документацией приложения и логом. Reason OOMKilled усиливает гипотезу о cgroup memory limit, Error остаётся общим обозначением. Я сохраняю startedAt и finishedAt, число попыток и spec контейнера, чтобы отличить мгновенный выход от процесса, который некоторое время обслуживал запросы.
Как я разделяю действие probes
Startup probe, если задана, удерживает liveness и readiness от исполнения до успешного старта; её повторный провал приводит к перезапуску контейнера. Liveness проверяет, способен ли уже запущенный процесс продолжать работу, и после серии неудач kubelet завершает контейнер. Readiness лишь исключает Pod из endpoint-ов Service и сама по себе контейнер не перезапускает. Поэтому я читаю спецификацию всех probes вместе с Events и состоянием Pod, а не делаю вывод по одному HTTP-коду. Сообщения kubelet о failed liveness или startup probe, Unhealthy и Killing объясняют инициатора рестарта. Провалы только readiness при неизменном restart count указывают на недоступность для трафика, но не объясняют цикл запусков. Дополнительно сверяю путь, порт, scheme, command, initial delay и фактическое время готовности конкретной версии приложения. Я не переношу постоянные тайминги между сервисами: медленная миграция схемы и зависший процесс требуют разных проверок и разных окон наблюдения.
Events и ревизия возвращают контекст изменения
Kubernetes Events я выбираю по UID вовлечённого объекта и упорядочиваю по времени, учитывая, что это ограниченно хранимые, агрегируемые сообщения, а не вечный аудит. В цепочке могут быть Scheduled, Pulled, Created, Started, Unhealthy, Killing или FailedMount; повторяемость и соседство событий помогают восстановить порядок. Далее прохожу ownerReferences от Pod к ReplicaSet и Deployment. Аннотация deployment.kubernetes.io/revision и hash шаблона позволяют понять, относится ли сбой к новой ревизии, а история rollout — что именно считалось развёрнутым. Я сравниваю проблемный ReplicaSet с предыдущим по image digest, command, args, envFrom, volume mounts, serviceAccountName, probes и ресурсам, не выгружая значения Secret. Важно не обвинять последний rollout автоматически: новый Pod может впервые попасть на проблемный узел или встретить уже недоступную зависимость. Связь считается доказанной, когда временная линия и конкретное отличие ревизии согласуются с termination, Events и логом.
Память контейнера и давление на узел
Requests участвуют в планировании и классе QoS, а limits ограничивают потребление контейнера; их нельзя трактовать как фактическую память процесса. При превышении memory limit cgroup ядро может завершить процесс, после чего container status нередко содержит OOMKilled и соответствующий exit code. Я сопоставляю этот след с memory usage, working set, limit, журналом kubelet или runtime в разрешённом объёме и моментом роста нагрузки. Node pressure — другая ветвь: условия Node MemoryPressure, решения eviction manager и Events про Evicted показывают нехватку на уровне узла и вытеснение Pod, а не обязательно превышение cgroup конкретного контейнера. Под с eviction может исчезнуть и быть заменён новым, тогда как локальный OOM часто увеличивает restart count внутри того же Pod. У процесса возможен и внутренний отказ выделения памяти без OOM kill, поэтому текст приложения и exit code остаются существенными. Изменять limit наугад опасно: сначала я проверяю профиль, QoS, соседнюю нагрузку и семантику runtime.
Как отсекаю image pull, конфигурацию и зависимости
Не всякий неработающий Pod находится именно в CrashLoopBackOff. ErrImagePull и ImagePullBackOff возникают до запуска контейнера: restart count может не расти, а Events содержат ошибку registry, digest, авторизации или загрузки. Я проверяю точное state.reason и подтверждаю, был ли процесс вообще создан. Ошибка конфигурации проявляется иначе: отсутствующий ConfigMap или Secret, неверный ключ и невозможный mount часто видны в FailedMount либо CreateContainerConfigError; приложение также может стартовать и завершиться, если обязательная переменная пуста. Недоступная база, DNS, очередь или внешний API обычно оставляют в предыдущем логе сообщения соединения и повторов. Я сверяю EndpointSlice, разрешение имени и доступ из того же namespace только разрешёнными read-only проверками либо заранее согласованным изолированным Pod. Успешный TCP connect ещё не подтверждает протокол, учётные данные или нужную схему. Альтернативу отбрасываю только по положительному свидетельству другой ветви, а не по отсутствию удобной строки.
Логи становятся доказательством, когда привязаны ко времени
Для перезапускающегося контейнера я беру текущий лог и previous log, если runtime ещё хранит предыдущий экземпляр, с точными timestamps и именем контейнера. У много-контейнерного Pod отдельно проверяю init containers и sidecars: ошибка может принадлежать не тому процессу, который первым попал в карточку. Фрагмент считается доказательством, когда совпадает с finishedAt, exit code, событием kubelet и ревизией. Я не публикую лог целиком. Перед рабочей записью заменяю токены, Authorization headers, cookies, пароли, строки подключения, приватные адреса, персональные данные и значения Secret понятными метками; исходный доступ остаётся в защищённом контуре с ограниченным сроком хранения. Маскирование не должно уничтожить причинную деталь: сохраняю тип исключения, имя параметра без значения, код ответа и корреляционный идентификатор, если он не секретен. Строка stack trace без временной связи остаётся подсказкой, но не окончательным выводом.
От сигнала Zabbix к проверяемому решению
В Zabbix я строю LLD по namespace, workload, Pod UID и container name, а прототипы items получают restart count, state reason, last termination reason, exit code и revision. Mapping переводит машинные причины в читаемые значения, но сохраняет исходный код. Триггер учитывает изменение рестартов и связанный reason в рамках жизненного цикла UID, чтобы старое значение нового одноимённого Pod не создавало ложную причинность. После сбора доказательств исправление выбирается по ветви: образ, конфигурация, probe, зависимость, ресурс или код процесса. Rollout restart я допускаю только как безопасную проверку после устранения предполагаемой причины, при наличии исправной предыдущей ревизии или иной стратегии отката, достаточной доступности реплик и контрольной точки из метрик, Events и логов. До запуска фиксирую revision и состояние endpoints; после — наблюдаю новые UID и тот же набор признаков. Если симптом повторяется либо доступность ухудшается, останавливаю проверку и возвращаю заранее определённую ревизию. Сам рестарт ничего не лечит и без этих предусловий стирает важные следы.
Сигналы, которые сопоставляю
- Pod UID и revision, изменение restart count, last termination reason с exit code, релевантные Events, временно связанный previous log и отличие спецификации, которое объясняет наблюдаемое поведение.
- Я считаю гипотезу подтверждённой, когда независимые источники сходятся по одному экземпляру и интервалу времени, а конкурирующие ветви имеют проверяемое опровержение.
- Events, предыдущие логи и удаляемые Pod недолговечны, поэтому сначала сохраняются минимальные обезличенные факты, а уже затем планируется воздействие.
- {#NAMESPACE}
- {#WORKLOAD}
- {#POD_UID}
- {#CONTAINER}
- {#REVISION}
- container restart count
- current state reason
- last termination reason
- last exit code
- Pod readiness condition
- LLD связывает элементы с неизменяемым Pod UID, а теги события несут namespace, workload, container и revision. Value mapping добавляет русское пояснение reason и exit code, не заменяя исходное значение.
- Эскалация строится по совместному изменению рестартов, причине последнего завершения и состоянию workload; исчезновение discovery entity обрабатывается с периодом сохранения, выбранным по жизненному циклу конкретного кластера.
Отвергнутые альтернативы
- Два канала наблюдения и их границы: kube-state-metrics читает Kubernetes API и публикует нормализованные метрики о желаемом и наблюдаемом состоянии объектов. Он удобен для массового обнаружения Pod и контейнеров, динамики restart count, причин ожидания или завершения и связей с владельцами. Но это не журнал kubelet, не поток событий и не источник stdout приложения; набор метрик также зависит от версии экспортера и разрешённых ресурсов. Прямой collection через Kubernetes API даёт актуальные поля Pod status, containerStatuses, lastState, условия, ownerReferences, Events и ревизии ReplicaSet. Он богаче контекстом, зато требует пагинации, контроля нагрузки, обработки исчезающих объектов и аккуратного RBAC. В Zabbix я использую kube-state-metrics для устойчивых временных рядов и LLD, а API-запрос — для подтверждения состояния и атрибутов инцидента. Если источники расходятся, сравниваю время выборки и UID объекта: имя Pod могло уже перейти к новому экземпляру. Ни один канал по отдельности не доказывает корневую причину завершения процесса.
- Карточка сбоя: рестарты, причина и код выхода: Сначала я фиксирую namespace, Pod UID, имя контейнера и владельца, затем читаю restartCount, текущее state.reason, lastState.terminated.reason и exitCode. Рост restart count важен как изменение на временной шкале, а не как вечное число: рестарты могут относиться к прошлому эпизоду. Текущий reason Waiting/CrashLoopBackOff описывает ожидание следующего запуска, тогда как last termination reason сообщает, чем закончилась предыдущая попытка. Exit code 0 способен означать, что основной процесс штатно выполнил короткую задачу, хотя контроллер ожидает долговечный сервис; ненулевой код требует сопоставления с документацией приложения и логом. Reason OOMKilled усиливает гипотезу о cgroup memory limit, Error остаётся общим обозначением. Я сохраняю startedAt и finishedAt, число попыток и spec контейнера, чтобы отличить мгновенный выход от процесса, который некоторое время обслуживал запросы.
- Как я разделяю действие probes: Startup probe, если задана, удерживает liveness и readiness от исполнения до успешного старта; её повторный провал приводит к перезапуску контейнера. Liveness проверяет, способен ли уже запущенный процесс продолжать работу, и после серии неудач kubelet завершает контейнер. Readiness лишь исключает Pod из endpoint-ов Service и сама по себе контейнер не перезапускает. Поэтому я читаю спецификацию всех probes вместе с Events и состоянием Pod, а не делаю вывод по одному HTTP-коду. Сообщения kubelet о failed liveness или startup probe, Unhealthy и Killing объясняют инициатора рестарта. Провалы только readiness при неизменном restart count указывают на недоступность для трафика, но не объясняют цикл запусков. Дополнительно сверяю путь, порт, scheme, command, initial delay и фактическое время готовности конкретной версии приложения. Я не переношу постоянные тайминги между сервисами: медленная миграция схемы и зависший процесс требуют разных проверок и разных окон наблюдения.
- Events и ревизия возвращают контекст изменения: Kubernetes Events я выбираю по UID вовлечённого объекта и упорядочиваю по времени, учитывая, что это ограниченно хранимые, агрегируемые сообщения, а не вечный аудит. В цепочке могут быть Scheduled, Pulled, Created, Started, Unhealthy, Killing или FailedMount; повторяемость и соседство событий помогают восстановить порядок. Далее прохожу ownerReferences от Pod к ReplicaSet и Deployment. Аннотация deployment.kubernetes.io/revision и hash шаблона позволяют понять, относится ли сбой к новой ревизии, а история rollout — что именно считалось развёрнутым. Я сравниваю проблемный ReplicaSet с предыдущим по image digest, command, args, envFrom, volume mounts, serviceAccountName, probes и ресурсам, не выгружая значения Secret. Важно не обвинять последний rollout автоматически: новый Pod может впервые попасть на проблемный узел или встретить уже недоступную зависимость. Связь считается доказанной, когда временная линия и конкретное отличие ревизии согласуются с termination, Events и логом.
- Память контейнера и давление на узел: Requests участвуют в планировании и классе QoS, а limits ограничивают потребление контейнера; их нельзя трактовать как фактическую память процесса. При превышении memory limit cgroup ядро может завершить процесс, после чего container status нередко содержит OOMKilled и соответствующий exit code. Я сопоставляю этот след с memory usage, working set, limit, журналом kubelet или runtime в разрешённом объёме и моментом роста нагрузки. Node pressure — другая ветвь: условия Node MemoryPressure, решения eviction manager и Events про Evicted показывают нехватку на уровне узла и вытеснение Pod, а не обязательно превышение cgroup конкретного контейнера. Под с eviction может исчезнуть и быть заменён новым, тогда как локальный OOM часто увеличивает restart count внутри того же Pod. У процесса возможен и внутренний отказ выделения памяти без OOM kill, поэтому текст приложения и exit code остаются существенными. Изменять limit наугад опасно: сначала я проверяю профиль, QoS, соседнюю нагрузку и семантику runtime.
- Как отсекаю image pull, конфигурацию и зависимости: Не всякий неработающий Pod находится именно в CrashLoopBackOff. ErrImagePull и ImagePullBackOff возникают до запуска контейнера: restart count может не расти, а Events содержат ошибку registry, digest, авторизации или загрузки. Я проверяю точное state.reason и подтверждаю, был ли процесс вообще создан. Ошибка конфигурации проявляется иначе: отсутствующий ConfigMap или Secret, неверный ключ и невозможный mount часто видны в FailedMount либо CreateContainerConfigError; приложение также может стартовать и завершиться, если обязательная переменная пуста. Недоступная база, DNS, очередь или внешний API обычно оставляют в предыдущем логе сообщения соединения и повторов. Я сверяю EndpointSlice, разрешение имени и доступ из того же namespace только разрешёнными read-only проверками либо заранее согласованным изолированным Pod. Успешный TCP connect ещё не подтверждает протокол, учётные данные или нужную схему. Альтернативу отбрасываю только по положительному свидетельству другой ветви, а не по отсутствию удобной строки.
- Логи становятся доказательством, когда привязаны ко времени: Для перезапускающегося контейнера я беру текущий лог и previous log, если runtime ещё хранит предыдущий экземпляр, с точными timestamps и именем контейнера. У много-контейнерного Pod отдельно проверяю init containers и sidecars: ошибка может принадлежать не тому процессу, который первым попал в карточку. Фрагмент считается доказательством, когда совпадает с finishedAt, exit code, событием kubelet и ревизией. Я не публикую лог целиком. Перед рабочей записью заменяю токены, Authorization headers, cookies, пароли, строки подключения, приватные адреса, персональные данные и значения Secret понятными метками; исходный доступ остаётся в защищённом контуре с ограниченным сроком хранения. Маскирование не должно уничтожить причинную деталь: сохраняю тип исключения, имя параметра без значения, код ответа и корреляционный идентификатор, если он не секретен. Строка stack trace без временной связи остаётся подсказкой, но не окончательным выводом.
- От сигнала Zabbix к проверяемому решению: В Zabbix я строю LLD по namespace, workload, Pod UID и container name, а прототипы items получают restart count, state reason, last termination reason, exit code и revision. Mapping переводит машинные причины в читаемые значения, но сохраняет исходный код. Триггер учитывает изменение рестартов и связанный reason в рамках жизненного цикла UID, чтобы старое значение нового одноимённого Pod не создавало ложную причинность. После сбора доказательств исправление выбирается по ветви: образ, конфигурация, probe, зависимость, ресурс или код процесса. Rollout restart я допускаю только как безопасную проверку после устранения предполагаемой причины, при наличии исправной предыдущей ревизии или иной стратегии отката, достаточной доступности реплик и контрольной точки из метрик, Events и логов. До запуска фиксирую revision и состояние endpoints; после — наблюдаю новые UID и тот же набор признаков. Если симптом повторяется либо доступность ухудшается, останавливаю проверку и возвращаю заранее определённую ревизию. Сам рестарт ничего не лечит и без этих предусловий стирает важные следы.
Конфигурация и сбор
{"correlation_rule": "Эскалация строится по совместному изменению рестартов, причине последнего завершения и состоянию workload; исчезновение discovery entity обрабатывается с периодом сохранения, выбранным по жизненному циклу конкретного кластера.", "discovery_entities": ["{#NAMESPACE}", "{#WORKLOAD}", "{#POD_UID}", "{#CONTAINER}", "{#REVISION}"], "item_prototypes": ["container restart count", "current state reason", "last termination reason", "last exit code", "Pod readiness condition"], "mapping_rule": "LLD связывает элементы с неизменяемым Pod UID, а теги события несут namespace, workload, container и revision. Value mapping добавляет русское пояснение reason и exit code, не заменяя исходное значение."}
Контролируемая проверка и свидетельство
До контролируемого изменения я фиксирую Deployment revision, image digest, число доступных реплик, endpoints, restart count и контрольный временной интервал. Предусловия rollout restart: предполагаемая причина уже устранена, стратегия Deployment не создаёт неприемлемую потерю доступности, предыдущая рабочая ревизия доступна, команда отката согласована, а наблюдатель не совмещает проверку с другим rollout. Контрольная точка — появление новых Pod UID с ожидаемой revision, готовность endpoints и отсутствие прежней комбинации termination, Events и лога. При повторении сбоя или деградации выполняется остановка rollout либо возврат к зафиксированной ревизии согласно регламенту владельца. Перезапуск без изменения причины не считается исправлением.
Фрагмент рабочего регламента
- Зафиксировать namespace, Pod UID, container, ownerReferences и текущую revision до исчезновения объекта.
- Сопоставить restartCount, state.reason, lastState.terminated.reason, exitCode и времена старта и завершения.
- Прочитать Events по UID, конфигурацию probes и отличия текущего ReplicaSet от предыдущего.
- Проверить requests, limits, QoS, признаки cgroup OOM, Node conditions и eviction как отдельные ветви.
- Получить current и previous logs, связать строки со временем и отредактировать секретные данные.
- Исключить image pull, mount, конфигурацию и зависимость на основании наблюдаемых признаков.
- Сохранить контрольную точку Zabbix и план отката до любого согласованного изменения.
Откат
До контролируемого изменения я фиксирую Deployment revision, image digest, число доступных реплик, endpoints, restart count и контрольный временной интервал. Предусловия rollout restart: предполагаемая причина уже устранена, стратегия Deployment не создаёт неприемлемую потерю доступности, предыдущая рабочая ревизия доступна, команда отката согласована, а наблюдатель не совмещает проверку с другим rollout. Контрольная точка — появление новых Pod UID с ожидаемой revision, готовность endpoints и отсутствие прежней комбинации termination, Events и лога. При повторении сбоя или деградации выполняется остановка rollout либо возврат к зафиксированной ревизии согласно регламенту владельца. Перезапуск без изменения причины не считается исправлением.
Ограничения вывода
Набор доступных метрик зависит от версий Kubernetes, kube-state-metrics, container runtime и шаблона Zabbix. Причины в status могут быть общими, Events удаляются, previous log теряется после дополнительных замен контейнера, а managed-платформа может ограничивать журналы узла. Поэтому я не задаю одинаковое число рестартов, интервал probe, лимит памяти или время ожидания для всех workloads. Вывод переносится только после проверки UID, revision, политики хранения и рабочего профиля конкретного сервиса.
Источники
- Kubernetes: Container Lifecycle Hooks and States: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#container-states
- Kubernetes: Configure Liveness, Readiness and Startup Probes: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/
- Kubernetes: Resource Management for Pods and Containers: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
- Zabbix: Kubernetes monitoring: https://www.zabbix.com/integrations/kubernetes
- Zabbix: Low-level discovery: https://www.zabbix.com/documentation/current/en/manual/discovery/low_level_discovery

