Knep för att bearbeta mått i Kapacitor

Troligtvis frågar ingen idag varför det är nödvändigt att samla in tjänstemått. Nästa logiska steg är att ställa in en varning för de insamlade mätvärdena, som kommer att meddela om eventuella avvikelser i data i kanaler som är bekväma för dig (e-post, Slack, Telegram). I online-hotellbokningstjänsten Ostrovok.ru alla mätvärden för våra tjänster hälls in i InfluxDB och visas i Grafana, och grundläggande varning är också konfigurerad där. För uppgifter som "du måste beräkna något och jämföra med det", använder vi Kapacitor.

Knep för att bearbeta mått i Kapacitor
Kapacitor är en del av TICK-stacken som kan bearbeta mätvärden från InfluxDB. Den kan koppla ihop flera mätningar (join), beräkna något användbart från mottagna data, skriva tillbaka resultatet till InfluxDB, skicka en varning till Slack/Telegram/mail.

Hela stapeln är cool och detaljerad dokumentation, men det kommer alltid att finnas användbara saker som inte uttryckligen anges i manualerna. I den här artikeln bestämde jag mig för att samla ett antal sådana användbara, icke-uppenbara tips (den grundläggande syntaxen för TICKscipt beskrivs här) och visa hur de kan tillämpas med hjälp av ett exempel på att lösa ett av våra problem.

Låt oss gå!

float & int, räknefel

Ett absolut standardproblem, löst genom kast:

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

Använder default()

Om en tagg/fält inte fylls i kommer beräkningsfel att uppstå:

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

fyll i join (inre vs yttre)

Som standard kommer join att kassera punkter där det inte finns någon data (inre).
Med fill('null') kommer en yttre koppling att utföras, varefter du behöver göra en default() och fylla i de tomma värdena:

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

Det finns fortfarande en nyans här. I exemplet ovan, om en av serierna (res1 eller res2) är tom, kommer den resulterande serien (data) också att vara tom. Det finns flera biljetter om detta ämne på Github (1633, 1871, 6967) – vi väntar på fixar och lider lite.

Använda villkor i beräkningar (om i lambda)

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

Sista fem minuterna från pipelinen för perioden

Till exempel måste du jämföra värdena för de senaste fem minuterna med föregående vecka. Du kan ta två partier av data i två separata partier eller extrahera en del av data från en större period:

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

Ett alternativ för de sista fem minuterna skulle vara att använda en BarrierNode, som stänger av data före den angivna tiden:

|barrier()
        .period(5m)

Exempel på användning av Go-mallar i meddelande

Mallar motsvarar formatet från paketet text.mallNedan är några vanligt förekommande pussel.

om annat

Vi gör saker och ting i ordning och triggar inte folk med text igen:

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

Två siffror efter decimaltecknet i meddelandet

Förbättra läsbarheten för meddelandet:

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

Expanderande variabler i meddelande

Vi visar mer information i meddelandet för att svara på frågan "Varför skriker det"?

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

Unik varningsidentifierare

Detta är en nödvändig sak när det finns mer än en grupp i data, annars genereras bara en varning:

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

Anpassade hanterare

Den stora listan med hanterare inkluderar exec, som låter dig köra ditt skript med de godkända parametrarna (stdin) - kreativitet och inget mer!

En av våra seder är ett litet Python-skript för att skicka meddelanden till slack.
Först ville vi skicka en auktorisationsskyddad grafanabild i ett meddelande. Skriv sedan OK i tråden till föregående varning från samma grupp, och inte som ett separat meddelande. Lite senare - lägg till meddelandet det vanligaste misstaget under de senaste X minuterna.

Ett separat ämne är kommunikation med andra tjänster och eventuella åtgärder som initieras av en varning (endast om din övervakning fungerar tillräckligt bra).
Ett exempel på en hanterarbeskrivning, där slack_handler.py är vårt självskrivna 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"]

Hur felsöker man?

Tillval med loggutgång

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

Titta (cli): kapacitor -url host-eller-ip:9092 loggar lvl=fel

Alternativ med httpOut

Visar data i den aktuella pipeline:

|httpOut('something')

Titta (få): host-eller-ip:9092/kapacitor/v1/tasks/task_name/something

Avrättningsschema

  • Varje uppgift returnerar ett exekveringsträd med användbara siffror i formatet grafviz.
  • Ta ett block punkt.
  • Klistra in den i tittaren, njut av.

Var annars kan du få en rake?

tidsstämpel i influxdb vid återskrivning

Till exempel ställer vi in ​​en varning för summan av förfrågningar per timme (groupBy(1h)) och vill spela in varningen som inträffade i influxdb (för att vackert visa problemet på grafen i grafana).

influxDBOut() kommer att skriva tidsvärdet från varningen till tidsstämpeln; följaktligen kommer punkten på diagrammet att skrivas tidigare/senare än varningen kom.

