大家好,我是蓝胖子,关于性能分析的视频和文章我也大大小小出了有一二十篇了,算是已经有了一个系列,之前的代码已经上传到github.com/HobbyBear/performance-analyze,接下来这段时间我将在之前内容的基础上,结合自己在公司生产上构建监控系统的经验,详细的展示如何对线上服务进行监控,内容涉及到的指标设计,软件配置,监控方案等等你都可以拿来直接复刻到你的项目里,这是一套非常适合中小企业的监控体系。
前面我们完成了日志监控系统的搭建,这一节将会介绍在开发中比较紧密的应用服务监控的内容了。
项目代码已经上传到github
github.com/HobbyBear/easymonitor
首先要搞懂对于应用服务的监控,我们需要监控哪些东西。
第一个就是应用现在提供服务的质量,通过在【升职加薪秘籍】我在服务监控方面的实践(1)-监控蓝图 里那一节里介绍的四大黄金指标可以很好反应这一点,分别是延迟,流量,错误数,饱和度,这四个维度可以映射为应用程序的接口处理时长,qps,接口报错数,饱和读可以从应用服务的当前协程,线程数,cpu,内存等指标反映出来。
第二个是应用的内部运行状态的指标,应用服务质量可以看成是从应用外部观察应用的运行状态。内部运行状态的指标,像垃圾回收,协程调度延迟,这些指标则是从应用自身运行时框架反应出来的,这些指标在分析应用服务产生的问题时会很有帮助。这些指标也一般会由程序语言提供,或者语言周边的工具也会有提供,像golang程序,是语言自带的有这些指标暴露出来。
第三个方面是应用保留现场的能力,这个方面其实不属于建立监控指标了,而是程序在出问题时,能够主动的对问题现场留下分析证据。拿golang程序来说,通过go tool pprof的相关命令能够分析出应用程序此时使用cpu或者内存,协程等资源的情况,并且关联到具体的代码,而程序出问题时,如果能够自动执行这样的命令就能达到保留问题现场的目的。
下面,我们挨个看下对于这3个方面,我们应该怎么做。
前面我们提到过四大黄金指标反应到应用程序上就是,接口处理时长,qps,接口报错数,饱和读可以从应用服务的当前协程,线程数,cpu,内存等指标反映出来。
现在我们来看看如何如何在程序代码里融入它们。
我们需要通过prometheus来暴露这些指标,在golang里,有prometheus 客户端库,我们引入它。
github.com/prometheus/client_golang/prometheus
关于prometheus 指标的几种类型,分别有counter,guage,histogram,summary,对于histogram不好理解的同学可以看看我之前的两篇文章prometheus描点原理和prometheus Histogram 统计原理 ,基本上懂了histogram,其余指标类型就都容易理解了。
注意,因为这一节内容涉及到promql语句的书写,所以对prometheus描点不熟悉的同学最好还是看完prometheus描点原来再来学习本节的内容
对于接口时长的统计,一般会采用histogram指标类型统计接口的耗时分位数,比如p99分位数,代表在100个请求里,按接口耗时从小到大排列,第99个接口耗时多大。
histogram类型的指标代码定义如下,type 代表接口的类型,除了http,还可以表示grpc等等,method值的get,post等请求方式,status值请求的响应码,api是接口的路由,
var serverHandleHistogram = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Name: "server_handle_seconds",
}, []string{"type", "method", "status", "api"})
在这个指标里并没有标签表面指标来自哪个服务的,是因为在prometheus配置文件里我们已经指明了服务名和机器节点,配置如下:
global:
scrape_interval: 15s # 默认抓取周期
scrape_configs:
- job_name: 'webapp'
scrape_interval: 5s
metrics_path: /metrics
static_configs:
- targets: [ 'mynode:8090' ]
relabel_configs:
- source_labels: [ __address__ ]
target_label: instance
regex: (.*):\d+
replacement: $1
这样会在采集指标时,默认加上这两个维度。
要开启这个指标需要往指标的注册中心进程注册,代码如下
var (
MetricsReg = prometheus.NewRegistry()
)
MetricsReg.MustRegister(serverHandleHistogram)
接着启动一个http endpoint对外暴露指标信息,代码如下
http.Handle("/metrics", promhttp.HandlerFor(MetricsReg, promhttp.HandlerOpts{Registry: MetricsReg}))
go func() {
http.ListenAndServe(":8090", http.DefaultServeMux)
}()
指标记录的时机可以放在http路由中间件里面去做,这样在请求处理逻辑前后,我们正好可以进行相关的埋点操作。代码如下,我们在next.ServeHTTP是实际的业务处理逻辑,处理完以后,通过RecordServerHandlerSeconds对请求的时长进行统计,并且还对不是200的请求做了日志记录。
func MetricMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rw := &responseWriter{ResponseWriter: w}
// 在处理请求之前执行的逻辑
// 可以在这里进行请求验证、日志记录等操作
reqBody, err := httputil.DumpRequest(r, true)
if err != nil {
log.Errorf("dump request fail err=%s", err)
}
now := time.Now()
// 调用下一个处理程序
next.ServeHTTP(rw, r)
RecordServerHandlerSeconds(TypeHTTP, r.Method, rw.Status(), r.URL.Path, time.Now().Sub(now).Seconds())
if rw.Status() != http.StatusOK {
log.Warnf("request fail request=%s code=%d", string(reqBody), rw.statusCode)
}
})
}
注意,由于请求量可能比较多,不可能对所有请求参数以及响应结果都进行记录,所以我们可以选择性的按某些条件去筛选请求进行日志记录,比如这里我将接口失败的请求记录下来,方便往后的问题排查。
当需要看某段时间内接口的耗时情况时,我们可以这样写promql语句:
histogram_quantile(0.9,sum(irate(server_handle_seconds_bucket{}[5m])) by (le,host)) > 0
上面的promql就代表按主机维度看每台主机接口的p90分位数接口耗时是多少。你也可以按接口路由或者其他维度对耗时进行聚合统计,
注意下,我们的histogram_quantile表达式的值都限制了 > 0 ,这是因为histogram_quantile在计算分位数时,如果发现桶的元素少于两个就会返回NAN,所以需要去掉这种干扰元素。
接着我们看下qps的指标如何埋点,记录qps的指标可以采用prometheus的counter类型对接口请求数进行累加,利用irate或者rate函数对其进行计算斜率得到qps。
定义counter类型的指标代码如下:
serverHandleCounter = prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "server_handle_total",
}, []string{"type", "method", "api"})
和接口耗时统计类似,我们同样是将qps的埋点放在中间件里。
func RecordServerCount(metricType string, method string, api string) {
serverHandleCounter.With(prometheus.Labels{
"type": metricType,
"method": method,
"api": api,
}).Inc()
}
func MetricMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
....
// 对qps进行埋点统计
RecordServerCount(TypeHTTP, r.Method, r.URL.Path)
next.ServeHTTP(rw, r)
.....
})
}
计算qps,可以这样来写promql,通过sum by语句将各个主机相同的接口路由聚合起来,查看5m内接口的qps是多少。
sum(irate(server_handle_total{type="http"}[5m])) by (api,method)
接着,我们看下接口报错数如何写promql语句来观察。我们创建histogram指标时会创建一个server_handle_seconds_count 指标,它表示桶中一共记录了多少个记录。由于我们的指标标签上有status维度,所以可以按接口路由维度筛选出那些失败的接口路由随时间变化的情况。api路径和方法名唯一确定一条路由。
sum(rate(server_handle_seconds_sum{type="http",status !="200"}[5m])) by (api,method)
应用程序的饱和度可以拿当前服务正在处理的请求数已经当前应用程序占用的cpu已以及内存信息去判定,当达到一个比较高的水位后,应用程序的接口耗时在处于增大的趋势则可以认为应用服务达到了饱和。
在golang的 prometheus client已经帮我们暴露了这些指标,同时也暴露了runtime内部的指标,例如垃圾回收耗时等。我们只需要像下面那样在指标注册中心注册便可以暴露它们了。
MetricsReg = prometheus.NewRegistry()
MetricsReg.MustRegister(
collectors.NewGoCollector(
collectors.WithGoCollectorRuntimeMetrics(collectors.GoRuntimeMetricsRule{Matcher: regexp.MustCompile("/.*")}),
),
collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}),
)
对应的promql语句的写法和监控面板配置可以看github项目代码里的配置,通过导入面板的json模板就可以了,这块比较简单,我就不再继续展开了。
最后我们来看下如何保留问题现场,要解决这个问题,首先要确定触发问题发生的条件,这些条件可以是协程数量达到某个值,也可以是cpu,内存上涨,也可以是接口qps达到某个上限。
接着便是如何保留问题现场的问题。在golang里,我们可以通过go tool pprof 工具来分析应用程序的cpu,内存等资源的消耗以及代码阻塞情况,并将其定位到具体的代码。关于pprof 工具的原理我之前出过一个系列:
# golang pprof 监控系列(1) —— go trace 统计原理与使用
# golang pprof监控系列(2) —— memory,block,mutex 使用
# golang pprof 监控系列(3) —— memory,block,mutex 统计原理
# golang pprof 监控系列(4) —— goroutine thread 统计原理
# golang pprof 监控系列(5) —— cpu 占用率 统计原理
如果能在问题发生时自动执行pprof 相关的命令,生成了pprof文件,就可以后续通过pprof文件分析当时程序是哪段逻辑导致的问题了。
目前已经有相关的开源工具在做这件事情,holmes这个项目实现了这个逻辑
https://github.com/mosn/holmes
但是注意,生成的pprof文件只能在出现问题的机器上,所以要分析pprof文件还得再去对应机器上进行下载,这在机器过多时,可能不太方便,所以你完全可以写一个程序,自动将生成的pprof文件上传到一台机器上,pprof文件可以用 应用名+机器名+时间+采集类型 取名,这样在看pprof文件时也更加方便。
这一节,通过对应用服务建立黄金指标和使用holmes做到保留问题现场,构建起了对应用服务的监控。并且你可以回忆整个过程,是不是在指标异常的时候,我们可以很方便快速的定位到问题代码,这也是监控的目的,不仅发现问题,还要能定位问题。在下一节我将会介绍如何从对mysql进行监控,并且能够通过mysql监控指标,找到应用代码不合理有风险的地方。
在万千人海中,相遇就是缘分,为了这份缘分,给作者点个赞👍🏻不过分吧。