Trikovi za obradu metrike u Kapacitoru

Najvjerovatnije, danas se niko ne pita zašto je potrebno prikupljati metriku usluge. Sljedeći logičan korak je postavljanje upozorenja za prikupljene metrike, koje će obavještavati o eventualnim odstupanjima u podacima na kanalima koji vam odgovaraju (mail, Slack, Telegram). U online servisu za rezervacije hotela Ostrovok.ru sve metrike naših usluga se prelivaju u InfluxDB i prikazuju u Grafani, a tu se konfiguriše i osnovno upozorenje. Za zadatke poput „treba nešto izračunati i uporediti s tim“, koristimo Kapacitor.

Trikovi za obradu metrike u Kapacitoru
Kapacitor je dio TICK steka koji može obraditi metriku iz InfluxDB-a. Može povezati nekoliko mjerenja zajedno (pridružiti), izračunati nešto korisno iz primljenih podataka, zapisati rezultat natrag u InfluxDB, poslati upozorenje na Slack/Telegram/mail.

Cijeli niz je cool i detaljan dokumentaciju, ali uvijek će biti korisnih stvari koje nisu eksplicitno naznačene u priručnicima. U ovom članku odlučio sam prikupiti nekoliko takvih korisnih, neočiglednih savjeta (opisana je osnovna sintaksa TICKscipt-a ovdje) i pokazati kako se mogu primijeniti na primjeru rješavanja jednog od naših problema.

Idemo!

float & int, greške u proračunu

Apsolutno standardni problem, riješen kroz kaste:

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

Korištenje default()

Ako oznaka/polje nije popunjeno, doći će do grešaka u proračunu:

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

popuni spoj (unutarnji vs vanjski)

Podrazumevano, spajanje će odbaciti tačke u kojima nema podataka (unutrašnje).
Sa fill('null'), izvršit će se vanjsko spajanje, nakon čega trebate napraviti default() i popuniti prazne vrijednosti:

var data = res1
    |join(res2)
        .as('res1', 'res2)
        .fill('null')
    |default()
        .field('res1.value', 0.0)
        .field('res2.value', 100.0)

Ovdje još uvijek postoji nijansa. U gornjem primjeru, ako je jedna od serija (res1 ili res2) prazna, rezultirajuća serija (podaci) će također biti prazna. Postoji nekoliko ulaznica na ovu temu na Githubu (1633, 1871, 6967) – čekamo popravke i malo patimo.

Korišćenje uslova u proračunima (ako je u lambda)

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

Poslednjih pet minuta od gasovoda za period

Na primjer, trebate uporediti vrijednosti posljednjih pet minuta sa prethodnom sedmicom. Možete uzeti dvije grupe podataka u dvije odvojene grupe ili izdvojiti dio podataka iz većeg perioda:

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

Alternativa za posljednjih pet minuta bila bi korištenje BarrierNode, koji odsijeca podatke prije određenog vremena:

|barrier()
        .period(5m)

Primjeri korištenja Go predložaka u poruci

Šabloni odgovaraju formatu iz paketa text.templateIspod su neke zagonetke koje se često susreću.

ako-drugo

Dovodimo stvari u red i više ne izazivamo ljude tekstom:

|alert()
    ...
    .message(
        '{{ if eq .Level "OK" }}It is ok now{{ else }}Chief, everything is broken{{end}}'
    )

Dvije cifre iza decimalnog zareza u poruci

Poboljšanje čitljivosti poruke:

|alert()
    ...
    .message(
        'now value is {{ index .Fields "value" | printf "%0.2f" }}'
    )

Proširivanje varijabli u poruci

Više informacija prikazujemo u poruci kako bismo odgovorili na pitanje “Zašto viče”?

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

Jedinstveni identifikator upozorenja

Ovo je neophodna stvar kada postoji više grupa u podacima, inače će se generirati samo jedno upozorenje:

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

Prilagođeni rukovaoci

Velika lista rukovalaca uključuje exec, koji vam omogućava da izvršite svoju skriptu sa prosleđenim parametrima (stdin) - kreativnost i ništa više!

Jedan od naših običaja je mala Python skripta za slanje obavijesti u slack.
Isprva smo željeli poslati sliku grafana zaštićenu autorizacijom u poruci. Nakon toga napišite OK u nizu na prethodno upozorenje iz iste grupe, a ne kao posebnu poruku. Malo kasnije - dodajte u poruku najčešću grešku u zadnjih X minuta.

Posebna tema je komunikacija s drugim servisima i sve radnje pokrenute upozorenjem (samo ako vaš nadzor radi dovoljno dobro).
Primjer opisa rukovatelja, gdje je slack_handler.py naša skripta koja je sama napisala:

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

Kako otkloniti greške?

Opcija sa izlazom dnevnika

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

Gledajte (cli): kapacitor -url host-or-ip:9092 zapisi lvl=greška

Opcija sa httpOut

Prikazuje podatke u trenutnom cjevovodu:

|httpOut('something')

Gledajte (dobite): host-or-ip:9092/kapacitor/v1/tasks/ime_zadatka/nešto

Shema izvršenja

  • Svaki zadatak vraća stablo izvršenja sa korisnim brojevima u formatu graphviz.
  • Uzmi blok tačka.
  • Zalijepi u preglednik, uživajte.

Gdje još možete dobiti grablje?

vremenska oznaka u influxdb pri povratnom upisu

Na primjer, postavili smo upozorenje za zbir zahtjeva po satu (groupBy(1h)) i želimo snimiti upozorenje koje se dogodilo u influxdb (da lijepo prikažemo činjenicu problema na grafu u grafani).

influxDBOut() će zapisati vrijednost vremena od upozorenja do vremenske oznake; shodno tome, tačka na grafikonu će biti upisana ranije/kasnije nego što je stiglo upozorenje.

Kada je potrebna tačnost: zaobilazimo ovaj problem pozivanjem prilagođenog rukovaoca, koji će pisati podatke u influxdb sa trenutnom vremenskom oznakom.

docker, izrada i implementacija

Prilikom pokretanja, kapacitor može učitati zadatke, šablone i rukovaoce iz direktorija specificiranog u konfiguraciji u bloku [load].

Da biste ispravno kreirali zadatak, potrebne su vam sljedeće stvari:

  1. Ime datoteke – prošireno u id/ime skripte
  2. Vrsta – stream/batch
  3. dbrp – ključna riječ koja označava u kojoj se bazi podataka + politika skripta pokreće (dbrp “dobavljač.” “autogen”)

Ako neki batch zadatak ne sadrži liniju s dbrp-om, cijeli servis će odbiti da se pokrene i iskreno će o tome pisati u dnevnik.

U hronografu, naprotiv, ova linija ne bi trebalo da postoji, ne prihvata se preko interfejsa i generiše grešku.

Hak prilikom izrade kontejnera: Dockerfile izlazi sa -1 ako postoje linije sa //.+dbrp, što će vam omogućiti da odmah shvatite razlog neuspjeha prilikom sastavljanja build-a.

pridruži jedan mnogima

Primjer zadatka: trebate uzeti 95. percentil radnog vremena usluge za sedmicu, uporediti svaki minut od posljednjih 10 sa ovom vrijednošću.

Ne možete napraviti spajanje jedan-prema više, zadnja/srednja/srednja vrijednost preko grupe tačaka pretvara čvor u tok, vratit će se greška “ne mogu dodati podređene neusklađene rubove: serija -> tok”.

Rezultat serije, kao varijabla u lambda izrazu, također nije zamijenjen.

Postoji opcija da sačuvate potrebne brojeve iz prve grupe u datoteku putem udf-a i učitate ovu datoteku putem sideloada.

Šta smo ovim riješili?

Imamo oko 100 hotelskih dobavljača, svaki od njih može imati nekoliko veza, nazovimo to kanal. Ima otprilike 300 ovih kanala, svaki od kanala može otpasti. Od svih zabilježenih metrika pratit ćemo stopu grešaka (zahtjeva i grešaka).

Zašto ne grafana?

Upozorenja o greškama konfigurisana u Grafani imaju nekoliko nedostataka. Neki su kritični, pred nekima možete zatvoriti oči, ovisno o situaciji.

Grafana ne zna izračunati između mjerenja + upozorenje, ali nam je potrebna stopa (zahtjevi-greške)/zahtjevi.

Greške izgledaju gadno:

Trikovi za obradu metrike u Kapacitoru

I manje zla kada se gleda sa uspješnim zahtjevima:

Trikovi za obradu metrike u Kapacitoru

U redu, možemo unaprijed izračunati stopu u servisu prije grafane, au nekim slučajevima ovo će uspjeti. Ali ne u našoj, jer... za svaki kanal svoj se omjer smatra „normalnim“, a upozorenja rade prema statičkim vrijednostima (tražimo ih očima, mijenjamo ih ako su česta upozorenja).

Ovo su primjeri "normalnog" za različite kanale:

Trikovi za obradu metrike u Kapacitoru

Trikovi za obradu metrike u Kapacitoru

Zanemarujemo prethodnu tačku i pretpostavljamo da je „normalna“ slika slična za sve dobavljače. Sada je sve u redu, a možemo li proći sa upozorenjima u grafani?
Možemo, ali zaista ne želimo, jer moramo izabrati jednu od opcija:
a) napraviti puno grafikona za svaki kanal posebno (i bolno ih pratiti)
b) ostavite jedan grafikon sa svim kanalima (i izgubite se u šarenim linijama i prilagođenim upozorenjima)

Trikovi za obradu metrike u Kapacitoru

Kako si to uradio?

Opet, postoji dobar početni primjer u dokumentaciji (Izračunavanje stopa u udruženim serijama), može se zaviriti ili uzeti kao osnova u sličnim problemima.

Šta smo na kraju uradili:

  • pridružite se dvije serije za nekoliko sati, grupiranje po kanalima;
  • popuniti niz po grupama ako nije bilo podataka;
  • uporedi medijanu zadnjih 10 minuta s prethodnim podacima;
  • vičemo ako nešto nađemo;
  • upisujemo izračunate stope i upozorenja koja su se dogodila u influxdb;
  • pošaljite korisnu poruku slacku.

Po mom mišljenju, uspjeli smo postići sve što smo željeli na kraju (pa čak i malo više sa prilagođenim rukovaocima) što je ljepše moguće.

Možete pogledati na github.com primjer koda и minimalno kolo (graf.viz) rezultirajuću skriptu.

Primjer rezultirajućeg koda:

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)

Šta je zaključak?

Kapacitor je odličan u izvođenju nadzornih upozorenja sa gomilom grupiranja, obavljanju dodatnih proračuna na osnovu već snimljenih metrika, izvođenju prilagođenih radnji i pokretanju skripti (udf).

Barijera za ulazak nije velika - pokušajte ako grafana ili drugi alati ne zadovoljavaju u potpunosti vaše želje.

izvor: www.habr.com

Dodajte komentar