Triky na spracovanie metrík v Kapacitore

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.

Triky na spracovanie metrík v Kapacitore
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:

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

vyplniť spojenie (vnútorné vs. vonkajšie)

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:

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

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:

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"]

Ako ladiť?

Možnosť s výstupom denníka

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

Sledujte (cli): kapacitor -url host-or-ip:9092 logov lvl=chyba

Možnosť s httpOut

Zobrazuje údaje v aktuálnom kanáli:

|httpOut('something')

Sledujte (získajte): host-or-ip:9092/kapacitor/v1/tasks/názov_úlohy/niečo

Schéma vykonávania

  • Každá úloha vráti strom vykonávania s užitočnými číslami vo formáte grafviz.
  • Vezmite blok bodka.
  • Vložte ho do prehliadača, Užite si to.

Kde inde zoženiete hrable?

časová pečiatka v influxdb pri spätnom zápise

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:

  1. Názov súboru – rozbalený na id/názov skriptu
  2. Typ – prúd/dávka
  3. 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:

Triky na spracovanie metrík v Kapacitore

A menšie zlo pri pohľade na úspešné požiadavky:

Triky na spracovanie metrík v Kapacitore

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:

Triky na spracovanie metrík v Kapacitore

Triky na spracovanie metrík v Kapacitore

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)

Triky na spracovanie metrík v Kapacitore

Ako si to urobil?

V dokumentácii je opäť dobrý štartovací príklad (Výpočet sadzieb v rámci spojených sérií), možno nahliadnuť alebo brať ako základ v podobných problémoch.

Čo sme nakoniec urobili:

  • pripojte sa k dvom sériám v priebehu niekoľkých hodín, zoskupte sa podľa kanálov;
  • vyplňte sériu podľa skupiny, ak neexistujú žiadne údaje;
  • porovnajte medián za posledných 10 minút s predchádzajúcimi údajmi;
  • kričíme, ak niečo nájdeme;
  • zapisujeme vypočítané sadzby a upozornenia, ktoré sa vyskytli v influxdb;
  • poslať užitočnú správu slackovi.

Podľa mňa sa nám všetko, čo sme chceli dostať na konci (a pri custom handleroch ešte o niečo viac), podarilo dosiahnuť čo najkrajšie.

Môžete sa pozrieť na github.com príklad kódu и minimálny okruh (graphviz) výsledný skript.

Príklad výsledného kódu:

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.

Zdroj: hab.com

Pridať komentár