从 Go 语言的依赖库讲起(2)监控、分布式追踪和日志

我们通常会遇到线上甚至测试中代码出现问题,这些问题可能来自于我们开发过程中的引入的BUG,有些来自于我们的功能未得到理想结果的,甚至有一些问题来自于运行环境的。很多事情可能未必能够足够可控,尤其是上线之后才发现出现了问题。除了我们前面一篇文章中介绍了一些测试相关的内容,虽然可以解决一部分问题,但是这些并不能完全杜绝所有问题在线上一定不会出现任何问题。因此我们需要建立对发布/预发环境一套相对完善的监控、诊断机制,保证我们可以尽快进行故障的分析和溯源。

系统的可观察性

为了应对目前软件开发的复杂度带来的相关问题,可观察性(Observability) 这个概念被引入软件领域。传统的监控和报警主要关注系统的异常情况和失败因素,可观察性更关注的是从系统自身出发,去展现系统的运行状况,更像是一种对系统的自我审视。一个可观察的系统中更关注应用本身的状态,而不是所处的机器或者网络这样的间接证据。我们希望直接得到应用当前的吞吐和延迟信息,为了达到这个目的,我们就需要合理主动暴露更多应用运行信息。在当前的应用开发环境下,面对复杂系统我们的关注将逐渐由点到点线面体的结合,这能让我们更好的理解系统,不仅知道What,更能回答Why。

可观察性目前主要包含以下三大支柱:

  • 度量(Metrics):Metric 往往是一些聚合的信息,相比 Logging 丧失了一些具体信息,但是占用的空间要比完整日志小的多,可以用于监控和报警。

  • 分布式追踪(Tracing):Tracing 介于 Logging 和 Metric 之间, 以请求的维度,串联服务间的调用关系并记录调用耗时,即保留了必要的信息,又将分散的日志事件通过 Span 串联, 帮助我们更好的理解系统的行为、辅助调试和排查性能问题。

  • 日志(Logging):Logging 主要记录一些离散的事件,应用往往通过将定义好格式的日志信息输出到文件,然后用日志收集程序收集起来用于分析和聚合。

故障发现:监控设施

监控设施虽然对程序而言各种各样,但是在我们实际落地过程中,我们也会对网络环境/物理设备/业务系统等等各种可能存在潜在问题的系统进行监控和报警。这里监控系统和报警系统一般是属于监控设施部分,这一部分的选型一般是公司整体选型设计的,这里不额外讨论。这里讨论的具体事项可能需要根据各公司情况自行判定。

我们选型时则是选择了基于Prometheus+Grafana进行业务系统监控,因此需要对应的客户端进行输出结果。Prometheus采用了pull模式,需要开启对应的HTTP端口方便进行采集。官方有一个基础的入门教程,可以用以参考。

故障追溯:分布式追踪

分布式追踪我们目前采用的是Jaeger系统作为分布式追踪系统,我们可以根据用户反馈查看对应系统的请求,发现请求流经的系统和对应的处理时间。

Jaeger提供了各种语言的丰富客户端格式,作为Go编写的应用,自然也提供了Go语言的客户端库

故障追溯:日志

日志采集系统大家常见的包含ELK(EFK),后面更有性能更强资源占用更少的ClickHouse等等作为选择。这里我们重点不讨论外部系统的选择,更重要的将核心集中于常见库的选择中。

在常见日志记录中,同分布式追踪相同,我们通常需要对请求的相关ID进行记录,保证在对对应数据进行分析时,能够尽快筛选对应的请求和相关数据内容。这里我推荐使用rs/zerolog作为日志记录的日志库。虽然我们历史系统中大量的使用了zap作为日志输出驱动,但是在实际开发中,借助zerolog提供的WithContext方法,我们可以有针对性的,对每次请求,部分请求、请求内参数等各方面进行详细定制,方便我们在诊断具体内容时动态调整日志级别、附带每次请求ID等等的需求。

以常见Web框架Gin为例,我们可以将一些具体的请求数据保存至context.Context中,方便我们后续使用:

func RequestID(logger zerolog.Logger) gin.HandlerFunc {
	return func(c *gin.Context) {
		var reqID string
		// 可接受外部RequestID,常见于系统间互相调用
		if c.Request.Header.Get("X-Request-Id") == "" {
			reqID = uuid.New().String()
		} else {
			reqID = c.Request.Header.Get("X-Request-Id")
		}
		ctx := c.Request.Context()
		logger = logger.With().Str("requestID", reqID).Logger()
		// 绑定到context.Context
		ctx = logger.WithContext(ctx)
		c.Request = c.Request.WithContext(ctx)
		c.Next()
		c.Header("X-Request-Id", reqID)
	}
}

在使用过程中,我们需要动态调节日志等级,可以通过中间件或者其他形式控制:

func DebugLevel(config *viper.Viper) gin.HandlerFunc {
	return func(c *gin.Context) {
		if config.GetBool("debug") {
			logger := log.Ctx(c.Request.Context()).Level(zerolog.DebugLevel)
			ctx := logger.WithContext(c.Request.Context())
			c.Request = c.Request.WithContext(ctx)
		}
		c.Next()
	}
}
Built with Hugo
主题 StackJimmy 设计