Grafana Loki: оптимізація роботи – Recording Rules, кешування та паралельні запити

Автор |  12/08/2023
 

Отже, маємо Loki, встановленую з чарту у simple-scale mode, див. Grafana Loki: архітектура та запуск в Kubernetes з AWS S3 storage та boltdb-shipper.

Працює Loki все в AWS Elastic Kubernetes Service, встановлено з Loki Helm chart, в ролі long-term store використовуємо AWS S3, а для роботи з індексами Loki – BoltDB Shipper.

У Loki в 2.8 для роботи з індексами з’явився механізм TSDB, який мабуть скоро замінить BoltDB Shipper, але я його ще пробував. Див. Loki’s new TSDB Index.

І загалом все працює, все наче добре, але при отримані даних за тиждень або місяць в Grafana дуже часто отримуємо помилки 502/504 або “too many outstanding requests“.

Тож сьогодні трохи поглянемо на те, як можна оптимізувати Loki для кращого перфомансу.

Насправді, витратив дуже багато часу на те, щоб більш-менш розібратись з усім, що буде в цьому пості, бо документація Loki… Вона є. Її багато. Але зрозуміти з цієї документації якісь деталі реалізації, або як різні компоненти один з одним працюють місцями досить складно.

Хоча я не погоджусь з осноною тезою з I can’t recommend serious use of an all-in-one local Grafana Loki setup, проте згоден, що:

In general I’ve found the Loki documentation to be frustratingly brief in important areas such as what all of the configuration file settings mean and what the implications of setting them to various values are.

Тим не менш, якщо все ж витратити трохи часу на “причісування”, то загалом система працює дуже добре (принаймні, поки ми не маємо террабайтів логів в день, але зустрічав обговорення, де люди мають такі навантаження).

Отже, що ми можемо зробити, щоб пришвидшити процесс роботи для обробки запросів в Grafana dashboards та/або алертів з логів:

  • оптимізація запросів
  • використати Record Rules
  • включеня кешування запитів, індексів та chunks
  • оптимізувати роботу Queries

Поїхали.

Loki Pods && Components

Перед тим, як братись за оптимізацію давайте згадаємо що там в Loki взагалі є і як воно все разом працює.

Отже, маємо такі компоненти:

Тобто коли ми деплоїмо Loki Helm chart у simple-scale mode та без legacyReadTarget – то маємо поди Read, Write, Backend та Gateway:

  • Read:
    • querier: обробляє запити на отримання даних – спочатку намагається взяти дані з пам’яті Ingester, якщо там їх нема – то йде до long-term store
    • query frontend: опціональний сервіс для покращення швидкості роботи Querier: запити на отримання даних спочатку йдуть на Query Frontend, який розбиває велики запити на менші і виконує  формує чергу запитів, а Querier з цієї чегри бере запити на обробку. Крім того, Query Frontend може виконувати кешування відповідей, і части запитів обробляти зі свого кешу замість того, щоб виконувати цей запит на воркері, тобто на Querier
    • query scheduler: опціональний сервіс для покращеня скейлінгу Querier та Query Frontend, який бере на себе формування черги запитів, та передає їх до декількох Query Frontend
    • ingester: у Read-path відповідає на запити від Querier даними, які має в пам’яті (ті, що ще не було відправлені до long-term store)
  • Write:
    • distributor: приймає вхідні логи від клієнтів (Promtail, Fluent Bit, etc), перевіряє їх та відправляє до Ingester
    • ingester (again): приймає дані від Distributor, і формує chunks (блоки даних або фрагменти), які відправляє до long-term store
  • Backend:
    • ruler: перевіряє дані в логах по expressions, заданим в рулах, та створює алерти або метрики в Prometheus/VictoriaMetrics
    • compactor: відповідає за компресію індекс-файлів і retention даних у long-term storage
  • Gateway: звичайний Nginx, який відповідає за роутінг запитів до відповідних сервісів Loki

Table Manager, BoltDB Shipper та індекси

Окремо варто згадати про створення індексів.

