Хочу поделится опытом отправки логов java-приложений, развернутых в kubernetes, в базу clickhouse. Для отправки используем fluent-bit, который настроим на отправку логов по http в формате json_stream.++ Пара слов о fluent-bit
[[https://github.com/fluent/fluent-bit Fluent-bit]] работает с записями. Каждая запись состоит из тега и именованного массива значений.
*** Input
Секции input указывают как и откуда брать данные, какой тег присвоить записям и в какой парсер передать их для получения массива значений. Парсер указывается параметром Parser в секции Input.В нашем случае берём теги из названия файла при помощи regexp.
*** Parser
Секция parsers указывает как получить из сообщения массив значений. В случае с kubernetes все сообщения представляют из себя JSON с 3 полями: log, stream и time. В нашем случае поле log также содержит JSON.*** Filter
Пройдя парсинг, все сообщения попадают в фильтры, применение которых настраивается параметром Match. В каждый фильтр попадают только те сообщения, которые имеют тег, попадающий в regexp, указанный в Match.В нашем случае фильтры также удаляют сообщения, которые пришли из служебных namespace и лишние поля, чтобы сообщения смогли попасть в clickhouse без ошибок, даже если что-то пошло не так.
*** Помещение тегов в лог
Fluent-bit использует теги только для маршрутизации сообщений, подразумевается, что теги не должны попадать в лог. Для того, чтобы в лог попала информаци о том в каком namespace, contaner и pod произошло событие, применяется скрипт на lua.
Если распарсить сообщение не удалось или поле message оказалось пустым после парсинга, значит в выводе приложения был не JSON или он был в не верном формате. Тогда в поле message помещается всё сообщение без парсинга.
*** Output
Секция указывает куда направить сообщения. Применение секций Output определяется параметром Match аналогично с секцией Filter. В нашем случае сообщения уходят в clickhouse по http в формате json_stream.++ Примеры
Наши приложения выводят в логи в формате JSON в stdout, их собирает kubernetes и создает симлинки на них в /var/log/containers.
Логи работы пода java-pod-5cb8d7898-94sjc из деплоймента java-pod в неймспейсе default попадают в файл вида
/var/log/containers/java-pod-5cb8d7898-94sjc_default_java-pod-08bc375575ebff925cff93f0672af0d3d587890df55d6bd39b3b3a962fc2acb9.log
Пример записи
{"log":"{\"timeStamp\":\"2021-11-24T11:00:00.000Z\",\"message\":\"My message id: 8543796, country: RU\",\"logger\":\"com.YavaApp.app.service.YavaService\",\"thread\":\"http-nio-8080-exec-2\",\"level\":\"INFO\",\"levelValue\":40000}\n","stream":"stdout","time":"2021-11-24T11:00:00.000000000Z"}
Как видно из примера, в JSON-записи, поле log содержит в себе экранированный JSON, который также нужно разобрать, а из имени файла понятно к какому поду, деплою и неймспейсу принадлежит запись.
++ Clickhouse
По умолчанию clickhouse слушает команды по протоколу http на порту 8123. Менять эти настройки нет необходимости.
Создадим в clickhouse схему logs и таблицу log в ней.
create database logs;
use logs;
create table logs.log(
pod_time DateTime('Etc/UTC'),
namespace String,
container String,
pod String,
timeStamp String,
stream String,
thread String,
level String,
levelValue Int,
logger String,
message String
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(pod_time)
ORDER BY pod_time;++ Файлы конфигурации
Файл конфигурации fluent-bit для kubernetes будет выглядеть примерно так:
apiVersion: v1
kind: ConfigMap
metadata:
labels:
k8s-app: fluent-bit
name: fluent-bit
namespace: monitoring
data:
filter.conf: |
[FILTER]
Name lua
Match *
script make_tags.lua
call make_tags
[FILTER]
Name grep
Match kube.*
Exclude namespace monitoring
Exclude namespace metallb-system
Exclude namespace gitlab-managed-apps
Exclude namespace kube-*
[FILTER]
Name record_modifier
Match kube.*
Whitelist_key pod_time
Whitelist_key namespace
Whitelist_key container
Whitelist_key pod
Whitelist_key timeStamp
Whitelist_key stream
Whitelist_key thread
Whitelist_key level
Whitelist_key levelValue
Whitelist_key logger
Whitelist_key message
Record cluster k8s-test
fluent-bit.conf: |
[SERVICE]
Flush 1
Log_Level info
Daemon off
Parsers_File parsers.conf
@INCLUDE filter.conf
@INCLUDE input.conf
@INCLUDE output.conf
input.conf: |
[INPUT]
Name tail
Path /var/log/containers/*.log
Parser kub-logs
Refresh_Interval 5
Mem_Buf_Limit 20MB
Skip_Long_Lines On
DB /var/log/flb_kube_default.db
DB.Sync Normal
Tag kube.<namespace_name>.<container_name>.<pod_name>
Tag_Regex (?<pod_name>[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*)_(?<namespace_name>[^_]+)_(?<container_name>.+)-
make_tags.lua: |
function make_tags(tag, timestamp, record)
new_record = record
local tag_list = {}
for s in string.gmatch(tag, "[^.]+") do
table.insert(tag_list, s)
end
new_record["namespace"] = tag_list[2]
new_record["container"] = tag_list[3]
new_record["pod"] = tag_list[4]
if (record["message"] == nil) then
new_record["message"] = record["log"]
end
return 1, timestamp, new_record
end
output.conf: |
[OUTPUT]
Name http
Host []clickhouse-address[]
Port 8123
URI /?user=[]user[]&password=[]pass[]&database=logs&query=INSERT%20INTO%20log%20FORMAT%20JSONEachRow
Format json_stream
Json_date_key pod_time
Json_date_format epoch
parsers.conf: |
[PARSER]
Name kub-logs
Format json
Time_Key time
Time_Format %Y-%m-%dT%H:%M:%S.%L
# Command | Decoder | Field | Optional Action
# ==============|==============|=========|=================
Decode_Field_As escaped_utf8 log do_next
Decode_Field json logКонфигурация DaemonSet, который развернет по одному инстансу fluent-bit на каждой рабочей ноде.
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: fluent-bit
namespace: monitoring
labels:
k8s-app: fluent-bit
spec:
selector:
matchLabels:
k8s-app: fluent-bit
template:
metadata:
labels:
k8s-app: fluent-bit
spec:
priorityClassName: system-node-critical
containers:
- name: fluent-bit
image: fluent/fluent-bit:1.8
imagePullPolicy: Always
volumeMounts:
- name: config
mountPath: /fluent-bit/etc/fluent-bit.conf
subPath: fluent-bit.conf
- name: config
mountPath: /fluent-bit/etc/input.conf
subPath: input.conf
- name: config
mountPath: /fluent-bit/etc/output.conf
subPath: output.conf
- name: config
mountPath: /fluent-bit/etc/parsers.conf
subPath: parsers.conf
- name: config
mountPath: /fluent-bit/etc/filter.conf
subPath: filter.conf
- name: config
mountPath: /fluent-bit/etc/make_tags.lua
subPath: make_tags.lua
- name: var-log
mountPath: /var/log
- name: var-lib-fluent
mountPath: /var/lib/fluent
- name: var-lib-docker
mountPath: /var/lib/docker
readOnly: true
- name: run-log
mountPath: /run/log
- name: etcmachineid
mountPath: /etc/machine-id
readOnly: true
volumes:
- name: config
configMap:
defaultMode: 420
name: fluent-bit
- name: var-log
hostPath:
path: /var/log
- name: var-lib-fluent
hostPath:
path: /var/lib/fluent
- name: var-lib-docker
hostPath:
path: /var/lib/docker
- name: run-log
hostPath:
path: /run/log
- name: etcmachineid
hostPath:
path: /etc/machine-id
type: File
URL:
Обсуждается: http://www.opennet.dev/tips/info/3196.shtml
чем кликхаус для хранения и обработки логов лучше ES?
Быстрее, примерно в 100500 раз.
Смотря какие объемы логов.
На малых объемах логов - грамотно сделанная схема работы с clickhouse работает быстрее, чем дефолтная схема работы с ES. Это шуточка с двойным дном. Тюнить clickhouse приятнее, чем ES. Там вы рано или поздно упретесь в java с его GC. И тюнинг ES превратится в тюнинг java.На больших объемах логов ES умрет, а clickhouse - нет. Тем clickhouse и лучше.
Еще из объективных плюсов - грамотная схема с clickhouse позволит логам занимать меньше места. Опять же, становится актуально на больших объемах данных.
Разработчики сказали, что им так удобнее. Они уже использовали ES, не понравилось. Я никому не навязываю, просто делюсь опытом.
Очень неоптимальная схема, максимально нагружающая КХ в момент вставки. Вставлять в него данные рекомендуется блоками хотя бы от 100 тыс. записей, чтобы нормально отрабатывали алгоритмы сортировки в MergeTree.Ну и по мелочи, например, для namespace вместо String можно использовать LowCardinality(String).
По умолчанию fluetn-bit отправляет данные раз в 5 секунд. В документации clickhouse рекомендуется писать в базу не чаще раза в секунду. Действующая схема оптимальна для применения в моём случае, но нет универсальной схемы, которая подойдет всем. В статье я хотел поделиться опытом, который поможет адаптировать fluent-bit для других ситуаций, в которых он в принципе может быть полезен.
Основная проблема Clickhouse это отсутствие хорошей морды для просмотра логов. Во всяком так было на момент когда я пробовал. Разрабам такой вариант не понравился, CH язык запросов мало кто знает, ES был привычнее.Но пробовали так, делали. Правда, вместо fluent-bit мы написали своего демонюгу на Go чтобы вытаскивать логи из docker и journald. Сделали отправку батчами и оптимизировали парсеры и структуру во все щели: получались какие-то сумасшедшие цифры по производительности в миллионы строк/сек с кластера с минимальной нагрузкой на демон форвардинга. Писали напрямую с коллекторов в CH.
ЗЫ кому интересно поржать, могу выложить код схемы и демона
Интересно конечно, выкладывайте.
Мы тоже сначала свой костыль написали, но на java это смотрелось не очень - не самый подходящий для этого инструмент. Поэтому поковырявшись, настроили fluent-bit. Один экземпляр занимает 5Mb памяти и около мегабайта диска,а нагрузку на cpu и диск вообще не заметили.
> CH язык запросов мало кто знаетSQL-select для одной таблички мало кто знает из разработчиков, которые логи бэка смотрят? Хорошая команда.
Loki? не?
Не для моего кейса.
Почему, в чём основные минусы?
Что используете в роли просмотрщика логов?
Оверинжиниринг...
Почему не Vector https://vector.dev/ ?
Способ хороший, однако должен заметить, что приведённая конфигурация fluent-bit может терять чанки. Как я понял, пайплайн работает так: из файла читаются чанки и поднимаются в память. После успешного чтения в DB записывается новая позиция. Далее чанк обрабатывается парсерами, фильтрами и направляется в output. Всё это время чанк держится в памяти. Однако если в output отправить не удалось, то информация об этом не будет никуда сохранена. А если процесс перезапустится, так и не успев отправить этот чанк, то чанк будет потерян: перечитываться заново чанк не будет (т.к. в базе уже записана новая позиция), а сам чанк был в памяти, и после рестарта процесса не сохранился.Чтобы избежать потери логов, нужно настроить filesystem storage и бесконечные ретраи в output. Тогда после чтения чанк сразу будет записываться на диск, и только потом будет производиться его обработка. Если процесс рестартится, то он перечитывает все сохранённые чанки и продолжает попытку их обработки и отправки. А бесконечные ретраи нужны чтобы эти чанки не дропались после N-ого неуспешного ретрая отправки.
Бесконечные попытки отправки мы делать не стали т.к. потеря логов при недоступности clickhouse - это не очень большая проблема и с таким мы смиримся легко.
С проблемой потери чанков, не отправленных в момент перезапуска или при недоступности clickhouse, мы еще не сталкивались. Можно подробнее об этой проблеме? Или ссылку на статью, где это писано?