Triks for å behandle beregninger i Kapacitor

Mest sannsynlig er det i dag ingen som spør hvorfor det er nødvendig å samle inn tjenesteberegninger. Det neste logiske trinnet er å sette opp et varsel for de innsamlede beregningene, som vil varsle om eventuelle avvik i dataene i kanaler som er praktiske for deg (post, Slack, Telegram). I online hotellbestillingstjenesten Ostrovok.ru all metrikk for tjenestene våre helles inn i InfluxDB og vises i Grafana, og grunnleggende varsling er også konfigurert der. For oppgaver som "du må beregne noe og sammenligne med det," bruker vi Kapacitor.

Triks for å behandle beregninger i Kapacitor
Kapacitor er en del av TICK-stakken som kan behandle beregninger fra InfluxDB. Den kan koble flere målinger sammen (join), beregne noe nyttig fra de mottatte dataene, skrive resultatet tilbake til InfluxDB, sende et varsel til Slack/Telegram/mail.

Hele stabelen er kul og detaljert dokumentasjon, men det vil alltid være nyttige ting som ikke er eksplisitt angitt i manualene. I denne artikkelen bestemte jeg meg for å samle en rekke slike nyttige, ikke-åpenbare tips (den grunnleggende syntaksen til TICKscipt er beskrevet her) og vis hvordan de kan brukes ved å bruke et eksempel på løsning av et av problemene våre.

La oss gå!

float & int, regnefeil

Et absolutt standardproblem, løst gjennom kaster:

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

Bruker standard()

Hvis en kode/felt ikke er fylt ut, vil det oppstå beregningsfeil:

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

fyll inn join (indre vs ytre)

Som standard vil join forkaste punkter der det ikke er data (indre).
Med fill('null'), vil en ytre sammenføyning utføres, hvoretter du må gjøre en default() og fylle inn de tomme verdiene:

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

Det er fortsatt en nyanse her. I eksemplet ovenfor, hvis en av seriene (res1 eller res2) er tom, vil den resulterende serien (data) også være tom. Det er flere billetter om dette emnet på Github (1633, 1871, 6967) – vi venter på rettelser og lider litt.

Bruke betingelser i beregninger (hvis i lambda)

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

Siste fem minutter fra rørledningen for perioden

For eksempel må du sammenligne verdiene for de siste fem minuttene med forrige uke. Du kan ta to batcher med data i to separate batcher eller trekke ut deler av dataene fra en større periode:

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

Et alternativ for de siste fem minuttene ville være å bruke en BarrierNode, som avskjærer data før den angitte tiden:

|barrier()
        .period(5m)

Eksempler på bruk av Go-maler i melding

Maler tilsvarer formatet fra pakken tekst.malNedenfor er noen vanlige oppgaver.

hvis-annet

Vi setter ting i orden og trigger ikke folk med tekst igjen:

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

To sifre etter desimaltegnet i meldingen

Forbedre lesbarheten til meldingen:

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

Utvide variabler i melding

Vi viser mer informasjon i meldingen for å svare på spørsmålet "Hvorfor roper det"?

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

Unik varselidentifikator

Dette er en nødvendig ting når det er mer enn én gruppe i dataene, ellers vil bare ett varsel bli generert:

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

Tilpassede behandlere

Den store listen over behandlere inkluderer exec, som lar deg kjøre skriptet ditt med de beståtte parameterne (stdin) - kreativitet og ingenting mer!

En av våre skikker er et lite Python-skript for å sende varsler til slakk.
Først ønsket vi å sende et autorisasjonsbeskyttet grafanabilde i en melding. Etterpå skriver du OK i tråden til forrige varsel fra samme gruppe, og ikke som en egen melding. Litt senere - legg til meldingen den vanligste feilen de siste X minuttene.

Et eget tema er kommunikasjon med andre tjenester og eventuelle handlinger initiert av et varsel (bare hvis overvåkingen din fungerer godt nok).
Et eksempel på en handlerbeskrivelse, der slack_handler.py er vårt selvskrevne 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"]

Hvordan feilsøke?

Alternativ med loggutgang

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

Se (cli): kapacitor -url vert-eller-ip:9092 logger lvl=feil

Alternativ med httpOut

Viser data i gjeldende pipeline:

|httpOut('something')

Se (få): vert-eller-ip:9092/kapacitor/v1/oppgaver/oppgavenavn/noe

Utførelsesordning

  • Hver oppgave returnerer et utførelsestre med nyttige tall i formatet graphviz.
  • Ta en blokk dot.
  • Lim den inn i seer, Nyt.

Hvor ellers kan du få en rake?

tidsstempel i influxdb på tilbakeskrivning

For eksempel setter vi opp et varsel for summen av forespørsler per time (groupBy(1h)) og ønsker å registrere varselet som skjedde i influxdb (for å vakkert vise problemet på grafen i grafana).

influxDBOut() vil skrive tidsverdien fra varselet til tidsstemplet; følgelig vil punktet på diagrammet bli skrevet tidligere/senere enn varselet kom.

Når nøyaktighet er nødvendig: vi omgår dette problemet ved å kalle en tilpasset behandler, som vil skrive data til influxdb med gjeldende tidsstempel.

docker, bygge og distribusjon

Ved oppstart kan kapacitor laste oppgaver, maler og behandlere fra katalogen spesifisert i konfigurasjonen i [load]-blokken.

For å lage en oppgave riktig, trenger du følgende ting:

  1. Filnavn – utvidet til skript-id/navn
  2. Type – stream/batch
  3. dbrp – nøkkelord for å indikere hvilken database + policy skriptet kjører i (dbrp “leverandør.” “autogen”)

Hvis en batchoppgave ikke inneholder en linje med dbrp, vil hele tjenesten nekte å starte og vil ærlig skrive om det i loggen.

I kronograf, tvert imot, bør denne linjen ikke eksistere; den aksepteres ikke gjennom grensesnittet og genererer en feil.

Hack når du bygger en container: Dockerfile avsluttes med -1 hvis det er linjer med //.+dbrp, som lar deg umiddelbart forstå årsaken til feilen når du setter sammen bygget.

bli med en til mange

Eksempeloppgave: du må ta den 95. persentilen av tjenestens driftstid i en uke, sammenligne hvert minutt av de siste 10 med denne verdien.

Du kan ikke gjøre en en-til-mange-sammenføyning, siste/gjennomsnitt/median over en gruppe punkter gjør noden til en strøm, feilen "kan ikke legge til underordnede kanter som ikke samsvarer: batch -> strøm" vil bli returnert.

Resultatet av en batch, som en variabel i et lambda-uttrykk, erstattes heller ikke.

Det er en mulighet for å lagre de nødvendige tallene fra første batch til en fil via udf og laste denne filen via sideload.

Hva løste vi med dette?

Vi har rundt 100 hotellleverandører, hver av dem kan ha flere forbindelser, la oss kalle det en kanal. Det er omtrent 300 av disse kanalene, hver av kanalene kan falle av. Av alle registrerte beregninger vil vi overvåke feilraten (forespørsler og feil).

Hvorfor ikke grafana?

Feilvarsler konfigurert i Grafana har flere ulemper. Noen er kritiske, noen kan du lukke øynene for, avhengig av situasjonen.

Grafana vet ikke hvordan man regner mellom målinger + varsling, men vi trenger en rate (requests-errors)/requests.

Feilene ser stygge ut:

Triks for å behandle beregninger i Kapacitor

Og mindre ond når den ses med vellykkede forespørsler:

Triks for å behandle beregninger i Kapacitor

Ok, vi kan forhåndsberegne satsen i tjenesten før grafana, og i noen tilfeller vil dette fungere. Men ikke i vår, fordi... for hver kanal anses dets eget forhold som "normalt", og varsler fungerer i henhold til statiske verdier (vi ser etter dem med øynene, endre dem hvis det er hyppige varsler).

Dette er eksempler på "normal" for forskjellige kanaler:

Triks for å behandle beregninger i Kapacitor

Triks for å behandle beregninger i Kapacitor

Vi ser bort fra det forrige punktet og antar at det "normale" bildet er likt for alle leverandører. Nå er alt bra, og vi kan klare oss med varsler i grafana?
Vi kan, men vi vil egentlig ikke, fordi vi må velge ett av alternativene:
a) lag mange grafer for hver kanal separat (og følg dem smertefullt)
b) la ett diagram med alle kanaler (og gå deg vill i de fargerike linjene og tilpassede varslene)

Triks for å behandle beregninger i Kapacitor

Hvordan gjorde du det?

Igjen, det er et godt starteksempel i dokumentasjonen (Beregner priser på tvers av sammenføyde serier), kan kikkes på eller legges til grunn i lignende problemer.

Hva vi gjorde til slutt:

  • bli med i to serier på noen få timer, gruppert etter kanaler;
  • fyll ut serie for gruppe hvis det ikke var data;
  • sammenligne medianen for de siste 10 minuttene med tidligere data;
  • vi roper om vi finner noe;
  • vi skriver de beregnede ratene og varslene som skjedde i influxdb;
  • send en nyttig melding til slack.

Etter min mening klarte vi å oppnå alt vi ønsket å få til på slutten (og enda litt mer med tilpassede handlere) så vakkert som mulig.

Du kan se på github.com kodeeksempel и minimal krets (graphviz) det resulterende skriptet.

Et eksempel på den resulterende koden:

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)

Hva er konklusjonen?

Kapacitor er ypperlig til å utføre overvåkingsvarsler med en haug med grupperinger, utføre ytterligere beregninger basert på allerede registrerte beregninger, utføre tilpassede handlinger og kjøre skript (udf).

Inngangsbarrieren er ikke veldig høy - prøv det hvis grafana eller andre verktøy ikke fullt ut tilfredsstiller dine ønsker.

Kilde: www.habr.com

Legg til en kommentar