Temppuja mittareiden käsittelyyn Kapacitorissa

Todennäköisesti nykyään kukaan ei kysy, miksi palvelumittareita on tarpeen kerätä. Seuraava looginen askel on asettaa kerätyille mittareille hälytys, joka ilmoittaa mahdollisista poikkeamista tiedoissa sinulle sopivissa kanavissa (posti, Slack, Telegram). Hotellin online-varauspalvelussa Ostrovok.ru kaikki palveluidemme mittarit kaadetaan InfluxDB:hen ja näytetään Grafanassa, ja siellä on myös perushälytys. Tehtävissä, kuten "sinun täytyy laskea jotain ja verrata siihen", käytämme Kapacitoria.

Temppuja mittareiden käsittelyyn Kapacitorissa
Kapacitor on osa TICK-pinoa, joka voi käsitellä InfluxDB:n mittareita. Se voi yhdistää useita mittauksia (liittyä), laskea jotain hyödyllistä vastaanotetuista tiedoista, kirjoittaa tuloksen takaisin InfluxDB:hen, lähettää hälytyksen Slackiin/Telegramiin/mailiin.

Koko pino on siisti ja yksityiskohtainen dokumentointi, mutta aina löytyy hyödyllisiä asioita, joita ei ole erikseen mainittu oppaissa. Tässä artikkelissa päätin kerätä joukon sellaisia ​​hyödyllisiä, ei-ilmeisiä vinkkejä (TICKsciptin perussyntaksi on kuvattu täällä) ja näytä, kuinka niitä voidaan soveltaa esimerkin avulla yhden ongelmamme ratkaisemisesta.

Mennään!

float & int, laskentavirheet

Täysin tavallinen ongelma, joka ratkaistaan ​​kastien kautta:

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

Käytä oletusarvoa ()

Jos tunnistetta/kenttää ei täytetty, tapahtuu laskentavirheitä:

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

täytä liitos (sisäinen vs ulompi)

Oletusarvoisesti liittäminen hylkää kohdat, joissa ei ole tietoja (sisäinen).
Fil('null') -komennolla suoritetaan ulompi liitos, jonka jälkeen sinun on tehtävä default() ja täytettävä tyhjät arvot:

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

Tässä on vielä vivahde. Yllä olevassa esimerkissä, jos jokin sarjoista (res1 tai res2) on tyhjä, tuloksena oleva sarja (data) on myös tyhjä. Githubissa on useita lippuja tästä aiheesta (1633, 1871, 6967) – odotamme korjauksia ja vähän kärsimystä.

Ehtojen käyttö laskelmissa (jos lambda)

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

Viimeiset viisi minuuttia putkesta ajanjaksolle

Sinun on esimerkiksi verrattava viimeisen viiden minuutin arvoja edelliseen viikkoon. Voit ottaa kaksi tietoerää kahtena erillisenä eränä tai poimia osan tiedoista suuremmalta ajanjaksolta:

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

Vaihtoehtona viimeisen viiden minuutin aikana olisi käyttää BarrierNodea, joka katkaisee tiedot ennen määritettyä aikaa:

|barrier()
        .period(5m)

Esimerkkejä Go-mallien käyttämisestä viestissä

Mallit vastaavat pakkauksen muotoa text.templateAlla on joitain usein kohtaamia pulmia.

tai muuten

Laitamme asiat järjestykseen emmekä provosoi ihmisiä uudelleen tekstillä:

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

Kaksi numeroa desimaalipilkun jälkeen viestissä

Viestin luettavuuden parantaminen:

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

Viestin muuttujien laajentaminen

Näytämme viestissä lisätietoja vastataksemme kysymykseen "Miksi se huutaa"?

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

Ainutlaatuinen hälytystunniste

Tämä on välttämätön asia, kun tiedoissa on useampi kuin yksi ryhmä, muuten vain yksi hälytys luodaan:

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

Mukautettu käsittelijä

Suuri luettelo käsittelijöistä sisältää exec, jonka avulla voit suorittaa komentosarjasi läpäistyillä parametreilla (stdin) - luovuutta ja ei mitään muuta!

