Knep för att bearbeta mått i Kapacitor

Troligtvis har ingen idag en fråga om varför det är nödvändigt att samla in servicestatistik. Nästa logiska steg är att skapa en varning för de insamlade mätvärdena, som meddelar dig om eventuella avvikelser i data i de kanaler som passar dig (e-post, Slack, Telegram). I online-hotellbokningstjänsten Ostrovok.ru Alla mätvärden för våra tjänster laddas upp i InfluxDB och visas i Grafana, där grundläggande aviseringar också konfigureras. För uppgifter som "behöver beräkna något och jämföra med detta" 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 sammanfoga flera mätvärden, beräkna något användbart från mottagen data, skriva resultatet tillbaka till InfluxDB och skicka en varning till Slack/Telegram/mail.

Hela stacken har en cool och detaljerad dokumentation, men det finns alltid användbara saker som inte uttryckligen anges i manualerna. I den här artikeln har jag bestämt 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 exemplet på att lösa ett av våra problem.

Låt oss gå!

flyttal & heltal, beräkningsfel

Ett helt standardproblem, löst genom gjutning:

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

Använda default()

Om taggen/fältet inte är ifyllt kommer beräkningsfel att uppstå:

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

fyll i koppling (inre vs yttre)

Som standard kommer join att ignorera punkter där det inte finns några data (inner).
När fill('null') utförs en outer join, 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. Om en av serierna (res1 eller res2) är tom i exemplet ovan, kommer även den slutliga serien (data) att vara tom. Det finns flera ärenden om detta ämne på GitHub (1633, 1871, 6967) – vi väntar på lösningar och lider lite.

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

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

De sista fem minuterna från rörledningen för perioden

Till exempel behöver du jämföra värdena från de senaste fem minuterna med föregående vecka. Du kan ta två databatcher i två separata batcher eller hämta 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 är att använda en BarrierNode-nod, som stänger av data före den angivna tiden:

|barrier()
        .period(5m)

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

Mallarna motsvarar formatet från paketet text.mall, nedan följer några vanliga problem.

om annat

Låt oss få ordning på saker och ting och inte trigga folk med sms i onödan:

|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 meddelandets läsbarhet:

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

Utveckla variabler i meddelandet

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

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

Unik varningsidentifierare

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

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

Anpassade hanterare

Den stora listan med hanterare inkluderar exec, vilket låter dig exekvera ditt skript med de skickade parametrarna (stdin) – kreativitet och inget mer!

En av våra anpassade funktioner är ett litet Python-skript för att skicka aviseringar till Slack.
Först ville vi skicka en bild från Grafana, skyddad av auktorisering, i ett meddelande. Skriv sedan OK i tråden till föregående varning från samma grupp, och inte i ett separat meddelande. Lite senare – lägg till i meddelandet det vanligaste felet under de senaste X minuterna.

Ett separat ämne är kopplingen till andra tjänster och eventuella åtgärder som initieras av varningen (endast om din övervakning fungerar tillräckligt bra).
Ett exempel på en hanterarbeskrivning, där slack_handler.py är vårt anpassade 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?

Alternativ med utdata för att logga

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

Titta (cli): kondensator -url värd-eller-IP:9092 loggar nivå=fel

Alternativ med httpOut

Visar data i den aktuella pipelinen:

|httpOut('something')

Titta (få): värd-eller-IP:9092/kondensator/v1/uppgifter/uppgiftsnamn/något

Utförandeschema

  • Varje uppgift returnerar ett exekveringsträd med användbara tal i formatet grafviz.
  • Vi tar ett block punkt.
  • Klistra in i visningsprogrammet, låt oss njuta.

Var annars kan man bli rakad?

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 registrera varningen som inträffade i influxdb (för att tydligt visa problemets faktum i grafen i grafana).

influxDBOut() skriver tidsvärdet från varningen till tidsstämpeln, så punkten på diagrammet kommer att skrivas tidigare/senare än varningen anlände.

När precision krävs: vi löser detta problem genom att anropa en anpassad hanterare, som skriver data till influxdb med den aktuella tidsstämpeln.

docker, bygga och driftsätta

