Trikovi za obradu metrike u Kapacitoru

Najvjerojatnije se danas nitko ne pita zašto je potrebno prikupljati metriku usluge. Sljedeći logičan korak je postavljanje upozorenja za prikupljene metrike, koje će vas obavijestiti o svim odstupanjima u podacima na kanalima koji vam odgovaraju (pošta, Slack, Telegram). U online servisu za rezervacije hotela Ostrovok.ru sva metrika naših usluga pretočena je u InfluxDB i prikazana u Grafani, a tu je konfigurirano i osnovno uzbunjivanje. Za zadatke poput "trebate nešto izračunati i usporediti s tim", koristimo Kapacitor.

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

Cijeli niz je cool i detaljan dokumentacija, ali uvijek će biti korisnih stvari koje nisu izričito naznačene u priručnicima. U ovom sam članku odlučio prikupiti niz takvih korisnih, neočitih savjeta (opisana je osnovna sintaksa TICKscipta здесь) i pokazati kako se mogu primijeniti na primjeru rješavanja jednog od naših problema.

Idemo!

float & int, pogreške u izrač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 izračunu:

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

popuniti spoj (unutarnji protiv vanjski)

Prema zadanim postavkama, pridruživanje će odbaciti točke u kojima nema podataka (unutarnje).
Uz fill('null'), izvršit će se vanjsko spajanje, nakon čega trebate napraviti default() i ispuniti 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 jedan niz (res1 ili res2) prazan, rezultirajući niz (podaci) također će biti prazan. Postoji nekoliko ulaznica na ovu temu na Githubu (1633, 1871, 6967) – čekamo popravke i malo patimo.

Korištenje uvjeta u izračunima (ako su u lambda)

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

Zadnjih pet minuta od cjevovoda za razdoblje

Na primjer, trebate usporediti vrijednosti zadnjih pet minuta s prethodnim tjednom. Možete uzeti dvije serije podataka u dvije odvojene serije ili izdvojiti dio podataka iz većeg razdoblja:

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

Alternativa za posljednjih pet minuta bila bi upotreba BarrierNode, koji prekida podatke prije navedenog vremena:

|barrier()
        .period(5m)

Primjeri korištenja Go predložaka u poruci

Predlošci odgovaraju formatu iz paketa tekst.predložakIspod su neke zagonetke koje se često susreću.

ako-drugo

Dovodimo stvari u red i više ne pokrećemo ljude tekstom:

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

Dvije znamenke nakon decimalne točke u poruci

Poboljšanje čitljivosti poruke:

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

Proširivanje varijabli u poruci

Prikazujemo više informacija u poruci za odgovor 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 od jedne grupe u podacima, inače će se generirati samo jedno upozorenje:

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

Prilagođeni rukovatelji

Veliki popis rukovatelja uključuje exec, koji vam omogućuje da izvršite svoju skriptu s proslijeđenim parametrima (stdin) - kreativnost i ništa više!

Jedan od naših običaja je mala Python skripta za slanje obavijesti na slack.
Prvo smo porukom htjeli poslati sliku grafane zaštićenu autorizacijom. Nakon toga napišite OK u nit prethodnog upozorenja iz iste grupe, a ne kao zasebnu 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 koje pokreće uzbuna (samo ako vaš nadzor radi dovoljno dobro).
Primjer opisa rukovatelja, gdje je slack_handler.py naša skripta koju smo sami napisali:

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 pogreške?

Opcija s izlazom dnevnika

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

Pogledajte (cli): kapacitor -url host-ili-ip:9092 zapisnici lvl=greška

Opcija s httpOut

Prikazuje podatke u trenutnom cjevovodu:

|httpOut('something')

Pogledajte (preuzmite): host-ili-ip:9092/kapacitor/v1/tasks/task_name/something

Shema izvršenja

  • Svaki zadatak vraća stablo izvršenja s korisnim brojevima u formatu grafviz.
  • Uzmi blok točka.
  • Zalijepite u preglednik, uživati.

Gdje drugdje možete dobiti grablje?

vremenska oznaka u influxdb pri povratnom pisanju

Na primjer, postavili smo upozorenje za zbroj zahtjeva po satu (groupBy(1h)) i želimo zabilježiti upozorenje koje se dogodilo u influxdb (kako bismo lijepo prikazali činjenicu problema na grafikonu u grafani).

influxDBOut() će upisati vremensku vrijednost od upozorenja u vremensku oznaku; sukladno tome, točka na grafikonu bit će zapisana prije/kasnije nego što je upozorenje stiglo.