När noggrannhet krävs: vi undviker detta problem genom att anropa en anpassad hanterare, som kommer att skriva data till influxdb med den aktuella tidsstämpeln.

docker, bygg och driftsättning

Vid uppstart kan kapacitor ladda uppgifter, mallar och hanterare från den katalog som anges i konfigurationen i [load]-blocket.

För att skapa en uppgift korrekt behöver du följande saker:

  1. Filnamn – utökat till skript-id/namn
  2. Typ – stream/batch
  3. dbrp – nyckelord för att indikera vilken databas + policy skriptet körs i (dbrp “leverantör.” “autogen”)

Om någon batchuppgift inte innehåller en rad med dbrp, kommer hela tjänsten att vägra starta och kommer ärligt att skriva om det i loggen.

I kronograf, tvärtom, borde denna linje inte existera, den accepteras inte via gränssnittet och genererar ett fel.

Hacka när du bygger en container: Dockerfile avslutas med -1 om det finns rader med //.+dbrp, vilket gör att du omedelbart kan förstå orsaken till felet när du monterar bygget.

gå med en till många

Exempeluppgift: du måste ta den 95:e percentilen av tjänstens drifttid under en vecka, jämför varje minut av de senaste 10 med detta värde.

Du kan inte göra en en-till-många koppling, sista/medelvärde/median över en grupp av punkter förvandlar noden till en ström, felet "kan inte lägga till underordnade kanter som inte matchar: batch -> ström" kommer att returneras.

Resultatet av en batch, som en variabel i ett lambdauttryck, ersätts inte heller.

Det finns ett alternativ att spara de nödvändiga numren från den första batchen till en fil via udf och ladda denna fil via sidladdning.

Vad löste vi med detta?

Vi har cirka 100 hotellleverantörer, var och en av dem kan ha flera kopplingar, låt oss kalla det en kanal. Det finns cirka 300 av dessa kanaler, var och en av kanalerna kan falla av. Av alla registrerade mätvärden kommer vi att övervaka felfrekvensen (förfrågningar och fel).

Varför inte grafana?

Felvarningar konfigurerade i Grafana har flera nackdelar. Vissa är kritiska, vissa kan man blunda för, beroende på situationen.

Grafana vet inte hur man räknar mellan mätningar + larm, men vi behöver en hastighet (requests-errors)/requests.

Felen ser otäcka ut:

Knep för att bearbeta mått i Kapacitor

Och mindre ont när det ses med framgångsrika förfrågningar:

Knep för att bearbeta mått i Kapacitor

Okej, vi kan förberäkna kursen i tjänsten innan grafana, och i vissa fall kommer detta att fungera. Men inte i vår, för... för varje kanal anses dess eget förhållande vara "normalt", och varningar fungerar enligt statiska värden (vi letar efter dem med våra ögon, ändrar dem om det finns frekventa varningar).

Det här är exempel på "normala" för olika kanaler:

Knep för att bearbeta mått i Kapacitor

Knep för att bearbeta mått i Kapacitor

Vi bortser från föregående punkt och antar att den ”normala” bilden är likartad för alla leverantörer. Nu är allt bra, och vi kan klara oss med varningar i grafana?
Vi kan, men vi vill verkligen inte, eftersom vi måste välja ett av alternativen:
a) gör många grafer för varje kanal separat (och följ dem smärtsamt)
b) lämna ett diagram med alla kanaler (och gå vilse i färgglada linjer och anpassade varningar)

Knep för att bearbeta mått i Kapacitor

Hur gjorde du det?

Återigen, det finns ett bra startexempel i dokumentationen (Beräknar priser över sammanfogade serier), kan kikas på eller tas som grund i liknande problem.

Vad vi gjorde till slut:

  • gå med i två serier på några timmar, grupperade efter kanaler;
  • fyll i serien för grupp om det inte fanns några uppgifter;
  • jämför medianen för de senaste 10 minuterna med tidigare data;
  • vi skriker om vi hittar något;
  • vi skriver de beräknade hastigheterna och varningarna som inträffade i influxdb;
  • skicka ett användbart meddelande till slack.

Enligt min åsikt lyckades vi uppnå allt vi ville få på slutet (och till och med lite mer med anpassade hanterare) så vackert som möjligt.

Du kan titta på github.com kodexempel и minimal krets (graphviz) det resulterande skriptet.

Ett exempel på den resulterande 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)

Och vad är slutsatsen?

Kapacitor är bra på att utföra övervakningsvarningar med ett gäng grupperingar, utföra ytterligare beräkningar baserade på redan inspelade mätvärden, utföra anpassade åtgärder och köra skript (udf).

Inträdesbarriären är inte särskilt hög - prova det om grafana eller andra verktyg inte helt uppfyller dina önskemål.

Källa: will.com

Lägg en kommentar