По-перше – Table manager, бо особисто мені з його документацій було не дуже зрозуміло використовується він зараз, чи ні. Бо з одного боку в values.yaml він має enabled=false, з іншого – в логах Write-інстансів він подекуди з’являється.

Отже, що маємо про індекси:

  • Table Manager вже depreacted, і використовується тільки у випадку, якщо індекси зберігається у зовнішніх сховищах – DynamoDB, Cassandra, etc
  • файли індексів створються Ingester в каталозі active_index_directory (по-дефолту /var/loki/index), коли chunks з пам’яті готові до відправки до long-term storage – див. Ingesters
  • механізм boltdb-shipper відповідає за відправку індексів з інстансів Ingester до long-term store (S3)

Loki queries optimization

Переглянув Best practices, і спробував рекомендації на практиці, але насправді не помітив різниці.

Проте все ж додам сюди кратко, бо в принципі вони виглядають досить логічно.

Перевіряв за допомогою запросів типу:

[simterm]

$ time logcli query '{app="loki"} |="promtail" | logfmt' --since=168h

[/simterm]

І час виконання все одно був дуже різний навіть при виконанні одного й того ж запросу, незалежно від спроб оптимізації запросу за рахунок використання селекторів чи фільтрів.

Label or log stream selectors

На відміну від ELK, Loki не індексує весь текст в логах, а тільки timestamp та labels:

Тож запит у вигляді {app=~".*"}  буде виконуватись довше, ніж при використанні точного stream selector, тобто {app="loki"}.

Чим більш точний stream selector буде використано – тим менше даних Loki буде вигружати даних з long-term store та обробляти для відповіді – запит {app="loki", environment="prod"} буде швидшим, ніж просто вибрати всі стріми з {app="loki"}.

Line Filters та regex

Використовуйте Line filters, та уникайте регулярок в запитах.

Тобто запит {app="loki"} |= "promtail" буде швидшим, ніж просто {app="loki"}, і швидшим, аніж {app="loki"} |~ "prom.+".

LogQL Parsers

Парсери по швидкості роботи:

  1. pattern
  2. logfmt
  3. JSON
  4. regex

І не забувайте про Log Filter: запит {app="loki"} |= "promtail" | logftm буде швидшим, ніж {app="loki"} | logfmt.

А тепер перейдемо до параметрів Loki, які дозволять пришвидшити обробку запитів та зменшать використання CPU/Memory його компонентами.

Ruler та Recording Rules

А ось Recording Rules дали прям дуже відчутний буст.

Взагалі Ruler виявився набагато цікавішим, аніж просто виконувати запити для алертів.

Він чудово підходить для будь яких запитів, бо ми можемо створити Recording Rule, а результати відправляти Prometheus/VicrtoriaMetrics через remote_write, після чого виконувати запити на алерти або в дашбордах Grafana прямо з Prometheus/VicrtoriaMetrics замість того, щоб кожного разу виконувати їх в Loki, і працює це набагато швидше, ніж описувати запит в самій Grafana або алерт-рул у файлі конфігу Ruler.

Отже, щоб зберігати результати в Prometheus/VicrtoriaMetrics – в параметрах Ruler додаємо WAL-директорію, куди Ruler буде записувати результати запитів, та налаштовуємо remote_write, куди він буде зберігати результати запитів:

...
    rulerConfig:
      storage:
        type: local
        local:
          directory: /var/loki/rules
      alertmanager_url: http://vmalertmanager-vm-k8s-stack.dev-monitoring-ns.svc:9093
      enable_api: true
      ring:
        kvstore:
          store: inmemory
      wal:
        dir: /var/loki/ruler-wal      
      remote_write:
        enabled: true
        client:
          url: http://vmsingle-vm-k8s-stack.dev-monitoring-ns.svc:8429/api/v1/write
...

І за хвилинку перевіряємо метрику в VicrtoriaMetrics:

В Grafana результат швидкості побудови графіків просто вражаючий.

Якщо запит:

sum(
  rate({__aws_cloudwatch_log_group=~"app-prod-approdapiApiGatewayAccessLogs.*"} | json | path!="/status_monitor" | status!~"200" [5m])
)
by (
  status, __aws_cloudwatch_log_group
)

Записати в Recording Rule як:

- record: overview:backend:apilogs:prod:sum_rate
  expr: |
  sum(
    rate({__aws_cloudwatch_log_group=~"app-prod-appprodapiApiGatewayAccessLogs.*"} | json | path!="/status_monitor" | status!~"200" [5m])
)
by (
  status, __aws_cloudwatch_log_group
)
  labels:
    component: backend

То швидкість його виконання в дашборді 148 ms:

А якщо робити запит напряму з дашборди – то іноді по кілька секунд:

Кешування

Loki може зберігати дані в кеші, щоб потім віддавати дані з пам’яті або диску, а не виконувати запит “з нуля” і не завантажувати файли індексів та блоків даних з S3.

Теж дало досить відчутний результат по швидкості виконання запросів.

Документація – Caching.

Loki має три типи кешу:

  • Index Cache: boltdb-shipper може тримати індекси для Queriers локально, щоб не завантажувати їх кожного разу з S3, див. Queriers
  • Query results cache: збегірати результати запросів в кеші
  • Chunk cache: зберігати дані з S3 в локальному кеші

Що маємо в параметрах, див Grafana Loki configuration parameters:

  • query_range: параметри розділення великих запитів на менші і кешування запитів в Loki query-frontend
    • cache_results: виставляємо в true
    • results_cache: налаштування бекенду кешу
  • boltdb_shipper:
    • cache_location: шлях для збереження індексів BoltDB для використання в запитах
  • storage_config:
    • index_queries_cache_config: параметри кешування індексів
  • chunk_store_config:
    • chunk_cache_config: налаштування бекенду кешу

Query та Chunk cache

Документація – Caching.

У values Helm-чарту вже маємо блок для memcached – можна брати для прикладу.

В ролі бекенду кешу може бути embedded_cache, Memcached, Redis або fifocache (deprecated – зараз це embedded_cache).

Спробуємо з Memcached, бо Redis в Kubernetes колись крутив – більше не хочу 🙂

Додаємо репозиторій:

[simterm]

$ helm repo add bitnami https://charts.bitnami.com/bitnami

[/simterm]

Вставновлюємо інстанс Memcached для chunks chache:

[simterm]

$ helm -n dev-monitoring-ns upgrade --install chunk-cache bitnami/memcached

[/simterm]

І для результатів запитів:

[simterm]

$ helm -n dev-monitoring-ns upgrade --install results-cache bitnami/memcached

[/simterm]

Знаходимо Services:

[simterm]

$ kk -n dev-monitoring-ns get svc | grep memcache
chunk-cache-memcached                                  ClusterIP   172.20.120.199   <none>        11211/TCP                    116s
results-cache-memcached                                ClusterIP   172.20.53.0      <none>        11211/TCP                    86s

[/simterm]

Оновлюємо values нашого чарту:

...
loki:
  ...
  memcached:
    chunk_cache:
      enabled: true
      host: chunk-cache-memcached.dev-monitoring-ns.svc
      service: memcache
      batch_size: 256
      parallelism: 10
    results_cache:
      enabled: true
      host: results-cache-memcached.dev-monitoring-ns.svc
      service: memcache
      default_validity: 12h
...

Деплоїмо та перевіряємо конфіг в подах Loki:

[simterm]

$ kk -n dev-monitoring-ns exec -ti loki-write-0 -- cat /etc/loki/config/config.yaml
...
chunk_store_config:
  chunk_cache_config:
    memcached:
      batch_size: 256
      parallelism: 10
    memcached_client:
      host: chunk-cache-memcached.dev-monitoring-ns.svc
      service: memcache
...
query_range:
  align_queries_with_step: true
  cache_results: true
  results_cache:
    cache:
      default_validity: 12h
      memcached_client:
        host: results-cache-memcached.dev-monitoring-ns.svc
        service: memcache
        timeout: 500ms
...

[/simterm]

Перевіримо чи пішли дані в Memcached – встановлюємо telnet:

[simterm]

$ sudo pacman -S inetutils

[/simterm]

Відкриваємо порт:

[simterm]

$ kk -n dev-monitoring-ns port-forward svc/chunk-cache-memcached 11211

[/simterm]

Підключаємось:

[simterm]

$ telnet 127.0.0.1 11211

[/simterm]

І перевіряємо ключі:

[simterm]

stats slabs
STAT 10:chunk_size 752
STAT 10:chunks_per_page 1394
STAT 10:total_pages 2
STAT 10:total_chunks 2788
STAT 10:used_chunks 1573
...

[/simterm]

Є, значить дані з Loki йдуть.

Ну і глянемо чи дало кешування якийсь результат.

До включення кешування – 600 мілісекунд:

Після включення кешування – 144 мілісекунди:

Index cache

Додаємо ще один інстанс Memcached:

[simterm]

$ helm -n dev-monitoring-ns upgrade --install index-cache bitnami/memcached

[/simterm]

Оновлюємо values (от не знаю, чому є query_range та chunk_cache, але не зробили якийсь index_cache):

...
    storage_config:
      ...
      boltdb_shipper:
        active_index_directory: /var/loki/botldb-index
        shared_store: s3
        cache_location: /var/loki/boltdb-cache
      index_queries_cache_config:
        memcached:
            batch_size: 256
            parallelism: 10
        memcached_client:
            host: index-cache-memcached.dev-monitoring-ns.svc
            service: memcache
...

Деплоїмо, перевіряємо конфіг в поді ще раз:

[simterm]

$ kk -n dev-monitoring-ns exec -ti loki-read-5c4cf65b5b-gl64t cat /etc/loki/config/config.yaml
...
storage_config:
  ...
  index_queries_cache_config:
    memcached:
      batch_size: 256
      parallelism: 10
    memcached_client:
      host: index-cache-memcached.dev-monitoring-ns.svc
      service: memcache
...

[/simterm]

І результат виконання того ж самого запиту тепер 66 ms:

Query Frontend та паралельні запити

Документація – Query Frontend.

Query Frontend працює як load balancer для Queriers, і розбиває запроси за великий проміжок часу на частини, після чого віддає їх Queriers для виконання паралельно, а після виконання запросу збирає результати обратно в одну відповідь.

Для цього в limits_config задається split_queries_by_interval з дефолтом в 30 хвилин.

Параметри паралелізму задаються через querier max_concurrent – кількість одночасних потоків для виконання запитів. Пишуть, що можна ставити х2 від ядер CPU.

Крім того в limits_config задається ліміт на загальну кількість одночасних виконань через max_query_parallelism, яке має бути кількість Queriers (read-поди) помножена на max_concurrent. Хоча поки не знаю, як це настраювати якщо для read-подів включати автоскейлінг.

У нас моніторинг працює на t3.medium з 4 vCPU, тож поставимо max_concurrent == 8:

...
    querier:
      max_concurrent: 8

    limits_config:
      max_query_parallelism: 8
      split_queries_by_interval: 15m
...

Логування slow queries

Для дебагу повільних запросів можна налаштувати параметр log_queries_longer_than (default 0 – відключено):

...
    frontend:
      log_queries_longer_than: 1s
...

Тоді в логах Readers будуть такі запроси з param_query:

Результати оптимізації

В цілому це все, що знайшов по оптимізації, і результат дійсно приємний.

Якщо тестовий запит до всіх описаних вище налаштувань виконувався 5.35 секунди:

То після – 98 мілісекунд:

Timeouts та 502/504

Окремо варто сказати про таймаути на виконання запитів.

Хоча після всіх налаштувань по перформансу я 502/504 не бачу, але якщо вони виникають то можна спробувати підвищити ліміти таймаутів:

...
    server:
      # Read timeout for HTTP server
      http_server_read_timeout: 3m
      # Write timeout for HTTP server
      http_server_write_timeout: 3m    
...

Посилання по темі