使用 Prometheus 进行应用监控

应用监控的定义与作用

对于工程师们来说,软件某一阶段的开发任务完成就意味着"done"了。然而从软件的生命周期来说,编码的完成只是开始,软件还需要符合预期地运行并试图达到人们对它设定的目标,而监控就是检验这两点的常用可视化方法。

按照监控的对象通常可以将监控分为基础设施监控,中间件监控,应用监控和业务监控,它们各自的监控对象与作用如下表所示:

监控对象判断软件是否符合预期地运行判断业务目标是否达到
基础设施监控服务器、存储等软件的运行环境
中间件监控数据库、消息队列等公用软件
应用监控实现具体业务需求的软件
业务监控业务指标

其中基础设施、中间件和应用层级的监控,由于都在软硬件系统内,它们之中任意一环出现问题都有可能导致软件运行出现异常,实际场景中这些监控通常需要互相配合、关联分析。

而应用级别的监控,由于本身是业务的重要载体,应用监控有时也能直接反应业务指标是否达到,比如应用的吞吐量和时延等指标,当这些指标是业务的侧重点时,应用监控实际上就发挥了业务监控的作用。

应用监控利器 Prometheus

Prometheus是一套开源的监控体系,以指标为度量单位来描述应用的运行情况。

组件及生态

在这里插入图片描述
这张图片是Prometheus官网上的架构图,可以看到 Prometheus 除了主要的 Prometheus Server 提供采集和存储指标的时序数据以外,还包括接受推送方式获取指标的 Pushgateway 和管理告警规则的 Alertmanger 等组件。在应用监控中我们主要关注的是 Prometheus Server 和集成到各类应用中负责产生指标的 prometheus client 库。

特性

Prometheus官网中介绍的特性主要有以下几点:

  • 多维度的指标数据模型(prometheus中每条时序数据包括时间戳,指标名称和标签等维度)
  • 指标查询语言PromQL(通过对原始指标数据进行标签筛选,以及取变化率、topN等数据处理函数操作,使得指标的表达更具灵活性)
  • 不依赖于分布式存储,实现单节点自治
  • 基于HTTP协议拉取时序数据(相比于Zabbix中使用json-rpc协议,HTTP协议更符合Web应用中远程调用的主流)

在k8s集群中部署 Prometheus Operator

Prometheus Operator 是在 k8s 集群中部署和维护 prometheus 服务的一种方式,它在 prometheus server 和 alertmanger 等服务端组件的基础上,还把监控对象和告警规则等配置也给"资源化"了,更容易在 k8s 集群中管理。

演示项目 kube-prometheus

Github 中的 kube-prometheus 项目是从 prometheus operator 中分离出来的,主要用来快速搭建一个演示环境。

直接通过 kuebctl apply 命令在k8s集群中部署 prometheus operator:

git clone https://github.com/coreos/kube-prometheus.git
cd kube-prometheus/manifests
kubectl apply -f setup
kubeclt apply -f .

以上命令中的 kubectl apply -f setup 主要创建 monitoring 命名空间,以及prometheus operator 这个控制器还有其他 CRD (自定义资源声明)。

kubectl apply -f .则创建刚才定义好的自定义资源,以及各自的 ServiceAccount 和相关配置。

各自定义资源的拓扑关系如下:
在这里插入图片描述

ServiceMonitor的作用

其中跟应用监控关系最密切的就是ServiceMonitor资源,它的yaml文件类似这样:

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  labels:
    k8s-app: alertmanager
  name: alertmanager
  namespace: monitoring
spec:
  endpoints:
  - interval: 30s
    port: web
  selector:
    matchLabels:
      alertmanager: main

ServiceMonitor通过标签筛选需要被监控的对象(k8s service),并且指定从它的哪个端口和url路径来拉取指标,并定义拉取操作的间隔时间。

ServiceMonitor本质是对 prometheus 配置中指标数据来源(endpoint)的抽象,每新建一个 service monitor 资源,prometheus operator 就会在自定义资源 promethues 的配置文件中添加相应的配置,以达到和使用原生 prometheus 相同的效果,这就把原来需要需要手动和统一配置的任务通过 crd 来自动化实现了。

上述例子中几乎所有资源都有对应的 Service Monitor,说明它们都有一个http/https 接口来暴露指标数据,这是因为谷歌还有coreos在设计这些组件时就要求每个组件都要有暴露自身状态的接口,它们在样式上是符合Prometheus 规范的文本信息,类似这样:

# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.
# TYPE go_gc_duration_seconds summary
go_gc_duration_seconds{quantile="0"} 0
go_gc_duration_seconds{quantile="0.25"} 0
go_gc_duration_seconds{quantile="0.5"} 0
go_gc_duration_seconds{quantile="0.75"} 0
go_gc_duration_seconds{quantile="1"} 0.0008235
go_gc_duration_seconds_sum 0.0008235
go_gc_duration_seconds_count 8

每一个或一组指标都会包含描述信息、指标类型、指标名称和实时数据这些,prometheus在获取http接口中的文本信息后会进一步将它们转化为自己的时序数据模型(加上时间戳维度等)。

Go应用中实现自定义指标

在我们自己开发的应用程序中,也可以通过集成 prometheus 官方提供的client库来对外暴露 prometheus 风格的指标数据。

以 Go 语言开发的应用为例,首先在项目中新建一个子目录(包)用来声明和注册需要对外暴露的应用指标:

stat/prometheus.go:

package stat

import "github.com/prometheus/client_golang/prometheus"

var (
	testRequestCounter = prometheus.NewCounter(prometheus.CounterOpts{
		Name: "test_request_total",
		Help: "Total count of test request",
	})
)

func init() {
	prometheus.MustRegister(testRequestCounter)
}

示例中声明了一个 Counter 类型的指标,用于统计测试http请求的总数,在 init 函数中对该指标进行注册,之后我们在其他go文件中引入该包时就会自动注册这些指标。

Counter类型是 prometheus 4种指标类型(Counter, Gauge, Histogram, Summary)的一种,用于描述只增不减的数据,比如http服务接收的请求数,具体可以查看 prometheus 的官方文档。

main.go:

package main

import (
	"github.com/gin-gonic/gin"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promhttp"
	"github.com/go-app/stat" //刚才写的声明指标的包
)
func main() {
    r := gin.Default()
	r.GET("metrics",gin.WrapH(promhttp.Handler()))
	r.GET("test",func(c *gin.Context) {
	   stat.testRequestCounter.Inc()
		c.JSON(http.StatusOK, gin.H{
			"text": "hello world",
		})
	})
	r.Run(":9090")
}

指标声明后还需要在合适的实际触发对指标数据的采集,比如这个例子中在每次访问 /test 请求时在 handle 函数中使请求计数器加1,如果是要统计所有的请求数的话,还可以把采集数据的操作放在中间件中,使任何请求都会触发计数器加1。

实际场景中的应用监控比上述例子复杂得多,因为不同的应用程序可以采集的监控指标不同,即使是同类型的应用,在不同的业务场景下需要采集的指标也会有不同的侧重。但是在谷歌的 SRE 实践中仍然总结出了4个黄金指标,分别是:

  • 延迟 服务处理请求所需要的时间
  • 流量 对系统负载的度量,在http服务中通常是每秒的请求数
  • 错误 请求失败的速率
  • 饱和度 服务容量有多"满",通常是系统中某个最为受限的资源的某个具体指标的度量

这些指标也可以通过 prometheus 的4中基本指标类型去表示,大致的关系是:

Counter ==> 请求量,请求流量
Gauge ==> 系统的饱和度(实时)
Histogram ==> 请求延时在各个区间的分布
Summary ==> 请求延时的中位数,9分位数,95分位数等

根据黄金指标的指导理念,我又设计了一个更复杂一些的示例:
假设有一个固定容量的消息队列,通过http的 /push 和 /pop 请求可以使队列增加或减少一条记录,在队列容量快要满或者快要全空的时候,请求的延时和错误率都会相应增加。

以下是完整的示例代码和最终展示的Grafana图表,仅供参考:
stat/prometheus.go:

package stat

import "github.com/prometheus/client_golang/prometheus"

var (
	MqRequestCounter = prometheus.NewCounterVec(prometheus.CounterOpts{
		Namespace: "mq",
		Name: "request_total",
		Help: "Total count of success request",
	},[]string{"direction"})
	MqErrRequestCounter = prometheus.NewCounterVec(prometheus.CounterOpts{
		Namespace: "mq",
		Name: "err_request_total",
		Help: "Total count of failed request",
	},[]string{"direction"})
	MqRequestDurationHistogram = prometheus.NewHistogramVec(prometheus.HistogramOpts{
		Namespace: "mq",
		Name: "request_duration_distribution",
		Help: "Distribution state of request duration",
		Buckets: prometheus.LinearBuckets(110,10,5),
	},[]string{"direction"})
	MqRequestDurationSummary = prometheus.NewSummaryVec(prometheus.SummaryOpts{
		Namespace: "mq",
		Name: "request_duration_quantiles",
		Help: "Quantiles of request duration",
		Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
	},[]string{"direction"})
	MqCapacitySaturation = prometheus.NewGauge(prometheus.GaugeOpts{
		Namespace: "mq",
		Name: "capacity_saturation",
		Help: "Capacity saturation of the message queue",
	})
)

func init() {
	prometheus.MustRegister(MqRequestCounter,MqErrRequestCounter,MqRequestDurationHistogram,MqRequestDurationSummary,MqCapacitySaturation)
}

main.go:

package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promhttp"
	"github.com/go-app/stat"
	"math/rand"
	"net/http"
	"strings"
	"time"
)

type queueConfig struct {
	length int
	maxErrorRate float64
	maxDuration time.Duration
}

type messageQueue struct {
	qc queueConfig
	queue []string
}

var pushErrCount, popErrCount int

func (m *messageQueue) push() (ok bool,duration time.Duration){
	startTime := time.Now()
	stat.MqRequestCounter.With(prometheus.Labels{"direction":"push"}).Inc()
	factor := float64(len(m.queue))/float64(m.qc.length)
	fixedDuration := time.Duration(float64(m.qc.maxDuration)*factor) + time.Millisecond*100
	time.Sleep(fixedDuration)
	errorRate := m.qc.maxErrorRate * factor
	if rand.Intn(100) < int(errorRate*100) {
		ok = false
		pushErrCount += 1
		stat.MqErrRequestCounter.With(prometheus.Labels{"direction":"push"}).Inc()
	} else {
		ok = true
		m.queue = append(m.queue,"#")
	}
	duration = time.Now().Sub(startTime)
	durationMs := float64(duration/time.Millisecond)
	stat.MqRequestDurationHistogram.With(prometheus.Labels{"direction":"push"}).Observe(durationMs)
	stat.MqRequestDurationSummary.With(prometheus.Labels{"direction":"push"}).Observe(durationMs)
	fmt.Printf("%v",strings.Join(m.queue,""))
	fmt.Printf("\t Factor: %v Success:%v Duration:%v PushErrCount:%v\n",factor,ok,duration,pushErrCount)
	return
}

func (m *messageQueue) pop() (ok bool,duration time.Duration){
	startTime := time.Now()
	stat.MqRequestCounter.With(prometheus.Labels{"direction":"pop"}).Inc()
	factor := float64(m.qc.length-len(m.queue))/float64(m.qc.length)
	fixedDuration := time.Duration(float64(m.qc.maxDuration)*factor) + time.Millisecond*100
	time.Sleep(fixedDuration)
	errorRate := m.qc.maxErrorRate * factor
	if rand.Intn(100) < int(errorRate*100) {
		ok = false
		popErrCount += 1
		stat.MqErrRequestCounter.With(prometheus.Labels{"direction":"pop"}).Inc()
	} else {
		ok = true
		m.queue = m.queue[:len(m.queue)-1]
	}
	duration = time.Now().Sub(startTime)
	durationMs := float64(duration/time.Millisecond)
	stat.MqRequestDurationHistogram.With(prometheus.Labels{"direction":"pop"}).Observe(durationMs)
	stat.MqRequestDurationSummary.With(prometheus.Labels{"direction":"pop"}).Observe(durationMs)
	fmt.Printf("%v",strings.Join(m.queue,""))
	fmt.Printf("\t Factor: %v Success:%v Duration:%v PopErrCount:%v\n",factor,ok,duration,popErrCount)
	return
}

func main() {
	r := gin.Default()
	r.GET("metrics",gin.WrapH(promhttp.Handler()))
	api := r.Group("api")
	qc := queueConfig{
		length: 100,
		maxErrorRate: 0.2,
		maxDuration: 50*time.Millisecond,
	}
	mq := messageQueue{
		qc: qc,
		queue: make([]string,0,qc.length),
	}

	rand.Seed(time.Now().UnixNano())
	api.POST("push",func(c *gin.Context) {
		ok, duration := mq.push()
		c.JSON(http.StatusOK,gin.H{
			"success": ok,
			"duration": duration,
			"length": len(mq.queue),
		})
	})
	api.POST("pop",func(c *gin.Context) {
		ok, duration := mq.pop()
		c.JSON(http.StatusOK,gin.H{
			"success": ok,
			"duration": duration,
			"length": len(mq.queue),
		})
	})
	go func() {
		for {
			saturation := float64(len(mq.queue))/float64(mq.qc.length)
			stat.MqCapacitySaturation.Set(saturation)
			time.Sleep(time.Second*5)
		}
	}()
	r.Run(":9090")
}

client.go (用于持续对应用发起请求):

package main

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"os"
	"time"
)

type Body struct {
	Success  bool          `json:"success"`
	Duration time.Duration `json:"duration"`
	Length   int           `json:"length"`
}

func main() {
	pushFlag := true
	var baseUrl string
	if len(os.Args[1:]) == 0 {
		baseUrl = "http://localhost:9090"
	} else {
		baseUrl = os.Args[1:][0]
	}
	pushUrl := baseUrl + "/api/push"
	popUrl := baseUrl + "/api/pop"
	for {
		if pushFlag {
			resp, err := http.Post(pushUrl, "application/json", nil)
			if err != nil {
				fmt.Fprint(os.Stderr, err)
			}
			bodyStr, err := ioutil.ReadAll(resp.Body)
			if err != nil {
				fmt.Fprint(os.Stderr, err)
			}
			body := &Body{}
			err = json.Unmarshal(bodyStr, body)
			if err != nil {
				fmt.Fprint(os.Stderr, err)
			}
			fmt.Printf("%v\n", body)
			resp.Body.Close()
			if body.Length == 100 {
				pushFlag = false
			}
		} else {
			resp, err := http.Post(popUrl, "application/json", nil)
			if err != nil {
				fmt.Fprint(os.Stderr, err)
			}
			bodyStr, err := ioutil.ReadAll(resp.Body)
			if err != nil {
				fmt.Fprint(os.Stderr, err)
			}
			body := &Body{}
			err = json.Unmarshal(bodyStr, body)
			if err != nil {
				fmt.Fprint(os.Stderr, err)
			}
			fmt.Printf("%v\n", body)
			resp.Body.Close()
			if body.Length == 0 {
				pushFlag = true
			}
		}
	}
}

Grafana图表:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wonain

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值