Triky na spracovanie metrík v Kapacitore

S najväčšou pravdepodobnosťou sa už nikto nezamýšľa nad tým, prečo je potrebné zhromažďovať metriky služieb. Ďalším logickým krokom je nastavenie upozornení na zhromaždené metriky, ktoré vás budú informovať o akýchkoľvek odchýlkach údajov prostredníctvom vami preferovaných kanálov (e-mail, Slack, Telegram). V online službe rezervácie hotelov Ostrovok.ru Všetky metriky z našich služieb sú streamované do InfluxDB a zobrazované v Grafane, kde je tiež nakonfigurované základné upozornenia. Pre úlohy typu „potrebujem niečo vypočítať a porovnať to s týmto“ používame Kapacitor.

Triky na spracovanie metrík v Kapacitore
Kapacitor je súčasťou TICK stacku, ktorý dokáže spracovať metriky z InfluxDB. Dokáže spojiť viacero dimenzií, vypočítať užitočné dáta z výsledných dát, zapísať výsledok späť do InfluxDB a posielať upozornenia do Slacku, Telegramu alebo e-mailu.

Celý zásobník má skvelý a detailný vzhľad dokumentácia, ale vždy sa nájdu užitočné veci, ktoré nie sú v manuáloch výslovne uvedené. V tomto článku som sa rozhodol zhromaždiť niekoľko takýchto užitočných, nie zrejmých tipov (základná syntax TICKscipt je popísaná tu) a ukážte, ako ich možno aplikovať na príklade riešenia jedného z našich problémov.

Poďme!

chyby vo výpočtoch s float a int

Úplne štandardný problém, vyriešený odlievaním:

var alert_float = 5.0
var alert_int = 10
data|eval(lambda: float("value") > alert_float OR float("value") < float("alert_int"))

Používanie predvoleného()

Ak značka/pole nie je vyplnené, vyskytnú sa chyby vo výpočte:

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

vyplňte spojenie (vnútorné vs. vonkajšie)

V predvolenom nastavení funkcia join zahodí body, kde nie sú žiadne údaje (vnútorné).
Pri použití funkcie fill('null') sa vykoná vonkajšie spojenie, po ktorom je potrebné vykonať funkciu 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 však jedno upozornenie. Ak je jedna zo sérií (res1 alebo res2) v uvedenom príklade prázdna, výsledná séria (dáta) bude tiež prázdna. Na túto tému existuje niekoľko problémov na GitHube (1633, 1871, 6967) – čakáme na opravy a trochu trpíme.

Použitie podmienok vo výpočtoch (ak je v lambda)

