טריקים לעיבוד מדדים בקפאסיטור

סביר להניח שהיום אף אחד לא שואל מדוע יש צורך לאסוף מדדי שירות. השלב ההגיוני הבא הוא להגדיר התראה למדדים שנאספו, שתודיע על כל חריגה בנתונים בערוצים הנוחים לכם (מייל, סלאק, טלגרם). בשירות הזמנת המלון המקוון Ostrovok.ru כל המדדים של השירותים שלנו מוזגים לתוך InfluxDB ומוצגים ב-Grafana, וגם התראה בסיסית מוגדרת שם. עבור משימות כמו "אתה צריך לחשב משהו ולהשוות איתו", אנו משתמשים בקיפאסיטור.

טריקים לעיבוד מדדים בקפאסיטור
Capacitor הוא חלק מחסנית 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 בהודעה

תבניות מתאימות לפורמט מהחבילה text.templateלהלן כמה חידות שנתקלות בהן לעתים קרובות.

אחרת

אנחנו עושים סדר בדברים ולא מפעילים אנשים עם טקסט שוב:

|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 לשליחת הודעות לרעה.
בהתחלה רצינו לשלוח בהודעה תמונת גראפנה מוגנת בהרשאה. לאחר מכן, כתוב OK בשרשור להתראה הקודמת מאותה קבוצה, ולא כהודעה נפרדת. קצת אחר כך - הוסיפו להודעה את הטעות הנפוצה ביותר ב-X הדקות האחרונות.

נושא נפרד הוא תקשורת עם שירותים אחרים וכל פעולות שיוזמו על ידי התראה (רק אם הניטור שלך עובד מספיק טוב).
דוגמה לתיאור מטפל, שבו 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): kapacitor -url מארח או ip:9092 יומנים lvl=שגיאה

אפשרות עם httpOut

מציג נתונים בצינור הנוכחי:

|httpOut('something')

צפו (קבלו): מארח או ip:9092/kapacitor/v1/tasks/task_name/something

תכנית ביצוע

  • כל משימה מחזירה עץ ביצוע עם מספרים שימושיים בפורמט graphviz.
  • קח בלוק נקודה.
  • הדבק אותו בצופה, תהנה.

איפה עוד אפשר להשיג מגרפה?

חותמת זמן ב-influxdb על כתיבה חוזרת

לדוגמה, הגדרנו התראה לסכום הבקשות לשעה (groupBy(1h)) ורוצים לתעד את ההתראה שהתרחשה ב-influxdb (כדי להראות יפה את עובדת הבעיה על הגרף בגראפנה).

influxDBOut() יכתוב את ערך הזמן מההתראה לחותמת הזמן; בהתאם, הנקודה בתרשים תיכתב מוקדם/מאוחר מההתראה שהגיעה.

כאשר נדרש דיוק: אנו עוקפים את הבעיה על ידי קריאה למטפל מותאם אישית, שיכתוב נתונים ל-influxdb עם חותמת הזמן הנוכחית.

docker, בנייה ופריסה

בעת ההפעלה, Capacitor יכול לטעון משימות, תבניות ומטפלים מהספרייה שצוינה בתצורה בבלוק [load].

כדי ליצור משימה נכונה, אתה צריך את הדברים הבאים:

  1. שם הקובץ - מורחב למזהה/שם סקריפט
  2. סוג – זרם/אצווה
  3. dbrp - מילת מפתח לציון באיזה מסד נתונים + מדיניות הסקריפט פועל (dbrp "ספק." "autogen")

אם משימת אצווה כלשהי אינה מכילה שורה עם dbrp, השירות כולו יסרב להתחיל ויכתוב עליה ביושר ביומן.

בכרונוגרף, להיפך, השורה הזו לא אמורה להתקיים; היא לא מתקבלת דרך הממשק ומחוללת שגיאה.

פריצה בעת בניית קונטיינר: Dockerfile יוצא עם -1 אם יש שורות עם //.+dbrp, מה שיאפשר לך להבין מיד את הסיבה לכשל בעת הרכבת ה-build.

להצטרף אחד לרבים

משימה לדוגמה: עליך לקחת את האחוזון ה-95 מזמן ההפעלה של השירות למשך שבוע, להשוות כל דקה מתוך 10 האחרונות לערך זה.

אתה לא יכול לעשות הצטרפות של אחד לרבים, אחרון/ממוצע/חציון מעל קבוצת נקודות הופך את הצומת לזרם, השגיאה "לא ניתן להוסיף קצוות לא תואמים בילד: אצווה -> זרם" תוחזר.

גם התוצאה של אצווה, כמשתנה בביטוי למבדה, אינה מוחלפת.

ישנה אפשרות לשמור את המספרים הדרושים מהאצווה הראשונה לקובץ דרך udf ולטעון את הקובץ הזה באמצעות sideload.

מה פתרנו עם זה?

יש לנו כ-100 ספקי מלונות, לכל אחד מהם יכולים להיות כמה קשרים, נקרא לזה ערוץ. ישנם כ-300 ערוצים אלה, כל אחד מהערוצים עלול ליפול. מבין כל המדדים שנרשמו, נעקוב אחר שיעור השגיאות (בקשות ושגיאות).

למה לא גראפנה?

להתראות שגיאה המוגדרות ב-Grafana יש מספר חסרונות. חלקם קריטיים, חלקם אפשר לעצום עיניים, תלוי במצב.

גרפאנה לא יודע לחשב בין מדידות + התראה, אבל צריך תעריף (בקשות-טעויות)/בקשות.

השגיאות נראות מגעילות:

טריקים לעיבוד מדדים בקפאסיטור

ופחות רע כשמסתכלים עליהם בבקשות מוצלחות:

טריקים לעיבוד מדדים בקפאסיטור

אוקיי, אנחנו יכולים לחשב מראש את התעריף בשירות לפני גראפנה, ובמקרים מסוימים זה יעבוד. אבל לא אצלנו, כי... עבור כל ערוץ היחס שלו נחשב "נורמלי", וההתראות פועלות לפי ערכים סטטיים (אנחנו מחפשים אותן בעיניים, משנים אותן אם יש התראות תכופות).

אלו הן דוגמאות של "רגיל" עבור ערוצים שונים:

טריקים לעיבוד מדדים בקפאסיטור

טריקים לעיבוד מדדים בקפאסיטור

אנו מתעלמים מהנקודה הקודמת ומניחים שהתמונה ה"רגילה" דומה עבור כל הספקים. עכשיו הכל בסדר, ואנחנו יכולים להסתדר עם התראות בגראפנה?
אנחנו יכולים, אבל אנחנו ממש לא רוצים, כי אנחנו צריכים לבחור אחת מהאפשרויות:
א) ליצור הרבה גרפים עבור כל ערוץ בנפרד (וללוות אותם בכאב)
ב) השאר תרשים אחד עם כל הערוצים (ותלך לאיבוד בקווים הצבעוניים ובהתראות המותאמות אישית)

טריקים לעיבוד מדדים בקפאסיטור

איך עשית את זה?

שוב, יש דוגמה התחלה טובה בתיעוד (חישוב תעריפים על פני סדרות מצטרפות), ניתן להציץ או לקחת כבסיס בבעיות דומות.

מה עשינו בסוף:

  • להצטרף לשתי סדרות תוך מספר שעות, מקבץ לפי ערוצים;
  • מלא את הסדרה לפי קבוצה אם לא היו נתונים;
  • השוו את החציון של 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)

ומה המסקנה?

Capacitor מעולה בביצוע התראות-ניטור עם חבורה של קבוצות, ביצוע חישובים נוספים על סמך מדדים שכבר נרשמו, ביצוע פעולות מותאמות אישית והרצת סקריפטים (udf).

מחסום הכניסה אינו גבוה במיוחד - נסה זאת אם גרפנה או כלים אחרים אינם מספקים את רצונותיך במלואם.

מקור: www.habr.com

קנה אירוח אמין לאתרים עם הגנת DDoS, שרתי VPS VDS 🔥 קנה אחסון אתרים אמין עם הגנת DDoS, שרתי VPS VDS | ProHoster