在故障处理时,完善的监控系统可以帮助我们快速地发现问题与定位问题。对Go服务而言,如何监控Go服务的核心指标呢?比如协程数、内存使用量、线程数等等。本文将为大家介绍如何基于Prometheus构建Go服务监控系统。
运行时监控
如何监控Go服务的运行时指标呢?
第一步当然是采集Go服务的运行时指标了,常用的运行时指标包括线程数、协程数、内存使用量、GC耗时等。如何采集呢?幸运的是,Go语言为我们提供了SDK,通过这些SDK我们可以很方便地获取到这些运行时指标,如下面代码所示:
// 获取线程数
runtime.ThreadCreateProfile(nil)
// 获取协程数
runtime.NumGoroutine()
// 获取GC统计指标,
debug.ReadGCStats(&stats)
// 该结构体定义了内存统计指标
type MemStats struct {
Alloc uint64 // 已分配堆内存字节数
……
}
在上面的代码中,我们可以通过这几个函数或者结构体获取到Go服务常用的运行时指标。
第二步,如何导出与查看这些运行时指标呢?我们可以借助Prometheus,这是一款开源的监控与报警系统,并且提供了多种语言的客户端库,其中就包括Go语言。Prometheus客户端库使用方式如下:
package main
import (
"net/http"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
func main() {
// 注册路由
http.Handle("/metrics", promhttp.Handler())
http.ListenAndServe(":2112", nil)
}
在上面的代码中,只需要一行代码就能引入Prometheus客户端库。通过这种方式,我们对外暴露了一个接口,只需要调用该接口就能获取到Go服务的运行时指标。当然,在实际项目开发过程中,我们通常会使用一些Web框架,比如gin。这时候就不能使用上述代码引入Prometheus客户端库了。我们以gin框架为例,引入Prometheus客户端库的代码如下:
// 注册路由
router.GET("/metrics", controller.Metrics)
// 请求处理方法
func Metrics(c *gin.Context) {
handler := promhttp.Handler()
handler.ServeHTTP(c.Writer, c.Request)
}
在上面的代码中,我们引入了Prometheus客户端库,编译并运行项目,使用curl命令手动发起HTTP请求,结果如下所示:
$ http://127.0.0.1:9090/metrics
# HELP go_goroutines Number of goroutines that currently exist
go_goroutines 12
# HELP go_memstats_alloc_bytes Number of bytes allocated and still in use.
go_memstats_alloc_bytes 7.3335512e+08
……
参考上面的输出结果,以井号“#”开始的描述是对后续指标的解释。指标go_goroutines表示当前的协程数,指标go_memstats_alloc_bytes表示已申请并在使用的堆内存量。当然,我们这里省略了很多Go服务运行时指标。
通过这种方式,我们可以导出Go服务的运行时指标,只是这些指标都是以文本方式呈现的,可观测性不太好,我们希望能够可视化展示这些指标。这就需要用到Prometheus了:一方面,它可以帮助我们定时收集监控指标;另一方面,它还支持可视化展示监控指标。当然,在使用Prometheus时,首先需要配置需要采集指标的目标服务地址与访问接口。参考下面的配置:
// Prometheus下载地址
https://prometheus.io/download/
// 配置采集任务,配置文件是prometheus.yml
- job_name: "go-mall"
scrape_interval: 10s
metrics_path: "/metrics"
static_configs:
- targets: ["xxxx:9090"]
// Prometheus启动命令
./prometheus --config.file=prometheus.yml --web.listen-address=:9094 --web.enable-lifecycle --storage.tsdb.retention=7d --web.enable-admin-api
可以看到,我们启动 Prometheus 时监听的端口号是9094,即我们可以通过该端口访问 Prometheus 提供的Web系统。打开系统之后,依次单击Status→Targets菜单项,可以查看目标服务状态,之后单击Graph菜单项,可以配置监控指标的可视化看板。Prometheus支持通过表达式配置可视化看板,我们不仅可以配置具体的指标,也可以配置复杂的表达式,表达式支持一些常用的运算符、聚合函数等(有兴趣的读者可以研究一下PromQL,这是Prometheus内置的数据查询语言)。我们以协程数与内存使用量指标为例,配置好的可视化看板如图1和图2所示。
参考图1和图2,我们通过ab压测工具分别在9点26分30秒时刻、9点27分30秒时刻发起了一些请求,可以明显看到这时候的协程数、堆内存使用量都有明显变化。
自定义监控
那如果我们想自定义一些监控指标该如何实现呢?比如服务或者接口的访问QPS、响应时间等。这就需要我们对Prometheus的几种指标类型以及Prometheus客户端库的使用有一些了解。
Prometheus支持4种类型的指标,分别为Counter、Gauge、Histograms与Summary,各类型指标含义如下。
- Counter:计数器类型指标,可以用来统计请求数、错误数等。
- Gauge:计量器类型指标,可以用来统计内存使用量、CPU使用率等。
- Histograms:直方图,可以用来统计请求延迟落在哪一个区间,比如服务或者接口的访问时间P99。
- Summary:与Histograms类型指标比较类似,也可以用来统计服务或者接口的响应时间P50、P90、P99等,只是Summary是在Go服务侧计算的,Histograms是在Prometheus侧计算的。
QPS监控指标
通过上面的解释,我们可以了解到,服务或者接口的访问QPS可以基于Counter实现,访问时间P50、P90、P99等可以基于Histograms或者Summary实现。基于Counter实现QPS监控指标的代码如下所示:
// 定义Counter指标采集器
var counter = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "http_request_total",
Help: "The total number of HTTP request",
},
[]string{"uri"},
)
// 注册Counter指标采集器
func init() {
prometheus.MustRegister(counter)
}
// 声明gin框架中间件
func Qps(c *gin.Context) {
counter.WithLabelValues(c.Request.RequestURI).Inc()
}
// 使用中间件
router.Use(middleware.Qps)
在上面的代码中,函数prometheus.NewCounterVec用于创建一个Counter类型的指标采集器,该指标的名称为http_request_total,表示HTTP请求的总访问量。可以看到,我们还声明了一个名为uri的标签。另外,在累加Counter指标时,我们还携带了标签的值,也就是请求地址。那么,标签是什么呢?因为我们的需求不仅仅是统计整个服务的QPS,还需要统计每个接口的QPS,标签可以用来过滤监控指标,进而统计整个服务以及每个接口的访问QPS。
编译并运行项目,使用curl命令手动访问metrics接口查看服务的监控指标,结果如下所示:
$ http://127.0.0.1:9090/metrics
http_request_total{uri="/api/goods/detail"} 10000
http_request_total{uri="/metrics"} 3
……
参考上面的输出结果,我们可以看到每一个接口累计的访问量。但是,我们需要的监控指标不是QPS吗?那么,如何根据该访问量计算QPS呢?Prometheus有一个内置函数irate,可以用来计算平均QPS。基于该函数与访问量指标配置的QPS监控看板如图3和图4所示。
参考图3和图4,我们在10点06分左右通过ab压测工具发起了一些请求,10点12分左右请求完成,可以看到接口访问量与接口QPS的曲线还是比较吻合的。
P99 监控指标
接下来将讲解如何基于Prometheus统计服务或者接口的响应时间,比如P50、P90、P99等。我们先解释一下P50、P90、P99等的概念,以P99为例,这意味着99%的请求的响应时间都小于给定时间,只有1%的请求的响应时间大于该时间。基于Summary实现P99监控指标的代码如下所示:
// 定义Summary指标采集器
var summary = prometheus.NewSummaryVec(
prometheus.SummaryOpts{
Name: "http_request_delay",
Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
},
[]string{"uri"},
)
// 注册Summary指标采集器
func init() {
prometheus.MustRegister(summary)
}
// 声明gin框架中间件
func Delay(c *gin.Context) {
startTime := time.Now()
// 执行下一个中间件
c.Next()
latency := time.Now().Sub(startTime)
summary.WithLabelValues(c.Request.RequestURI).
Observe(float64(latency.Milliseconds()))
}
// 使用中间件
router.Use(middleware.Delay)
在上面的代码中,函数prometheus.NewSummaryVec用于创建一个Summary类型的指标采集器,该指标的名称为”http_request_delay”。字段Objectives是一个散列表,键和值都是浮点数,键表示计算的分位数,如P50、P90、P99,值表示允许的误差(通过误差换取时间空间)。
编译并运行项目,手动通过ab压测工具发起请求,并通过Prometheus平台查看P99监控指标,如图5所示。
参考图5所示,可以看到99%请求的响应时间都小于350毫秒。另外我们还可以将P50、P90、P99这三个指标和ab压测工具统计的指标做一个对比,验证我们采集的监控指标是否合理。
总结
完善的监控系统是构建高可用Go服务的基石,本文主要介绍了如何基于Prometheus构建Go服务监控系统,包括Go服务的运行时指标与自定义监控指标。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。