Kada je potrebna točnost: rješavamo ovaj problem pozivanjem prilagođenog rukovatelja, koji će pisati podatke u influxdb s trenutnom vremenskom oznakom.

docker, izgradnja i implementacija

Prilikom pokretanja, kapacitor može učitati zadatke, predloške i rukovatelje iz direktorija navedenog u konfiguraciji u bloku [load].

Da biste ispravno izradili zadatak, trebate sljedeće stvari:

  1. Naziv datoteke – prošireno u ID/naziv skripte
  2. Tip – tok/serija
  3. dbrp – ključna riječ za označavanje u kojoj se bazi podataka + pravilo skripte izvodi (dbrp “dobavljač.” “autogen”)

Ako neki paketni zadatak ne sadrži redak s dbrp-om, cijela će se usluga odbiti pokrenuti i iskreno će o tome pisati u dnevniku.

U kronografu, naprotiv, ova linija ne bi trebala postojati; ne prihvaća se kroz sučelje i generira pogrešku.

Hack prilikom izgradnje spremnika: Dockerfile izlazi s -1 ako postoje linije s //.+dbrp, što će vam omogućiti da odmah shvatite razlog neuspjeha prilikom sastavljanja builda.

pridružite se jednom mnogima

Primjer zadatka: trebate uzeti 95. percentil vremena rada usluge za tjedan dana, usporedite svaku minutu od zadnjih 10 s ovom vrijednošću.

Ne možete izvršiti spajanje jedan prema više, zadnja/srednja vrijednost/srednja vrijednost preko grupe točaka pretvara čvor u tok, vratit će se pogreška "nije moguće dodati podređene neusklađene rubove: serija -> tok".

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

Postoji mogućnost spremanja potrebnih brojeva iz prve serije u datoteku putem udf-a i učitavanje ove datoteke putem sideload-a.

Što smo ovim riješili?

Imamo oko 100 hotelskih dobavljača, svaki od njih može imati nekoliko veza, nazovimo to kanalom. Tih kanala ima otprilike 300, svaki od njih može otpasti. Od svih zabilježenih metrika, pratit ćemo stopu pogrešaka (zahtjevi i pogreške).

Zašto ne grafana?

Upozorenja o pogreškama konfigurirana u Grafani imaju nekoliko nedostataka. Neki su kritični, na neke možete zatvoriti oči, ovisno o situaciji.

Grafana ne zna kalkulirati između mjerenja + uzbunjivanje, nego nam treba stopa (zahtjevi-greške)/zahtjevi.

Pogreške izgledaju gadno:

Trikovi za obradu metrike u Kapacitoru

I manje zlo kada se gleda s uspješnim zahtjevima:

Trikovi za obradu metrike u Kapacitoru

U redu, možemo unaprijed izračunati stopu u usluzi prije grafana, au nekim će slučajevima to funkcionirati. Ali ne kod nas, jer... za svaki kanal se vlastiti omjer smatra "normalnim", a upozorenja rade prema statičkim vrijednostima (tražimo ih očima, mijenjamo ih ako postoje česta upozorenja).

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

Trikovi za obradu metrike u Kapacitoru

Trikovi za obradu metrike u Kapacitoru

Zanemarujemo prethodnu točku i pretpostavljamo da je "normalna" slika slična za sve dobavljače. Sada je sve u redu i možemo li proći s dojavama u grafani?
Možemo, ali stvarno ne želimo, jer moramo izabrati jednu od opcija:
a) napraviti puno grafikona za svaki kanal posebno (i bolno ih popratiti)
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 napravio?

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

Što smo na kraju učinili:

  • spojite dvije serije u nekoliko sati, grupiranje po kanalima;
  • ispunite nizove po skupinama ako nije bilo podataka;
  • usporediti medijan zadnjih 10 minuta s prethodnim podacima;
  • vičemo ako nešto nađemo;
  • pišemo izračunate stope i upozorenja koja su se dogodila u influxdb;
  • poslati korisnu poruku slacku.

Po mom mišljenju, sve ono što smo željeli dobiti na kraju (i još malo više s custom handlerima) uspjeli smo postići što ljepše.

Možete ga vidjeti na github.com primjer koda и minimalni sklop (grafviz) rezultirajuća skripta.

Primjer dobivenog 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)

Koji je zaključak?

Kapacitor je izvrstan u izvođenju nadzornih upozorenja s hrpom grupiranja, izvođenju dodatnih izračuna na temelju već snimljenih metrika, izvođenju prilagođenih radnji i pokretanju skripti (udf).

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

Izvor: www.habr.com

Dodajte komentar