在故障处理时,完善的监控系统可以帮助我们快速地发现问题与定位问题。对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所示。

image.png

image.png

  参考图1和图2,我们通过ab压测工具分别在9点26分30秒时刻、9点27分30秒时刻发起了一些请求,可以明显看到这时候的协程数、堆内存使用量都有明显变化。

自定义监控

  那如果我们想自定义一些监控指标该如何实现呢?比如服务或者接口的访问QPS、响应时间等。这就需要我们对Prometheus的几种指标类型以及Prometheus客户端库的使用有一些了解。

  Prometheus支持4种类型的指标,分别为Counter、Gauge、Histograms与Summary,各类型指标含义如下。

  1. Counter:计数器类型指标,可以用来统计请求数、错误数等。
  2. Gauge:计量器类型指标,可以用来统计内存使用量、CPU使用率等。
  3. Histograms:直方图,可以用来统计请求延迟落在哪一个区间,比如服务或者接口的访问时间P99。
  4. 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所示。

image.png
image.png

  参考图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所示。

image.png

  参考图5所示,可以看到99%请求的响应时间都小于350毫秒。另外我们还可以将P50、P90、P99这三个指标和ab压测工具统计的指标做一个对比,验证我们采集的监控指标是否合理。

总结

  完善的监控系统是构建高可用Go服务的基石,本文主要介绍了如何基于Prometheus构建Go服务监控系统,包括Go服务的运行时指标与自定义监控指标。


李烁
156 声望92 粉丝