Κόλπα για την επεξεργασία μετρήσεων στο Kapacitor

Πιθανότατα, σήμερα κανείς δεν ρωτά γιατί είναι απαραίτητο να συλλέγουμε μετρήσεις υπηρεσιών. Το επόμενο λογικό βήμα είναι να ρυθμίσετε μια ειδοποίηση για τις μετρήσεις που συλλέγονται, η οποία θα ειδοποιεί για τυχόν αποκλίσεις στα δεδομένα σε κανάλια που σας βολεύουν (mail, Slack, Telegram). Στην ηλεκτρονική υπηρεσία κρατήσεων ξενοδοχείων Ostrovok.ru όλες οι μετρήσεις των υπηρεσιών μας μεταφέρονται στο InfluxDB και εμφανίζονται στο Grafana, ενώ εκεί διαμορφώνεται και η βασική ειδοποίηση. Για εργασίες όπως "πρέπει να υπολογίσετε κάτι και να το συγκρίνετε με αυτό", χρησιμοποιούμε το Kapacitor.

Κόλπα για την επεξεργασία μετρήσεων στο Kapacitor
Ο Kapacitor είναι μέρος της στοίβας TICK που μπορεί να επεξεργαστεί μετρήσεις από το InfluxDB. Μπορεί να συνδέσει πολλές μετρήσεις μαζί (join), να υπολογίσει κάτι χρήσιμο από τα ληφθέντα δεδομένα, να γράψει το αποτέλεσμα πίσω στο InfluxDB, να στείλει μια ειδοποίηση στο Slack/Telegram/mail.

Ολόκληρη η στοίβα είναι δροσερή και λεπτομερής τεκμηρίωση, αλλά πάντα θα υπάρχουν χρήσιμα πράγματα που δεν αναφέρονται ρητά στα εγχειρίδια. Σε αυτό το άρθρο, αποφάσισα να συγκεντρώσω μια σειρά από τέτοιες χρήσιμες, μη προφανείς συμβουλές (περιγράφεται η βασική σύνταξη του TICKscipt εδώ) και δείξτε πώς μπορούν να εφαρμοστούν χρησιμοποιώντας ένα παράδειγμα επίλυσης ενός από τα προβλήματά μας.

Πάμε!

float & int, σφάλματα υπολογισμού

Ένα απολύτως τυπικό πρόβλημα, που λύνεται μέσω κάστες:

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

Χρήση προεπιλογής()

Εάν δεν συμπληρωθεί μια ετικέτα/πεδίο, θα προκύψουν σφάλματα υπολογισμού:

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

συμπλήρωση σύνδεσης (εσωτερική έναντι εξωτερικής)

Από προεπιλογή, η σύνδεση θα απορρίψει σημεία όπου δεν υπάρχουν δεδομένα (εσωτερικά).
Με το fill('null'), θα πραγματοποιηθεί μια εξωτερική ένωση, μετά την οποία πρέπει να κάνετε μια προεπιλογή() και να συμπληρώσετε τις κενές τιμές:

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

Υπάρχει ακόμα μια απόχρωση εδώ. Στο παραπάνω παράδειγμα, εάν μία από τις σειρές (res1 ή res2) είναι κενή, η προκύπτουσα σειρά (δεδομένα) θα είναι επίσης κενή. Υπάρχουν πολλά εισιτήρια για αυτό το θέμα στο Github (1633, 1871, 6967) – περιμένουμε διορθώσεις και ταλαιπωρούμε λίγο.

Χρήση συνθηκών στους υπολογισμούς (αν είναι λάμδα)

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

Τελευταία πέντε λεπτά από τον αγωγό για την περίοδο

Για παράδειγμα, πρέπει να συγκρίνετε τις τιμές των τελευταίων πέντε λεπτών με την προηγούμενη εβδομάδα. Μπορείτε να πάρετε δύο παρτίδες δεδομένων σε δύο ξεχωριστές παρτίδες ή να εξαγάγετε μέρος των δεδομένων από μια μεγαλύτερη περίοδο:

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

Μια εναλλακτική λύση για τα τελευταία πέντε λεπτά θα ήταν η χρήση ενός BarrierNode, το οποίο διακόπτει τα δεδομένα πριν από την καθορισμένη ώρα:

|barrier()
        .period(5m)

Παραδείγματα χρήσης προτύπων Go στο μήνυμα

Τα πρότυπα αντιστοιχούν στη μορφή από το πακέτο κείμενο.πρότυποΠαρακάτω είναι μερικά παζλ που συναντάτε συχνά.

αν-αλλιώς

Βάζουμε τα πράγματα σε μια σειρά και δεν ενεργοποιούμε τους χρήστες με κείμενο για άλλη μια φορά:

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

Δύο ψηφία μετά την υποδιαστολή στο μήνυμα

Βελτίωση της αναγνωσιμότητας του μηνύματος:

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

Επέκταση μεταβλητών στο μήνυμα

Εμφανίζουμε περισσότερες πληροφορίες στο μήνυμα για να απαντήσουμε στην ερώτηση "Γιατί φωνάζει";

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

Μοναδικό αναγνωριστικό ειδοποίησης

Αυτό είναι απαραίτητο όταν υπάρχουν περισσότερες από μία ομάδες στα δεδομένα, διαφορετικά θα δημιουργηθεί μόνο μία ειδοποίηση:

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

Προσαρμοσμένος χειριστής

Η μεγάλη λίστα των χειριστών περιλαμβάνει το exec, το οποίο σας επιτρέπει να εκτελέσετε το σενάριό σας με τις περασμένες παραμέτρους (stdin) - δημιουργικότητα και τίποτα περισσότερο!

Ένα από τα έθιμα μας είναι ένα μικρό σενάριο Python για την αποστολή ειδοποιήσεων στο slack.
Αρχικά, θέλαμε να στείλουμε μια εικόνα grafana που προστατεύεται από εξουσιοδότηση σε ένα μήνυμα. Στη συνέχεια, γράψτε OK στο νήμα στην προηγούμενη ειδοποίηση από την ίδια ομάδα και όχι ως ξεχωριστό μήνυμα. Λίγο αργότερα - προσθέστε στο μήνυμα το πιο συνηθισμένο λάθος τα τελευταία Χ λεπτά.

Ένα ξεχωριστό θέμα είναι η επικοινωνία με άλλες υπηρεσίες και τυχόν ενέργειες που ξεκινούν από μια ειδοποίηση (μόνο εάν η παρακολούθησή σας λειτουργεί αρκετά καλά).
Ένα παράδειγμα περιγραφής προγράμματος χειρισμού, όπου το slack_handler.py είναι το αυτο-γραμμένο σενάριο μας:

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

Πώς να κάνετε εντοπισμό σφαλμάτων;

Επιλογή με έξοδο καταγραφής

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

Ρολόι (cli): capacitor -url host-ή-ip:9092 αρχεία καταγραφής lvl=σφάλμα

Επιλογή με httpOut

Εμφανίζει δεδομένα στον τρέχοντα αγωγό:

|httpOut('something')

Παρακολουθήστε (λάβετε): host-ή-ip:9092/kapacitor/v1/tasks/task_name/something

Σχέδιο εκτέλεσης

  • Κάθε εργασία επιστρέφει ένα δέντρο εκτέλεσης με χρήσιμους αριθμούς στη μορφή graphviz.
  • Πάρτε ένα μπλοκ κουκκίδα.
  • Επικολλήστε το στο πρόγραμμα προβολής, απολαμβάνω.

Πού αλλού μπορείτε να πάρετε μια τσουγκράνα;

χρονική σήμανση στο influxdb κατά την επιστροφή

Για παράδειγμα, ρυθμίζουμε μια ειδοποίηση για το άθροισμα των αιτημάτων ανά ώρα (groupBy(1h)) και θέλουμε να καταγράψουμε την ειδοποίηση που προέκυψε στο influxdb (για να δείξουμε όμορφα το γεγονός του προβλήματος στο γράφημα στο grafana).

Η influxDBOut() θα γράψει την τιμή του χρόνου από την ειδοποίηση έως τη χρονική σήμανση, κατά συνέπεια, το σημείο στο γράφημα θα γραφτεί νωρίτερα/αργότερα από την άφιξη της ειδοποίησης.

