5. Observability в Kubernetes
Эксплуатация телеметрии в динамических средах
«Цель вычислений — понимание сути явлений, а не просто числа».
— Ричард Хэмминг
В предыдущих статьях мы рассматривали Observability абстрактно: библиотеки Go, локальные сокеты и сетевые вызовы. Но в реальности подавляющее большинство современных Go-бэкендов развертывается и масштабируется в среде Kubernetes (K8s).
Kubernetes переворачивает привычные правила игры с ног на голову. Здесь больше нет постоянных серверов с фиксированными IP-адресами, настроенных раз и навсегда. Поды рождаются, перемещаются планировщиком между физическими нодами, деградируют и уничтожаются за считанные секунды. В таком эфемерном океане контейнеров любая статическая конфигурация обречена на провал.
Разберем, как выстроить надежный контур наблюдаемости в Kubernetes, не разорившись на ресурсах и не потеряв контроль над поведением сервисов.
Динамический мир Kubernetes
Kubernetes создает принципиально новые вызовы для системной инженерии:
- Эфемерность: Жизненный цикл пода измеряется часами или днями. Попытка мониторить под как отдельный «хост» приводит к взрывному раздуванию временных рядов (Series Churn).
- Многоуровневость (Multi-Tenancy): В одном физическом кластере соседствуют десятки изолированных неймспейсов (Namespaces), делящие общий пул процессоров и памяти.
- Виртуализация ресурсов (Cgroups): Рантайм приложения изолирован механизмом Linux cgroups, что напрямую влияет на поведение сборщика мусора Go и планировщика операционной системы.
Архитектура Observability в K8s
Классический подход «Push» (агент внутри приложения отправляет метрики) в K8s менее популярен, чем подход Pull с использованием Prometheus Operator.
flowchart TD
subgraph A ["Kubernetes Cluster"]
A1["Go Application Pod"] -->|"Expose /metrics"| A2["Service Monitor"]
A3["Prometheus Operator"] -->|"Reads CRD"| A2
A3 -->|"Configures"| A4["Prometheus Server"]
A1 -->|"stdout"| A5["Container Log"]
A6["DaemonSet<br>Promtail/FluentBit"] -->|"Read"| A5
end
subgraph B ["Infrastructure"]
B1["Loki / Tempo"]
end
A4 -->|"Remote Write"| B1
A6 -->|"Push Logs"| B1
style A fill:#1e293b,stroke:#38bdf8,stroke-width:2px,color:#fff
style A1 fill:#312e81,stroke:#818cf8,stroke-width:2px,color:#fff
style A6 fill:#78350f,stroke:#f59e0b,stroke-width:2px,color:#fff
style B fill:#0f172a,stroke:#475569,stroke-width:1px,color:#fff
Prometheus Operator
Это стандарт де-факто для мониторинга в K8s. Вместо того чтобы вручную править конфиг prometheus.yml и перезагружать контейнер при каждом добавлении нового сервиса, Operator делает это автоматически.
- CRD (Custom Resource Definition): Вы создаете ресурс
ServiceMonitor, который говорит: «Следи за всеми сервисами, у которых есть лейблapp=backend». - Service Discovery: Prometheus сам находит все поды, попадающие под критерий, и начинает их скрейпить.
Prometheus Operator непрерывно отслеживает Kubernetes API и автоматически генерирует конфиг скрейпинга для ядра Prometheus при любом изменении подов или сервисов.
Логирование: Sidecar vs DaemonSet
В статье 4. Централизованные логи мы определили, что приложения должны писать в stdout. В K8s вопрос в том, кто эти логи забирает.
1. DaemonSet (Agent on Node)
На каждой ноде кластера запускается один под-агент (например, Promtail или Fluent Bit).
- Принцип: Агент монтирует директорию
/var/logс хоста, читает файлы контейнеров и отправляет в Loki. - Плюсы: Эффективно. Не потребляет ресурсы приложения. Один процесс на ноду.
- Минусы: Нельзя парсить логи приложений по-разному на уровне приложения (конфиг общий).
2. Sidecar (Agent in Pod)
В каждый под вашего приложения добавляется второй контейнер — логгер.
- Принцип: Оба контейнера монтируют общий
emptyDirvolume. Приложение пишет в файл, sidecar читает и отправляет. - Плюсы: Максимальная гибкость (каждый микросервис может иметь свой формат/токен).
- Минусы: Расточительно. Если у вас 100 подов, у вас 100 лишних процессов. Это увеличивает потребление памяти и CPU.
Under the Hood: Container Metrics (cAdvisor)
Kubernetes сам по себе предоставляет метрики через Metrics Server, но для детального Observability мы используем cAdvisor (встроен в Kubelet).
Go-разработчику метрики, которые вы видите в Grafana (CPU, Memory Usage), — это не то, что показывает top внутри контейнера.
- CPU: В Linux контейнерах CPU измеряется в Cores (millicores).
100m= 0.1 ядра. Метрикаcontainer_cpu_usage_seconds_total— это кумулятивное время, использованное контейнером. - Memory: Есть
working_set_bytes(RSS + Page Cache) иusage_bytes. OOM Killer в K8s смотрит наworking_set_bytes. Если ваше Go-приложение аллоцирует много памяти, которая не используется активно, но не освобождена (RSS), вы получите OOM Kill, даже если метрикаusageказалась нормальной.
Cost and Scale: Cardinality в K8s
В Kubernetes проблема кардинальности (обсуждавшаяся в статье 4. Cardinality и стоимость метрик) обостряется.
Если вы добавите лейбл pod_name в метрику, а потом сделаете RollingUpdate деплоя, K8s создаст новые поды с новыми именами.
Prometheus будет создавать новые временные ряды для каждого нового пода. Старые ряды будут «гнить» (stale), но занимать место.
Каждый деплой создает тысячи устаревших серий в памяти TSDB.
Best Practice:
Никогда не используйте лейблы, которые меняются при редеплое (pod_name, container_id, instance_ip) в метриках высокого кардинальности. Используйте статические лейблы (deployment, service, namespace).
Итог
- Prometheus Operator — стандарт управления мониторингом в K8s через CRD (
ServiceMonitor). - DaemonSet — эффективный способ сбора логов с нод.
- cAdvisor — источник метрик контейнеров (CPU/Mem).
- В Go 1.19+ runtime «подружился» с cgroups, автоматически учитывая лимиты памяти контейнера.
В следующей, заключительной статье раздела, мы подведем итоги и сформулируем, что значит быть Observable System: 6. Итоги раздела. Observable system.