Yksi tavoistamme on pieni Python-skripti ilmoitusten lähettämiseen löysälle.
Aluksi halusimme lähettää valtuussuojatun grafana-kuvan viestissä. Kirjoita sen jälkeen OK säikeeseen samasta ryhmästä edelliseen hälytykseen, älä erillisenä viestinä. Hieman myöhemmin - lisää viestiin yleisin virhe viimeisen X minuutin aikana.

Erillinen aihe on kommunikointi muiden palvelujen kanssa ja hälytyksen käynnistämät toimet (vain jos valvontasi toimii riittävän hyvin).
Esimerkki käsittelijän kuvauksesta, jossa slack_handler.py on itse kirjoittamamme skripti:

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

Kuinka virheenkorjaus tehdään?

Vaihtoehto lokitulolla

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

Katso (cli): kapacitor -url isäntä tai IP:9092 lokit lvl=error

Vaihtoehto httpOutilla

Näyttää nykyisen liukuhihnan tiedot:

|httpOut('something')

Katso (hanki): isäntä tai IP:9092/kapacitor/v1/tasks/tehtävän_nimi/jotain

Toteutussuunnitelma

  • Jokainen tehtävä palauttaa suorituspuun, jossa on hyödyllisiä numeroita muodossa Graphviz.
  • Ota lohko piste.
  • Liitä se katseluohjelmaan, nauttia.

Mistä muualta saa haravan?

aikaleima influxdb:ssä kirjoittamisen yhteydessä

Asetamme esimerkiksi hälytyksen tuntikohtaisten pyyntöjen summalle (groupBy(1h)) ja haluamme tallentaa influxdb:ssä tapahtuneen hälytyksen (näyttääksesi kauniisti ongelman tosiasian grafanan kaaviossa).

influxDBOut() kirjoittaa aika-arvon hälytyksestä aikaleimaan; vastaavasti kaavion piste kirjoitetaan aikaisemmin/myöhemmin kuin hälytys saapui.

Kun tarkkuus vaaditaan: kiertämme tämän ongelman kutsumalla mukautetun käsittelijän, joka kirjoittaa tiedot influxdb:hen nykyisellä aikaleimalla.

telakointi, rakentaminen ja käyttöönotto

Käynnistettäessä kapacitor voi ladata tehtäviä, malleja ja käsittelijöitä [load]-lohkon konfiguraatiossa määritetystä hakemistosta.

Tehtävän luomiseksi oikein tarvitset seuraavat asiat:

  1. Tiedostonimi – laajennettu komentosarjan tunnukseksi/nimeksi
  2. Tyyppi – stream/erä
  3. dbrp – avainsana, joka ilmaisee, missä tietokannassa + käytännössä komentosarja suoritetaan (dbrp “supplier.” “autogen”)

Jos jokin erätehtävä ei sisällä riviä dbrp:llä, koko palvelu kieltäytyy käynnistymästä ja kirjoittaa siitä rehellisesti lokiin.

Chronografissa päinvastoin tätä riviä ei pitäisi olla; sitä ei hyväksytä rajapinnan kautta ja se tuottaa virheen.

Hakkerointi konttia rakennettaessa: Dockerfile poistuu merkinnällä -1, jos siinä on rivit //.+dbrp, jonka avulla voit heti ymmärtää epäonnistumisen syyn koontiversiota koottaessa.

liity yksi moniin

Esimerkkitehtävä: sinun on otettava palvelun viikon käyttöajasta 95. prosenttipiste, vertaa jokaista minuuttia viimeisestä 10:stä tähän arvoon.

Et voi tehdä yhdestä moneen -liitosta, viimeinen/keskiarvo/mediaani pisteryhmän yli muuttaa solmun virraksi, palautetaan virhe "ei voi lisätä aliyhteensopimattomia reunoja: erä -> virta".

Erän tulosta, joka on muuttuja lambda-lausekkeessa, ei myöskään korvata.

