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.
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:
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:
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:
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:
Pangalan ng file β pinalawak sa script id/name
Uri β stream/batch
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:
At hindi gaanong kasamaan kapag tiningnan nang may matagumpay na mga kahilingan:
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:
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)
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.
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.