Сбор логов в Kubernetes: Fluent Bit, Promtail и архитектура DaemonSet на практике | AdminWiki

Сбор логов в Kubernetes: Fluent Bit, Promtail и архитектура DaemonSet на практике

30 сентября 2026 12 мин. чтения

Контейнеры в Kubernetes пишут логи в stdout и stderr, а рантайм (containerd или CRI-O) сохраняет эти потоки на ноду в файлах /var/log/pods/<namespace>_<pod>_<uid>/<container>/0.log. Каждая строка такого файла - отдельный JSON-объект. Читает их агент, запущенный по одному поду на каждую ноду в режиме DaemonSet, и отправляет дальше в Loki, Elasticsearch или OpenSearch.

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

Дальше разбираем три архитектуры сбора, сравниваем Fluent Bit и Promtail (и упоминаем Grafana Alloy), показываем конфиги DaemonSet, формат CRI-логов и ротацию, склейку Java stack trace, обогащение метаданными pod, namespace и labels, а также sidecar для приложений со своим лог-файлом.

Почему DaemonSet, а не Deployment или sidecar по умолчанию

Логи лежат на ноде, а не внутри пода приложения. Чтобы их прочитать, агент должен видеть каталог /var/log/pods через hostPath. Deployment так не умеет: его под не привязан к конкретному узлу. DaemonSet закрывает задачу напрямую.

ПодходКогда использоватьМинусы
DaemonSetСтандартный сбор stdout и stderr всех подов на нодеНужны tolerations для control-plane и RBAC на чтение метаданных pod
SidecarПриложение пишет в собственный файл, нужен отдельный парсерДополнительные 50-100 МБ RAM на каждый под, дублирование конфигов
DeploymentЦентрализованная обработка потока: агрегация, маршрутизацияНе читает локальные файлы ноды напрямую

Чтобы агент запустился и на control-plane, добавьте toleration node-role.kubernetes.io/control-plane:NoSchedule. Для чтения метаданных pod достаточно ServiceAccount с правами get, list, watch на pods и namespaces.

Fluent Bit или Promtail: что выбрать в 2026 году

Оба агента читают файлы логов и отправляют записи в хранилище, но устроены по-разному и заточены под разные бэкенды. Fluent Bit написан на C, потребляет мало памяти и умеет писать в Elasticsearch, OpenSearch, Kafka, S3 и Loki. Promtail написан на Go, глубоко интегрирован с Grafana Loki и повторяет стиль конфигов Prometheus.

КритерийFluent BitPromtail
ЯзыкCGo
Потребление RAM на нодуоколо 10-30 МБоколо 50-100 МБ
БэкендыLoki, Elasticsearch, OpenSearch, Kafka, S3Loki
МультистрочностьMultiline.parser в tailpipeline_stages multiline
Фильтры и трансформацииmodify, grep, nest, kubernetes, luapipeline_stages, relabel_configs
Service discoverykubernetes filterkubernetes_sd_configs

Значения по памяти - ориентир для типичной инсталляции, а не гарантия. Реальное потребление зависит от числа подов, объёма потока и набора включённых фильтров. Сверяйтесь с метриками агента на вашем кластере.

Отдельно про статус инструментов. В публичной документации Grafana для новых установок Loki продвигается единый агент Grafana Alloy, который наследует pipeline_stages от Promtail и работает на конфиге River. Первичных источников, подтверждающих это для вашей версии, у нас нет, поэтому сверьте актуальный статус Promtail и Alloy в документации Grafana перед стартом. Действующие конфиги Promtail при этом продолжают работать, и их много в продакшене.

Когда Fluent Bit выигрывает

Логи уходят в Elasticsearch, OpenSearch или Kafka, а не только в Loki. В этом случае нативный output избавляет от промежуточного сервиса. Встроенные фильтры modify, grep и nest позволяют разбирать и преобразовывать записи прямо в агенте, без внешнего обработчика. При кластере в 200 нод разница в памяти становится заметной: Fluent Bit держит около 15 МБ RSS на ноду, Promtail на той же нагрузке потребляет ближе к 80 МБ, то есть на весь парк это несколько гигабайт разницы.

