5. Observability в Kubernetes

Эксплуатация телеметрии в динамических средах

«Цель вычислений — понимание сути явлений, а не просто числа».
— Ричард Хэмминг

В предыдущих статьях мы рассматривали Observability абстрактно: библиотеки Go, локальные сокеты и сетевые вызовы. Но в реальности подавляющее большинство современных Go-бэкендов развертывается и масштабируется в среде Kubernetes (K8s).

Kubernetes переворачивает привычные правила игры с ног на голову. Здесь больше нет постоянных серверов с фиксированными IP-адресами, настроенных раз и навсегда. Поды рождаются, перемещаются планировщиком между физическими нодами, деградируют и уничтожаются за считанные секунды. В таком эфемерном океане контейнеров любая статическая конфигурация обречена на провал.

Разберем, как выстроить надежный контур наблюдаемости в Kubernetes, не разорившись на ресурсах и не потеряв контроль над поведением сервисов.


Динамический мир Kubernetes

Kubernetes создает принципиально новые вызовы для системной инженерии:

  1. Эфемерность: Жизненный цикл пода измеряется часами или днями. Попытка мониторить под как отдельный «хост» приводит к взрывному раздуванию временных рядов (Series Churn).
  2. Многоуровневость (Multi-Tenancy): В одном физическом кластере соседствуют десятки изолированных неймспейсов (Namespaces), делящие общий пул процессоров и памяти.
  3. Виртуализация ресурсов (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)

В каждый под вашего приложения добавляется второй контейнер — логгер.

  • Принцип: Оба контейнера монтируют общий emptyDir volume. Приложение пишет в файл, 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).


Итог

  1. Prometheus Operator — стандарт управления мониторингом в K8s через CRD (ServiceMonitor).
  2. DaemonSet — эффективный способ сбора логов с нод.
  3. cAdvisor — источник метрик контейнеров (CPU/Mem).
  4. В Go 1.19+ runtime «подружился» с cgroups, автоматически учитывая лимиты памяти контейнера.

В следующей, заключительной статье раздела, мы подведем итоги и сформулируем, что значит быть Observable System: 6. Итоги раздела. Observable system.