Швидше за все сьогодні вже ні в кого не виникає питання, навіщо потрібно збирати метрики сервісів. Наступний логічний крок - налаштувати алертинг на метрики, що збирається, який буде сповіщати про будь-які відхилення в даних у зручні вам канали (пошту, Slack, Telegram). У сервісі онлайн-бронювання готелів Ostrovok.ru всі метрики наших сервісів ллються в InfluxDB і відображаються в Grafana, там налаштований базовий алертинг. Для завдань типу "потрібно порахувати щось і порівняти з цим" ми використовуємо Kapacitor.
Kapacitor – частина TICK-стеку, що вміє обробляти метрики з InfluxDB. Він може з'єднати кілька вимірювань між собою (join), з отриманих даних обчислити щось корисне, записати результат назад до InfluxDB, відправити алерт у Slack/Telegram/пошту.
Весь стек має круту та докладну документаціюАле завжди знайдуться корисні штуки, які в явному вигляді в мануалах не вказані. У цій статті я вирішив зібрати низку таких корисних неочевидних порад (основний синтаксис TICKscipt описаний тут) і показати, як їх можна застосовувати, на прикладі вирішення одного з наших завдань.
Поїхали!
float & int, помилки обчислень
Абсолютно стандартна проблема вирішується через каст:
var alert_float = 5.0
var alert_int = 10
data|eval(lambda: float("value") > alert_float OR float("value") < float("alert_int"))
Використання default()
Якщо тег/поле не заповнено, виникнуть помилки у обчисленнях:
За замовчуванням join відкине точки, де немає даних (inner).
При fill('null') буде виконано outer join, після якого потрібно зробити default() і заповнити порожні значення:
var data = res1
|join(res2)
.as('res1', 'res2)
.fill('null')
|default()
.field('res1.value', 0.0)
.field('res2.value', 100.0)
Тут все одно є нюанс. Якщо у прикладі вище одна із серій (res1 або res2) буде порожньою, підсумкова серія (data) також буде порожньою. На цю тему є кілька тикетів на гітхабі.1633, 1871, 6967) – чекаємо фіксів і трохи страждаємо.
Використання умов у обчисленнях (if у lambda)
|eval(lambda: if("value" > 0, true, false)
Останні п'ять хвилин із пайплайну за період
Наприклад, вам потрібно порівняти значення останніх п'яти хвилин із попереднім тижнем. Можна взяти дві пачки даних двома окремими batch'ами або витягнути частину даних із більшого періоду:
Альтернативою останніх п'яти хвилин може бути використання ноди BarrierNode, яка відсікає дані раніше зазначеного часу:
|barrier()
.period(5m)
Приклади використання Go'шних шаблонів у message
Шаблони відповідають формату з пакета text.template, Нижче кілька часто зустрічаються завдань.
якщо ще
Наводимо лад, не тригерим людей текстом зайвий раз:
|alert()
...
.message(
'{{ if eq .Level "OK" }}It is ok now{{ else }}Chief, everything is broken{{end}}'
)
Дві цифри після коми у message
Поліпшуємо читабельність повідомлення:
|alert()
...
.message(
'now value is {{ index .Fields "value" | printf "%0.2f" }}'
)
Розгортання змінних у message
Виводимо в повідомлення більше інформації для відповіді на питання «Чому репетує-то»?
var warnAlert = 10
|alert()
...
.message(
'Today value less then '+string(warnAlert)+'%'
)
Унікальний ідентифікатор алерту
Потрібна штука, коли в даних більше однієї групи, інакше генеруватиметься лише один алерт:
|alert()
...
.id('{{ index .Tags "myname" }}/{{ index .Tags "myfield" }}')
Кастомні handler's
У великому списку хендлерів є exec, який дозволяє виконати свій скрипт з переданими параметрами (stdin) – творчість та й годі!
Один з наших кастомів – це невеликий скрипт для виправлення повідомлень у слак.
Спочатку нам захотілося відправляти у повідомленні картинку з графани, захищеною авторизацією. Після – писати OK в тред до попереднього алерту з тієї ж групи, а чи не окремим повідомленням. Ще трохи пізніше – дописувати у повідомлення найчастішу помилку за останні Х хвилин.
Окрема тема – зв'язок з іншими сервісами та будь-які дії, ініційовані алертом (тільки якщо ваш моніторинг працює досить добре).
Приклад опису хендлера, де slack_handler.py – наш самописний скрипт:
Наприклад, ми налаштовуємо алерт на суму запитів за годину (groupBy(1h)) і хочемо записати алерт в influxdb (щоб красиво показати факт наявності проблеми на графіку в grafana).
influxDBOut() запише в timestamp значення time з алерту, відповідно, точка на графіку буде записана раніше/пізніше, ніж прийшов алерт.
Коли потрібна точність: обходимо цю проблему через виклик кастомного handler'а, який запише дані в influxdb з поточним timestamp'ом.
docker, складання та деплой
При старті kapacitor може підвантажувати таски, шаблони і хендлер з директорії, прописаної в конфізі, в блоці [load].
Для коректного створення таски потрібні такі речі:
Назва файлу – розгортається в id/назва скрипта
Тип – stream/batch
dbrp - кейворд для вказівки в якій базі + політиці працює скрипт (dbrp "supplier". "autogen")
Якщо в якомусь batch-тягу не буде рядка з dbrp, весь сервіс відмовиться запускатися і чесно напише про це в балку.
У chronograf'е ж, навпаки, цього рядка не повинно бути, через інтерфейс вона не приймається і видає помилку.
Хак при складанні контейнера: Dockerfile виходить з -1, якщо є рядки з //.+dbrp, що дозволить відразу зрозуміти причину фейлу при складанні білда.
join один до багатьох
Завдання: потрібно взяти 95-й перцентиль часу роботи сервісу за тиждень, порівняти кожну хвилину з 10 останніх з цим значенням.
Не можна зробити один до багатьох, last/mean/median по групі точок перетворюють ноду в stream, повернеться помилка «cannot add child mismatched edges: batch -> stream».
Результат batch'а, як змінної в lambda-вираженні, теж не підставляється.
Є варіант зберігати потрібні цифри з першого батча до файлу через udf і завантажувати цей файл через sideload.
Що ми вирішували цим?
Ми маємо близько 100 постачальників готелів, до кожного з них може бути кілька підключень, назвемо це каналом. Цих каналів приблизно 300, кожен із каналів може відвалитися. З усіх записуваних метрик моніторитимемо рейт помилок (requests і errors).
Чому не графана?
Алерти помилково, налаштовані в графані, мають кілька мінусів. Якісь критичні, на якісь можна заплющити очі, залежно від ситуації.
Графана не вміє обчислення між вимірами + алертинг, а нам потрібен рейт (requests-errors)/requests.
Помилки виглядають злісно:
І менш зло, якщо дивитися з успішними запитами:
Окей, ми можемо заздалегідь порахувати рейт у сервісі до графани, і в якихось випадках це підійде. Але над нашому, т.к. для кожного каналу своє співвідношення вважається «нормальним», а алерти працюють за статичними значеннями (шукаємо очима, міняємо, якщо часто алертит).
Це приклади «нормально» для різних каналів:
Нехтуємо попереднім пунктом та припустимо, що у всіх постачальників «нормальна» картина схожа. Тепер все добре, і ми можемо обійтися алертами в grafana?
Можемо, але дуже не хочеться, бо треба вибирати один із варіантів:
а) зробити безліч графіків під кожен канал окремо (і болісно їх супроводжувати)
б) залишити один графік з усіма каналами (і загубитися в квітчастих лініях та налаштованих алертах)
Як зробили?
Знову ж таки, у документації є хороший стартовий приклад (Calculating rates across joined series), можна піддивитися або взяти за основу в аналогічних завданнях.
Що зробили в результаті:
join двох серій за кілька годин, угруповання каналами;
заповнюємо серії за групами, якщо даних не було;
порівнюємо медіану останніх 10 хвилин із попередніми даними;
кричимо, якщо щось виявили;
пишемо пораховані рейти і алерти в influxdb;
відправляємо корисне повідомлення у slack.
На мою думку, нам максимально красиво вдалося все, що хотіли б отримати на виході (і навіть трохи більше з кастомними хендлерами).
dbrp "supplier"."autogen"
var name = 'requests.rate'
var grafana_dash = 'pczpmYZWU/mydashboard'
var grafana_panel = '26'
var period = 8h
var todayPeriod = 10m
var every = 1m
var warnAlert = 15
var warnReset = 5
var reqQuery = 'SELECT sum("count") AS value FROM "supplier"."autogen"."requests"'
var errQuery = 'SELECT sum("count") AS value FROM "supplier"."autogen"."errors"'
var prevErr = batch
|query(errQuery)
.period(period)
.every(every)
.groupBy(1m, 'channel', 'supplier')
var prevReq = batch
|query(reqQuery)
.period(period)
.every(every)
.groupBy(1m, 'channel', 'supplier')
var rates = prevReq
|join(prevErr)
.as('req', 'err')
.tolerance(1m)
.fill('null')
// заполняем значения нулями, если их не было
|default()
.field('err.value', 0.0)
.field('req.value', 0.0)
// if в lambda: считаем рейт, только если ошибки были
|eval(lambda: if("err.value" > 0, 100.0 * (float("req.value") - float("err.value")) / float("req.value"), 100.0))
.as('rate')
// записываем посчитанные значения в инфлюкс
rates
|influxDBOut()
.quiet()
.create()
.database('kapacitor')
.retentionPolicy('autogen')
.measurement('rates')
// выбираем данные за последние 10 минут, считаем медиану
var todayRate = rates
|where(lambda: duration((unixNano(now()) - unixNano("time")) / 1000, 1u) < todayPeriod)
|median('rate')
.as('median')
var prevRate = rates
|median('rate')
.as('median')
var joined = todayRate
|join(prevRate)
.as('today', 'prev')
|httpOut('join')
var trigger = joined
|alert()
.warn(lambda: ("prev.median" - "today.median") > warnAlert)
.warnReset(lambda: ("prev.median" - "today.median") < warnReset)
.flapping(0.25, 0.5)
.stateChangesOnly()
// собираем в message ссылку на график дашборда графаны
.message(
'{{ .Level }}: {{ index .Tags "channel" }} err/req ratio ({{ index .Tags "supplier" }})
{{ if eq .Level "OK" }}It is ok now{{ else }}
'+string(todayPeriod)+' median is {{ index .Fields "today.median" | printf "%0.2f" }}%, by previous '+string(period)+' is {{ index .Fields "prev.median" | printf "%0.2f" }}%{{ end }}
http://grafana.ostrovok.in/d/'+string(grafana_dash)+
'?var-supplier={{ index .Tags "supplier" }}&var-channel={{ index .Tags "channel" }}&panelId='+string(grafana_panel)+'&fullscreen&tz=UTC%2B03%3A00'
)
.id('{{ index .Tags "name" }}/{{ index .Tags "channel" }}')
.levelTag('level')
.messageField('message')
.durationField('duration')
.topic('slack_graph')
// "today.median" дублируем как "value", также пишем в инфлюкс остальные филды алерта (keep)
trigger
|eval(lambda: "today.median")
.as('value')
.keep()
|influxDBOut()
.quiet()
.create()
.database('kapacitor')
.retentionPolicy('autogen')
.measurement('alerts')
.tag('alertName', name)
А висновок який?
Kapacitor чудово вміє виконувати моніторинг-алертинг із купою угруповань, проводити додаткові обчислення за вже записаними метриками, виконувати кастомні дії та запускати скрипти (udf).
Поріг входження не дуже високий - спробуйте його, якщо графана чи інші інструменти не до кінця задовольняють ваші хотілки.