针对中小型项目,介绍一下简单的 Prometheus 监控方案。
Prometheus 帮助我们解决了 Metrics 监控的难题,后续出现的 Thanos 解决了 Prometheus 存储扩展的难题。总体来说,Prometheus 已经是一个非常成熟的监控方案。
各大云厂商也相继推出了云版本的 Prometheus,用户可以不用去考虑如何运维 Prometheus,降低了人工运维成本。
既然 Prometheus 的运行问题已经得到解决,接下来就看看,如何使用它。
Prometheus 的 Client 接口设计的很合理,使用上也没什么问题。随之会出现几个棘手的问题。
不要小看这个问题,监控什么跟设计 API 一样不是一件容易的事。如果我们强行往代码里【塞入】Prometheus 监控相关代码,我敢保证,代码会很乱。
举个例子,监控一个函数运行了多长时间,我们要做如下几个事。
光上面几个逻辑,至少需要20+行代码,如果每个函数都是如此,整个项目的代码会非常【难看】。
对于追求【干净,简介】代码的我们来说,这是一个难受的体验。
这里我们介绍 rk-boot/v2 + Prometheus + Grafana 的解决方案。这个方案里,我们使用两个方法解决上述棘手的问题。
简单来说,就是在函数里面添加两行代码,监控这个函数。
Golang 不像 Spring 一样有 annotation 的支持,可以在函数上面标记一个 @xxx 可以轻松实现函数监控。不过,我们可以在代码里添加【两行代码】,以最低成本实现函数监控。
有一个通用 Grafana 监控 Dashboard 是一个很幸福的事情,毕竟上手 Grafana 不是几个小时就能搞定的事情。
我们需要创建一个 prometheus.yml 文件,告诉 prometheus 从哪里收集监控数据。
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
$ docker run -p 9090:9090 -v /Users/dongxuny/workspace/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml prom/prometheus
初始化账号: admin
初始化密码:admin
$ docker run -p 3000:3000 grafana/grafana
rk-boot/v2 是可以通过 YAML 文件启动 Golang 流行框架的依赖库,里面包含了很多实用中间件。
为了模拟微服务,我们同时还下载 rk-gin/v2 来启动 gin-gonic 服务。
$ go get github.com/rookie-ninja/rk-boot/v2
$ go get github.com/rookie-ninja/rk-gin/v2
boot.yaml 文件告诉 rk-boot/v2 启动哪些 Gin 配套的服务。
---
gin:
- name: greeter
port: 8080
enabled: true
prom:
enabled: true # 告诉 Gin 启动 Prometheus 客户端
middleware:
prom:
enabled: true # 告诉 Gin 启动 Prometheus API 监控
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
}
$ go run main.go
$ curl localhost:8080/v1/greeter
boot.yaml 文件告诉 Gin 启动 API 监控中间件,所以我们可以监控两个东西。
rk-prom: 自动记录每个 API 的 Metrics,有默认 grafana dashboard
rk-cursor: 代码里嵌入的监控(就是我们写的那两行代码),有默认 grafana dashboard
我们可以看到 API 的监控了,不需要任何代码改动。
我们可以看到 Cursor 的监控了
rk-prom 是一个自带的监控中间件,会默认监控所有 API 的运行时间,错误码。包含的监控项有【运行时间】,【API 可用性】,【API 速率】
如果想要看到本地的输出的监控数据,可以查看 localhost:8080/metrics
rk-cursor 是一个 struct,通过 Click() 方法获取一个 Pointer struct,再通过 pointer.Release() 方法结束监控。
会默认监控所有 API 的运行时间,错误码。包含的监控项有【Function 运行时间】,【Function 可用性】,【Function 速率】。
rk-cursor 可以实现如下三个监控。
对于上面的代码进行一行改动。
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
rk-cursor 会使用 runtime.Caller() 函数记录上一层调用函数的名字。
举个例子,Greeter() 函数调用 B() 函数和 C() 函数,那么我们可以观察 Greeter() 函数的里,B() 和 C() 花了多尝试时间。
我们稍微修改一下 main.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层的调用链。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。