Sztuczki do przetwarzania metryk w Kapacitor

Dziś nikt już nie ma wątpliwości, po co zbierać metryki serwisów. Kolejnym logicznym krokiem jest skonfigurowanie alertów na zbierane metryki, które będą powiadamiać o wszelkich odchyleniach w danych w wygodnych dla Ciebie kanałach (e-mail, Slack, Telegram). W serwisie rezerwacji online hoteli Ostrovok.ru wszystkie metryki naszych serwisów trafiają do InfluxDB i są wyświetlane w Grafana, gdzie także skonfigurowano podstawowe alerty. Do zadań typu „trzeba coś policzyć i porównać” używamy Kapacitor.

Sztuczki do przetwarzania metryk w Kapacitor
Kapacitor jest częścią stosu TICK, który potrafi przetwarzać metryki z InfluxDB. Może łączyć kilka miar (join), obliczyć coś użytecznego z uzyskanych danych, zapisać wynik z powrotem do InfluxDB i wysłać alert do Slacka/Telegramu/e-maila.

Cały stos ma świetną i szczegółową dokumentację, ale zawsze można znaleźć przydatne rzeczy, które nie są wprost opisane w dokumentacji. W tym artykule postanowiłem zebrać szereg takich przydatnych, nieoczywistych wskazówek (podstawowa składnia TICKscipt jest opisana tutaj) i pokazać, jak można je zastosować, na przykładzie rozwiązania jednego z naszych zadań.

Zaczynamy!

float & int, błędy obliczeń

Absolutnie standardowy problem, który można rozwiązać przez rzutowanie:

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

Użycie default()

Jeśli tag/pole nie jest wypełnione, wystąpią błędy w obliczeniach:

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

fill w join (inner vs outer)

Domyślnie join odrzuci punkty, gdzie dane są niedostępne (inner).
Przy użyciu fill(‘null’) zostanie wykonany outer join, po czym trzeba będzie zrobić default() i wypełnić puste wartości:

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

Był tu jednak pewien niuans. Jeśli w powyższym przykładzie jedna z serii (res1 lub res2) będzie pusta, seria końcowa (data) również będzie pusta. Na ten temat jest kilka zgłoszeń na GitHubie (1633, 1871, 6967) – czekamy na poprawki i trochę cierpimy.

Użycie warunków w obliczeniach (if w lambda)

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

Ostatnie pięć minut z potoku za okres

Na przykład musisz porównać wartości z ostatnich pięciu minut z poprzednim tygodniem. Można wziąć dwie paczki danych dwiema oddzielnymi partiami lub wyciągnąć część danych z większego okresu:

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

Alternatywą na ostatnie pięć minut może być użycie węzła BarrierNode, który odcina dane wcześniej niż podany czas:

|barrier()
        .period(5m)

Przykłady użycia szablonów w Go w komunikacie

Szablony odpowiadają formatowi z pakietu text.template, poniżej kilka często występujących zadań.

if-else

Porządkujemy, nie wyzwalając ludzi tekstem niepotrzebnie:

|alert()
    ...
    .message(
        '{{ if eq .Level "OK" }}Wszystko jest teraz ok{{ else }}Szefie, wszystko się zepsuło{{end}}'
    )

Dwie cyfry po przecinku w komunikacie

Poprawiamy czytelność komunikatu:

|alert()
    ...
    .message(
        'aktualna wartość to {{ index .Fields "value" | printf "%0.2f" }}'
    )

Rozwijać zmienne w komunikacie

Wyświetlamy więcej informacji w komunikacie, aby odpowiedzieć na pytanie „Dlaczego krzyczy?”

var warnAlert = 10
  |alert()
    ...
    .message(
       'Dzisiaj wartość jest mniejsza niż '+string(warnAlert)+'%'
    )

Unikalny identyfikator alertu

Potrzebna rzecz, gdy w danych jest więcej niż jedna grupa, w przeciwnym razie zostanie wygenerowany tylko jeden alert:

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

Niestandardowe handler’y

W dużej liście handlerów znajduje się exec, który pozwala wykonać własny skrypt z przekazanymi parametrami (stdin) – to tylko kreatywność!

Jednym z naszych customów jest mały skrypt w Pythona do wysyłania powiadomień do Slacka.
Na początku chcieliśmy wysyłać w wiadomości obrazek z Grafany, zabezpieczony autoryzacją. Następnie – pisać OK w wątku poprzedniego alertu z tej samej grupy, a nie jako osobnej wiadomości. Jeszcze później – dodawać w komunikacie najczęstszy błąd za ostatnie X minut.

