Новинка Виртуальный VDS сервер в Финляндии от 390р
Выберите продукт

Loki без кардинальности боли: пайплайны, парсинг и контроль меток

Кардинальность меток в Loki — ключевая боль: множатся потоки, тормозят запросы, растут счета. Разбираем на практике, как проектировать метки, собирать логи через Promtail, строить пайплайны парсинга и держать кардинальность под контролем.
Loki без кардинальности боли: пайплайны, парсинг и контроль меток

Когда Loki только внедряют, его любят за дешёвое хранение, быстрые запросы и знакомую философию меток. Но именно метки чаще всего «стреляют в ногу»: один невинный user_id или request_id в лейблах — и вы получаете взрыв кардинальности, миллионы потоков, длинные внутренние списки и медленные запросы. В этой статье разберёмся, как проектировать метки, какие поля держать в теле логов, как строить пайплайны Promtail для безопасного парсинга и редактирования, и как проверять кардинальность до того, как она станет проблемой.

Как Loki мыслит: потоки, метки и их цена

В Loki каждая уникальная комбинация меток определяет «поток» (stream). Слишком подробные метки множат потоки, повышают накладные расходы на индексацию и ухудшают производительность запросов. Поэтому основная стратегия — держать в метках только низкокардинальные и стабильные признаки, а всё изменчивое и уникальное оставлять в теле сообщения и разбирать на этапе запроса.

Безопасные примеры для меток: job, env, service, instance, region, укрупнённый status (например, HTTP-код), тип события. Опасные примеры: user_id, request_id, session, сырой URL, UUID, IP-адрес целиком, путь с динамическими сегментами.

Жёсткое правило: метки — это измерения для агрегации; всё, что уникально на каждую запись, — оставляйте в тексте и разбирайте на запросе.

Пайплайны Promtail: контур управления кардинальностью

Promtail — первый рубеж. Именно здесь удобно нормализовать сообщения, вырезать шум/секреты, извлекать поля и выборочно повышать их до меток. При этом важно «поднимать» в метки только те поля, которые действительно стабильны и полезны в агрегировании.

Минимальный скелет конфига Promtail с пайплайном:

scrape_configs:
- job_name: apps
  static_configs:
  - targets: [localhost]
    labels:
      job: apps
      env: prod
      __path__: /var/log/apps/*.log
  pipeline_stages:
  - json:
      expressions:
        ts: time
        level: level
        msg: message
        path: http.path
        status: http.status
        ip: client_ip
  - labels:
      level:
      status:
  - template:
      source: path
      template: '{{ RegexReplaceAll "[0-9a-fA-F-]{8,}" ":id" .Value }}'
  - output:
      source: msg
  • json извлекает поля из структурированного сообщения.
  • labels поднимает только низкокардинальные метки — уровень и HTTP-статус.
  • template нормализует путь, заменяя любые похожие на ID сегменты на :id. Это оставит сырой уникальный путь в тексте безопасным для дальнейшего разбора на запросе.
  • output назначает «чистое» поле сообщения, которое попадёт в Loki.

Пример пайплайна Promtail с нормализацией и безопасными метками

Парсинг: JSON, logfmt и регулярки без боли

Если ваши логи уже структурированы (JSON, logfmt), используйте соответствующие стадии. Это быстрее и стабильнее, чем универсальные регулярки.

JSON

pipeline_stages:
- json:
    expressions:
      level: level
      msg: message
      status: http.status
      route: http.route
- labels:
    level:
    status:

Храните в метках только level и укрупнённый status. Поле http.route часто содержит динамику — оставьте его в сообщении или нормализуйте.

logfmt

pipeline_stages:
- logfmt:
- labels:
    level:
    status:

Стадия logfmt сама распознаёт пары ключ=значение. Выбирайте, что поднимать в метки. Избыточное количество имён в метках увеличит «ширину» потока.

Regex: только для нормализации и извлечения

Регулярки используйте точечно: вырезать/обезличить, выделить несколько ключей. Не пытайтесь ими «разобрать всё» — это и медленнее, и ломается чаще при изменениях формата.

pipeline_stages:
- regex:
    expression: 'path=(?P<path>\S+) status=(?P<status>\d+)'
- template:
    source: path
    template: '{{ RegexReplaceAll "/[0-9]+" "/:num" .Value }}'
- labels:
    status:

Если у вас много Nginx-доступов, подумайте и про IO профилирование логов и кешей — см. разбор временных каталогов Nginx и tmpfs для IO.

Ветвления, отбрасывание шума и редактирование

Условные пайплайны позволяют чисто разделять обработку разных типов сообщений в одном файле.

pipeline_stages:
- match:
    selector: '{job="apps"}'
    stages:
    - match:
        selector: '{level="debug"}'
        action: drop
    - json:
        expressions:
          level: level
          msg: message
          status: http.status
    - labels:
        level:
        status:
    - drop:
        source: msg
        expression: "^healthcheck$"

Стадия drop полезна для шумных healthcheck-строк, а ветвление по селектору — чтобы применять разные парсеры к разным источникам.

Для маскировки секретов используйте replace:

pipeline_stages:
- replace:
    expression: 'password=\S+'
    replace: 'password=***'

Контроль меток до отправки: relabel_configs

Помимо пайплайнов, в Promtail есть relabel_configs — мощный слой для манипуляций с метками ещё до попадания в пайплайн или после него (в зависимости от места определения). Это лучший способ отрезать лишнее, переименовать и привести набор меток к «бюджету» кардинальности.

scrape_configs:
- job_name: apps
  static_configs:
  - targets: [localhost]
    labels:
      job: apps
      env: prod
      __path__: /var/log/apps/*.log
  relabel_configs:
  - action: labeldrop
    regex: (filename|stream|container_id)
  - action: replace
    source_labels: [env]
    target_label: environment
  - action: labelkeep
    regex: (job|environment|instance)

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

Ограничения на стороне Loki

Даже при аккуратных пайплайнах полезно иметь «страховку» в конфиге Loki. Лимиты позволяют прерывать неконтролируемое разрастание потоков и отбрасывать слишком большие строки.

limits_config:
  max_streams_per_user: 50000
  ingestion_rate_mb: 4
  ingestion_burst_size_mb: 8
  max_line_size: 256kb
  reject_old_samples: true
  reject_old_samples_max_age: 168h

Ключевая настройка — max_streams_per_user: если кто-то начнёт генерировать уникальные метки на каждый запрос, инжест сразу остановится с ошибкой, вместо того чтобы тихо «съесть» всё место и ресурсы. max_line_size помогает отсекать случайно зашедшие дампы или трейс-лупы.

Если разворачиваете Loki самостоятельно, удобнее разместить его на изолированном узле с гарантированным CPU/IO — подойдёт VDS с SSD и достаточным IOPS. Так проще выдерживать пики инжеста и хранение индекса.

FastFox VDS
Облачный VDS-сервер
Виртуальные серверы с быстрым запуском и гибкой конфигурацией от 390₽ / мес
Доступные локации
Россия Нидерланды Финляндия

Проверяем кардинальность до продакшена

Главная техника: не поднимайте поле в метки, пока не проверите его будущую кардинальность с помощью запросного парсинга. В LogQL можно парсить на лету и агрегировать как будто это метка — без изменения хранения.

JSON-пример

sum by (status) (
  count_over_time(( {job="apps"} | json | status!="" )[5m])
)

Так вы увидите распределение по status без поднятия этого поля в лейблы. Если оно стабильное (например, 200/404/500), можно смело делать из него метку.

Парсинг Nginx-доступов

sum by (status) (
  count_over_time(( {job="nginx"}
    | pattern `<ip> - - [<ts>] "<method> <path> HTTP/<proto>" <status> <size>`
  )[1m])
)

Статус — хорошая метка. А вот path почти всегда слишком детализирован. Можно заранее нормализовать путь в пайплайне или нормализовать в шаблоне запроса и агрегировать по укрупнённой форме:

sum by (norm_path) (
  count_over_time(( {job="nginx"}
    | pattern `<ip> - - [<ts>] "<method> <path> HTTP/<proto>" <status> <size>`
    | line_format "{{ RegexReplaceAll \\"/v[0-9]+\\" \\"/vX\\" .path }}"
    | regexp `(?P<norm_path>[^?]+)`
  )[5m])
)

Здесь мы вместо метки используем временные поля на стороне запроса. Если распределение по norm_path устойчивое и ограниченное, можно рассмотреть поднятие в постоянную метку (но только если это действительно нужно для запросов).

Многострочные логи и трейс-дампы

Stack trace часто приходит как один логический ивент в нескольких строках. Если не собрать его в одну запись, вы утонете в шуме и получите сложные для чтения фрагменты.

pipeline_stages:
- multiline:
    firstline: '^[0-9T:.+-]+'
    max_lines: 500
    max_wait_time: 3s

Сигнатуру firstline подберите под формат вашего времени/префикса. Ограничивайте max_lines и max_wait_time, чтобы не блокировать пайплайн при «обрывах».

Производительность Promtail: очереди и батчи

Чтобы не перегружать Loki и не терять логи во всплесках, проверьте параметры клиента Promtail: размер батча и ожидание.

clients:
- url: http://loki:3100/loki/api/v1/push
  batchwait: 1s
  batchsize: 1048576
  max_retries: 10
  backoff_config:
    min_period: 500ms
    max_period: 5s
    max_retries: 8

Слишком маленькие батчи породят лишние HTTP-запросы, слишком большие — задержки и буферизацию. Балансируйте под свой поток логов.

systemd, syslog и контейнеры

Journald

scrape_configs:
- job_name: systemd
  journal:
    json: true
    max_age: 12h
    labels:
      job: systemd
  relabel_configs:
  - action: labelkeep
    regex: (job|env|unit|priority)

В journald много служебных полей, не все стоит поднимать. Оставляйте только действительно полезные для агрегации. Детали по inotify и путям удобно сверить со статьёй inotify и пути systemd в CI.

Контейнеры

Автообнаружение в Kubernetes или Docker часто приносит десятки технических меток. Настройте белые списки и нормализацию: применяйте labelkeep, переименовывайте в один общий service, отбрасывайте container_id, если он не нужен для агрегаций.

Чек-лист проектирования меток и пайплайнов

  • Определите «бюджет» кардинальности: какие метки точно нужны для основных дашбордов и оповещений.
  • Не поднимайте в метки поля, чья кардинальность > O(10^2)–O(10^3) для вашего масштаба.
  • Сначала используйте запросный парсинг (| json, | logfmt, | regexp, | pattern) и агрегируйте; только потом — постоянные метки.
  • Нормализуйте пути и ID в пайплайнах (template с RegexReplaceAll), оставляйте сырьё в теле.
  • Отбрасывайте шум (drop по уровню, healthcheck-пути, тестовые префиксы).
  • Используйте relabel_configs для белых списков меток и удаления лишнего.
  • Включите защитные лимиты в Loki: max_streams_per_user, max_line_size, лимиты инжеста.
  • Проверяйте пайплайны на стейджинге с нагрузкой, смотрите число потоков и распределение меток.

Диагностика: какие метки «распухают»

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

topk(20, count by (route) (
  count_over_time(( {job="apps"} | json | route!="" )[15m])
))

Если route показывает тысячи уникальных значений, нормализуйте его. Аналогично можно проверять любые другие поля. Прелесть в том, что вы делаете это без изменения схемы хранения.

Примеры запросов LogQL для оценки кардинальности меток

Типовые анти‑паттерны и их исправления

  • Метка request_id или user_id. Исправление: оставить в теле, при необходимости — нормализовать и использовать на запросе.
  • Сырой path как метка. Исправление: шаблонизировать в template или нормализовать на запросе, хранить сырой путь в теле.
  • Поднятие десятков имён меток из logfmt/JSON «на всякий случай». Исправление: белые списки в labels и relabel_configs.
  • Отсутствие лимитов. Исправление: задать лимиты Loki и алерты на рост числа потоков.

Заключение

Loki отлично справляется с большими объёмами логов, если держать под контролем главное — кардинальность меток. Стройте пайплайны Promtail вокруг идеи «метки только для стабильных измерений», всё остальное — в теле. Нормализуйте, маскируйте, отбрасывайте шум до отправки. Проверяйте идею новой метки через запросный парсинг, и лишь затем поднимайте её в схему. Включайте ограничители в Loki, следите за количеством потоков и используйте запросные шаблоны для безопасных экспериментов. Так вы получите быстрые запросы, предсказуемые ресурсы и спокойные ночи админа.

Поделиться статьей

Вам будет интересно

Как безопасно выполнить bootstrap MariaDB Galera после полного останова

Как безопасно выполнить bootstrap MariaDB Galera после полного останова

После одновременной остановки всех узлов Galera обычного запуска MariaDB недостаточно. Сначала найдите сервер с последней транзакц ...
CoreDNS в Kubernetes: исправляем CrashLoopBackOff, SERVFAIL и DNS-петлю

CoreDNS в Kubernetes: исправляем CrashLoopBackOff, SERVFAIL и DNS-петлю

Разберём, почему CoreDNS перезапускается или отвечает SERVFAIL, как обнаружить замкнутую пересылку DNS-запросов, проверить доступн ...
Apache Kafka KRaft на VDS: отдельные controller и broker

Apache Kafka KRaft на VDS: отдельные controller и broker

Соберём новый Kafka-кластер без ZooKeeper на шести Linux VDS: три узла controller образуют отказоустойчивый metadata quorum, а три ...