Привет! Меня зовут Игорь, я управляющий партнёр и системный архитектор в KTS.
Мы занимаемся разными проектами, от создания корпоративных систем до нестандартных спецпроектов, мобильной разработкой и DevOps. Накопленный опыт позволяет помогать нашим клиентам справляться с инфраструктурой и её проблемными местами с помощью разных инструментов.
В этой статье, подготовленной по мотивам моего доклада в «Школе мониторинга» Slurm, хочу поделиться своим набором best practices «Как лучше всего настроить Grafana Loki для сбора логов в инфраструктуре».
На мой взгляд, порог входа в Loki достаточно низкий, и в Интернете много туториалов. Поэтому я расскажу о более сложных и не совсем очевидных настройках, с которыми не раз сталкивался при работе с Grafana Loki.
Что будет в статье:
Задача сбора логов
Четыре основных вопроса, которые нужно себе задать перед тем, как пытаться интегрировать какую-либо систему сбора логов:
Как собрать логи?
Как извлечь из них нужные метаданные, чтобы в будущем было легче идентифицировать логи?
Как хранить эти данные, чтобы быстрее их записывать и находить? (самый сложный вопрос, пожалуй)
Как найти логи?
Каждая система, будь то syslog, Elasticsearch, или системы, которые построены на ClickHouse — даже сама Grafana Loki — отвечают на эти вопросы по-разному.
Поэтому когда мы будем обсуждать архитектуру, то вернёмся к тому, чем концептуально отличается Grafana Loki от Elasticsearch, и почему она выигрывает в стоимости хранения логов.
Пайплайн сбора логов обычно выглядит просто и понятно.
Итак, у нас есть большое количество разнообразных источников данных, откуда к нам прилетают логи: Kubernetes-кластеры, виртуальные машины, Docker-контейнеры и другие. Они проходят фазу сбора, процессинга, фильтрации.
Затем сохраняются в определенном виде, в зависимости от того, какое хранилище вы используете. Например, в виде базы данных, если работаете с ClickHouse, или в S3 Bucket в Grafana Loki. Но обратите внимание, что у каждого пользователя, который извлекает данные с другой стороны, могут быть разные сценарии действий. Например: извлечь логи за год или за последние 10 минут, чтобы отфильтровать данные по ним.
Способы запуска Loki
Существуют три способа запуска, которые, по большому счету, отличаются масштабированием.
Single-binary
Этот способ самый простой и используется в основном в первичных туториалах по Loki. Логика такая: мы берем бинарь, запускаем его, подключаем к storage.
Роль Storage может исполнять как файловая система, на которой запущен наш процесс, так и удалённый S3 Bucket. Здесь это значения не имеет. Такой подход имеет свои плюсы. Например, лёгкость запуска — потребуется минимум конфигурации, которую мы рассмотрим ниже. Минус — плохая отказоустойчивость: если машина выпадает, то логи не пишутся вообще.
Ситуацию можно улучшить так:
На двух разных виртуальных машинах запустить один и тот же процесс Loki с одинаковым конфигом
Объединить их в кластер с помощью секции
memberlist
Перед этим поставить любой прокси —например Nginx или Haproxy
Главное — подключить всё к одному Storage
Соответственно, можно масштабировать процесс дальше, то есть запустить три, четыре, пять узлов.
Получится примерно такая схема:
Обратите внимание, что Loki занимается как записью, так и чтением. Поэтому нагрузка равномерно распределяется по всем инстансам. Но по факту она совсем неравномерная, потому что в одни моменты времени бывает много чтений, а в другие — много записей.
SSD - Simple Scalable Deployment
Второй способ следует из первого и позволяет разделить процессы чтения и записи, чтобы мы могли запустить, например, более дискозависимые процессы на одном «железе», а менее дискозависимые на другом.
Для запуска вам необходимо передать флажок -target=write
или -target=read
, и в каждом из этих процессов запускаются те сущности, которые отвечают за конкретный путь запроса: write или read. Точно так же нужно поставить прокси перед всеми инстансами, который будет проксировать:
запросы на запись → на узлы записи
остальные запросы → на узлы с чтением
Этот способ запуска Grafana считает наиболее рекомендуемым в плане работы и Grafana активно развивает именно его.
Microservices mode
Microservices mode — более развёрнутый путь, когда мы каждый компонент Loki запускаем самостоятельно.
Компонентов очень много, но они легко отделяются друг от друга, и их можно распределить на две группы или даже три.
Группа компонентов на запись: дистрибьюторы и инджесторы, которые пишут в Storage.
Группа компонентов на чтение: query-frontend, querier, index gateway. Это те компоненты, которые занимаются исполнением запросов.
Все остальные утилитарные компоненты: например, кеши, compactor и другие.
Как устроена архитектура Grafana Loki
Остановимся чуть подробнее на архитектуре, чтобы в дальнейшем понимать, что вообще конфигурируется в той или иной секции.
Для начала — как индексируются данные в блоке. В отличие от Elasticsearch, который по дефолту индексирует все документы полнотекстово и целиком, Grafana Loki идет по другому пути - он индексирует не содержимое логов, а только их метаданные, то есть время и лейблы.
Эти лейблы очень похожи на Prometheus-лейблы. Я думаю, многие из вас с ними знакомы.
В итоге в Grafana мы храним очень маленький индекс данных, потому что данных в нем очень мало. Здесь хочу заметить, что у Elasticsearch индекс раздувается зачастую больше, чем сами данные.
Неиндексируемые данные мы храним как они есть в порядке появления. Если их нужно отфильтровать, пользуемся "grep", своего рода встроенным в Loki.
Stream — уникальный набор лейблов, несмотря на то, что логи могут идти из одного источника.
В данном случае лейбл component="supplier"
порождает новый стрим. Нам это понадобится в дальнейшем, т.к. настройки, которые связаны с рейт-лимитами и ограничениями, зачастую распространяются именно на стрим.
Чанки — набор из нескольких строчек логов. Вы взяли строчки лога, поместили их в одну сущность, назвали ее chunk, сжали и положили в Storage.
Теперь вернёмся к архитектуре и подробнее рассмотрим write path и read path и их различия.
Write path. Точкой входа для записи логов в Loki является сущность под названием «Дистрибьютор». Это stateless-компонент. Его задача — распределить запрос на один или несколько инджесторов.
Инджесторы — это уже stateful-компоненты. Они объединены в так называемый hash ring — систему консистентного хеширования. Она позволяет проще добавлять и удалять инджесторы из этого кластера. Все они подключены к одному Storage.
Read path устроен сложнее, но принцип похожий. Есть stateless-компоненты — query frontend и querier. Query frontend — компонент, который помогает разделить запрос, чтобы быстрее его выполнить.
Например, нужно запросить данные за месяц. С помощью query frontend делим запрос на более мелкие интервалы и направляем в параллель на несколько querier, а потом объединяем результат.
Querier — компонент, который запрашивает логи из Storage. Если есть новые данные из инжесторов, которые в Storage еще не записаны, querier запрашивает их тоже.
Минимальная конфигурация Loki (filesystem)
Эта конфигурация нацелена на работу с файловой системой, когда Loki запущен в single instance режиме. Остановлюсь на более важных конфигурационных секциях.
Loki — это инструмент, который постоянно развивается, чтобы упростить и улучшить работу с конфигурацией. Пару версий назад появилось одно из самых лучших изменений — секция common. Когда в конфигурации есть несколько повторяющихся элементов, common объединяет их в разных частях конфига. То есть, если раньше приходилось настраивать ring
, storage
и другие элементы для инджестора, querier, дистрибьютора отдельно, то сейчас это все можно сделать в одном месте.
auth_enabled: false
server:
http_listen_port: 3100
common:
path_prefix: /tmp/loki
storage:
filesystem:
chunks_directory: /tmp/loki/chunks
rules_directory: /tmp/loki/rules
replication_factor: 1
ring:
instance_addr: 127.0.0.1
kvstore:
store: inmemory
schema_config:
configs:
- from: 2020-09-07
store: boltdb-shipper
object_store: filesystem
schema: v12
index:
prefix: loki_index_
period: 24h
Здесь видно, что storage
настроен в виде файловой системы, и указано, где хранятся чанки. Там же можно указать, где хранить индекс, правила для алертинга и т.д.
schema_config — это конфиг для указания, как хранятся данные: чанки, индекс. Здесь в целом ничего не меняется с давних времен, но иногда появляются новые версии схемы. Поэтому рекомендую периодически читать чендж-логи, чтобы вовремя обновлять схемы в Loki и иметь последние улучшения.
Как только появляется несколько инстансов, необходимо объединить их в кластер, который работает на протоколе memberlist (также можно использовать сторонние системы, такие как etcd или consul). Это Gossip-протокол. Он автоматически находит узлы Loki по определенному принципу:
auth_enabled: false
server:
http_listen_port: 3100
common:
path_prefix: /tmp/loki
storage:
filesystem:
chunks_directory: /tmp/loki/chunks
rules_directory: /tmp/loki/rules
replication_factor: 1
ring:
kvstore:
store: memberlist
schema_config:
configs:
- from: 2020-09-07
store: boltdb-shipper
object_store: filesystem
schema: v12
index:
prefix: loki_index_
period: 24h
memberlist:
join_members:
- loki:7946
Есть множество разных способов автоматической конфигурации кластера, которые отлично работают. Например, в секции memberlist.join_members
можно указать разные настройки:
Адрес одного хоста
Список адресов
dns+loki.local:7946
— Loki сделает A/AAAA DNS запрос для получения списка хостовdnssrv+_loki._tcp.loki.local
— Loki сделает SRV DNS запрос для получения не только списка хостов, но и портовdnssrvnoa+_loki._tcp.loki.local
— SRV DNS — запрос без A/AAAA запроса
Зачем это нужно? Внутри Loki есть компоненты, которые должны знать друг о друге. Например, дистрибьюторы должны знать об инджесторах. Поэтому они регистрируются в одном кольце. После этого дистрибьюторы знают, на какие инджесторы отправить запрос на запись. Еще в пример можно привести компакторы, которые должны работать в единственном экземпляре в кластере.
S3 в качестве storage
S3 — наиболее рекомендованный способ хранения логов в Loki, особенно, если вы деплоите Loki в Kubernetes. Когда мы используем S3 в качестве storage, немного меняется конфигурация:
auth_enabled: false
server:
http_listen_port: 3100
common:
path_prefix: /tmp/loki
storage:
s3: # Секция filesystem меняется на s3
s3: https://storage.yandexcloud.net
bucketnames: loki-logs
region: ru-central1
access_key_id:
secret_access_key:
replication_factor: 1
ring:
kvstore:
store: memberlist
schema_config:
configs:
- from: 2020-09-07
store: boltdb-shipper
object_store: filesystem
schema: v12
index:
prefix: loki_index_
period: 24h
memberlist:
join_members:
- loki:7946
Несколько советов:
Используйте
https://
вместоs3://
- так вы гарантируете использование шифрованного соединенияbucketnames
можно указать несколько для распределения храненияМожно использовать
ACCESS_KEY_ID
иSECRET_ACCESS_KEY
переменные окружения для конфигурации ключей доступа к S3
Мы меняем storage
на S3, указываем endpoint
, bucketnames
, и другие конфигурации, которые относятся к S3.
Обратите внимание, что bucketnames
во множественном числе, а значит, их можно указать сразу несколько. Тогда Loki начнёт равномерно распределять чанки по всем указанным бакетам, чтобы снизить нагрузку на один бакет. Например, это нужно, когда в вашем хостере есть ограничения по RPS на бакет.
Конфигурация кластерных и High Availability решений
Допустим, мы записали логи в несколько узлов. Один из них отказал и запросить данные из него нельзя, потому что он не успел записать их в storage.
High Availability в Loki обеспечивается через опцию replication_factor
. Благодаря этой настройке дистрибьютор отправляет запрос на запись логов не в одну реплику инджестеров, а сразу в несколько.
auth_enabled: false
server:
http_listen_port: 3100
common:
path_prefix: /tmp/loki
storage:
s3:
s3: https://storage.yandexcloud.net
bucketnames: loki-logs
region: ru-central1
access_key_id:
secret_access_key:
replication_factor: 3 # Обратите внимание на это поле
ring:
kvstore:
store: memberlist
schema_config:
configs:
- from: 2020-09-07
store: boltdb-shipper
object_store: filesystem
schema: v12
index:
prefix: loki_index_
period: 24h
memberlist:
join_members:
- loki:7946
replication_factor:
Distributor отправляет чанки в несколько Ingester
Минимум – 3 для 3х нод
Позволяет не работать 1 из 3 нод
maxFailure = (replication_factor / 2) +1
Дистрибьютор отправляет чанки сразу в несколько инджесторов. Для дедупликации данных при использовании boltdb-shipper применяется секция конфига chunk_cache_config
для чанков и write_dedupe_cache_config
для индексов. Кроме этого querier'ы участвуют в дедупликации данных, сравнивая timestamp и сами логи (подробнее про это тут и тут).
Тайм-ауты
Достаточно тяжелая тема, потому что очень часто при неправильной настройке Loki можно встретить ошибки типа 502, 504.
Чтобы лучше разобраться в ошибках, нужно, во-первых, увеличить таймауты до достаточных значений в вашем проекте, а во-вторых — правильно сконфигурировать несколько видов таймаутов.
Таймауты
http_server_{write,read}_timeout
настраивают базовый таймаут на время ответа веб сервераquerier.query_timeout
иquerier.engine.timeout
настраивают максимальное время работы движка, непосредственно исполняющего запросы на чтениеserver: http_listen_port: 3100 http_server_write_timeout: 310s http_server_read_timeout: 310s querier: query_timeout: 300s engine: timeout: 300s
В случае использования прокси перед Loki, например NGINX, следует увеличить таймауты и в нём (
proxy_read_timeout
иproxy_write_timeout
)server { proxy_read_timeout = 310s; proxy_send_timeout = 310s; }
Также необходимо увеличить таймаут на стороне Grafana. Это настраивается в секции
[dataproxy]
в конфиге.[dataproxy] timeout = 310
Лучше всего, если вы поставите минимальный из всех 4-х видов таймаутов у querier (в примере – 300s). Так он завершится первым, а все следующие — например НТТР-серверы, Nginx или Grafana — чуть дольше.
Дефолтный тайм-аут очень маленький, поэтому я рекомендую увеличивать эти значения.
Размеры сообщений
Тема может показаться сложной, потому что природа происхождения некоторых ошибок неочевидна.
Размеры сообщений, grpc_server_max_{recv,send}_msg_size
— это ограничения на возможный размер логов. При этом дефолтные значения очень маленькие. Например, если есть большой stack trace и в одной лого-линии отправляются логи размером 20 Мб, то он в принципе не влезет в этот лимит. Значит, его нужно увеличивать.
server:
http_listen_port: 3100
grpc_server_max_recv_msg_size: 104857600 # 100 Mb
grpc_server_max_send_msg_size: 104857600 # 100 Mb
ingester_client:
grpc_client_config:
max_recv_msg_size: 104857600 # 100 Mb
max_send_msg_size: 104857600 # 100 Mb
Дефолт 4Mb
Непосредственно влияет на размер логов обрабатываемых Loki
Энкодинг чанков тоже нельзя обойти стороной. Дефолтное значение — gzip, то есть максимальное сжатие. Grafana рекомендует переключиться на snappy — и я по опыту с ними согласен. Тогда логи может и занимают чуть-чуть больше места в сторадже, но становятся более производительными чтения и записи данных.
ingester:
chunk_encoding: snappy
Дефолт — gzip
Лучшее сжатие
Медленнее запросы
Рекомендуем snappy
Сжатие чуть хуже
Однако очень быстрое кодирование/декодирование
Чанки
С чанками связано много настроек относительно их размеров и периодов времени жизни. Рекомендую их сильно не трогать. Но при этом нужно понимать, что вы делаете, когда меняете значения.
ingester:
chunk_idle_period: 2h
chunk_target_size: 1536000
max_chunk_age: 2h
Дефолты достаточно хорошие:
chunk_block_size
иchunk_retain_period
не рекомендуется менять совсем.сhunk_target_size
можно увеличить, если чанки в основном полные. Это даст им больше пространства.сhunk_idle_period
означает, сколько чанк будет жить в памяти инджестора, если в нём нет вообще никаких записей. Так вот, если ваши стримы в основном медленные и полупустые, лучше увеличить период. По дефолту — 30 минут.
Параллелизм
Еще один важный вопрос связан с конкурентностью.
querier:
max_concurrent: 8
limits_config:
max_query_parallelism: 24
split_queries_by_interval: 15m
frontend_worker:
match_max_concurrent: true
querier.max_concurrent
показывает, сколько запросов в параллель может обрабатывать один querier. Рекомендовано ставить примерно удвоенное количество CPU, дефолт = 10 (будьте внимательны к этим цифрам).limits_config.max_query_parallelism
показывает, сколько максимум параллельности есть у тенанта. Значения querier.max_concurrent должны матчится с max query parallelism по формуле:[queriers count] * [max_concurrent] >= [max_query_parallelism]
В нашем примере должны быть запущены минимум 3querier
, чтобы обеспечить параллелизм 24.
Оптимизация Write Path
Здесь есть несколько настроек, связанных с записью — ingestion_write_mb, ingestion_burst_size_mb
.
limits_config:
ingestion_rate_mb: 20
ingestion_burst_size_mb: 30
Если здесь стоят достаточно низкие дефолты, рекомендую увеличить их. Это позволит гораздо больше и чаще писать логи. Остальные значения относятся к tenant, поэтому с ними нужно быть аккуратнее.
Для стримов есть отдельная настройка — per_stream_rate_limit
.
limits_config:
per_stream_rate_limit: "3MB"
per_stream_rate_limit_burst: "10MB"
На примере показаны более-менее нормальные дефолты. Но если вы начинаете в них упираться, то рекомендую разбить стрим на несколько — добавить лейбл. Это уменьшит rate-limit стрима. В обратной ситуации можно пробовать увеличивать лимиты.
Объемы данных в Grafana Cloud
Эти данные я вытащил из одной их презентации.
Grafana Loki обрабатывает:
500 МБ логов в секунду
43 ТВ в день;
1,25 ПВ в месяц.
Они стремятся обеспечивать нагрузку примерно 10 МВ в секунду на инджестор. При этом использование памяти всего лишь около 10 GB.
Эти данные позволяют пользователям примерно представить, какую инфраструктуру им запускать. Для примера: у нас на одном из проектов rate гораздо ниже, около 20 или 30 GB в день. Тем не менее три инджестора справляются с таким потоком данных с большим запасом.
Итоги
В статье и докладе, по которому она написана, я постарался подробно описать работу с Loki в контексте конфигурирования:
задача сборов логов и её 4 основных вопроса
архитектура Loki
3 вида запуска Loki
HA в Loki с помощью replication factor
конфигурация тайм-аутов и размера сообщений
параллелизм — следите за этими настройками очень внимательно!
оптимизация write path — в целом несложный процесс. Вам нужно просто посмотреть на показатели на графике и оптимизировать соответствующим образом
Другие статьи про DevOps для начинающих:
Другие статьи про DevOps для продолжающих:
За последние годы де-факто стандартом оркестрации и запуска приложений стал Kubernetes. Поэтому умение управлять Kubernetes-кластерами является особенно важным в работе любого современного DevOps-инженера.
Порог входа может казаться достаточно высоким из-за большого числа компонентов и связей между ними внутри системы. На курсе «Деплой приложений в Kubernetes». мы рассмотрим самые важные концепции, необходимые для управления кластерами любой сложности и научим применять эти знания на практике.
? Почитать про курс подробнее можно здесь: https://vk.cc/cn0ty6