Osobny temat – powiązanie z innymi usługami i jakiekolwiek działania inicjowane przez alert (tylko jeśli twoje monitorowanie działa wystarczająco dobrze).
Przykład opisu handlera, gdzie slack_handler.py to nasz własny skrypt:

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

Jak debugować?

Wariant z wypisaniem do logu

|log()
      .level("error")
      .prefix("coś")

Sprawdź (cli): kapacitor -url host-or-ip:9092 logs lvl=error

Wariant z httpOut

Pokazuje dane w bieżącym pipeline’ie:

|httpOut('coś')

Sprawdzaj (get): host-or-ip:9092\/kapacitor\/v1\/tasks\/task_name\/something

Schemat wykonania

  • Każde zadanie zwraca drzewo wykonania z użytecznymi danymi w formacie graphviz.
  • Bierzemy blok dot.
  • Wstawiamy do viewer’a, cieszymy się.

Gdzie jeszcze można zdobyć timestamp w influxdb przy odwrotnej zapisie

znacznik czasu w influxdb przy odwrotnej rejestracji

Na przykład, konfigurujemy alert na ilość zapytań na godzinę (groupBy(1h)) i chcemy zapisać powstały alert w influxdb (aby ładnie pokazać fakt wystąpienia problemu na wykresie w grafana).

influxDBOut() zapisze w timestamp wartość time z alertu, odpowiednio, punkt na wykresie będzie zapisany wcześniej/później, niż przyszedł alert.

Gdy wymagana jest dokładność: obejmujemy ten problem przez wywołanie niestandardowego handlera, który zapisze dane w influxdb z bieżącym timestampem.

docker, budowanie i wdrażanie

Podczas uruchamiania kapacitor może ładować zadania, szablony i handlera z katalogu określonego w konfiguracji, w bloku [load].

Do poprawnego stworzenia zadania potrzebne są następujące elementy:

  1. Nazwa pliku – rozwija się w id/nazwę skryptu
  2. Typ – stream/batch
  3. dbrp – słowo kluczowe do wskazania, w której bazie + polityce działa skrypt (dbrp "supplier"."autogen")

Jeśli w jakimkolwiek zadaniu batch nie będzie linii z dbrp, cały serwis odmówi uruchomienia i szczerze napisze o tym w logu.

W chronografie jednak, w przeciwieństwie, tej linii nie powinno być, przez interfejs nie jest akceptowana i zwraca błąd.

Haczyk przy budowie kontenera: Dockerfile kończy się z -1, jeśli znajdują się w nim linie z //.+dbrp, co pozwoli od razu zrozumieć przyczynę niepowodzenia przy budowie.

join jeden do wielu

Przykład zadania: trzeba wziąć 95. percentyl czasu pracy usługi za tydzień, porównać każdą minutę z ostatnich 10 z tą wartością.

Nie można zrobić join jeden do wielu, last/mean/median według grupy punktów przekształca nodę w stream, zwróci błąd „cannot add child mismatched edges: batch -> stream”.

Wynik batcha, jako zmienna w lambda-wyrażeniu, również nie jest podstawiany.

Jest opcja, aby zapisywać potrzebne liczby z pierwszego batcha do pliku przez udf i ładować ten plik przez sideload.

Co tym rozwiązaliśmy?

Mamy około 100 dostawców hoteli, do każdego z nich może być kilka połączeń, nazwijmy to kanałem. Jest ich około 300, każdy kanał może odpaść. Spośród wszystkich rejestrowanych metryk będziemy monitorować wskaźnik błędów (requests i errors).

Dlaczego nie grafana?

Alarmy po błędach skonfigurowane w grafanie mają kilka wad. Niektóre są krytyczne, na inne można przymknąć oko, w zależności od sytuacji.

Grafana nie potrafi obliczeń między pomiarami + alerting, a my potrzebujemy wskaźnika (requests-errors)/requests.

Błędy wyglądają złowrogo:

Sztuczki do przetwarzania metryk w Kapacitor

I mniej złowrogo, jeśli patrzeć na sukcesywnie wykonane zapytania:

Sztuczki do przetwarzania metryk w Kapacitor

OK, możemy wstępnie obliczyć wskaźnik w serwisie do Grafany, i w niektórych przypadkach to będzie odpowiednie. Ale nie w naszym, ponieważ dla każdego kanału oblicza się inne „normalne” proporcje, a alerty działają na podstawie statycznych wartości (szukamy wzrokowo, zmieniamy, jeśli alerty są zbyt częste).