On mahdollisuus tallentaa tarvittavat numerot ensimmäisestä erästä tiedostoon udf:n kautta ja ladata tämä tiedosto sivulatauksella.

Mitä ratkaisimme tällä?

Meillä on noin 100 hotellitoimittajaa, joista jokaisella voi olla useita yhteyksiä, kutsutaan sitä kanavaksi. Näitä kanavia on noin 300, jokainen kanava voi pudota. Kaikista tallennetuista mittareista seuraamme virheprosenttia (pyynnöt ja virheet).

Miksei grafana?

Grafanaan määritetyillä virhehälytyksillä on useita haittoja. Jotkut ovat kriittisiä, joistakin voit sulkea silmäsi tilanteesta riippuen.

Grafana ei osaa laskea mittausten + hälytyksen välillä, mutta tarvitsemme nopeuden (pyynnöt-virheet)/pyynnöt.

Virheet näyttävät ikäviltä:

Temppuja mittareiden käsittelyyn Kapacitorissa

Ja vähemmän pahaa, kun sitä tarkastellaan onnistuneiden pyyntöjen kanssa:

Temppuja mittareiden käsittelyyn Kapacitorissa

Okei, voimme laskea hinnan etukäteen palvelussa ennen grafanaa, ja joissain tapauksissa tämä toimii. Mutta ei meidän, koska... jokaiselle kanavalle sen omaa suhdetta pidetään "normaalina", ja hälytykset toimivat staattisten arvojen mukaan (etsimme niitä silmillämme, vaihdamme niitä, jos hälytyksiä tulee usein).

Nämä ovat esimerkkejä "normaalista" eri kanaville:

Temppuja mittareiden käsittelyyn Kapacitorissa

Temppuja mittareiden käsittelyyn Kapacitorissa

Jätämme edellisen kohdan huomiotta ja oletamme, että "normaali" kuva on samanlainen kaikille toimittajille. Nyt kaikki on hyvin, ja selviämmekö grafanan hälytyksistä?
Voimme, mutta emme todellakaan halua, koska meidän on valittava yksi seuraavista vaihtoehdoista:
a) tehdä paljon kaavioita jokaiselle kanavalle erikseen (ja tuskallisesti mukana)
b) jätä yksi kaavio, jossa on kaikki kanavat (ja eksy värikkäisiin linjoihin ja mukautettuihin hälytyksiin)

Temppuja mittareiden käsittelyyn Kapacitorissa

Miten teit sen?

Dokumentaatiossa on jälleen hyvä aloitusesimerkki (Lasketaan liitettyjen sarjojen hintoja), voidaan kurkistaa tai ottaa pohjaksi vastaavissa ongelmissa.

Mitä teimme lopulta:

  • liitä kaksi sarjaa muutamassa tunnissa ryhmittelemällä kanavien mukaan;
  • täytä sarja ryhmittäin, jos tietoja ei ollut;
  • vertaa viimeisen 10 minuutin mediaania aikaisempiin tietoihin;
  • huudamme jos löydämme jotain;
  • kirjoitamme lasketut hinnat ja influxdb:ssä tapahtuneet hälytykset;
  • lähetä hyödyllinen viesti slackille.

Mielestäni onnistuimme saavuttamaan kaiken mitä halusimme lopussa (ja vielä vähän enemmän custom handlereilla) mahdollisimman kauniisti.

Voit katsoa github.com koodiesimerkki и minimaalinen piiri (graphviz) tuloksena oleva käsikirjoitus.

Esimerkki tuloksena olevasta koodista:

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)

Ja mikä on johtopäätös?

Kapacitor on erinomainen suorittamaan valvontahälytyksiä useilla ryhmittelyillä, suorittamaan lisälaskelmia jo tallennettujen mittareiden perusteella, suorittamaan mukautettuja toimintoja ja suorittamaan komentosarjoja (udf).

Pääsyn este ei ole kovin korkea - kokeile sitä, jos grafana tai muut työkalut eivät täysin täytä toiveitasi.

Lähde: will.com

Lisää kommentti