Հիմնականում, այսօր իրական հարց չէ, թե ինչու պետք է հավաքեք ծառայությունների մետրիկաները։ Հաջորդ տրամաբանական քայլը՝ կարգավորել ալերտը հավաքված մետրիկաների համար, որը կտեղեկացնի ցանկացած խախտումների մասին տվյալներում ձեզ հարմար ալիքներով (փոստ, Slack, Telegram)։ Հյուրանոցների օնլայն բուկինգից ծառայությունում մեր ծառայությունների բոլոր մետրիկաները մտնում են 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()-ի օգտագործում
Եթե թեգ/հետքը չհամակցված է, հաշվարկներում սխալներ կառաջանան։
|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) նույնպես կլինի դատարկ։ Այս թեմայով մի քանի տիքետ կա GitHub-ում (, , ) – սպասում ենք շտկումներին և մի փոքր տառապում։
Հաշվարկներում պայմանների օգտագործում (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-ում
Շաբլոնները համապատասխանել են , ներքևում մի քանի հաճախ հանդիպող խնդիրներ։
if-else
Տարածում ենք կարգը, չենք տրիգեր ոչ մեկի ավելորդ տեքստով։
|alert()
...
.message(
'{{ if eq .Level "OK" }}Այժմ ամեն բան կարգին է{{ else }}Անձնակազմ, ամեն բան կոտրված է{{end}}'
)
Երկու տրված թիվ message-ում
Կատալոգում բարելավում ենք հաղորդագրության ընթերցելիությունը:
|alert()
...
.message(
' այժմ արժեքը {{ index .Fields "value" | printf "%0.2f" }}'
)
Ալեր տեքստում փոփոխականների բացում
Համապատասխանության հարցին "Ինչու է խոսում" ավելի շատ տեղեկություն ենք տրամադրում:
var warnAlert = 10
|alert()
...
.message(
'Այսօր արժեքը նվազ է '+string(warnAlert)+'%'-ից
)
Ալերի յուրահատուկ նույնականացում
Հարմար բան, երբ տվյալները ավելի քան մեկ խումբ ունեն, հակառակ դեպքում միայն մեկ ալեր կստեղծվի:
|alert()
...
.id('{{ index .Tags "myname" }}\/{{ index .Tags "myfield" }}')
Կաստոմային handler-ներ
Մեծ ցուցակում есть exec, որը թույլ է տալիս կատարել ձեր սկրիպտը փոխանցված պարամետրերով (stdin) – միայն ստեղծագործություն:
Մեր կաստոմներից մեկը փոքր Python սկրիպտ է, որը թույլ է տալիս ստանալ ծանուցումներ Slack-ում:
Սկզբից մեզ ուզվում էր հաղորդման մեջ առարկա ուղարկել Grafana-ից, որը պաշտպանված էր հեղինակմամբ: Հետո՝ գրել OK նախորդ ալերի թելիկում նույն խմբից, այլ ոչ թե առանձին հաղորդմամբ: Ավելին՝ գրել հաղորդման մեջ վերջին X րոպեների ընթացքում ամենատարածված սխալը:
Անցորդ թեմա՝ այլ ծառայություններ, կապվածություն և ալերով նախաձեռնված գործողությունները (միայն տրամադրվածը եթե ձեր մոնիտորինգը լուրջ աշխատում է):
Handler-ի նկարագրության օրինակ, որտեղ 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("ինչ-որ բան")
Դիտել (cli): kapacitor -url :9092 logs lvl=error
HttpOut տարբերակը
Տրամադրում է տվյալներ ընթացիկ փողում:
|httpOut('ինչ-որ բան')
Դիտել (get): :9092\/kapacitor\/v1\/tasks\/task_name\/ինչ-որ բան
Կատարելիության սխեմա
- Յուրաքանչյուր tarefa վերադարձնում է կատարողականի ծառ, օգտակար թվեր ձևաչափով .
- Գնում ենք բլոկը .
- Մուտքագրենք տեսողություն, .
Որտեղ էլ կարող ենք վերցնել գրառումները
timestamp in influxdb հակառակ գրանցման
Օրինակ, մենք կարգավորում ենք ալեր հարցերի գումարած ժամի (groupBy(1h)) և ցանկանում ենք գրանցել տեղի ունեցած ալերը influxdb-ում (որպեսզի գեղեցիկ ցույց տանք նրա առկայության փաստը графիկում Grafana-ում):
influxDBOut() գրանցելու է timestamp արժեքը ալերի ժամանակից, հետևաբար, կետը կգրանցվի վաղուց/ուշապես, քան ալերը եկավ:
Երբ պահանջվում է ճշտություն. շրջանցենք այս խնդիրը՝ զանգահարելով կաստոմային handler, որը կհամապատասխանեցնի տվյալները influxdb-ին ներկա timestamp-ով:
docker, հավաքում և տեղադրում
Kapacitor-ի մեկնարկման ժամանակ այն կարող է загрузить tasks, templates և handlers config-ում նշված դիրքից, [load] բլոկում:
Կազմելու տասկի համար անհրաժեշտ են հետևյալը:
- Nam-ի ֆայլը – բացվում է id/սկրիպտի անվանում
- Տեսակը – stream/batch
- dbrp – բանաձև, որը ցույց է տալիս, որ բազայում + քաղաքականությունը, որի համար աշխատում է սկրիպտը (dbrp "supplier"."autogen")
Եթե որևէ batch-tasks չի լինի dbrp տող, ամբողջ ծառայությունը կադարձվի գործարկելուց հրաժարվելու և իջեցված կլինի լոգում:
Chronograf-ում այս տողը չպետք է լինի, որովհետեւ միջերեսի միջոցով դա չի ընդունվում և տվյալ սխալ է:
Կոնտեյների կառուցման հաքը. Dockerfile-ը դառնում է -1, եթե կան ուլեր՝ //.+dbrp, ինչը կօգնի անմիջապես հասկանալ կառուցման ձախողման պատճառը:
join մեկը շատի վրա
Հ مثال՝ անհրաժեշտ է վերցնել ծառայության աշխատանքի 95-րդ տոկոսի ժամանակը մեկ շաբաթվա ընթացքում, համեմատել վերջին 10 րոպեների յուրաքանչյուր րոպեն այս արժեքի հետ:
Չեք կարող անել join մեկը շատի վրա. last/mean/median ըստ կետերի խմբի փոխում են հանգույցը stream-ի, կհայտնվի «cannot add child mismatched edges: batch -> stream» սխալը:
Batch-ի արդյունքը, որպես փոփոխական lambda արտահայտությունում, նույնպես չի ներդրվում:
Կա տարբերակ պահպանել անհրաժեշտ թվերը առաջին batch-ից ֆայլի միջոցով udf-ի միջոցով և բեռնել այդ ֆայլը sideloadի միջոցով:
Ինչով լուծեցինք սա:
Մենք ունենք մոտ 100 հյուրանոցների մատակարար, յուրաքանչյուրի համար կարող է լինել մի քանի միացում, կոչենք դա ալիք: Այս ալիքները առ alrededor 300 են, յուրաքանչյուր ալիք կարող է կտրել: Ամենամատչելի մետրիկաներից մենք հետեւելու ենք սխալի մակարդակը (request-ներ և errors):
Pourquoi pas Grafana?
Grafana-ում սխալների համար սահմանված ալերտները ունեն որոշակի բացասական կողմեր: Որոշները κρίական են, մյուսների վրա կարելի է աչք փակել, իրավիճակից կախված:
Grafana-ն չի կարող հաշվել չափումների միջև + ալերտինգ, իսկ մեզ անհրաժեշտ է մակարդակ (requests-errors)/requests:
Սխալները երևում են չարիքաբառի համար:

Եվ ավելի քիչ չարիքաբառ, եթե դիտենք հաջող կապերը:

Օկ, նախօրոք կարող ենք հաշվել մակարդակը ծառայությունում մինչև Grafana, և որոշ դեպքերում դա կարող է ստացվել: Բայց ոչ մեր դեպքում, քանի որ յուրաքանչյուր ալիքի համար իր հարաբերակցությունը համարվում է «նորմալ», և ալերտները աշխատում են ստատիկ արժեքների վրա (հետազոտում ենք աչկերով, փոխում ենք, եթե հաճախ է ալերտը):
Ս αυτά ենք պտտում «նորմալ» օրինակներ տարբեր ալիքների համար:


Անտեսենք նախորդ կետը և ենթադրենք, որ բոլոր մատակարարների «նորմալ» պատկերը նման է: Հիմա արդեն ամեն ինչ լավ է, և կարող ենք բավարարվել Grafana-ում ալերտներով:
Կարող ենք, բայց շատ ցանկալի չէ, որովհետև պետք է ընտրել մեկ տարբերակ:
ա) անել բազմաթիվ գրաֆիկներ յուրաքանչյուր ալիքի համար առանձին (և ցավալի կերպով դրանք հետ պահել)
բ) թողնել մեկ գրաֆիկ բոլոր ալիքների համար (և կորցնել գունաթափ գծերի և սահմանված ալերտների մեջ)

