Трукі для апрацоўкі метрык у Kapacitor

Хутчэй за ўсё, сёння ўжо ні ў кога не ўзнікае пытанне, навошта трэба збіраць метрыкі сервісаў. Наступны лагічны крок - наладзіць алертынг на збіраныя метрыкі, які будзе апавяшчаць аб любых адхіленнях у дадзеных у зручныя вам каналы (пошту, Slack, Telegram). У сэрвісе анлайн-браніравання гатэляў Ostrovok.ru усе метрыкі нашых сэрвісаў льюцца ў InfluxDB і адлюстроўваюцца ў Grafana, тамака жа наладжаны базавы алертынг. Для задач тыпу "трэба палічыць нешта і параўнаць з гэтым" мы выкарыстоўваем Kapacitor.

Трукі для апрацоўкі метрык у 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()

Калі тэг/поле не запоўнена, узнікнуць памылкі ў вылічэннях:

|default()
        .tag('status', 'empty')
        .field('value', 0)

fill у join (inner vs outer)

Па змаўчанні 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'амі ці выцягнуць частку дадзеных з большага перыяду:

 |where(lambda: duration((unixNano(now()) - unixNano("time"))/1000, 1u) < 5m)

Альтэрнатывай для апошніх пяці хвілін можа быць выкарыстанне ноды 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 - наш самапісны скрыпт:

topic: slack_graph
id: slack_graph.alert
match: level() != INFO AND changed() == TRUE
kind: exec
options:
  prog: /sbin/slack_handler.py
  args: ["-c", "CHANNELID", "--graph", "--search"]

Як дэбажыць?

Варыянт з высновай у лог

|log()
      .level("error")
      .prefix("something")

Глядзець (cli): kapacitor -url host-or-ip:9092 logs lvl=error

Варыянт з httpOut

Паказвае дадзеныя ў бягучым пайплайне:

|httpOut('something')

Глядзець (get): host-or-ip:9092/kapacitor/v1/tasks/task_name/something

Схема выканання

Дзе яшчэ можна атрымаць граблямі

timestamp у influxdb пры зваротным запісе

Напрыклад, мы наладжваем алерт на суму запытаў за гадзіну (groupBy(1h)) і жадаем запісаць які здарыўся алерт у influxdb (каб хораша паказаць факт наяўнасці праблемы на графіцы ў grafana).

influxDBOut() запіша ў timestamp значэнне time з алерта, адпаведна, кропка на графіцы будзе запісана раней/пазней, чым прыйшоў алерт.

Калі патрабуецца дакладнасць: абыходзім гэтую праблему праз выклік кастамнага handler'а, які запіша дадзеныя ў influxdb з бягучым timestamp'ам.

docker, зборка і дэплой

Пры старце kapacitor можа падгружаць цягі, шаблоны і хендлеры з дырэкторыі, прапісанай у канфігу, у блоку [load].

Для карэктнага стварэння цягі патрэбныя наступныя рэчы:

  1. Назва файла - разгортваецца ў id/назва скрыпту
  2. Тып - stream / batch
  3. dbrp - кейворд для ўказання ў якой базе + палітыцы працуе скрыпт (dbrp "supplier". "autogen")

Калі ў нейкі batch-цягніку не будзе радка з dbrp, увесь сэрвіс адмовіцца запускацца і сапраўды напіша пра гэта ў лог.

У chronograf'е ж, наадварот, гэтага радка быць не павінна, праз інтэрфейс яна не прымаецца і выдае памылку.

Хак пры зборцы кантэйнера: Dockerfile выходзіць з -1, калі ёсць радкі з //.+dbrp, што дазволіць адразу зразумець чыннік фэйла пры зборцы білда.

join адзін да многіх

Задача-прыклад: трэба ўзяць 95-ы перцентиль часу працы сэрвісу за тыдзень, параўнаць кожную хвіліну з 10 апошніх з гэтым значэннем.

Нельга зрабіць join адзін да шматлікіх, last/mean/median па групе кропак ператвараюць ноду ў stream, вернецца памылка "cannot add child mismatched edges: batch -> stream".

Вынік batch'а, як зменнай у lambda-выразе, таксама не падстаўляецца.

Ёсць варыянт захоўваць патрэбныя лічбы з першага батча ў файл праз udf і загружаць гэты файл праз sideload.

Што мы гэтым рашалі?

У нас ёсць каля 100 пастаўшчыкоў гатэляў, да кожнага з іх можа быць некалькі падлучэнняў, назавем гэта каналам. Гэтых каналаў прыкладна 300, кожны з каналаў можа адваліцца. З усіх якія запісваюцца метрык будзем маніторыць рэйт памылак (requests і errors).

Чаму не графана?

Алерты памылкова, настроеныя ў графане, маюць некалькі мінусаў. Нейкія крытычныя, на нейкія можна заплюшчыць вочы, у залежнасці ад сітуацыі.

Графана не ўмее вылічэнні паміж вымярэннямі + алертынг, а нам жа патрэбен рэйт (requests-errors)/requests.

Памылкі выглядаюць злосна:

Трукі для апрацоўкі метрык у Kapacitor

І менш злосна, калі глядзець з паспяховымі запытамі:

Трукі для апрацоўкі метрык у Kapacitor

Окей, мы можам папярэдне палічыць рэйт у сэрвісе да графаны, і ў нейкіх выпадках гэта падыдзе. Але не ў нашым, т.я. для кожнага канала свае суадносіны лічыцца «нармальным», а алерты працуюць па статычных значэннях (шукаем вочкамі, змяняем, калі часта алертыт).

Гэта прыклады "нармальна" для розных каналаў:

Трукі для апрацоўкі метрык у Kapacitor

Трукі для апрацоўкі метрык у Kapacitor

Пагарджаем папярэднім пунктам і выкажам здагадку, што ва ўсіх пастаўшчыкоў "нармальная" карціна падобная. Цяпер-то ўсё добра, і мы можам абысціся алертамі ў grafana?
Можам, але вельмі не жадаецца, таму што трэба выбіраць адзін з варыянтаў:
а) зрабіць мноства графікаў пад кожны канал асобна (і пакутліва іх суправаджаць)
б) пакінуць адзін графік з усімі каналамі (і згубіцца ў квятчастых лініях і настроеных алертах)

Трукі для апрацоўкі метрык у Kapacitor

Як зрабілі?

Ізноў жа, у дакументацыі ёсць добры стартавы прыклад (Calculating rates across joined series), можна падглядзець або ўзяць за аснову ў аналагічных задачах.

Што зрабілі ў выніку:

  • join двух серый за некалькі гадзін, групоўка па каналах;
  • запаўняем серыі па групах, калі дадзеных не было;
  • параўноўваем медыяну апошніх 10 хвілін з папярэднімі дадзенымі;
  • крычым, калі нешта знайшлі;
  • пішам палічаныя рэйты і здарыліся алерты ў influxdb;
  • адпраўляем карыснае паведамленне ў slack.

На мой погляд, нам максімальна прыгожа атрымалася ўсё, што хацелі б атрымаць на выхадзе (і нават крыху больш з кастамнымі хэндлерамі).

На github.com можна паглядзець прыклад кода и мінімальную схему (graphviz) атрыманага скрыпту.

Прыклад атрыманага кода:

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).

Парог уваходжання не вельмі высокі - паспрабуйце яго, калі графана ці іншыя прылады не да канца задавальняюць вашыя жаданні.

Крыніца: habr.com

Дадаць каментар