|eval(lambda: if("value" > 0, true, false)

Posledných päť minút z potrubia za dané obdobie

Napríklad potrebujete porovnať hodnoty za posledných päť minút s hodnotami za predchádzajúci týždeň. Môžete vziať dve sady ú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 pre posledných päť minút je použitie uzla BarrierNode, ktorý preruší prenos dát pred zadaným časom:

|barrier()
        .period(5m)

Príklady použitia šablón Go v aplikácii Message

Šablóny zodpovedajú formátu z balíka text.šablóna, nižšie sú uvedené niektoré často sa vyskytujúce problémy.

keby-inak

Urobme si poriadok a nedráždime ľudí zbytočným 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širovanie premenných v správe

V správe zobrazujeme viac informácií, aby sme odpovedali na otázku „Prečo kričí?“.

var warnAlert = 10
  |alert()
    ...
    .message(
       'Today value less then '+string(warnAlert)+'%'
    )

Jedinečný identifikátor upozornenia

Toto je užitočná funkcia, keď sa v údajoch nachádza viac ako jedna skupina, inak sa vygeneruje iba jedno upozornenie:

|alert()
      ...
      .id('{{ index .Tags "myname" }}/{{ index .Tags "myfield" }}')

Vlastné obslužné programy

Veľký zoznam obslužných programov obsahuje aj exec, ktorý vám umožňuje spustiť skript s odovzdanými parametrami (stdin) – čistá kreativita!

Jeden z našich vlastných skriptov je malý Python skript na odosielanie notifikácií do Slacku.
Najprv sme chceli poslať obrázok z Grafany v správe chránenej autorizáciou. Potom sme chceli napísať „OK“ do vlákna pre predchádzajúce upozornenie z tej istej skupiny, a nie v samostatnej správe. O niečo neskôr sme chceli do správy pridať najčastejšiu chybu za posledných X minút.

Samostatnou témou je prepojenie 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 do protokolu

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

Sledujte (cli): kondenzátor -url hostiteľ alebo IP adresa:9092 protokoly úroveň=chyba

Možnosť s httpOut

Zobrazuje údaje v aktuálnom kanáli:

|httpOut('something')

Pozrite si (získajte): hostiteľ alebo IP adresa:9092/kondenzátor/v1/úlohy/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.
  • Berieme blok bodka.
  • Vložiť do prehliadača, poďme si to užiť.

Kde inde sa dá zhrabať?

časová pečiatka v influxdb počas spätného zápisu

Napríklad, nastavili sme upozornenie na súčet požiadaviek za hodinu (groupBy(1h)) a chceme toto upozornenie zaznamenať do influxdb (aby sme pekne znázornili fakt problému v grafe v Grafane).

influxDBOut() zapíše časovú hodnotu z upozornenia do časovej pečiatky, takže bod na grafe bude zapísaný skôr/neskôr, ako upozornenie prišlo.

Keď je potrebná presnosť: tento problém obídeme volaním vlastného obslužného programu, ktorý zapíše dáta do influxdb s aktuálnou časovou pečiatkou.

Docker, zostavenie a nasadenie

Pri spustení môže kapacitor načítať úlohy, šablóny a obslužné programy z adresára uvedeného v konfigurácii v bloku [load].

Na správne vytvorenie úlohy potrebujete nasledujúce veci:

  1. Názov súboru – rozbalí sa na ID/názov skriptu
  2. Typ – prúd/dávka
  3. dbrp – kľúčové slovo na určenie, v ktorej databáze + politike sa skript spúšťa (dbrp "supplier"."autogen")

Ak ktorákoľvek dávková úloha neobsahuje riadok s dbrp, celá služba sa odmietne spustiť a úprimne o tom zapíše do protokolu.

V Chronografe by sa tento riadok naopak nemal nachádzať; rozhranie ho neakceptuje a vráti chybu.

Hack pre zostavenie kontajnera: Dockerfile ukončí prácu s -1, ak existujú riadky s //.+dbrp, čo vám umožní okamžite pochopiť dôvod zlyhania zostavenia.

pripojiť sa jeden k mnohým

Príklad úlohy: vezmite 95. percentil dostupnosti služby za týždeň a porovnajte každú minútu z posledných 10 minút s touto hodnotou.

Nemôžete vykonať spojenie typu jeden k mnohým; last/mean/median nad skupinou bodov zmení uzol na prúd a vráti sa chyba „cannot add child mismatched edges: batch -> stream“.

Výsledok dávky, ako premenná vo výraze lambda, sa tiež nenahrádza.

Existuje možnosť uložiť požadované čísla z prvej dávky do súboru pomocou udf a načítať tento súbor pomocou sideload.

Čo sme týmto riešili?

Máme približne 100 poskytovateľov hotelových služieb, z ktorých každý môže mať viacero pripojení, nazvime ich kanálmi. Existuje približne 300 týchto kanálov a ktorýkoľvek z nich môže zlyhať. Zo všetkých zaznamenaných metrík budeme monitorovať mieru chybovosti (žiadosti a chyby).

Prečo nie Grafana?

Upozornenia na chyby nakonfigurované v Grafane majú niekoľko nevýhod. Niektoré sú kritické, zatiaľ čo iné možno ignorovať v závislosti od situácie.

Grafana nedokáže vykonávať interdimenzionálne výpočty + upozornenia, ale potrebujeme mieru (požiadavky-chyby)/požiadavky.

Chyby vyzerajú hrozne:

Triky na spracovanie metrík v Kapacitore

A menej zlé, ak sa na to pozriete s úspešnými požiadavkami:

Triky na spracovanie metrík v Kapacitore

Dobre, môžeme si sadzbu v službe pred Grafanou vopred vypočítať a v niektorých prípadoch je to v poriadku. Ale nie v našej, pretože každý kanál má svoj vlastný „normálny“ pomer a upozornenia sú založené na statických hodnotách (sledujeme ich a upravujeme ich, ak sa upozornenia zobrazujú často).

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

Ignorujme predchádzajúci bod a predpokladajme, že všetci dodávatelia majú podobný „normálny“ obraz. Takže teraz je všetko v poriadku a vystačíme si s upozorneniami v Grafane?
Môžeme, ale naozaj nechceme, pretože si musíme vybrať jednu z možností:
a) vytvoriť veľa grafov pre každý kanál samostatne (a ich namáhavo udržiavať)
b) nechajte jeden graf so všetkými kanálmi (a stratte sa v farebných čiarach a prispôsobených upozorneniach)

Triky na spracovanie metrík v Kapacitore

Ako si to urobil/a?

Opäť je v dokumentácii dobrý príklad (Výpočet sadzieb v rámci spojených sérií), môžete si to pozrieť alebo to použiť ako základ pri podobných problémoch.

Čo sme nakoniec urobili:

  • spojiť dve epizódy v priebehu niekoľkých hodín, zoskupiť podľa kanálov;
  • ak neboli k dispozícii žiadne údaje, sériu vyplníme podľa skupín;
  • porovnávame medián za posledných 10 minút s predchádzajúcimi údajmi;
  • zakričte, ak niečo nájdeme;
  • vypočítané sadzby a vzniknuté upozornenia zapisujeme do influxdb;
  • Pošlite užitočnú správu Slacku.

Podľa môjho názoru sa nám podarilo dosiahnuť všetko, čo sme chceli, čo najkrajšie to bolo možné (a s vlastnými obsluhami ešte trochu viac).

Môžeš sa pozrieť na github.com príklad kódu и minimálny diagram (graphviz) prijatý 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)

Takže aký je záver?

Kapacitor je skvelý na monitorovanie a upozorňovanie viacerých skupín, vykonávanie dodatočných výpočtov na základe už zaznamenaných metrík, vykonávanie vlastných akcií a spúšťanie skriptov (udf).

Vstupný prah nie je veľmi vysoký – vyskúšajte to, ak Grafana alebo iné nástroje úplne neuspokoja vaše potreby.

Zdroj: hab.com

Kúpte si spoľahlivý hosting pre stránky s DDoS ochranou, VPS VDS servery 🔥 Kúpte si spoľahlivý webhosting s ochranou DDoS, VPS VDS servery | ProHoster