Отже, маємо 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:
- 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
Парсери по швидкості роботи:
pattern
logfmt
JSON
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-frontendcache_results
: виставляємо в trueresults_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 ...
Посилання по темі
- Getting Started with Grafana Loki, Part 1: The Concepts
- Getting Started with Grafana Loki, Part 2: Up and Running
- Grafana Loki Configuration Nuances
- Use Loki with KubeDB