Когда Promtail (или Grafana Alloy) удобнее

Вы уже держите Grafana и Loki. Тогда конфиг ближе к стилю Prometheus, pipeline_stages читаемы, а service discovery через kubernetes_sd_configs даёт метаданные pod без дополнительных плагинов. Подробный разбор агентов под Loki, включая Alloy и Vector, мы собрали в материале про сбор логов в Grafana Loki. Общая логика выбора такая: простые пайплайны и бэкенд Loki - Promtail или Alloy, а Elasticsearch, OpenSearch, Kafka и сложные трансформации - Fluent Bit. Если ещё определяетесь с хранилищем, пригодится сравнение ELK, Loki и Graylog по стоимости и сложности.

Разворачиваем Fluent Bit как DaemonSet: манифест и проверка

Порядок действий: ServiceAccount и RBAC, ConfigMap с конфигами, DaemonSet с томами и tolerations. Такой же подход разобран в руководстве по настройке Fluent Bit и Filebeat в Docker и Kubernetes, где добавлен расчёт дискового пространства под логи.

Какие тома и права реально нужны агенту

  • hostPath /var/log/pods, режим readOnly - основной источник логов.
  • hostPath /var/lib/containerd/io.containerd.grpc.v1.cri/containers, режим readOnly - нужен, если фильтру требуются данные по container ID.
  • hostPath /etc/machine-id, режим readOnly - даёт уникальный идентификатор ноды.
  • RBAC: только get, list, watch на pods и namespaces.

Монтировать docker.sock не нужно, режим privileged: true не требуётся. Минимального набора тома и прав достаточно для чтения stdout и stderr.

apiVersion: v1
kind: ServiceAccount
metadata:
  name: fluent-bit
  namespace: logging
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: fluent-bit-read
rules:
  - apiGroups: [""]
    resources: ["pods", "namespaces"]
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: fluent-bit-read
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: fluent-bit-read
subjects:
  - kind: ServiceAccount
    name: fluent-bit
    namespace: logging

ConfigMap с fluent-bit.conf:

[SERVICE]
    Flush         1
    Log_Level     info
    Parsers_File  /fluent-bit/etc/parsers.conf
    HTTP_Server   On
    HTTP_Listen   0.0.0.0
    HTTP_Port     2020

[INPUT]
    Name              tail
    Path              /var/log/pods/*/*/*.log
    Tag               kube.*
    Parser            cri
    DB                /var/log/flb_kube.db
    Mem_Buf_Limit     50MB
    Skip_Long_Lines   On
    Refresh_Interval  10
    Rotate_Wait       5

[FILTER]
    Name                kubernetes
    Match               kube.*
    Kube_URL            https://kubernetes.default.svc:443
    Kube_CA_File        /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
    Kube_Token_File     /var/run/secrets/kubernetes.io/serviceaccount/token
    Kube_Tag_Prefix     kube.var.log.pods.
    Merge_Log           On
    Keep_Log            Off
    Labels              On
    Annotations         Off

[OUTPUT]
    Name        loki
    Match       kube.*
    Host        loki-gateway.logging.svc.cluster.local
    Port        80
    Labels      job=fluent-bit,namespace=$kubernetes['namespace_name'],app=$kubernetes['labels']['app'],container=$kubernetes['container_name']

parsers.conf с разбором строки CRI:

[PARSER]
    Name        cri
    Format      regex
    Regex       ^(?[^ ]+) (?stdout|stderr) (?[^ ]*) (?.*)$
    Time_Key    time
    Time_Format %Y-%m-%dT%H:%M:%S.%L%z

Фрагмент DaemonSet с томами, tolerations и лимитами:

volumes:
  - name: varlogpods
    hostPath:
      path: /var/log/pods
  - name: varlibcontainerd
    hostPath:
      path: /var/lib/containerd/io.containerd.grpc.v1.cri/containers
  - name: machineid
    hostPath:
      path: /etc/machine-id
  - name: config
    configMap:
      name: fluent-bit-config

volumeMounts:
  - name: varlogpods
    mountPath: /var/log/pods
    readOnly: true
  - name: varlibcontainerd
    mountPath: /var/lib/containerd/io.containerd.grpc.v1.cri/containers
    readOnly: true
  - name: machineid
    mountPath: /etc/machine-id
    readOnly: true

tolerations:
  - key: node-role.kubernetes.io/control-plane
    operator: Exists
    effect: NoSchedule

resources:
  requests:
    cpu: 50m
    memory: 64Mi
  limits:
    memory: 256Mi

Проверка, что агент видит логи на каждой ноде

kubectl get ds -n logging
kubectl get pods -n logging -o wide
kubectl logs -n logging -l app=fluent-bit --tail=50
kubectl exec -n logging <fluent-bit-pod> -- ls /var/log/pods | head
kubectl exec -n logging <fluent-bit-pod> -- tail -n 5 /var/log/pods/<ns>_<pod>_<uid>/<container>/0.log
curl http://<pod-ip>:2020/api/v1/metrics

Вывод kubectl get pods -o wide показывает, что на каждой ноде ровно один под агента. Команда tail по умолчанию печатает последние 10 строк файла и входит в состав GNU coreutils в Ubuntu, поэтому доступна прямо внутри контейнера агента (источник). Если нужно следить за файлом в реальном времени, добавьте -f и фильтр: цепочка вида tail -f log | grep --line-buffered 'X' | grep -v 'Y' не будет выглядеть зависшей, тогда как без буферизации два grep подряд могут надолго молчать.

Разворачиваем Promtail как DaemonSet: конфиг и проверка

Логика та же, меняется конфиг и клиент. Сначала ServiceAccount с ClusterRole на pods, namespaces и nodes, затем ConfigMap с promtail.yaml, затем DaemonSet с hostPath /var/log/pods.

relabel_configs: как связать файл на ноде с pod и namespace

Promtail не угадывает, какому поду принадлежит файл. Блок kubernetes_sd_configs с role: pod отдаёт список подов с набором служебных меток __meta_kubernetes_pod_name, __meta_kubernetes_namespace и __meta_kubernetes_pod_label_*. Правила relabel_configs переносят их в обычные labels, а из __meta_kubernetes_pod_uid и имени контейнера собирается путь файла. Так одна метка __path__ превращается в конкретный файл /var/log/pods/<uid>/<container>/0.log.

server:
  http_listen_port: 3101

positions:
  filename: /run/promtail/positions.yaml

clients:
  - url: http://loki-gateway.logging.svc.cluster.local/loki/api/v1/push

