SRE Google运维解密 —— 基于时间序列数据进行有效报警

SRE Google运维解密 —— 基于时间序列数据进行有效报警

四月 07, 2022

前言:入职小米也快一年了,忙碌的一年,消耗的一年,想想自己能写些什么,好像获得的都是软技能。遂又把刚入职时给团队分享的内容搬了来。。。

1
2
3
4
5
SRE和传统的IT运维有很大区別, SRE真正实现了DevOps :
首先,SRE 深度参与开发阶段的工作,对应用程序的设计实现方式、依赖库、运行时的资源消耗都有严格的规约;
其次, SRE 工程师本身也要做不少编程工作,来实现各种工具用以自动解决问题和故障, 换句话说SRE强调的是对问题和故障的自动处理,而非人工干预;再者,按照 SRE 的约定,开发人员自行负责程序上线部署更新,毕竞开发人员对自己开发的程序更熟悉,易于处理程序上线过程中遇到的问题;
总之,作为 Google 的DevOps 实践, SRE 非常注重开发和运维职能的结合,极大地加快业务应用迭代周期,提升了IT对业务的支撑能力;
随着 DevOps 在国内的宣传推广,国内的很多企业客户也逐渐接受了DevOps 的理念, 但是在具体落地实践 DevOps 的过程中缺乏实际案例作为参照。本书的推出,方便了国内广大IT人员在落地 DevOps 过程中参照 Google 的 SRE 实践。

《SRE Google运维解密》基于时间序列数据进行有效报警
Google监控的发展

引用原文:

我们可能会想现在的监控都是基于时间序列的,但其实谷歌十年前的时候就在使用了。Borg出现于2003年,是Google的调度服务,而Borgmon是Google的监控服务,是对Borg的补充。在Google之外当前使用的主流监控工具prometheus与Google的Borgmon十分相似。

什么是时间序列数据
时间序列(time series)数据是一系列有序的数据,通常是等时间间隔的采样数据。时间序列存储最简单的定义就是数据格式里包含timestamp字段的数据。时间序列数据在查询时,对于时间序列总是会带上一个时间范围去过滤数据,查询的结果里也总是会包含timestamp字段。

监控的工作流图

应用软件的监控埋点
所谓“埋点”,是数据采集领域(尤其是用户行为数据采集领域)的术语,指的是针对特定用户行为或事件进行捕获、处理和发送的相关技术及其实施过程。比如用户某个icon点击次数、观看某个视频的时长等等。埋点的技术实质是先监听软件应用运行过程中的事件,当需要关注的事件发生时进行判断和捕获。

关于埋点的名词概念解释
CollectorRegistry 管理所有的Collector, 通过Register方法把需要的Collector注册进去
Collector 一堆metrics的集合(为了模块化编程)
Metric 具体的监控度量单位(例如counter, gauge)
counter 累计值
gauge 当前值

怎么埋点(写一个代码埋点小例子 – QPS )
(说明代码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
package main

import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"net/http"
"time"
)

// client模块实现了 collector
// 负责和客户端交互的模块的监控数据
type ClientCollector struct {
requestCount prometheus.Counter
processTime prometheus.Gauge
requestCounts *prometheus.CounterVec
}

//prometheus用go定义了collector接口
//实现collector方法
func (c *ClientCollector) Describe(descs chan<- *prometheus.Desc) {
c.requestCount.Describe(descs)
c.processTime.Describe(descs)
c.requestCounts.Describe(descs)
}

func (c *ClientCollector) Collect(metrics chan<- prometheus.Metric) {
c.requestCount.Collect(metrics)
c.processTime.Collect(metrics)
c.requestCounts.Collect(metrics)
}

// 负责收集和db交互的模块的监控数据
type DBCollector struct {
SaveCount prometheus.Counter
}

func (c *DBCollector) Describe(descs chan<- *prometheus.Desc) {
c.SaveCount.Describe(descs)
}

func (c *DBCollector) Collect(metrics chan<- prometheus.Metric) {
c.SaveCount.Collect(metrics)
}


// 假设这个程序, 一方面要处理客户端的请求, 另一方面要和数据库交互
func main() {

// 负责前端模块的指标收集
// 定义初始化变量
clientCollector := &ClientCollector{
requestCount: prometheus.NewCounter(prometheus.CounterOpts{
Name: "client_request_count",
}),
processTime: prometheus.NewGauge(prometheus.GaugeOpts{
Name: "client_process_time",
}),

//初始化带标签的变量
requestCounts: prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "http_requests",
}, []string{"host"}),
}

// 负责数据库模块的指标收集
dbCollector := &DBCollector{
SaveCount: prometheus.NewCounter(prometheus.CounterOpts{
Name: "db_save_count",
}),
}

// 把一个具体的collector放在collectorRegistry里面
prometheus.Register(clientCollector)
prometheus.Register(dbCollector)

//起一个线程让业务层调用
go func() {
for {
clientCollector.requestCount.Inc()
clientCollector.processTime.Set(float64(time.Now().UnixNano()))

clientCollector.requestCounts.WithLabelValues("host1").Inc()
clientCollector.requestCounts.WithLabelValues("host2").Inc()

dbCollector.SaveCount.Inc()
time.Sleep(time.Second)
}
}()

// prometheus golang版本的sdk
// 默认提供go程序的一些监控值, 比如 gc, goroutine数量, 内存等等
//启动一个http接口
http.Handle("/metrics", promhttp.Handler())
http.ListenAndServe(":8000", nil)
}

QPS本质就是收集counter,通过计算公式 △v/△t 例如统计精度为5分钟的QPS,v(t) - v(t-5m) / 5m
设定的计算精度如果小于收集周期,数据可能会出现0值

监控指标的收集
四个黄金指标
引用原文:

收集有push 和 pull 两种方式,按周期定时抓取或者周期性的推送,同一周期的多个数据,会被最新的覆盖掉。
pull方式收集,以go举一个小例子提供http接口进行收集

1
2
http.Handle("/metrics", promhttp.Handler())
http.ListenAndServe(":8000", nil)

时间序列是怎么存储的

场景:
小赵写了一个python脚本定时去拉取一个服务器的各种指标,然后发送至监控服务器。脚本实现方法是每拉取一个指标就上传给监控服务器,这时小赵发现了一个问题,监控数据永远都只有一个值,而且是不定的值。小赵开始思考原因,发现这些不同的指标需要以一个job在一个发送中上传。小赵阅读了此书发现这是由监控服务器的存储机制决定的。

引用原文:

在time-series数据库中,标识一个time-series的标签必须同时有以下几个标签:

举一个go的小例子说明定义标签

1
2
3
4
5
6
requestCounts: prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "http_requests",
}, []string{"host"}),

clientCollector.requestCounts.WithLabelValues("host1").Inc()
clientCollector.requestCounts.WithLabelValues("host2").Inc()

举一个prometheus标签的例子

1
neo_nginx_ingress_controller_requests_qps{cluster="c3-pre-neo-cluster01",controller_class="b2cback",controller_deployment="nginx-ic-b2cback",controller_namespace="neo-ingress-controllers",controller_pdl="b2cback",domain="a-d.pre.mi.com",ingress="a-d-pre-com-com",job="ingress-controllers",namespace="b2cback",status="200",thanos="rule",tree_id="1124",tree_path="b2c.b2cback.a-d-mi-com.a-d-mi-com.cn-pre"}

报警
传统的监控,通过在服务器上运行脚本,存储返回值进行图形化展示,并检查返回值判断是否报警。Google内部使用Borgmon做为监控报警平台。在Google之外,我们可以使用Prometheus作为基于时间序列数据监控报警的工具,进而实践SRE提供的白盒监控理念。

设置报警的几个原则

黑盒监控
Google的SRE大量依赖于白盒监控
引用原文:

白盒监控

主要关注的是原因,也就是系统内部暴露的一些指标,例如 redis 的 info 中显示 redis slave down,这个就是 redis info 显示的一个内部的指标,重点在于原因,可能是在黑盒监控中看到 redis down,而查看内部信息的时候,显示 redis port is refused connection。

主要关注的现象,一般都是正在发生的东西,例如出现一个告警,业务接口不正常,那么这种监控就是站在用户的角度能看到的监控,重点在于能对正在发生的故障进行告警。

Prometheus Blackbox_exporter黑盒监测
Blackbox Exporter 是 Prometheus 社区提供的 官方黑盒监控解决方案,允许用户通过http\HTTPS\DNS\TCP\ICMP的方式对网络进行探测
https://github.com/prometheus/blackbox_exporter

HTTP 测试
定义 Request Header 信息
判断 Http status / Http Respones Header / Http Body 内容

TCP 测试
业务组件端口状态监听
应用层协议定义与监听

ICMP 测试
主机探活机制

POST 测试
接口联通性
SSL 证书过期时间