Mga trick para sa pagpoproseso ng mga sukatan sa Kapacitor

Malamang, ngayon walang nagtatanong kung bakit kinakailangan na mangolekta ng mga sukatan ng serbisyo. Ang susunod na lohikal na hakbang ay ang pag-set up ng alerto para sa mga nakolektang sukatan, na mag-aabiso tungkol sa anumang mga paglihis sa data sa mga channel na maginhawa para sa iyo (mail, Slack, Telegram). Sa online na hotel booking service Ostrovok.ru lahat ng sukatan ng aming mga serbisyo ay ibinubuhos sa InfluxDB at ipinapakita sa Grafana, at naka-configure din doon ang pangunahing pag-alerto. Para sa mga gawain tulad ng "kailangan mong kalkulahin ang isang bagay at ihambing dito," ginagamit namin ang Kapacitor.

Mga trick para sa pagpoproseso ng mga sukatan sa Kapacitor
Ang Kapacitor ay bahagi ng TICK stack na maaaring magproseso ng mga sukatan mula sa InfluxDB. Maaari itong ikonekta ang ilang mga sukat nang magkasama (sumali), kalkulahin ang isang bagay na kapaki-pakinabang mula sa natanggap na data, isulat ang resulta pabalik sa InfluxDB, magpadala ng alerto sa Slack/Telegram/mail.

Ang buong stack ay cool at detalyado dokumentasyon, ngunit palaging may mga kapaki-pakinabang na bagay na hindi tahasang ipinahiwatig sa mga manwal. Sa artikulong ito, nagpasya akong mangolekta ng isang bilang ng mga kapaki-pakinabang, hindi halatang tip (ang pangunahing syntax ng TICKscipt ay inilarawan dito) at ipakita kung paano ito mailalapat gamit ang isang halimbawa ng paglutas ng isa sa ating mga problema.

Sabihin pumunta!

float & int, mga error sa pagkalkula

Isang ganap na karaniwang problema, na nalutas sa pamamagitan ng mga kasta:

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

Gamit ang default()

Kung hindi napunan ang isang tag/field, magaganap ang mga error sa pagkalkula:

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

punan ang pagsali (panloob vs panlabas)

Bilang default, itatapon ng pagsali ang mga punto kung saan walang data (panloob).
Sa fill('null'), isasagawa ang panlabas na pagsali, pagkatapos nito kailangan mong gumawa ng default() at punan ang mga walang laman na halaga:

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

May nuance pa rin dito. Sa halimbawa sa itaas, kung ang isa sa mga serye (res1 o res2) ay walang laman, ang resultang serye (data) ay magiging walang laman din. Mayroong ilang mga tiket sa paksang ito sa Github (1633, 1871, 6967) – naghihintay kami ng mga pag-aayos at pagdurusa ng kaunti.

Paggamit ng mga kundisyon sa mga kalkulasyon (kung sa lambda)

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

Huling limang minuto mula sa pipeline para sa panahon

Halimbawa, kailangan mong ihambing ang mga halaga ng huling limang minuto sa nakaraang linggo. Maaari kang kumuha ng dalawang batch ng data sa dalawang magkahiwalay na batch o kunin ang bahagi ng data mula sa isang mas malaking panahon:

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

Ang isang alternatibo para sa huling limang minuto ay ang paggamit ng isang BarrierNode, na pumutol ng data bago ang tinukoy na oras:

|barrier()
        .period(5m)

Mga halimbawa ng paggamit ng mga template ng Go sa mensahe

Ang mga template ay tumutugma sa format mula sa package teksto.templateNasa ibaba ang ilang madalas na nakakaharap na palaisipan.

kung hindi

Inaayos namin ang mga bagay-bagay at hindi na muling nagti-trigger ng mga tao gamit ang text:

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

Dalawang digit pagkatapos ng decimal point sa mensahe

Pagpapabuti ng pagiging madaling mabasa ng mensahe:

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

Pagpapalawak ng mga variable sa mensahe

Nagpapakita kami ng higit pang impormasyon sa mensahe para sagutin ang tanong na "Bakit ito sumisigaw"?

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

Natatanging tagatukoy ng alerto

Ito ay isang kinakailangang bagay kapag mayroong higit sa isang pangkat sa data, kung hindi, isang alerto lamang ang bubuo:

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

Mga custom na handler