Vid uppstart kan kapacitor ladda uppgifter, mallar och hanterare från katalogen som anges i konfigurationsfilen, i blocket [load].

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

  1. Filnamn - expanderar till id/skriptnamn
  2. Typ – ström/batch
  3. dbrp – nyckelord för att ange i vilken databas + policy skriptet fungerar (dbrp "leverantör"."autogen")

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

I kronografen, däremot, borde den här raden inte finnas där; den accepteras inte via gränssnittet och returnerar ett fel.

Containerbyggknep: Dockerfile avslutas med -1 om det finns rader med //.+dbrp, vilket gör att du omedelbart kan förstå orsaken till byggfelet.

gå med i en av många

Exempeluppgift: du behöver ta den 95:e percentilen av tjänstens drifttid under en vecka och jämföra varje minut av de senaste 10 med detta värde.

Du kan inte göra en en-till-många-koppling, last/mean/median på en grupp punkter omvandlar noden till en ström, felet "kan inte lägga till underordnade felmatchade kanter: batch -> stream" kommer att returneras.

Resultatet av batchen, som en variabel i lambda-uttrycket, ersätts inte heller.

Det finns ett alternativ att spara de nödvändiga siffrorna från den första omgången till en fil via udf och läsa in denna fil via sideload.

Vad löste vi med detta?

Vi har ungefär 100 hotellleverantörer, och var och en av dem kan ha flera kopplingar, låt oss kalla det en kanal. Det finns ungefär 300 av dessa kanaler, och 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?

Felmeddelanden som konfigurerats i Grafana har flera nackdelar. Vissa är kritiska, andra kan ignoreras, beroende på situationen.

Grafana kan inte göra interdimensionella beräkningar + aviseringar, men vi behöver en frekvens (förfrågningar-fel)/förfrågningar.

Felen ser hemska ut:

Knep för att bearbeta mått i Kapacitor

Och mindre ont om man tittar på lyckade frågor:

Knep för att bearbeta mått i Kapacitor

Okej, vi kan förberäkna frekvensen i tjänsten före Grafana, och i vissa fall kommer det att fungera. Men inte i vår, eftersom för varje kanal anses dess eget förhållande vara "normalt", och varningar fungerar på statiska värden (vi tittar med våra ögon, ändrar om det varnar ofta).

Här är exempel på "normalt" för olika kanaler:

Knep för att bearbeta mått i Kapacitor

Knep för att bearbeta mått i Kapacitor

Låt oss bortse från föregående punkt och anta att alla leverantörer har en liknande "normal" bild. Nu är allt bra, och vi kan klara oss med varningar i Grafana?
Vi kan, men vi vill verkligen inte, för vi måste välja ett av alternativen:
a) skapa många grafer för varje kanal separat (och mödosamt underhålla dem)
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 finns det ett bra utgångsexempel i dokumentationen (Beräkning av räntor över sammanfogade serier), kan du ta en titt på det eller använda det som utgångspunkt i liknande problem.

Vad vi gjorde till slut:

  • gå med i två avsnitt på några timmar, grupperade efter kanal;
  • vi fyller i serien gruppvis om det inte fanns några data;
  • jämför medianvärdet för de senaste 10 minuterna med tidigare data;
  • ropa om vi hittar något;
  • vi skriver de beräknade hastigheterna och de varningar som inträffade till influxdb;
  • Skicka ett användbart meddelande till Slack.

Enligt min mening lyckades vi uppnå allt vi ville få till slut på vackraste sätt (och ännu lite mer med specialbyggda hanterare).

Du kan ta en titt på github.com exempelkod и minimalt schema (graphviz) det mottagna manuset.

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)

Så vad är slutsatsen?

Kapacitor är utmärkt på att övervaka och varna med en mängd grupper, utföra ytterligare beräkningar baserade på redan inspelade mätvärden, utföra anpassade åtgärder och köra skript (udf).

Inträdesgränsen är inte särskilt hög – prova det om Grafana eller andra verktyg inte helt uppfyller dina önskemål.

Källa: will.com

Köp pålitlig hosting för webbplatser med DDoS-skydd, VPS VDS-servrar 🔥 Köp pålitlig webbhotell med DDoS-skydd, VPS VDS-servrar | ProHoster