Oto przykłady „normalnych” wartości dla różnych kanałów:

Sztuczki do przetwarzania metryk w Kapacitor

Sztuczki do przetwarzania metryk w Kapacitor

Pomijając poprzedni punkt, zakładajmy, że u wszystkich dostawców „normalny” obraz jest podobny. Teraz wszystko wygląda dobrze i czy możemy polegać na alertach w Grafanie?
Możemy, ale bardzo nie chcemy, bo trzeba wybrać jedną z dwóch opcji:
a) stworzyć wiele wykresów dla każdego kanału osobno (i męczyć się z ich utrzymywaniem)
b) zostawić jeden wykres ze wszystkimi kanałami (i zgubić się w kolorowych liniach oraz ustawionych alertach)

Sztuczki do przetwarzania metryk w Kapacitor

Jak to zrobiliśmy?

Ponownie, w dokumentacji jest dobry przykład startowy (Obliczanie wskaźników dla połączonych serii), można zajrzeć lub wziąć to za podstawę w podobnych zadaniach.

Co zrobiliśmy ostatecznie:

  • połączenie dwóch serii w ciągu kilku godzin, grupowanie według kanałów;
  • uzupełniamy serie dla grup, jeśli wcześniej brakowało danych;
  • porównujemy medianę ostatnich 10 minut z wcześniejszymi danymi;
  • informujemy, jeśli coś wykryliśmy;
  • zapisujemy obliczone wskaźniki i zdarzone alerty w influxdb;
  • wysyłamy przydatną wiadomość do Slacka.

Moim zdaniem, udało nam się idealnie to, co chcieliśmy osiągnąć (a nawet trochę więcej z niestandardowymi handlerami).

Na github.com można zobaczyć przykład kodu i minimalny schemat (graphviz) uzyskanego skryptu.

Przykład otrzymanego kodu:

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')
 // wypełniamy wartości zerami, jeśli ich nie było
 |default()
 .field('err.value', 0.0)
 .field('req.value', 0.0)
 // if w lambda: obliczamy wskaźnik, tylko jeśli były błędy
 |eval(lambda: if("err.value" > 0, 100.0 * (float("req.value") - float("err.value")) / float("req.value"), 100.0))
 .as('rate')

// zapisujemy obliczone wartości w influx
rates
 |influxDBOut()
 .quiet()
 .create()
 .database('kapacitor')
 .retentionPolicy('autogen')
 .measurement('rates')

// wybieramy dane za ostatnie 10 minut, obliczamy medianę
var todayRate = rates
 |where(lambda: duration((unixNano(now()) - unixNano("time")) / 1000, 1u)  warnAlert)
 .warnReset(lambda: ("prev.median" - "today.median") < warnReset)
 .flapping(0.25, 0.5)
 .stateChangesOnly()
 // zbieramy w wiadomości link do wykresu panelu grafany
 .message(
 '{{ .Level }}: {{ index .Tags "channel" }} stosunek err/req ({{ index .Tags "supplier" }})
{{ if eq .Level "OK" }}Teraz jest ok{{ else }}
'+string(todayPeriod)+' mediana to {{ index .Fields "today.median" | printf "%0.2f" }}%, w porównaniu do poprzedniego '+string(period)+' to {{ 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=UTC0300'
 )
 .id('{{ index .Tags "name" }}{{ index .Tags "channel" }}')
 .levelTag('level')
 .messageField('message')
 .durationField('duration')
 .topic('slack_graph')

// "today.median" powielamy jako "value", a także zapisujemy w influx pozostałe pola alertu (keep)
trigger
 |eval(lambda: "today.median")
 .as('value')
 .keep()
 |influxDBOut()
 .quiet()
 .create()
 .database('kapacitor')
 .retentionPolicy('autogen')
 .measurement('alerts')
 .tag('alertName', name)

A jaki jest wynik?

Kapacitor doskonale potrafi przeprowadzać monitorowanie i alertowanie z wieloma grupowaniami, wykonywać dodatkowe obliczenia na już zapisanych metrykach, realizować niestandardowe działania oraz uruchamiać skrypty (UDF).

Próg wejścia nie jest zbyt wysoki – spróbuj go, jeśli Grafana lub inne narzędzia nie do końca spełniają Twoje oczekiwania.

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster