Triki do przetwarzania metryk w Kapacitorze

Chyba już dziś nikt nie zadaje sobie pytania, dlaczego konieczne jest zbieranie wskaźników usług. Kolejnym logicznym krokiem jest ustawienie alertu dla zebranych metryk, który powiadomi o wszelkich odchyleniach danych w dogodnych dla Ciebie kanałach (poczta, Slack, Telegram). W internetowym serwisie rezerwacji hoteli Ostrovok.ru wszystkie metryki naszych usług przelewane są do InfluxDB i wyświetlane w Grafanie, tam też konfigurowane są podstawowe alerty. Do zadań typu „trzeba coś obliczyć i porównać z tym” używamy Kapacitora.

Triki do przetwarzania metryk w Kapacitorze
Kapacitor jest częścią stosu TICK, który może przetwarzać metryki z InfluxDB. Potrafi połączyć ze sobą kilka pomiarów (join), obliczyć coś przydatnego z otrzymanych danych, zapisać wynik z powrotem do InfluxDB, wysłać alert do Slack/Telegram/mail.

Cały stos jest fajny i szczegółowy dokumentacja, ale zawsze znajdą się przydatne rzeczy, które nie są wyraźnie wskazane w instrukcjach. W tym artykule postanowiłem zebrać szereg takich przydatnych, nieoczywistych wskazówek (opisana została podstawowa składnia TICKscipt tutaj) i pokazać, jak można je zastosować na przykładzie rozwiązania jednego z naszych problemów.

Chodźmy!

float i int, błędy obliczeniowe

Absolutnie standardowy problem, rozwiązywany poprzez kasty:

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

Używanie domyślnej()

Jeżeli tag/pole nie zostanie wypełnione, wystąpią błędy w obliczeniach:

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

wypełnij połączenie (wewnętrzne vs zewnętrzne)

Domyślnie Join odrzuci punkty, w których nie ma danych (wewnętrzne).
Kiedy fill('null') zostanie wykonane złączenie zewnętrzne, po czym musisz wykonać funkcję 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)

Jest tu jeszcze niuans. W powyższym przykładzie, jeśli jedna z serii (res1 lub res2) jest pusta, wynikowa seria (dane) również będzie pusta. Na Githubie jest kilka zgłoszeń na ten temat (1633, 1871, 6967) – czekamy na poprawki i trochę cierpimy.

Używanie warunków w obliczeniach (jeśli w lambdzie)

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

Ostatnie pięć minut od rurociągu w tym okresie

Na przykład musisz porównać wartości z ostatnich pięciu minut z poprzednim tygodniem. Możesz pobrać dwie partie danych w dwóch oddzielnych partiach lub wyodrębnić część danych z większego okresu:

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

Alternatywą dla ostatnich pięciu minut byłoby użycie BarrierNode, który odcina dane przed określonym czasem:

|barrier()
        .period(5m)

Przykłady wykorzystania szablonów Go w wiadomościach

Szablony odpowiadają formatowi z paczki tekst.szablonPoniżej znajdują się często spotykane łamigłówki.

Jeśli inaczej

Uporządkowujemy i nie prowokujemy już ludzi tekstem:

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

Dwie cyfry po przecinku w komunikacie

Poprawa czytelności przekazu:

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

Rozwijanie zmiennych w wiadomości

W wiadomości wyświetlamy więcej informacji, aby odpowiedzieć na pytanie „Dlaczego krzyczy”?

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

Unikalny identyfikator alertu

Jest to konieczne, gdy w danych występuje więcej niż jedna grupa, w przeciwnym razie zostanie wygenerowany tylko jeden alert:

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

Niestandardowy moduł obsługi

Na dużej liście programów obsługi znajduje się exec, który pozwala na wykonanie skryptu z przekazanymi parametrami (stdin) – kreatywność i nic więcej!

Jednym z naszych zwyczajów jest mały skrypt w Pythonie służący do wysyłania powiadomień do Slacka.
Początkowo chcieliśmy wysłać w wiadomości zdjęcie grafanki chronione autoryzacją. Następnie wpisz OK w wątku do poprzedniego alertu z tej samej grupy, a nie jako osobną wiadomość. Nieco później - dodaj do wiadomości najczęstszy błąd w ciągu ostatnich X minut.

Osobnym tematem jest komunikacja z innymi usługami i wszelkie działania inicjowane przez alert (tylko jeśli Twój monitoring działa wystarczająco dobrze).
Przykład opisu modułu obsługi, gdzie slack_handler.py jest naszym samodzielnie napisanym skryptem:

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ć?

Opcja z wyjściem dziennika

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

Oglądaj (cli): kapacitor -url host-lub-ip:9092 logów lvl=błąd

Opcja z httpOut

Pokazuje dane w bieżącym potoku:

|httpOut('something')

Obejrzyj (zdobądź): host-lub-ip:9092/kapacitor/v1/tasks/nazwa_zadania/coś

Schemat wykonania

  • Każde zadanie zwraca drzewo wykonania z przydatnymi liczbami w formacie grafiz.
  • Weź blok kropka.
  • Wklej go do przeglądarki, Cieszyć się.

Gdzie jeszcze można zdobyć grabie?

znacznik czasu w influxdb przy zapisie zwrotnym

Przykładowo ustawiamy alert dla sumy żądań na godzinę (groupBy(1h)) i chcemy odnotować alert, który wystąpił w influxdb (aby pięknie pokazać fakt problemu na wykresie w grafanie).

influxDBOut() zapisze wartość czasu od alertu do znacznika czasu; odpowiednio, punkt na wykresie zostanie zapisany wcześniej/później niż nadejście alertu.

Gdy wymagana jest dokładność: obejdziemy ten problem, wywołując niestandardową procedurę obsługi, która zapisze dane do influxdb z bieżącym znacznikiem czasu.

okno dokowane, kompilacja i wdrażanie

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

Aby poprawnie utworzyć zadanie, potrzebujesz następujących rzeczy:

  1. Nazwa pliku – rozwinięta do identyfikatora/nazwy skryptu
  2. Typ – strumień/partia
  3. dbrp – słowo kluczowe wskazujące, w której bazie danych + polityce działa skrypt (dbrp „dostawca.” „autogen”)

Jeżeli jakieś zadanie wsadowe nie będzie zawierało linijki z dbrp to cała usługa odmówi uruchomienia i szczerze napisze o tym w logu.

W chronografie natomiast ta linia nie powinna istnieć, nie jest akceptowana przez interfejs i generuje błąd.

Hack podczas budowania kontenera: Dockerfile kończy działanie z -1, jeśli istnieją linie z //.+dbrp, co pozwoli ci natychmiast zrozumieć przyczynę niepowodzenia podczas składania kompilacji.

dołącz jeden do wielu

Przykładowe zadanie: musisz przyjąć 95. percentyl czasu działania usługi za tydzień, porównać każdą minutę z ostatnich 10 z tą wartością.

Nie można wykonać złączenia jeden do wielu, ostatnia/średnia/mediana nad grupą punktów zamienia węzeł w strumień, zostanie zwrócony błąd „nie można dodać niedopasowanych krawędzi podrzędnych: partia -> strumień”.

Wynik partii, jako zmienna w wyrażeniu lambda, również nie jest podstawiony.

Istnieje możliwość zapisania niezbędnych numerów z pierwszej partii do pliku poprzez udf i załadowania tego pliku poprzez sideload.

Co w ten sposób rozwiązaliśmy?

Mamy około 100 dostawców hotelowych, każdy z nich może mieć kilka połączeń, nazwijmy to kanałem. Tych kanałów jest około 300, każdy z nich może spaść. Spośród wszystkich zarejestrowanych wskaźników będziemy monitorować poziom błędów (żądania i błędy).

Dlaczego nie grafana?

Alerty o błędach skonfigurowane w Grafanie mają kilka wad. Niektóre są krytyczne, na inne można przymknąć oczy, w zależności od sytuacji.

Grafana nie wie, jak obliczyć pomiędzy pomiarami + alertami, ale potrzebujemy stawki (żądań-błędów)/żądań.

Błędy wyglądają paskudnie:

Triki do przetwarzania metryk w Kapacitorze

I mniej zła, gdy patrzy się na udane prośby:

Triki do przetwarzania metryk w Kapacitorze

OK, stawkę w serwisie możemy wstępnie przeliczyć przed grafaną i w niektórych przypadkach to się sprawdzi. Ale nie u nas, bo... dla każdego kanału jego własny współczynnik jest uważany za „normalny”, a alerty działają według wartości statycznych (szukamy ich oczami, zmieniamy, jeśli alerty są częste).

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

Triki do przetwarzania metryk w Kapacitorze

Triki do przetwarzania metryk w Kapacitorze

Pomijamy poprzedni punkt i zakładamy, że „normalny” obraz jest podobny dla wszystkich dostawców. Teraz wszystko jest w porządku i możemy obejść się bez alertów w grafanie?
Możemy, ale bardzo nie chcemy, bo musimy wybrać jedną z opcji:
a) zrobić wiele wykresów dla każdego kanału z osobna (i boleśnie im towarzyszyć)
b) zostaw jeden wykres ze wszystkimi kanałami (i zagub się w kolorowych liniach i spersonalizowanych alertach)

Triki do przetwarzania metryk w Kapacitorze

Jak to zrobiłeś?

Ponownie w dokumentacji znajduje się dobry przykład początkowy (Obliczanie stawek w połączonych szeregach), można je podglądać lub traktować jako podstawę w podobnych problemach.

Co ostatecznie zrobiliśmy:

  • dołącz do dwóch serii w ciągu kilku godzin, grupując według kanałów;
  • uzupełnij serię według grup, jeśli nie było danych;
  • porównaj medianę z ostatnich 10 minut z poprzednimi danymi;
  • krzyczymy, jeśli coś znajdziemy;
  • zapisujemy wyliczone stawki i alerty, które wystąpiły w influxdb;
  • wyślij przydatną wiadomość do Slacka.

Moim zdaniem udało nam się osiągnąć wszystko, co chcieliśmy na końcu (a nawet trochę więcej dzięki niestandardowym modułom obsługi) tak pięknie, jak to możliwe.

Możesz zajrzeć na github.com przykład kodu и obwód minimalny (graphviz) wynikowy skrypt.

Przykład wynikowego 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')
    // заполняем значения нулями, если их не было
    |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)

A jaki jest wniosek?

Kapacitor świetnie radzi sobie z wykonywaniem alertów monitorujących z wieloma grupami, wykonywaniem dodatkowych obliczeń w oparciu o już zarejestrowane metryki, wykonywaniem niestandardowych akcji i uruchamianiem skryptów (udf).

Bariera wejścia nie jest zbyt wysoka - wypróbuj, jeśli grafana lub inne narzędzia nie do końca zaspokajają Twoje pragnienia.

Źródło: www.habr.com

Dodaj komentarz