生产环境案例

场景分析

我们需要对三个集群进行监控,分别是

  • 开发-测试集群 : blackbox-dev
  • 预发布集群 : blackbox-pre
  • 生产集群 : blackbox-pro

/images/blackexporter/Untitled.png

Blackbox Exporter 配置

首先需要使用 BlackBox Exporter 实现一个探测 Module ,这个 Module 可以包含 HTTP、HTTPS、SSH 等方案,我们对目标进行监控的时候,可以直接使用 Prometheus 传入 Targets 以调用 Module

运行 Blackbox Exporter 时,需要用户提供探针的配置信息,这些配置信息可能是一些自定义的 HTTP 头信息,也可能是探测时需要的一些 TSL 配置,也可能是探针本身的验证行为,在 Blackbox Exporter 每一个探针配置称为一个 module,并且以 YAML 配置文件的形式提供给 Blackbox Exporter

如果我们想调用集群中的某个服务,必须先经过一个 API 网关,这个 API 网关兼备了访问控制的功能,每个用户需要在 Request 请求头中添加一个 Bearer Token —— 这取决于网关的具体配置,我们的探针也不例外,下面是 BlackBox Exporter 的配置示例

blackbox.yml

modules:
  http_2xx:
    prober: http
    timeout: 5s
    http:
      preferred_ip_protocol: ip4
      headers:
        Host: "probe.xxx.com"
        Cache-Control: no-cache
      bearer_token: '<token>'
  tcp_connect:
    prober: tcp
    timeout: 5s
    tcp:
      preferred_ip_protocol: ip4
  icmp:
    prober: icmp
    timeout: 5s
    icmp:
      preferred_ip_protocol: ip4
  http_2xx_home:
    prober: http
    timeout: 5s
    http:
      method: GET
      fail_if_body_not_matches_regexp:
      - "OK"
      fail_if_not_ssl: false
      preferred_ip_protocol: ip4

将服务启动后,我们访问 IP:9115 就能看见 WebUI 了

index

index

Prometheus

Prometheus 作为 Grafana 的数据源,也作为我们的信息处理与报警中心,主要的配置都在这里,因为我们只是对目前现有的方案做一个 BlackBox Exporter 配置的插入,所以只需要列出来关键配置就行,下面是 targets-dev.yml,即测试服的 API 地址的配置示例,我们一共需要配置三个文件,文件除了不同环境的 API 地址,全部都相同

- targets:
  - URL1
  - URL2

targets 的目录结构

config/targets/
├── dev-targets.yml
├── pre-targets.yml
└── pro-targets.yml

配置完成探测目标之后,需要配置 Prometheus 本身了,我们调用 BlackBox Exporter 的方法其实是以 GET 的方式实现的,就像这样

http://<IP>:9115/probe?target=<URL>&module=http_2xx

因此我们的配置就变成了下面这样

global:
  scrape_interval:     15s 
  evaluation_interval: 15s

alerting:
  alertmanagers:
  - static_configs:
    - targets:
      - alertmanager:9093

rule_files:
  - /etc/prometheus/rules/*.rules

scrape_configs:
  - job_name: "blackbox-dev"
    metrics_path: /probe
    scheme: http
    params:
      module: [http_2xx]
    file_sd_configs:
      - files:
        - targets/dev-targets.yml
    relabel_configs:
      - source_labels: [__address__]
        target_label: __param_target
      - source_labels: [__param_target]
        target_label: instance
      - target_label: __address__
        replacement: blackbox_exporter:9115
  - job_name: "blackbox-pre"
    metrics_path: /probe
    scheme: http
    params:
      module: [http_2xx]
    file_sd_configs:
      - files:
        - targets/pre-targets.yml
    relabel_configs:
      - source_labels: [__address__]
        target_label: __param_target
      - source_labels: [__param_target]
        target_label: instance
      - target_label: __address__
        replacement: blackbox_exporter:9115
  - job_name: "blackbox-pro"
    metrics_path: /probe
    scheme: http
    params:
      module: [http_2xx]
    file_sd_configs:
      - files:
        - targets/pro-targets.yml
    relabel_configs:
      - source_labels: [__address__]
        target_label: __param_target
      - source_labels: [__param_target]
        target_label: instance
      - target_label: __address__
        replacement: blackbox_exporter:9115

 ...

至此我们已经能实现对目标的探测和数据的搜集存储

Grafana 可视化

这个 Dashboard 的的示意图

/images/blackexporter/Untitled%202.png

AlertManger

我们在 AlertManager 这边直接将 Webhook URL 指向我们的转发器 dd2wx,因为 AlertManager 并不支持发送到企业微信群机器人,干脆自己实现一遍转发

global:
  resolve_timeout: 5m

route:
  group_by: ['alertname']
  group_wait: 10s
  group_interval: 10s
  repeat_interval: 1h
  receiver: 'web.hook'
receivers:
  - name: 'web.hook'
    webhook_configs:
    - url: <dd2wxURL>
      send_resolved: true
inhibit_rules:
  - source_match:
      severity: 'critical'
    target_match:
      severity: 'warning'
    equal: ['alertname', 'dev', 'instance']

接下来是警报规则文件

groups:
- name: service-availability
  rules:
  - alert: dev-service-unavailability
    expr: avg_over_time(probe_success{job="blackbox-dev"}[3m]) * 100 < 95
    for: 5s
    annotations:
      summary: 测试服出现了服务不可用情况
  - alert: pre-service-unavailability
    expr: avg_over_time(probe_success{job="blackbox-pre"}[3m]) * 100 < 95
    for: 5s
    annotations:
      summary: 预发布出现了服务不可用情况
  - alert: pro-service-unavailability
    expr: avg_over_time(probe_success{job="blackbox-pro"}[3m]) * 100 < 95
    for: 5s
    annotations:
      summary: 生产环境出现了服务不可用情况

这个文件需要放在 Prometheus 的 rules 目录下面,文件内容为:当出现可用性小于 95% 的情况达 5s 的时候触发警报

Prometheus 完整的配置结构

$ tree prometheus/config/ -a

prometheus/config/
├── prometheus.yml
├── prometheus.yml.bak
├── rules
│   └── blackbox.rules
└── targets
    ├── dev-targets.yml
    ├── pre-targets.yml
    └── pro-targets.yml

2 directories, 6 files

DD2WX

这个是我自己写的一个转发器,将发过来的警报 Json 转化成 md 说明发送给企业微信机器人

模版 : alert_temp.tmpl

------------------------------
警报来了,共{{ .Count }}个实例
------------------------------
{{ range .AllInstance }}
    状态: {{ .Status }}
    告警名称: {{ .LabelsName }}
    服务名称: {{ .Name }}
    告警实例: {{ .Instance }}
    告警时间: {{ .StartsAt }}
    解决时间: {{ .EndsAt }}
    说明: {{ .Description }}
{{ end }}
------------------------------

Json Models : Json.go

package main

import "time"

type promeAlerts struct {
	Receiver string `json:"receiver"`
	Status   string `json:"status"`
	Alerts   []struct {
		Status string `json:"status"`
		Labels struct {
			Alertname           string `json:"alertname"`
			Env                 string `json:"env"`
			Instance            string `json:"instance"`
			Job                 string `json:"job"`
			KubernetesName      string `json:"kubernetes_name"`
			KubernetesNamespace string `json:"kubernetes_namespace"`
			KubernetesNode      string `json:"kubernetes_node"`
			Name                string `json:"name"`
		} `json:"labels"`
		Annotations struct {
			Summary string `json:"summary"`
		} `json:"annotations"`
		StartsAt     time.Time `json:"startsAt"`
		EndsAt       time.Time `json:"endsAt"`
		GeneratorURL string    `json:"generatorURL"`
		Fingerprint  string    `json:"fingerprint"`
	} `json:"alerts"`
	GroupLabels struct {
		Alertname string `json:"alertname"`
	} `json:"groupLabels"`
	CommonLabels struct {
		Alertname string `json:"alertname"`
	} `json:"commonLabels"`
	CommonAnnotations struct {
		Summary string `json:"summary"`
	} `json:"commonAnnotations"`
	ExternalURL string `json:"externalURL"`
	Version     string `json:"version"`
	GroupKey    string `json:"groupKey"`
}

type wxAlert struct {
	Msgtype string `json:"msgtype"`
	Text    struct {
		Content string `json:"content"`
	} `json:"text"`
}

main.go

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
	"html/template"
	"log"
	"net/http"
	"os"
	"time"
)

var wxWebhook string

func init() {
	wxWebhook = os.Getenv("WECHAT_BOOT_URL")
}

type Message struct {
	Count       int
	AllInstance []noTep
}

type noTep struct {
	LabelsName,
	Name,
	Instance,
	StartsAt,
	EndsAt,
	Status,
	Description string
}

func main() {
	if err := startServe(); err != nil {
		log.Fatal(err)
	}
}

func startServe() error {
	e := echo.New()
	e.HideBanner = true

	e.Use(middleware.BodyDump(func(c echo.Context, reqBody, resBody []byte) {
		log.Println(string(reqBody))
	}))

	e.POST("/connect", recv)
	e.GET("/connect", recv)

	return e.Start(":8080")
}

func recv(c echo.Context) error {
	u := &promeAlerts{}
	err := c.Bind(u)
	if err != nil {
		log.Println(err)
		return c.String(http.StatusOK, err.Error())
	}

	var msg wxAlert
	err = fillAlert(*u, &msg)
	if err != nil {
		return c.String(http.StatusOK, err.Error())
	}

	fmt.Println(send2Wx(msg).Error())
	return c.String(http.StatusOK, "")
}

func send2Wx(msg wxAlert) error {
	sendSms, err := json.Marshal(msg)
	if err != nil {
		return err
	}
	resp, err := http.Post(wxWebhook, "application/json; charset=utf-8", bytes.NewBuffer(sendSms))
	if err != nil {
		return err
	}

	return fmt.Errorf("%d", resp.StatusCode)
}

func fillAlert(data promeAlerts, msg *wxAlert) error {
	t := template.Must(template.ParseFiles("./alert_temp.tmpl"))
	var doc bytes.Buffer
	noMsg := make([]noTep, 0)

	for _, v := range data.Alerts {
		noMsg = append(noMsg, noTep{
			LabelsName:  v.Labels.Alertname,
			Name:        v.Labels.Name,
			Instance:    v.Labels.Instance,
			Status:      v.Status,
			StartsAt:    FormatTime(v.StartsAt),
			EndsAt:      FormatTime(v.EndsAt),
			Description: v.Annotations.Summary,
		})
	}

	err := t.Execute(&doc, Message{Count: len(noMsg), AllInstance: noMsg})
	if err != nil {
		return err
	}

	msg.Msgtype = "text"
	msg.Text.Content = doc.String()

	return nil
}

func FormatTime(t time.Time) string {
	return t.Format("2006-01-02 15:04:05")
}

参考

全部完成之后的效果

/images/blackexporter/Untitled%203.png