Хутчэй за ўсё, сёння ўжо ні ў кога не ўзнікае пытанне, навошта трэба збіраць метрыкі сервісаў. Наступны лагічны крок - наладзіць алертынг на збіраныя метрыкі, які будзе апавяшчаць аб любых адхіленнях у дадзеных у зручныя вам каналы (пошту, 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 апошніх з гэтым значэннем.
Нельга зрабіць join адзін да шматлікіх, 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).
Парог уваходжання не вельмі высокі - паспрабуйце яго, калі графана ці іншыя прылады не да канца задавальняюць вашыя жаданні.