微服务可观察性之日志

可观测性三角

Venn diagram with gradient

  • Metrics:度量数据,这些数据应该是可以用来做分析的,例如均值,99线等。对于一个请求来说,可以度量的东西很多:
  • Tracing:强大的是请求级别,即一个请求发生了什么;
  • Logging:强调的记录发生了某个事情,一个请求内部会发生很多事情;

日志级别

https://github.com/golang/glog ,是 google 提供的一个不维护的日志库,glog 有其他语言的一些版本,可以起到很大的借鉴作用。

1
2
3
4
if glog.V(2) {
glog.Info("Starting transaction...")
}
glog.V(2).Infoln("Processed", nItems, "elements")

通过 V(2) 打印不同层级的日志,避免出现由于打印日志过多影响性能。

它包含如下日志级别:

  • Info
  • Warning
  • Error
  • Fatal(会中断程序执行)

还有类似 log4gologgozap 等其他第三方日志库,他们还提供了设置日志级别的可见行,一般提供日志级别:

  • Trace
  • Debug
  • Info
  • Warning
  • Error
  • Critical

各个级别的定义和使用规范:

Warning

没人看告警,因为从定义上将,没有什么出错。也许将来会出问题,但这听起来像是别人的问题。我们尽可能的消除警告级别,它要么是一条消息性消息,那么是一个错误。我们参考 Go 语言设计的哲学,所有警告都是错误,其他语言的 warning 都可以忽略,除非 IDE 或者在 CICD 流程中强制他们为 error,然后逼着程序员们尽可能去消除。同样的,如果想要最终消除 warning 可以记录为 error,让代码作者重视起来。

Fatal

记录消息后,直接调用 os.Exit(1),这意味着:

  • 在其他 goroutine defer 语句不会被执行;
  • 各种 buffers 不会被 flush,包括日志的;
  • 临时文件或者目录不会被移除;

不要使用 fatal 记录日志,而是面向调用者返回错误。如果错误一直持续到 main.mainmain.main 就是在退出之前做处理任何清理操作的正确位置。

Error

也有很多人,在错误发生的地方要立马记录日志,尤其要使用 error 级别记录。

  • 处理 error
  • error 抛给调用者,在顶部打印日志;

如果选择通过日志记录来处理错误,那么根据定义,它不再是一个错误 - 您已经处理了它。记录错误的行为会处理错误,因此不再适合将其记录为错误。

image-20231130152007930

要么打印,要么往上抛。

image-20231130152012928

这里降级了,就属于正常状态。

这里产生了降级行为,本质属于有损服务,我更倾向于这里使用 Warning

Debug

相信只有两种事件应该记录:

  • 开发人员在开发或调试软件时关心的事情;
  • 用户在使用软件时关心的事情;

显示,它们分别是调试和信息级别。

log.Info 只需要将该行写入日志输出。不应该有关闭它的选项,因为用户只应该被告知对他们有用的事情。如果发生了一个无法处理的错误,它就会抛出到 main.mainmain.main 程序终止的地方。在最后的日志消息前面插入 fatal 前缀,或者直接写入 os.Stderr

log.Debug,是完全不同的事情。它由开发人员或支持工程师控制。在开发过程中,调试语句应该是丰富的,而不必求助于 tracedebug2(开发者知道)级别。日志包应该支持细粒度控制,以启用或禁用调试,并且只在包或更精细的范围内启用或禁用调试语句。

推荐 kratos 的设计和思考:https://github.com/go-kratos/kratos/tree/main/log

  • 没有具体的日志实现,只有抽象
  • log.Init(...) 初始化抽象,然后使用 interface
  • 通过对象,来调用 interface 注入不同的 level

Logger

package 使用的时候

1
2
3
4
5
package foo

import "mylogger"

var log = mylogger.GetLogger("github.com/project/foo")
  • foo 耦合了 mylogger
  • 所有使用 foo 的其他库,被透明依赖了 mylogger

当我们使用 kit

1
2
3
4
5
6
7
package foo

import "github.com/pkg/log"

type T struct {
logger log.Logger
}
image-20231130155603276

解耦需要打日志的类型与日志的实际类型之间的绑定。

日志选型

一个完整的集中式日志系统,需要包含以下几个主要特点:

  • 收集 - 能够采集多种来源的日志数据;
  • 传输 - 能够稳定的把日志传输到中央系统;
  • 存储 - 如何存储日志数据;
  • 分析 - 可以支持 UI 分析;
  • 警告 - 能够提供错误报告,监控机制;

ELK stack

image-20231130160401517

开源界鼎鼎大名 ELK stack,分别表示:ElasticSearchLogstashKibana,它们都是开源软件。新增一个 FileBeat,它是一个轻量级的日志收集处理工具(Agent),FileBeat 占用资源少,适合于在各个服务器上收集日志后传输给 Logstash,官方也推荐此工具。

image-20231130175503236

此架构由 Logstash 分布于各个节点上(以 Logstash Agent 的形式)搜集相关日志、数据,并经过分析、过滤后发送给远端服务器上的 Elasticsearch 进行存储。

Elasticsearch 将数据以分片的形式压缩存储并提供多种 API 供用户查询,操作。用户亦可以更直观的通过配置 Kibana Web 方便的对日志查询,并根据数据生成报表。

因为 logstatsh 属于 server 角色,必然出现流量集中式的热点问题,因此我们不建议使用这种部署方式,同时因为还需要做大量的 match 操作(格式化日志),消耗的 CPU 也很多,不利于 scale out

引入消息队列

为了解决Logstash Agent 的热点和维护性问题,可以将 logstash Agent 退化到本地微服务中

image-20231130175623920

此种架构引入了消息队列机制,位于各个节点上的 Logstath Agent 先将数据、日志传递给Kafka,并将队列中消息或数据间接传递给 LogstashLogstash 过滤、分析后将数据传递给 Elasticsearch 存储。最后由 Kibana 将日志和数据呈现给用户。因为引入了 Kafka,所以即使远端 Logstash server因故障停止运行,数据将会先被存储下来,从而避免数据丢失。

image-20231201173021618

更进一步的:

将收集端 logstash 替换为 beats,更灵活,消耗资源更少,扩展性更强。替代 JRuby 语言,使用 beats 外挂服务或者 sidercar

日志系统

基于上面的日志选型,设计一个符合高并发大容量的日志系统。

设计目标

  • 接入方式收敛;
  • 日志格式规范;
  • 日志解析对日志系统透明;
  • 系统高吞吐、低延迟;
  • 系统高可用、容量可扩展、高可运维性;

日志规范

一般使用 JSON 作为日志的输出格式(相比protocbufjson 更加广泛,但是前者性能更好):

  • time:日志产生时间,ISO8601 格式;
  • level:日志等级,ERRORWARNINFODEBUG
  • app_id:应用id,用于标示日志来源;也就是服务发现的 app_id
  • instance_id:实例id,用于区分同一应用不同实例,即 hostname

image-20231201174102166

设计与实现

日志从产生到可检索,经历几个阶段:

  • 生产 & 采集
  • 传输 & 切分
  • 存储 & 检索

采集

logstash

  • 监听 tcp/udp
  • 适用于通过网络上报日志的方式

filebeat

  • 直接采集本地生成的日志文件(这种比较简单和稳定)
  • 适用于日志无法定制化输出的应用

logagent

  • 物理机部署,监听 unixsocket,进程间通讯
  • 日志系统提供各种语言 SDK
  • 直接读取本地日志文件

image-20231201174418357

image-20231201174911511

传输

基于 Flume + Kafka 统一传输平台

基于 LogID 做日志分流:

  1. 一般级别
  2. 低级别
  3. 高级别(ERROR

大流量的环境下,可以使用 Flink + Kafka 来实现。

也可以通过 gateway 来将日志路由到不同的 Kafka 集群中。

image-20231201175010120

切分

kafka 消费日志,解析日志,写入 elasticsearch

image-20231201175208534

消费端:

bili-index:B站自研,golang 开发,逻辑简单,性能高,可定制化方便。(后续也会替换成 flink

  • 日志规范产生的日志(log agent 收集)

logstash:es 官方组件,基于 jruby 开发,功能强大,资源消耗高,性能低。

  • 处理未按照日志规范产生的日志(filebeatlogstash 收集),需配置各种日志解析规则。

存储和检索

存储和检索都是基于 elasticsearch 实现,es 多集群架构:

  • 日志分级、高可用

image-20231201175638694

单数据集群内:

master node + data node(hot/stale) + client node

  • 每日固定时间进行热 -> 冷迁移;热数据存储到 ssd 上,冷数据迁移到 hdd
  • index 提前一天创建,基于 template 进行 mapping 管理
  • 检索基于 kibana

文件

使用自定义协议,对于 SDK 质量、版本升级都有比较高的要求,因此我们长期会使用本地文件的方案实现:

  • 采集本地日志文件:位置不限,容器内 or 物理机

  • 配置自描述:不做中心化配置,配置由 app/paas 自身提供,agent 读取配置并生效

  • 日志不重不丢:多级队列,能够稳定地处理日志收集过程中各种异常

  • 可监控:实施监控运行状态

  • 完善的自我保护机制:限制自身对于宿主机资源的消耗,限制发送速度

容器日志采集

容器内应用日志采集:

基于 overlay2,直接从物理机上查找对应日志文件(容器日志映射到物理机目录)。

image-20231201180255277

观测内容

在服务上线后,应该观测的内容:

  • 错误率:特别是系统级别,还有业务层面的报错,下游服务返回的一些校验之类的错误
  • 平均响应时间
  • 99线或者999线
  • 平均响应时间和99线的差值:这个可以判断服务是否稳定

上线前后:

  • 要注意响应时间有没有大幅增加
  • 错误率有没有大幅增加
  • 有没有出现新的错误(是否有业务代码异常导致新的错误,也可能由于是下游服务返回)
  • 日志里面如果有 WARNWARN 有没有大幅增加(还有ERROR之类的,也需要观察正常的日志是否有产生,INFO级别)

便于观察时,就非常依赖错误码的设计和使用。

References

Metrics, tracing, and logging

Let’s talk about logging

Using The Log Package In Go

Design Philosophy On Logging

The package level logger anti pattern

Logtail采集概述

Logtail提升采集性能

Logtail技术分享(一) : Polling + Inotify 组合下的日志保序采集方案

Logtail技术分享(二) : 多租户隔离技术+双十一实战效果

elastic-stack

ELK原理与介绍

Filebeat 轻量型日志采集器

Filebeat Reference

Logstash 集中、转换和存储数据

Logstash Reference

Kibana 将数据转变为结果、响应和解决方案

Kibana Guide

Elasticsearch Guide

elasticsearch.cn

Graylog 日志系统架构