Kasama sa malaking listahan ng mga handler ang exec, na nagbibigay-daan sa iyong isagawa ang iyong script gamit ang mga naipasa na parameter (stdin) - pagkamalikhain at wala nang iba pa!

Ang isa sa aming mga kaugalian ay isang maliit na script ng Python para sa pagpapadala ng mga abiso sa malubay.
Noong una, gusto naming magpadala ng larawang grafana na protektado ng awtorisasyon sa isang mensahe. Pagkatapos, isulat ang OK sa thread sa nakaraang alerto mula sa parehong grupo, at hindi bilang isang hiwalay na mensahe. Maya-maya - idagdag sa mensahe ang pinakakaraniwang pagkakamali sa huling X minuto.

Ang isang hiwalay na paksa ay pakikipag-ugnayan sa iba pang mga serbisyo at anumang mga aksyon na pinasimulan ng isang alerto (kung gumagana lang nang maayos ang iyong pagsubaybay).
Isang halimbawa ng paglalarawan ng handler, kung saan ang slack_handler.py ay ang aming self-written na script:

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

Paano mag-debug?

Pagpipilian na may output ng log

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

Panoorin (cli): kapacitor -url host-o-ip:9092 log lvl=error

Pagpipilian sa httpOut

Nagpapakita ng data sa kasalukuyang pipeline:

|httpOut('something')

Panoorin (kunin): host-o-ip:9092/kapacitor/v1/tasks/task_name/something

Iskema ng pagpapatupad

  • Ang bawat gawain ay nagbabalik ng execution tree na may mga kapaki-pakinabang na numero sa format graphviz.
  • Kumuha ng isang bloke tuldok.
  • Idikit ito sa viewer, magsaya.

Saan ka pa makakakuha ng kalaykay?

timestamp sa influxdb sa writeback

Halimbawa, nag-set up kami ng alerto para sa kabuuan ng mga kahilingan kada oras (groupBy(1h)) at gusto naming i-record ang alertong naganap sa influxdb (upang maipakita nang maganda ang katotohanan ng problema sa graph sa grafana).

isusulat ng influxDBOut() ang halaga ng oras mula sa alerto hanggang sa timestamp; nang naaayon, ang punto sa tsart ay isusulat nang mas maaga/mamaya kaysa sa pagdating ng alerto.

Kapag kinakailangan ang katumpakan: inaayos namin ang problemang ito sa pamamagitan ng pagtawag sa isang custom na handler, na magsusulat ng data sa influxdb gamit ang kasalukuyang timestamp.

docker, build at deployment

Sa pagsisimula, ang kapacitor ay maaaring mag-load ng mga gawain, template at handler mula sa direktoryo na tinukoy sa config sa [load] block.

Upang makagawa ng isang gawain nang tama, kailangan mo ang mga sumusunod na bagay:

  1. Pangalan ng file – pinalawak sa script id/name
  2. Uri – stream/batch
  3. dbrp – keyword upang ipahiwatig kung aling database + patakaran ang tumatakbo sa script (dbrp β€œsupplier.” β€œautogen”)

Kung ang ilang batch na gawain ay hindi naglalaman ng isang linya na may dbrp, ang buong serbisyo ay tatangging magsimula at matapat na magsusulat tungkol dito sa log.

Sa chronograf, sa kabaligtaran, ang linyang ito ay hindi dapat umiiral; hindi ito tinatanggap sa pamamagitan ng interface at bumubuo ng isang error.

Pag-hack kapag gumagawa ng container: Lalabas ang Dockerfile na may -1 kung may mga linya na may //.+dbrp, na magbibigay-daan sa iyo na agad na maunawaan ang dahilan ng pagkabigo kapag nag-assemble ng build.

sumali sa isa hanggang sa marami

Halimbawang gawain: kailangan mong kunin ang 95th percentile ng oras ng pagpapatakbo ng serbisyo sa loob ng isang linggo, ihambing ang bawat minuto ng huling 10 sa halagang ito.

Hindi ka makakagawa ng one-to-many na pagsali, ang huli/mean/median sa isang pangkat ng mga puntos ay gagawing stream ang node, ibabalik ang error na "hindi makakapagdagdag ng mga child mismatched edge: batch -> stream".

Ang resulta ng isang batch, bilang isang variable sa isang lambda expression, ay hindi rin pinapalitan.

May opsyon na i-save ang mga kinakailangang numero mula sa unang batch hanggang sa isang file sa pamamagitan ng udf at i-load ang file na ito sa pamamagitan ng sideload.

Ano ang nalutas namin dito?

Mayroon kaming humigit-kumulang 100 mga supplier ng hotel, bawat isa sa kanila ay maaaring magkaroon ng ilang mga koneksyon, tawagin natin itong isang channel. Mayroong humigit-kumulang 300 sa mga channel na ito, ang bawat isa sa mga channel ay maaaring mahulog. Sa lahat ng naitalang sukatan, susubaybayan namin ang rate ng error (mga kahilingan at error).

Bakit hindi grafana?

Ang mga alerto ng error na na-configure sa Grafana ay may ilang mga disadvantages. Ang ilan ay kritikal, ang ilan ay maaari mong ipikit ang iyong mga mata, depende sa sitwasyon.

Hindi alam ng Grafana kung paano magkalkula sa pagitan ng mga sukat + pag-alerto, ngunit kailangan namin ng rate (mga kahilingan-error)/mga kahilingan.

Ang mga error ay mukhang masama:

Mga trick para sa pagpoproseso ng mga sukatan sa Kapacitor

At hindi gaanong kasamaan kapag tiningnan nang may matagumpay na mga kahilingan:

Mga trick para sa pagpoproseso ng mga sukatan sa Kapacitor

Okay, maaari naming paunang kalkulahin ang rate sa serbisyo bago ang grafana, at sa ilang mga kaso gagana ito. Ngunit hindi sa atin, dahil... para sa bawat channel ang sarili nitong ratio ay itinuturing na "normal", at ang mga alerto ay gumagana ayon sa mga static na halaga (hinahanap namin ang mga ito gamit ang aming mga mata, baguhin ang mga ito kung may mga madalas na alerto).

Ito ang mga halimbawa ng "normal" para sa iba't ibang channel:

Mga trick para sa pagpoproseso ng mga sukatan sa Kapacitor

Mga trick para sa pagpoproseso ng mga sukatan sa Kapacitor

Hindi namin binabalewala ang nakaraang punto at ipinapalagay na ang "normal" na larawan ay katulad para sa lahat ng mga supplier. Ngayon ay maayos na ang lahat, at makakayanan natin ang mga alerto sa grafana?
Kaya natin, pero ayaw talaga natin, dahil kailangan nating pumili ng isa sa mga opsyon:
a) gumawa ng maraming mga graph para sa bawat channel nang hiwalay (at masakit na samahan ang mga ito)
b) mag-iwan ng isang tsart kasama ang lahat ng mga channel (at mawala sa mga makukulay na linya at naka-customize na mga alerto)

Mga trick para sa pagpoproseso ng mga sukatan sa Kapacitor

Paano mo ito nagawa?

Muli, mayroong isang magandang panimulang halimbawa sa dokumentasyon (Pagkalkula ng mga rate sa lahat ng pinagsamang serye), maaaring silipin o gawing batayan sa mga katulad na problema.

Ang ginawa namin sa huli:

  • sumali sa dalawang serye sa loob ng ilang oras, pagpapangkat ayon sa mga channel;
  • punan ang serye ayon sa pangkat kung walang data;
  • ihambing ang median ng huling 10 minuto sa nakaraang data;
  • sumisigaw tayo kung may nahanap tayo;
  • isinusulat namin ang mga kinakalkula na rate at alerto na naganap sa influxdb;
  • magpadala ng isang kapaki-pakinabang na mensahe sa malubay.

Sa aking opinyon, nagawa naming makamit ang lahat ng gusto naming makuha sa dulo (at kahit kaunti pa sa mga custom na humahawak) nang kasing ganda hangga't maaari.

Makikita mo ito sa github.com halimbawa ng code ΠΈ minimal na circuit (graphviz) ang resultang script.

Isang halimbawa ng resultang code:

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)

Ano ang konklusyon?

Ang Kapacitor ay mahusay sa pagsasagawa ng mga alerto sa pagsubaybay na may maraming mga pagpapangkat, pagsasagawa ng mga karagdagang kalkulasyon batay sa mga naitalang sukatan, pagsasagawa ng mga custom na aksyon at pagpapatakbo ng mga script (udf).

Ang hadlang sa pagpasok ay hindi masyadong mataas - subukan ito kung ang grafana o iba pang mga tool ay hindi ganap na nasiyahan ang iyong mga hangarin.

Pinagmulan: www.habr.com

Magdagdag ng komento