S najväčšou pravdepodobnosťou sa dnes nikto nepýta, prečo je potrebné zbierať metriky služieb. Ďalším logickým krokom je nastavenie upozornenia na zhromaždené metriky, ktoré vás upozorní na prípadné odchýlky v údajoch v kanáloch, ktoré vám vyhovujú (pošta, Slack, Telegram). V online rezervačnej službe hotela Ostrovok.ru všetky metriky našich služieb sú naliate do InfluxDB a zobrazené v Grafane a je tam nakonfigurované aj základné upozorňovanie. Na úlohy typu „potrebujete niečo vypočítať a porovnať s tým“ používame Kapacitor.
Kapacitor je súčasťou zásobníka TICK, ktorý dokáže spracovať metriky z InfluxDB. Dokáže spojiť viacero meraní dokopy (spojiť), vypočítať niečo užitočné z prijatých dát, zapísať výsledok späť do InfluxDB, poslať upozornenie na Slack/Telegram/mail.
Celý zásobník je cool a detailný dokumentácia, ale vždy sa nájdu užitočné veci, ktoré nie sú výslovne uvedené v návodoch. V tomto článku som sa rozhodol zhromaždiť niekoľko takýchto užitočných, nie samozrejmých tipov (základná syntax TICKsciptu je popísaná tu) a ukážte, ako sa dajú aplikovať na príklade riešenia jedného z našich problémov.
Poďme!
float & int, chyby výpočtu
Absolútne štandardný problém, riešený cez kasty:
var alert_float = 5.0
var alert_int = 10
data|eval(lambda: float("value") > alert_float OR float("value") < float("alert_int"))
Použitie default()
Ak značka/pole nie je vyplnená, vyskytnú sa chyby vo výpočte:
V predvolenom nastavení spojenie zruší body, kde nie sú žiadne údaje (vnútorné).
Pomocou fill('null') sa vykoná vonkajšie spojenie, po ktorom musíte urobiť default() a vyplniť prázdne hodnoty:
var data = res1
|join(res2)
.as('res1', 'res2)
.fill('null')
|default()
.field('res1.value', 0.0)
.field('res2.value', 100.0)
Stále je tu nuansa. Vo vyššie uvedenom príklade, ak je jeden z radov (res1 alebo res2) prázdny, výsledný rad (údaje) bude tiež prázdny. Na Github existuje niekoľko lístkov na túto tému (1633, 1871, 6967) – čakáme na opravy a trochu trpíme.
Použitie podmienok vo výpočtoch (ak sú v lambda)
|eval(lambda: if("value" > 0, true, false)
Posledných päť minút od potrubia na dané obdobie
Napríklad musíte porovnať hodnoty za posledných päť minút s predchádzajúcim týždňom. Môžete vziať dve dávky údajov v dvoch samostatných dávkach alebo extrahovať časť údajov z väčšieho obdobia:
Alternatívou na posledných päť minút by bolo použitie BarrierNode, ktorý preruší údaje pred určeným časom:
|barrier()
.period(5m)
Príklady použitia šablón Go v správe
Šablóny zodpovedajú formátu z balenia text.šablónaNižšie sú uvedené niektoré často sa vyskytujúce hádanky.
keby-inak
Dávame veci do poriadku a už ľudí nespúšťame textom:
|alert()
...
.message(
'{{ if eq .Level "OK" }}It is ok now{{ else }}Chief, everything is broken{{end}}'
)
Dve číslice za desatinnou čiarkou v správe
Zlepšenie čitateľnosti správy:
|alert()
...
.message(
'now value is {{ index .Fields "value" | printf "%0.2f" }}'
)
Rozširujúce sa premenné v správe
V správe zobrazujeme viac informácií, aby sme odpovedali na otázku „Prečo to kričí“?
var warnAlert = 10
|alert()
...
.message(
'Today value less then '+string(warnAlert)+'%'
)
Jedinečný identifikátor výstrahy
Toto je nevyhnutné, ak je v údajoch viac ako jedna skupina, inak sa vygeneruje iba jedno upozornenie:
|alert()
...
.id('{{ index .Tags "myname" }}/{{ index .Tags "myfield" }}')
Vlastné manipulátory
Veľký zoznam obslužných programov obsahuje exec, ktorý vám umožňuje spustiť váš skript s odovzdanými parametrami (stdin) - kreativita a nič viac!
Jedným z našich zvykov je malý skript Python na odosielanie upozornení do Slacku.
Najprv sme chceli poslať do správy autorizačne chránený obrázok grafana. Potom napíšte OK do vlákna na predchádzajúce upozornenie z tej istej skupiny a nie ako samostatnú správu. O niečo neskôr – do správy pridajte najčastejšiu chybu za posledných X minút.
Samostatnou témou je komunikácia s inými službami a akékoľvek akcie iniciované upozornením (iba ak váš monitoring funguje dostatočne dobre).
Príklad popisu obslužného programu, kde slack_handler.py je náš vlastný skript:
Napríklad si nastavíme upozornenie na súčet požiadaviek za hodinu (groupBy(1h)) a chceme zaznamenať upozornenie, ktoré sa vyskytlo v influxdb (aby sa fakt problému krásne ukázal na grafe v grafane).
influxDBOut() zapíše časovú hodnotu z upozornenia do časovej pečiatky; podľa toho bude bod na grafe zapísaný skôr/neskôr, ako prišlo upozornenie.
Keď sa vyžaduje presnosť: tento problém obídeme zavolaním vlastného obslužného programu, ktorý zapíše údaje do influxdb s aktuálnou časovou značkou.
docker, zostavenie a nasadenie
Pri štarte môže kapacitor načítať úlohy, šablóny a handlery z adresára špecifikovaného v konfigurácii v bloku [load].
Na správne vytvorenie úlohy potrebujete nasledujúce veci:
Názov súboru – rozbalený na id/názov skriptu
Typ – prúd/dávka
dbrp – kľúčové slovo označujúce, v ktorej databáze a politike sa skript spúšťa (dbrp „dodávateľ.“ „autogen“)
Ak niektorá dávková úloha neobsahuje riadok s dbrp, celá služba sa odmietne spustiť a poctivo o tom zapíše do logu.
V chronografe by naopak tento riadok nemal existovať, cez rozhranie nie je akceptovaný a generuje chybu.
Hackovanie pri zostavovaní kontajnera: Dockerfile sa ukončí s -1, ak existujú riadky s //.+dbrp, čo vám umožní okamžite pochopiť dôvod zlyhania pri zostavovaní zostavy.
pripojiť jeden k mnohým
Príklad úlohy: potrebujete vziať 95. percentil prevádzkového času služby za týždeň, porovnať každú minútu z posledných 10 s touto hodnotou.
Nemôžete vykonať spojenie one-to-many, posledný/stredný/medián nad skupinou bodov premení uzol na prúd, vráti sa chyba „nie je možné pridať podradené nezhodné okraje: dávka -> prúd“.
Výsledok dávky, ako premenná vo výraze lambda, sa tiež nenahrádza.
Je tu možnosť uložiť potrebné čísla z prvej dávky do súboru cez udf a načítať tento súbor cez sideload.
Čo sme tým vyriešili?
Máme asi 100 hotelových dodávateľov, každý z nich môže mať viacero spojení, nazvime to kanál. Týchto kanálov je približne 300, pričom každý kanál môže vypadnúť. Zo všetkých zaznamenaných metrík budeme sledovať chybovosť (žiadosti a chyby).
Prečo nie grafana?
Chybové upozornenia nakonfigurované v Grafane majú niekoľko nevýhod. Niektoré sú kritické, pred niektorými môžete zavrieť oči, v závislosti od situácie.
Grafana nevie kalkulovať medzi meraniami + upozornením, ale potrebujeme sadzbu (žiadosti-chyby)/žiadosti.
Chyby vyzerajú nepríjemne:
A menšie zlo pri pohľade na úspešné požiadavky:
Dobre, vieme si vopred vypočítať sadzbu v službe pred grafanou a v niektorých prípadoch to bude fungovať. Ale nie v našom, pretože... pre každý kanál sa jeho vlastný pomer považuje za „normálny“ a upozornenia fungujú podľa statických hodnôt (hľadáme ich očami, meníme ich, ak sú upozornenia časté).
Toto sú príklady „normálneho“ pre rôzne kanály:
Ignorujeme predchádzajúci bod a predpokladáme, že „normálny“ obraz je podobný pre všetkých dodávateľov. Teraz je všetko v poriadku a môžeme si vystačiť s upozorneniami v grafane?
Môžeme, ale naozaj nechceme, pretože si musíme vybrať jednu z možností:
a) vytvorte veľa grafov pre každý kanál zvlášť (a bolestivo ich sprevádzajte)
b) nechajte jeden graf so všetkými kanálmi (a stratte sa vo farebných riadkoch a prispôsobených upozorneniach)
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)
aký je záver?
Kapacitor je skvelý pri vykonávaní monitorovacích upozornení s množstvom zoskupení, vykonávaní ďalších výpočtov na základe už zaznamenaných metrík, vykonávaní vlastných akcií a spúšťaní skriptov (udf).
Bariéra vstupu nie je príliš vysoká - skúste to, ak grafana alebo iné nástroje úplne neuspokoja vaše túžby.