Όταν απαιτείται ακρίβεια: επιλύουμε αυτό το πρόβλημα καλώντας έναν προσαρμοσμένο χειριστή, ο οποίος θα γράψει δεδομένα στο influxdb με την τρέχουσα χρονική σήμανση.

docker, κατασκευή και ανάπτυξη

Κατά την εκκίνηση, ο πυκνωτής μπορεί να φορτώσει εργασίες, πρότυπα και χειριστές από τον κατάλογο που καθορίζεται στη διαμόρφωση στο μπλοκ [load].

Για να δημιουργήσετε σωστά μια εργασία, χρειάζεστε τα ακόλουθα πράγματα:

  1. Όνομα αρχείου – επεκτάθηκε σε αναγνωριστικό/όνομα σεναρίου
  2. Τύπος – ροή/παρτίδα
  3. dbrp – λέξη-κλειδί για να υποδείξετε σε ποια βάση δεδομένων + πολιτική εκτελείται το σενάριο (dbrp "supplier.""autoogen")

Εάν κάποια εργασία δέσμης δεν περιέχει γραμμή με dbrp, ολόκληρη η υπηρεσία θα αρνηθεί να ξεκινήσει και θα γράψει ειλικρινά σχετικά με αυτό στο αρχείο καταγραφής.

Στο chronograf, αντίθετα, αυτή η γραμμή δεν πρέπει να υπάρχει, δεν γίνεται αποδεκτή μέσω της διεπαφής και δημιουργεί ένα σφάλμα.

Παραβίαση κατά την κατασκευή ενός κοντέινερ: Το Dockerfile εξέρχεται με -1 εάν υπάρχουν γραμμές με //.+dbrp, κάτι που θα σας επιτρέψει να κατανοήσετε αμέσως τον λόγο της αποτυχίας κατά τη συναρμολόγηση του build.

ενώστε ένα προς πολλά

Παράδειγμα εργασίας: πρέπει να πάρετε το 95ο εκατοστημόριο του χρόνου λειτουργίας της υπηρεσίας για μια εβδομάδα, να συγκρίνετε κάθε λεπτό των τελευταίων 10 με αυτήν την τιμή.

Δεν μπορείτε να κάνετε μια σύνδεση ένα προς πολλά, η τελευταία/μέση/διάμεσος σε μια ομάδα σημείων μετατρέπει τον κόμβο σε ροή, θα επιστραφεί το σφάλμα "δεν είναι δυνατή η προσθήκη θυγατρικών άκρων που δεν ταιριάζουν: παρτίδα -> ροή".

Το αποτέλεσμα μιας παρτίδας, ως μεταβλητή σε μια έκφραση λάμδα, επίσης δεν αντικαθίσταται.

Υπάρχει η επιλογή να αποθηκεύσετε τους απαραίτητους αριθμούς από την πρώτη παρτίδα σε ένα αρχείο μέσω udf και να φορτώσετε αυτό το αρχείο μέσω sideload.

Τι λύσαμε με αυτό;

Έχουμε περίπου 100 προμηθευτές ξενοδοχείων, ο καθένας από αυτούς μπορεί να έχει πολλές συνδέσεις, ας το πούμε κανάλι. Υπάρχουν περίπου 300 από αυτά τα κανάλια, καθένα από τα κανάλια μπορεί να πέσει. Από όλες τις καταγεγραμμένες μετρήσεις, θα παρακολουθούμε το ποσοστό σφαλμάτων (αιτήματα και σφάλματα).

Γιατί όχι γραφάνα;

Οι ειδοποιήσεις σφαλμάτων που έχουν ρυθμιστεί στο Grafana έχουν αρκετά μειονεκτήματα. Κάποια είναι επικριτικά, άλλα μπορείτε να κλείσετε τα μάτια σας, ανάλογα με την κατάσταση.

Η Grafana δεν ξέρει να υπολογίζει μεταξύ μετρήσεων + ειδοποίηση, αλλά χρειαζόμαστε ποσοστό (αιτήματα-λάθη)/ αιτήματα.

Τα λάθη φαίνονται άσχημα:

Κόλπα για την επεξεργασία μετρήσεων στο Kapacitor

Και λιγότερο κακό όταν προβάλλονται με επιτυχημένα αιτήματα:

Κόλπα για την επεξεργασία μετρήσεων στο Kapacitor

Εντάξει, μπορούμε να υπολογίσουμε εκ των προτέρων το ποσοστό στην υπηρεσία πριν από το grafana, και σε ορισμένες περιπτώσεις αυτό θα λειτουργήσει. Όχι όμως στα δικά μας γιατί... για κάθε κανάλι η δική του αναλογία θεωρείται "κανονική" και οι ειδοποιήσεις λειτουργούν σύμφωνα με στατικές τιμές (τις αναζητούμε με τα μάτια μας, τις αλλάζουμε αν υπάρχουν συχνές ειδοποιήσεις).

Αυτά είναι παραδείγματα "κανονικού" για διαφορετικά κανάλια:

Κόλπα για την επεξεργασία μετρήσεων στο Kapacitor

Κόλπα για την επεξεργασία μετρήσεων στο Kapacitor

Αγνοούμε το προηγούμενο σημείο και υποθέτουμε ότι η «κανονική» εικόνα είναι παρόμοια για όλους τους προμηθευτές. Τώρα όλα είναι καλά, και μπορούμε να τα βγάλουμε πέρα ​​με ειδοποιήσεις στο grafana;
Μπορούμε, αλλά πραγματικά δεν το θέλουμε, γιατί πρέπει να διαλέξουμε μία από τις επιλογές:
α) Κάντε πολλά γραφήματα για κάθε κανάλι ξεχωριστά (και συνοδεύστε τα με πόνο)
β) αφήστε ένα γράφημα με όλα τα κανάλια (και χαθείτε στις πολύχρωμες γραμμές και στις προσαρμοσμένες ειδοποιήσεις)

Κόλπα για την επεξεργασία μετρήσεων στο Kapacitor

Πώς το έκανες;

Και πάλι, υπάρχει ένα καλό παράδειγμα εκκίνησης στην τεκμηρίωση (Υπολογισμός ποσοστών μεταξύ των ενωμένων σειρών), μπορεί να εξεταστεί ή να ληφθεί ως βάση σε παρόμοια προβλήματα.

Τι κάναμε τελικά:

  • Εγγραφείτε σε δύο σειρές σε λίγες ώρες, ομαδοποιώντας ανά κανάλια.
  • συμπληρώστε τη σειρά ανά ομάδα εάν δεν υπήρχαν δεδομένα.
  • συγκρίνετε τη διάμεσο των τελευταίων 10 λεπτών με τα προηγούμενα δεδομένα.
  • φωνάζουμε αν βρούμε κάτι?
  • γράφουμε τους υπολογισμένους ρυθμούς και τις ειδοποιήσεις που εμφανίστηκαν στο influxdb.
  • στείλτε ένα χρήσιμο μήνυμα στο slack.

Κατά τη γνώμη μου, καταφέραμε να πετύχουμε όλα όσα θέλαμε στο τέλος (και ακόμη λίγο περισσότερο με προσαρμοσμένους χειριστές) όσο το δυνατόν πιο όμορφα.

Μπορείτε να δείτε στο github.com παράδειγμα κώδικα и ελάχιστο κύκλωμα (graphviz) το σενάριο που προκύπτει.

Ένα παράδειγμα του κώδικα που προκύπτει:

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)

Και ποιο είναι το συμπέρασμα;

Το Kapacitor είναι εξαιρετικό στην εκτέλεση ειδοποιήσεων παρακολούθησης με μια δέσμη ομαδοποιήσεων, στην εκτέλεση πρόσθετων υπολογισμών με βάση ήδη καταγεγραμμένες μετρήσεις, στην εκτέλεση προσαρμοσμένων ενεργειών και στην εκτέλεση σεναρίων (udf).

Το εμπόδιο εισόδου δεν είναι πολύ υψηλό - δοκιμάστε το εάν το grafana ή άλλα εργαλεία δεν ικανοποιούν πλήρως τις επιθυμίες σας.

Πηγή: www.habr.com

Προσθέστε ένα σχόλιο