Ինչ ենք արել?
Երիտասարդ բնիկը մեր դոկում ունի լավ ծանր կետեր (), կարելի է դիտել կամ հիմք վերցնել նման խնդիրների համար:
Ի՞նչ ենք արել արդյունքում:
- join երկու սերիաներ մի քանի ժամվա ընթացքում, խմբավորում ալիքների հիման վրա;
- համրող ենք սերիաները խմբերի միջոցով, եթե տվյալներ չկան;
- համեմատում ենք վերջին 10 րոպեների միջին արժեքը նախորդ տվյալների հետ;
- կիմանանք, եթե ինչ-որ բան հայտնաբերենք;
- հրապարակում ենք հաշված մակարդակները և տեղի ունեցած ալերտները influxdb-ում;
- ուղարկում ենք օգտակար հաղորդագրություն slack-ում:
Իմ կարծիքով, մեզ հաջողվել է առավելագույնս գեղեցկացնել ամբողջ այն, ինչ կցանկանայինք ստանալ ելքում (և նույնիսկ ավելի՝ հատուկ հենդլերներով):
github.com-ում կարելի է տեսնել: և ստացված սցրիպտի:
Ստացված կոդի օրինակը:
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) 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=UTC0300'
)
.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).
Порог вхождения достаточно низкий – попробуйте его, если Grafana или другие инструменты не полностью удовлетворяют ваши требования.
Ընտանիք: habr.com