scrape_configs:
  - job_name: kubernetes-pods
    kubernetes_sd_configs:
      - role: pod
    relabel_configs:
      - source_labels: [__meta_kubernetes_pod_node_name]
        target_label: __host__
      - action: labelmap
        regex: __meta_kubernetes_pod_label_(.+)
      - source_labels: [__meta_kubernetes_namespace]
        target_label: namespace
      - source_labels: [__meta_kubernetes_pod_name]
        target_label: pod
      - source_labels: [__meta_kubernetes_pod_container_name]
        target_label: container
      - source_labels: [__meta_kubernetes_pod_uid, __meta_kubernetes_pod_container_name]
        separator: /
        replacement: /var/log/pods/*$1/*.log
        target_label: __path__
    pipeline_stages:
      - cri: {}
      - multiline:
          firstline: '^\d{4}-\d{2}-\d{2}'
          max_wait_time: 3s
          max_lines: 1000

Проверка:

kubectl get ds -n logging
kubectl logs -n logging -l app=promtail --tail=50
curl http://<pod-ip>:3101/metrics | grep promtail_sent_bytes_total

Метрика promtail_sent_bytes_total растёт, когда записи реально уходят в Loki. Если она стоит на нуле, проверьте, что клиент указывает на рабочий адрес и что positions.yaml пишется в доступный каталог.

Формат логов контейнеров и ротация: что нужно знать агенту

Рантайм хранит логи как JSON, по одному объекту на строку. Типичная запись выглядит так:

{"log":"2026-09-30T12:00:00.123Z INFO started\n","stream":"stdout","time":"2026-09-30T12:00:00.123456789Z"}

Путь строится из namespace, имени pod, UID и имени контейнера: /var/log/pods/<namespace>_<pod>_<uid>/<container>/0.log. Число в начале имени файла - номер попытки запуска контейнера: после рестарта появляется 1.log и далее. Поле stream различает stdout и stderr, поле time хранит метку рантайма, а поле log содержит саму строку приложения.

Как не потерять логи при ротации

Ротацией управляет kubelet через параметры containerLogMaxSize (по умолчанию 10Mi) и containerLogMaxFiles (по умолчанию 5). Для шумных приложений поднимите первое значение до 50-100Mi, а второе до 10. Параметры задаются в конфиге kubelet; при установке пакетов DEB или RPM им управляет systemd, а kubeadm позволяет распространить настройку на все узлы через структурированный объект и добавить индивидуальные правки для отдельных нод (источник).

При ротации создаются файлы вида 0.log.20260101-120000, и агент должен переоткрывать поток. Опция -F у tail переоткрывает файл после ротации или удаления, тогда как -f продолжает держать старый дескриптор и теряет новые строки (источник). В Fluent Bit за это отвечает опция Rotate_Wait в плагине tail вместе с базой DB, которая запоминает позицию. В Promtail переоткрытие встроено, но убедитесь, что маска __path__ покрывает и ротированные файлы. Практическая проверка: во время тестовой ротации выполните tail -F по активному файлу внутри пода агента и убедитесь, что новые строки продолжают поступать.

Мультистрочные логи Java: склеиваем stack trace в одно событие

log4j и logback пишут исключение многострочно: первая строка с датой и уровнем, затем десятки строк с at, Caused by и ... N more. CRI сохраняет каждую строку отдельным объектом, и в Loki или Elasticsearch приходит набор разрозненных записей. Решение - multiline-парсер на стороне агента, который склеивает продолжение в одно событие.

Готовый regex для log4j и logback

Для Fluent Bit объявите MULTILINE_PARSER и подключите его к tail через multiline.parser:

[MULTILINE_PARSER]
    Name          java
    Type          regex
    Flush_timeout 3
    Rule          "start_state"  "/(^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})/"      "cont"
    Rule          "cont"         "/^\s+at |^Caused by:|^\s+\.\.\. \d+ more/"       "cont"

В блоке [INPUT] добавьте строку multiline.parser java. Для logback с форматом ISO8601 начало строки другое, поэтому firstline принимает вид ^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}. В Promtail то же самое делается стадией multiline в pipeline_stages:

pipeline_stages:
  - multiline:
      firstline: '^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}'
      max_wait_time: 3s
      max_lines: 1000

Если приложение уже пишет структурированный JSON, склейка не нужна: одна строка лога уже содержит полное событие, и достаточно JSON-парсера.

Как проверить, что событие склеилось

  • Включите log_level debug у агента, выполните tail -f по файлу с исключением и посмотрите, что на выход уходит одна запись, а не десять.
  • В Loki выполните запрос {namespace="test"} |= "Exception" и убедитесь, что весь stack trace виден в одной записи.
  • Следите за метрикой multiline_errors_total: она должна оставаться на нуле, иначе правило не совпадает с реальным форматом строк.

Обогащение логов метаданными pod, namespace и labels

Без метаданных поиск логов сводится к текстовому grep по всему потоку. Метаданные дают фильтрацию по namespace, приложению и контейнеру. В Fluent Bit это делает фильтр kubernetes: он сопоставляет тег записи с подом и добавляет поля namespace_name, pod_name, container_name и labels при Merge_Log On и Labels On. В Promtail эту роль выполняют relabel_configs, которые переносят __meta_kubernetes_namespace, __meta_kubernetes_pod_name и __meta_kubernetes_pod_label_app в обычные labels. Примеры обогащения на стороне агента вместе с отправкой JSON-логов разобраны в кейсе по централизованному сбору логов микросервисов.

Какие labels добавлять, а какие нет

Правило простое: в label выносите то, по чему фильтруете в большинстве запросов. Подходящие кандидаты - namespace, app, container, node, env. Опасные - pod_uid, container_id, request_id, trace_id, user_id: их значения уникальны для каждой записи или пода. В Loki labels индексируются, и 5000 подов на 10 меток дают 50 000 активных серий, что уже перегружает индекс. Такую информацию держите как поля документа, а не как labels. В фильтре kubernetes это означает Annotations Off: аннотации часто меняются и раздувают кардинальность. Проверка простая: в Grafana выполните {namespace="prod", app="api"} и убедитесь, что возвращаются только нужные логи, а число серий в метриках Loki не растёт лавинообразно.

Sidecar для приложений с нестандартными лог-файлами

DaemonSet не поможет, когда приложение пишет не в stdout, а в собственный файл внутри своего контейнера. Типичные случаи: legacy-сервис пишет в /var/log/app/app.log, приложение не поддерживает вывод в stdout, либо нужен отдельный парсер под специфичный формат. Тогда рядом с приложением ставят второй контейнер-агент, и оба монтируют общий том.

Манифест sidecar-пода: app + Fluent Bit

  • Тома: shared-logs (emptyDir) для логов и config (ConfigMap) для конфига агента.
  • Контейнер app монтирует shared-logs в /var/log/app и пишет туда файлы.
  • Контейнер fluent-bit монтирует shared-logs в /var/log/app и config в /fluent-bit/etc/.
  • Input агента: tail, path /var/log/app/*.log, tag app.log. Output: Loki или Elasticsearch.

Проверка: kubectl logs <pod> -c fluent-bit. Такой подход стоит дополнительных 50-100 МБ RAM на под, усложняет обновление агента и дублирует конфиги. Прежде чем идти в sidecar, попробуйте заставить приложение писать в stdout, например через symlink на /dev/stdout или параметр логирования в файл вида /dev/stdout. Общий конвейер, где логи и метрики идут по одному маршруту, описан в материале про маршрутизацию логов и метрик.

Чек-лист: проверяем пайплайн сбора логов перед продакшеном

  1. DaemonSet запущен на всех нодах: kubectl get pods -o wide показывает по одному поду агента на узел.
  2. Агент видит /var/log/pods: ls внутри пода возвращает каталоги с логами.
  3. Ротация настроена: заданы containerLogMaxSize и containerLogMaxFiles под реальный объём логов.
  4. Multiline-парсер протестирован на настоящем stack trace, метрика multiline_errors_total на нуле.
  5. Labels не раздувают кардинальность: в них нет pod UID, container ID и request ID.
  6. Метрики агента собираются: Fluent Bit на порту 2020, Promtail на 3101.
  7. Настроен алерт на отсутствие логов от критичного namespace.
  8. Заданы requests и limits, чтобы агент не вытеснялся и не съедал память ноды.
  9. RBAC минимальный: только get, list, watch на pods и namespaces, без privileged и docker.sock.
  10. Конфиг хранится в Git, а не правится в ConfigMap руками, с автоматическим применением.

Если в кластере используется Promtail, отдельно зафиксируйте план миграции на Grafana Alloy: синтаксис pipeline_stages сохраняется, но конфиг верхнего уровня меняется на River. Начните с одного namespace, прогоните полный путь до бэкенда и только потом раскатывайте агент на весь кластер.

Поделиться: