掘金 后端 ( ) • 2024-07-02 09:40

一、背景

在软件系统中,我们希望通过度量和监控系统中各组件的行为,从而了解系统的状态和性能,这种能力叫做“可观测性”。它可以帮助开发人员快速定位和解决系统中的问题,提高系统的稳定性和可靠性。可观测性分为三部分:

  1. 度量(Metrics):量化系统的性能和行为,例如 CPU 负载、内存使用量等,帮助我们理解和监控系统的整体性能。【本文介绍和实现 度量 的中间件 Prometheus
  2. 追踪(Tracing):追踪系统的请求和响应,帮助开发人员诊断和调试问题,了解系统中的路径和延迟。【实现 追踪 的中间件是OpenTelemetry】(下次再更新这部分)
  3. 日志(Logging):记录系统的状态和行为,用于了解系统的历史记录和实时状态变化。 image.png

大部分公司内部会使用 Grafana 来做仪表盘,查看数据、配置告警和监控。(下次再更新这部分) image.png

而本文只介绍如何接入 Prometheus 来展示 度量 指标。

二、配置

(1)使用 docker compose 安装Prometheus

prometheus:
  image: prom/prometheus:v2.47.2
  volumes:
    # 将本地的 prometheus 文件映射到容器内的配置文件
    - ./prometheus.yaml:/etc/prometheus/prometheus.yml
  ports:
    # 访问数据的端口
    - 9090:9090

(2)Prometheus 的配置文件

global:
  external_labels:
    monitor: 'prometheus-monitor'

scrape_configs:
  - job_name: "webook"
    scrape_interval: 5s
    scrape_timeout: 3s
    static_configs:
      - targets: ["host.docker.internal:8081"]

(3)在 main 函数中调用 initPrometheus() 来初始化 Prometheus

func initPrometheus() {
    go func() {
       http.Handle("/metrics", promhttp.Handler())
       http.ListenAndServe(":8081", nil)
    }()
}

(4)下图是 Prometheus 自带的界面,打开 localhost:9090 就可以访问 image.png

三、API 实现

Prometheus 有 4 种指标类型,实践中按需选择:

  1. Counter:计数器,只能增加,用于统计次数,比如说某件事发生了多少次。
  2. Gauge:度量,它可以增加也可以减少,比如说当前正在处理的请求数。
  3. Histogram:柱状图,对观察对象进行采样,然后分到一个个桶里面。
  4. Summary:采样点按照百分位进行统计,比如说 99 线、999 线等

本文将用 Summary 指标来实现“利用 Gin middleware 统计 HTTP 请求响应时间”,用Gauge 指标来实现“利用 Gin middleware 统计 HTTP 请求活跃数量

3.1 统计 HTTP 请求响应时间

package prometheus

import (
    "github.com/gin-gonic/gin"
    "github.com/prometheus/client_golang/prometheus"
    "strconv"
    "time"
)

type Builder struct {
    Name       string
    Namespace  string
    Subsystem  string
    InstanceID string
    Help       string
}

func NewBuilder(name string, namespace string, subsystem string, instanceID string, help string) *Builder {
    return &Builder{Name: name, Namespace: namespace, Subsystem: subsystem, InstanceID: instanceID, Help: help}
}

// BuildResponseTime 统计http请求响应时间
func (b *Builder) BuildResponseTime() gin.HandlerFunc {
    // 分 请求方法、命中的路由和响应码
    labels := []string{"method", "pattern", "status"}
    vector := prometheus.NewSummaryVec(prometheus.SummaryOpts{
       // note 这三个都不能有除了下划线以外的字符!!!
       Namespace: b.Namespace,
       Subsystem: b.Subsystem,
       Name:      b.Name + "_resp_time",
       Help:      b.Help,
       ConstLabels: map[string]string{
          // 部署到了多个实例
          "instance_id": b.InstanceID,
       },
       Objectives: map[float64]float64{
          0.5:   0.01,
          0.75:  0.01,
          0.90:  0.01,
          0.99:  0.001,
          0.999: 0.0001,
       },
    }, labels)
    prometheus.MustRegister(vector)
    return func(ctx *gin.Context) {
       // 取当前时间
       start := time.Now()
       // note 当执行完ctx.Next()后,控制权回到此中间件BuildResponseTime处,执行defer
       defer func() {
          // 用于上报 prometheus
          duration := time.Since(start).Milliseconds()
          method := ctx.Request.Method
          pattern := ctx.FullPath()
          status := ctx.Writer.Status()
          vector.WithLabelValues(method, pattern, strconv.Itoa(status)).Observe(float64(duration))
       }()

       // 执行下一个middleware
       ctx.Next()
    }
}

利用 wrk 进行压测:1个线程,持续时间为10min,连接数为50个,压测接口为 \users\signup

wrk -t1 -d10m -c50 -s signup.lua http://localhost:8080/users/signup

效果图:

666552b330a52654281423d20dbf4fc.png

3.2 统计 HTTP 请求活跃数量

// BuildActiveRequest 统计活跃请求数
func (b *Builder) BuildActiveRequest() gin.HandlerFunc {
    gauge := prometheus.NewGauge(prometheus.GaugeOpts{
       // note 这三个都不能有除了下划线以外的字符!!!
       Namespace: b.Namespace,
       Subsystem: b.Subsystem,
       Name:      b.Name + "_active_request",
       Help:      b.Help,
       ConstLabels: map[string]string{
          // 部署到了多个实例
          "instance_id": b.InstanceID,
       },
    })

    prometheus.MustRegister(gauge)
    return func(ctx *gin.Context) {
       gauge.Inc()
       defer gauge.Dec()

       ctx.Next()
    }
}

效果图:

4de81ab206a8c2b22a558a64b380c2a.png

在 middleware 中调用 Builder()

// note prometheus
prometheus.NewBuilder(
    "ecommerce_BG", "webook", "gin_http", "", "统计 gin 的http接口数据",
).BuildResponseTime(),
prometheus.NewBuilder(
    "ecommerce_BG", "webook", "gin_http", "", "统计 gin 的http接口数据",
).BuildActiveRequest(),