rk-boot/v2: 干净的 Prometheus 监控方案 (Golang)

2022-03-25 19:53:39 浏览数 (5)

针对中小型项目,介绍一下简单的 Prometheus 监控方案。

Prometheus 帮助我们解决了 Metrics 监控的难题,后续出现的 Thanos 解决了 Prometheus 存储扩展的难题。总体来说,Prometheus 已经是一个非常成熟的监控方案。

各大云厂商也相继推出了云版本的 Prometheus,用户可以不用去考虑如何运维 Prometheus,降低了人工运维成本。

既然 Prometheus 的运行问题已经得到解决,接下来就看看,如何使用它。

棘手的问题

Prometheus 的 Client 接口设计的很合理,使用上也没什么问题。随之会出现几个棘手的问题。

  • 监控什么
  • 代码里怎么写监控指标,Label 该怎么带
  • 怎么配置图形化

不要小看这个问题,监控什么跟设计 API 一样不是一件容易的事。如果我们强行往代码里【塞入】Prometheus 监控相关代码,我敢保证,代码会很乱。

举个例子,监控一个函数运行了多长时间,我们要做如下几个事。

  • 合理命名监控项
  • 合理配置 Label
  • 代码里嵌入计时代码
  • 处理错误逻辑
  • 图像化

光上面几个逻辑,至少需要20 行代码,如果每个函数都是如此,整个项目的代码会非常【难看】。

对于追求【干净,简介】代码的我们来说,这是一个难受的体验。

干净的解决方案

这里我们介绍 rk-boot/v2 Prometheus Grafana 的解决方案。这个方案里,我们使用两个方法解决上述棘手的问题。

简单来说,就是在函数里面添加两行代码,监控这个函数。

  • Wrap Prometheus 相关逻辑
  • 现成 Grafana 图表

代码干净

Golang 不像 Spring 一样有 annotation 的支持,可以在函数上面标记一个 @xxx 可以轻松实现函数监控。不过,我们可以在代码里添加【两行代码】,以最低成本实现函数监控。

现成图表(Grafana)

有一个通用 Grafana 监控 Dashboard 是一个很幸福的事情,毕竟上手 Grafana 不是几个小时就能搞定的事情。

Demo

1.Docker 启动 Prometheus

我们需要创建一个 prometheus.yml 文件,告诉 prometheus 从哪里收集监控数据。

  • prometheus.yml
代码语言:yaml复制
global:
  scrape_interval: 1s

scrape_configs:
  - job_name: 'greeter'
    scrape_interval: 1s
    metrics_path: "/metrics"
    static_configs:
      - targets: ['host.docker.internal:8080']        # Prometheus 运行在 docker 里面,不使用 localhost
  • 启动 Prometheus
代码语言:txt复制
$ docker run -p 9090:9090 -v /Users/dongxuny/workspace/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml prom/prometheus

2.使用 Docker 启动 grafana 并配置 Prometheus

初始化账号: admin

初始化密码:admin

代码语言:txt复制
$ docker run -p 3000:3000 grafana/grafana
  • 进入 localhost:3000
  • 选择 Prometheus 为数据源
  • 配置 Prometheus 地址

3.下载 rk-boot/v2

rk-boot/v2 是可以通过 YAML 文件启动 Golang 流行框架的依赖库,里面包含了很多实用中间件。

为了模拟微服务,我们同时还下载 rk-gin/v2 来启动 gin-gonic 服务。

代码语言:shell复制
$ go get github.com/rookie-ninja/rk-boot/v2
$ go get github.com/rookie-ninja/rk-gin/v2

4.配置 boot.yaml

boot.yaml 文件告诉 rk-boot/v2 启动哪些 Gin 配套的服务。

代码语言:yaml复制
---
gin:
  - name: greeter
    port: 8080
    enabled: true
    prom:
      enabled: true              # 告诉 Gin 启动 Prometheus 客户端
    middleware:
      prom:
        enabled: true            # 告诉 Gin 启动 Prometheus API 监控

5.写个 API

代码语言:go复制
package main

import (
	"context"
	"fmt"
	"github.com/gin-gonic/gin"
	"github.com/rookie-ninja/rk-boot/v2"
	"github.com/rookie-ninja/rk-entry/v2/cursor"
	"github.com/rookie-ninja/rk-gin/v2/boot"
	"github.com/rookie-ninja/rk-gin/v2/middleware/context"
	"net/http"
)

func main() {
	// Create a new boot instance.
	boot := rkboot.NewBoot()

	// Register handler
	ginEntry := rkgin.GetGinEntry("greeter")
	ginEntry.Router.GET("/v1/greeter", Greeter)

	// Bootstrap
	boot.Bootstrap(context.Background())

	// 必要!向 Prometheus Registry 注册 Prometheus Metrics
	ginEntry.PromEntry.Register(rkcursor.SummaryVec())

	// Wait for shutdown sig
	boot.WaitForShutdownSig(context.Background())
}

func Greeter(ctx *gin.Context) {
	// 启动此函数的监控,并使用 Release() 函数结束监控
	pointer := rkginctx.GetCursor(ctx).Click()
	defer pointer.Release()

	ctx.JSON(http.StatusOK, &GreeterResponse{
		Message: fmt.Sprintf("Hello %s!", ctx.Query("name")),
	})
}

type GreeterResponse struct {
	Message string
}

6.启动 main.go & 发送请求

代码语言:txt复制
$ go run main.go
$ curl localhost:8080/v1/greeter

7.查看图形化监控

boot.yaml 文件告诉 Gin 启动 API 监控中间件,所以我们可以监控两个东西。

rk-prom: 自动记录每个 API 的 Metrics,有默认 grafana dashboard

rk-cursor: 代码里嵌入的监控(就是我们写的那两行代码),有默认 grafana dashboard

  • 引入 rk-prom dashboard (代号:15111):

我们可以看到 API 的监控了,不需要任何代码改动。

  • 引入 rk-cursor dashboard (代号:15937):

我们可以看到 Cursor 的监控了

8.解释一下 rk-prom & rk-cursor dashboard

rk-prom 是一个自带的监控中间件,会默认监控所有 API 的运行时间,错误码。包含的监控项有【运行时间】,【API 可用性】,【API 速率】

如果想要看到本地的输出的监控数据,可以查看 localhost:8080/metrics

rk-cursor 是一个 struct,通过 Click() 方法获取一个 Pointer struct,再通过 pointer.Release() 方法结束监控。

会默认监控所有 API 的运行时间,错误码。包含的监控项有【Function 运行时间】,【Function 可用性】,【Function 速率】。

rk-cursor 使用方法

rk-cursor 可以实现如下三个监控。

  • 监控 Function 运行时间
  • 监控 Function 错误
  • 监控 Function 里调用其他 Function 的运行时间(相当于 1层的调用链)

监控 Function 错误

对于上面的代码进行一行改动。

代码语言:go复制
func Greeter(ctx *gin.Context) {
	// 启动此函数的监控,并使用 Release() 函数结束监控
	pointer := rkginctx.GetCursor(ctx).Click()
	defer pointer.Release()

	// 记录一个 Error,让 Prometheus 标记此次运行是错误的,并且打错误日志
	if time.Now().Second()%2 == 0 {
		pointer.ObserveError(errors.New("manually triggered error"))
	}
	
	ctx.JSON(http.StatusOK, &GreeterResponse{
		Message: fmt.Sprintf("Hello %s!", ctx.Query("name")),
	})
}

再运行一次,发送请求,并观察日志和 grafana dashboard

监控 Function 调用链

rk-cursor 会使用 runtime.Caller() 函数记录上一层调用函数的名字。

举个例子,Greeter() 函数调用 B() 函数和 C() 函数,那么我们可以观察 Greeter() 函数的里,B() 和 C() 花了多尝试时间。

我们稍微修改一下 main.go

代码语言:go复制
// Copyright (c) 2021 rookie-ninja
//
// Use of this source code is governed by an Apache-style
// license that can be found in the LICENSE file.
package main

import (
	"context"
	"fmt"
	"github.com/gin-gonic/gin"
	"github.com/rookie-ninja/rk-boot/v2"
	"github.com/rookie-ninja/rk-entry/v2/cursor"
	"github.com/rookie-ninja/rk-gin/v2/boot"
	"net/http"
	"time"
)

// 初始化一个全局 cursor
var gCursor = rkcursor.NewCursor(rkcursor.WithEntryNameAndType("greeter", "GinEntry"))

func main() {
	// Create a new boot instance.
	boot := rkboot.NewBoot()

	// Register handler
	ginEntry := rkgin.GetGinEntry("greeter")
	ginEntry.Router.GET("/v1/greeter", Greeter)

	// Bootstrap
	boot.Bootstrap(context.Background())

	// 必要!向 Prometheus Registry 注册 Prometheus Metrics
	ginEntry.PromEntry.Register(rkcursor.SummaryVec())

	// Wait for shutdown sig
	boot.WaitForShutdownSig(context.Background())
}

func B() {
	pointer := gCursor.Click()
	defer pointer.Release()

	time.Sleep(1 * time.Millisecond)
}

func C() {
	pointer := gCursor.Click()
	defer pointer.Release()

	time.Sleep(2 * time.Millisecond)
}

func Greeter(ctx *gin.Context) {
	pointer := gCursor.Click()
	defer pointer.Release()

	// 调用 B函数
	B()

	// 调用 C函数
	C()

	ctx.JSON(http.StatusOK, &GreeterResponse{
		Message: fmt.Sprintf("Hello %s!", ctx.Query("name")),
	})
}

type GreeterResponse struct {
	Message string
}

这个例子里,函数的入口是 main.Greeter(),这个函数的 parentOperation 标记为 -

main.Greeter() 调用了 B() 和 C(),在 parentOperation 里选择 main.Greeter(),我们就可以看到,1层的调用链。

0 人点赞