prometheus編程實踐(二):應用實例

1. 應用需求與設計思路

  視頻巡檢在視頻監控的運維繫統中是一個非常重要的功能,運維繫統會定義定時的視頻巡檢任務。每一輪的巡檢會對運維繫統中維護的IPC設備進行碼流的調用,來判斷IPC設備的運行狀態,碼流延遲等指標,並記錄在ES中供統計分析。目前在運維繫統的ES中保存了每個設備每次巡檢的得分和執行時間,我們以這兩個指標作爲放入prometheus時間序列中的指標,時間戳爲設備本次巡檢的完成時間戳。
  因此,在prometheus中定義的指標示例如下:

#每個設備每次巡檢的得分(時間戳爲巡檢任務完成時間):
vas_task_device_score{gbid="32050000001326000701",groupid="2c94e38c6aa06653016aee72d4552831",instance="ioms",job="zhangkai",taskid="default0000000000000000000000000"}  72 @1565057355
#每個設備每次巡檢的消耗時間(時間戳爲巡檢任務完成時間):
vas_task_device_duration{gbid="32050000001326000701",groupid="2c94e38c6aa06653016aee72d4552831",instance="ioms",job="zhangkai",taskid="default0000000000000000000000000"}   7 @1565069700

  每次巡檢任務的結果由巡檢服務寫入到ES中,我們需要編寫一個exporter,定時將ES中新產生的巡檢結果取出來,格式化爲指標提供給prometheus,這裏我們將exporter對ES的採集週期設置爲1分鐘,同樣也將prometheus的scrape的週期設爲1分鐘。

2. 爲什麼沒有使用push gateway

  1節的應用場景需要自定義時間戳,而且需要保證數據的完整性,根據前面對push gateway的描述,不適合使用push gateway。

3. prometheus對自定義時間戳指標的採集特性

  (及時性)若自定義時間戳,prometheus採集時會比較自定義的時間戳與當前時間,如兩者之間差別太大(如大於1小時),指標不被採集,報“Error on ingesting samples that are too old or are too far into the future”錯誤。
  (時序性)若自定義時間戳,prometheus採集指標時會將採集的時間戳與待插入時間序列中的時間戳進行比較,若採集的時間戳小於待插入時間序列的當前時間戳(插入老數據), prometheus會報“Error on ingesting out-of-order samples”錯誤。

4. Prometheus服務端的配置

  在Prometheus服務端配置了採集器的地址,並配置了告警規則和告警發送地址,服務器端的配置數據如下所示:

scrape_configs:
- job_name: prometheus
  honor_timestamps: true
  scrape_interval: 15s
  scrape_timeout: 10s
  metrics_path: /metrics
  scheme: http
  static_configs:
  - targets:
    - localhost:9090
- job_name: zhangkai
  honor_labels: true
  honor_timestamps: true
  scrape_interval: 1m
  scrape_timeout: 10s
  metrics_path: /metrics
  scheme: http
  static_configs:
  - targets:
    - 172.16.64.159:8081
    labels:
      instance: ioms
  - targets:
    - 172.16.64.159:8082
    labels:
      instance: viid

5. 指標採集exporter的編寫

  整個採集類的代碼如下所示:

@Slf4j
@Component
public class VasTaskCollector {

    private static final long TIME_OFFSET = 1 * 60 * 60 * 1000; // prometheus採集指標的時間偏移量,

    @Autowired
    EsUtil esutil;

    private long toOffset = 0; // 當前輪次採集的最大偏移量

    private long fromOffset = 0; // 當前輪次採集的開始偏移量                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       

    private long validFromOffset = 0; // 根據當前時間算出的prometheus有效採集偏移量

    @Scheduled(fixedDelay = GlobalConsts.VAS_TASK_COLLECT_INTERVAL)
    private void genVasTaskMetrics() {

        CloseableIterator<TaskInfo> taskInfoIterator = null;

        long currTime = (new Date()).getTime();
        validFromOffset = (currTime - TIME_OFFSET) / 1000; // 秒爲單位
        if (validFromOffset > fromOffset) {
            fromOffset = validFromOffset;
        }
        toOffset = (long) esutil.getMaxByField("task", "taskresult", "scanEndTime"); // 單位爲秒
        log.info("####find current turn's max scanEndTime in ES is :" + toOffset);
        if (toOffset >= fromOffset) {

            taskInfoIterator = esutil.queryTaskInfoByOffsetRange(fromOffset, toOffset);
            log.info("####search task info ES from " + fromOffset + " to " + toOffset);
            genVasTaskMetrics(taskInfoIterator);
            fromOffset = toOffset; // 爲下次循環準備
        }

    }

    /**
     * 產生巡檢任務的指標
     * 
     * @param taskInfoList
     */
    private void genVasTaskMetrics(CloseableIterator<TaskInfo> taskIt) {

        int count = 0;
        while (taskIt.hasNext()) {
            TaskInfo taskInfo = taskIt.next();
            log.debug(taskInfo.toString());
            long timeStamp = taskInfo.getScanEndTime() * 1000;
            String metric;
            /* 每個設備每次巡檢的得分 */
            metric = String.format("vas_task_device_score{taskid=\"%s\",groupid=\"%s\",gbid=\"%s\"} %d %d",
                    taskInfo.getTaskID(), taskInfo.getGroupID(), taskInfo.getGbID(), 
                    taskInfo.getScore(), timeStamp);
            CollectorConfig.metricQueue.add(metric);
            log.debug(metric);
            /* 每個設備每次巡檢的延時,單位爲秒 */
            metric = String.format("vas_task_device_duration{taskid=\"%s\",groupid=\"%s\",gbid=\"%s\"} %d %d",
                    taskInfo.getTaskID(), taskInfo.getGroupID(), taskInfo.getGbID(),
                    taskInfo.getScanEndTime() - taskInfo.getScanTime(), timeStamp);
            CollectorConfig.metricQueue.add(metric);
            log.debug(metric);
            count++;
        }
        taskIt.close();
        log.info("####add metrics of taskinfo count: " + count);
    }

}

  提供採集端點的controller代碼如下:

@Slf4j
@RestController
public class MetricController {
    
    /** 
     * 向prometheus提供指標數據
     * 
     * @param response
     * @throws IOException
     */
    @RequestMapping(value = "/metrics", method = RequestMethod.GET)
    private void pullMetrics(HttpServletResponse response) throws IOException {

        StringBuffer sb = new StringBuffer();
        Queue<String> queue = CollectorConfig.metricQueue;
        String metric = null;
        int count = 0;
        // 每次最多隻取QUEUE_MAX_SIZE數目的指標
        while ((queue.size() > 0) && (count < GlobalConsts.PULL_MAX_SIZE)) {
            if (null != metric) {
                sb.append(metric).append("\n");
            }
            metric = queue.poll();
            count++;
        }
        // 最後一條不加換行符
        if (null != metric) {
            sb.append(metric);
        }
        log.debug(sb.toString());
        log.info("****prometheus scrape metrics count:"+count);
        response.getWriter().print(sb.toString());
    }

    /**
     * 告警接收地址
     * 
     * @param alarms
     */
    @RequestMapping(value = "/api/v1/alerts", method = RequestMethod.POST)
    private void handleAlarms(@RequestBody String alarms) {
        if (null != alarms) {
            log.info("received alarms are:" + alarms);
            JSONArray alarmArray = JSON.parseArray(alarms);
            alarmArray.forEach(alarmObject -> {
                handleAlarmObject((JSONObject) alarmObject);
            });
        }
    }

    // 處理接收的每條告警
    private void handleAlarmObject(JSONObject alarmObj) {

        String alertName = alarmObj.getJSONObject("labels").getString("alertname");
        int alertValue = alarmObj.getJSONObject("labels").getIntValue("value");
        
        String alertTimeStr = alarmObj.getString("startsAt"); //時間格式"2019-07-19T06:42:08.639138396Z"
        alertTimeStr = alertTimeStr.substring(0, alertTimeStr.length()-7);
        alertTimeStr+= "+0000";
        
        long alertTime = -1;
        try {
            SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
            alertTime = df.parse(alertTimeStr).getTime();
        } catch (ParseException pe) {
            log.error("convert " + alertTimeStr + " error!");
        }

        log.info("告警名:" + alertName + "告警值:" + alertValue + "告警時間:" + alertTime);
        // todo: 這裏添加處理每條告警的代碼
    }
}

6. 可視化展示

  在grafana中創建儀表盤,設置好查詢表達式,並設置變量用於選取任務ID,實時監控結果如下:


7. 總結

  1. 對於業務應用需要保證數據完整性的場景下,需要在採集器中設置緩存避免指標值的丟失。採集週期應和指標生成周期保持一致。時間戳要用應用生成的時間戳。
  2. Prometheus適合於實時監控場景,特別是對實時時間滑窗(實時流)的處理非常方便。
  3. 可以使用Prometheus的HTTP API做自己的應用系統,需要注意的是“/api/v1/query_range”根據step查詢出來的時間序列中時間戳是Prometheus根據查詢時間計算出的時間戳,而非時間序列數據庫中的原始採集時間戳。
  4. 由於prometheus的數據模型中並沒有定義數據類型,因此標籤值都視爲文本類型,在數據查詢中非常依賴於基於標籤值的過濾,原來在關係模式中基於某些字段的比較與運算在這裏就用不上了,需要使用正則表達式來變通實現。如某個指標的標籤中有“month”這個標籤,以前在關係模式中可以使用“month>1 and month<5”這樣的查詢條件來進行過濾,在prometheus的查詢表達式則需要使用“[2-4]”來匹配。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章