Kemungkinan besar, saat ini tidak ada yang bertanya mengapa perlu mengumpulkan metrik layanan. Langkah logis berikutnya adalah menyiapkan peringatan untuk metrik yang dikumpulkan, yang akan memberi tahu tentang segala penyimpangan data di saluran yang nyaman bagi Anda (mail, Slack, Telegram). Dalam layanan pemesanan hotel online Ostrovok.ru semua metrik layanan kami dituangkan ke InfluxDB dan ditampilkan di Grafana, dan peringatan dasar juga dikonfigurasi di sana. Untuk tugas seperti βAnda perlu menghitung sesuatu dan membandingkannyaβ, kami menggunakan Kapacitor.
Kapacitor adalah bagian dari tumpukan TICK yang dapat memproses metrik dari InfluxDB. Itu dapat menghubungkan beberapa pengukuran bersama-sama (bergabung), menghitung sesuatu yang berguna dari data yang diterima, menulis hasilnya kembali ke InfluxDB, mengirim peringatan ke Slack/Telegram/mail.
Seluruh tumpukannya keren dan detail dokumentasi, tetapi akan selalu ada hal-hal berguna yang tidak disebutkan secara eksplisit dalam manual. Dalam artikel ini, saya memutuskan untuk mengumpulkan sejumlah tip yang berguna dan tidak jelas (sintaks dasar TICKscipt dijelaskan di sini) dan tunjukkan bagaimana penerapannya dengan menggunakan contoh penyelesaian salah satu masalah kita.
Mari kita pergi!
float & int, kesalahan perhitungan
Masalah yang benar-benar standar, diselesaikan melalui kasta:
var alert_float = 5.0
var alert_int = 10
data|eval(lambda: float("value") > alert_float OR float("value") < float("alert_int"))
Menggunakan bawaan()
Jika tag/field tidak diisi maka akan terjadi kesalahan perhitungan:
Secara default, join akan membuang titik-titik yang tidak ada datanya (batin).
Dengan fill('null'), gabungan luar akan dilakukan, setelah itu Anda perlu melakukan default() dan mengisi nilai yang kosong:
var data = res1
|join(res2)
.as('res1', 'res2)
.fill('null')
|default()
.field('res1.value', 0.0)
.field('res2.value', 100.0)
Masih ada nuansa di sini. Pada contoh di atas, jika salah satu rangkaian (res1 atau res2) kosong, maka rangkaian (data) yang dihasilkan juga akan kosong. Ada beberapa tiket tentang topik ini di Github (1633, 1871, 6967) β kami menunggu perbaikan dan sedikit menderita.
Menggunakan kondisi dalam perhitungan (jika dalam lambda)
|eval(lambda: if("value" > 0, true, false)
Lima menit terakhir dari jalur pipa untuk periode tersebut
Misalnya, Anda perlu membandingkan nilai lima menit terakhir dengan minggu sebelumnya. Anda dapat mengambil dua kumpulan data dalam dua kumpulan terpisah atau mengekstrak sebagian data dari periode yang lebih besar:
Alternatif untuk lima menit terakhir adalah menggunakan BarrierNode, yang memotong data sebelum waktu yang ditentukan:
|barrier()
.period(5m)
Contoh penggunaan template Go dalam pesan
Template sesuai dengan format dari paket teks.templateDi bawah ini adalah beberapa teka-teki yang sering ditemui.
jika ada
Kami membereskan segala sesuatunya dan tidak memicu orang dengan teks sekali lagi:
|alert()
...
.message(
'{{ if eq .Level "OK" }}It is ok now{{ else }}Chief, everything is broken{{end}}'
)
Dua digit setelah koma desimal dalam pesan
Meningkatkan keterbacaan pesan:
|alert()
...
.message(
'now value is {{ index .Fields "value" | printf "%0.2f" }}'
)
Memperluas variabel dalam pesan
Kami menampilkan lebih banyak informasi dalam pesan untuk menjawab pertanyaan βMengapa ia berteriakβ?
var warnAlert = 10
|alert()
...
.message(
'Today value less then '+string(warnAlert)+'%'
)
Pengidentifikasi peringatan unik
Ini adalah hal yang diperlukan ketika ada lebih dari satu grup dalam data, jika tidak, hanya satu peringatan yang akan dihasilkan:
|alert()
...
.id('{{ index .Tags "myname" }}/{{ index .Tags "myfield" }}')
Penangan khusus
Daftar besar penangan mencakup exec, yang memungkinkan Anda menjalankan skrip dengan parameter yang diteruskan (stdin) - kreativitas dan tidak lebih!
Salah satu kebiasaan kami adalah skrip Python kecil untuk mengirimkan notifikasi ke slack.
Pada awalnya, kami ingin mengirim gambar grafana yang dilindungi otorisasi dalam sebuah pesan. Setelah itu, tulis OK di thread peringatan sebelumnya dari grup yang sama, dan bukan sebagai pesan terpisah. Beberapa saat kemudian - tambahkan ke pesan kesalahan paling umum dalam X menit terakhir.
Topik terpisah adalah komunikasi dengan layanan lain dan tindakan apa pun yang dimulai oleh peringatan (hanya jika pemantauan Anda berfungsi dengan cukup baik).
Contoh deskripsi handler, dimana slack_handler.py adalah skrip yang kita tulis sendiri:
Misalnya, kami menyiapkan peringatan untuk jumlah permintaan per jam (groupBy(1h)) dan ingin mencatat peringatan yang terjadi di influxdb (untuk menunjukkan dengan indah fakta masalah pada grafik di grafana).
influxDBOut() akan menulis nilai waktu dari peringatan ke stempel waktu; oleh karena itu, titik pada grafik akan ditulis lebih awal/lambat dari peringatan tiba.
Ketika akurasi diperlukan: kami mengatasi masalah ini dengan memanggil pengendali khusus, yang akan menulis data ke influxdb dengan stempel waktu saat ini.
buruh pelabuhan, pembangunan dan penerapan
Saat startup, kapacitor dapat memuat tugas, templat, dan penangan dari direktori yang ditentukan dalam konfigurasi di blok [load].
Untuk membuat tugas dengan benar, Anda memerlukan hal-hal berikut:
Nama file β diperluas menjadi id/nama skrip
Ketik β aliran/batch
dbrp β kata kunci untuk menunjukkan database + kebijakan mana yang menjalankan skrip (dbrp βpemasok.β βautogenβ)
Jika beberapa tugas batch tidak berisi baris dengan dbrp, seluruh layanan akan menolak untuk memulai dan dengan jujur ββββakan menuliskannya di log.
Sebaliknya, dalam kronograf, baris ini seharusnya tidak ada; tidak diterima melalui antarmuka dan menghasilkan kesalahan.
Meretas saat membuat container: Dockerfile keluar dengan -1 jika ada baris dengan //.+dbrp, yang akan memungkinkan Anda untuk segera memahami alasan kegagalan saat merakit build.
bergabung satu ke banyak
Contoh tugas: Anda perlu mengambil persentil ke-95 dari waktu pengoperasian layanan selama seminggu, bandingkan setiap menit dalam 10 menit terakhir dengan nilai ini.
Anda tidak dapat melakukan penggabungan satu-ke-banyak, terakhir/rata-rata/median pada sekelompok titik mengubah simpul menjadi aliran, kesalahan βtidak dapat menambahkan tepi anak yang tidak cocok: batch -> aliranβ akan dikembalikan.
Hasil batch, sebagai variabel dalam ekspresi lambda, juga tidak disubstitusi.
Ada opsi untuk menyimpan nomor yang diperlukan dari batch pertama ke file melalui udf dan memuat file ini melalui sideload.
Apa yang kami pecahkan dengan ini?
Kita punya sekitar 100 supplier hotel yang masing-masing bisa punya beberapa koneksi, sebut saja channel. Channel ini berjumlah kurang lebih 300, setiap channelnya bisa putus. Dari semua metrik yang tercatat, kami akan memantau tingkat kesalahan (permintaan dan kesalahan).
Mengapa tidak Grafana?
Peringatan kesalahan yang dikonfigurasi di Grafana memiliki beberapa kelemahan. Ada yang kritis, ada yang bisa Anda tutup mata, tergantung situasinya.
Grafana tidak tahu cara menghitung antara pengukuran + peringatan, tetapi kita membutuhkan rate (permintaan-kesalahan)/permintaan.
Kesalahannya terlihat buruk:
Dan tidak terlalu jahat jika dilihat dengan permintaan yang berhasil:
Oke, kita bisa menghitung terlebih dahulu tarif di layanan sebelum grafana, dan dalam beberapa kasus ini akan berhasil. Tapi tidak di negara kita, karena... untuk setiap saluran, rasionya sendiri dianggap "normal", dan peringatan bekerja sesuai dengan nilai statis (kami mencarinya dengan mata kami, mengubahnya jika sering ada peringatan).
Ini adalah contoh βnormalβ untuk saluran yang berbeda:
Kami mengabaikan poin sebelumnya dan berasumsi bahwa gambaran βnormalβ serupa untuk semua pemasok. Sekarang semuanya baik-baik saja, dan kita bisa bertahan dengan peringatan di grafana?
Bisa, tapi sebenarnya tidak mau, karena harus memilih salah satu pilihan:
a) membuat banyak grafik untuk setiap saluran secara terpisah (dan menemaninya dengan susah payah)
b) tinggalkan satu grafik dengan semua saluran (dan tersesat dalam garis warna-warni dan peringatan yang disesuaikan)
bergabung dengan dua serial dalam beberapa jam, mengelompokkan berdasarkan saluran;
isi seri per kelompok bila belum ada datanya;
bandingkan median 10 menit terakhir dengan data sebelumnya;
kami berteriak jika kami menemukan sesuatu;
kami menulis tarif yang dihitung dan peringatan yang terjadi di influxdb;
kirim pesan berguna ke slack.
Menurut pendapat saya, kami berhasil mencapai semua yang kami ingin dapatkan pada akhirnya (dan bahkan lebih banyak lagi dengan penangan khusus) seindah mungkin.
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)
Dan apa kesimpulannya?
Kapacitor hebat dalam melakukan peringatan pemantauan dengan banyak pengelompokan, melakukan penghitungan tambahan berdasarkan metrik yang sudah dicatat, melakukan tindakan khusus, dan menjalankan skrip (udf).
Hambatan untuk masuk tidak terlalu tinggi - cobalah jika grafana atau alat lain tidak sepenuhnya memuaskan keinginan Anda.