光政的博客 2024年07月09日
日志的艺术
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

文章探讨了日志在开发和运维中的重要性,包括日志的作用、级别、规范、记录场景、属性、安全、可观测性等方面,以及应对日志管理难题的方案。

📝日志的作用:是调试助手和生产环境救星,可通过查询日志确定代码分支走向、API请求是否正确等,但日志打印需平衡,避免过多或过少带来的问题。

📋日志级别:常见级别从低到高为TRACE、DEBUG、INFO、WARN、ERROR、FATAL。本地环境常配置Trace级别,生产环境会根据情况调整,且存在一些复杂场景需注意。

📄日志记录:开发人员应根据业务和代码自行判断记录日志的时机,文中建议在多种场景下添加日志,如调用第三方系统、重要核心业务等。

🔒日志安全:需保证日志框架安全,不泄露敏感信息,确保符合公司隐私政策和合规性要求,同时注意日志的坏味道,如非英文打印、无上下文等。

👀日志的可观测性:随着系统复杂,日志管理需求变化,介绍了收集、结构化、存储与查询等方面的内容,如Elasticsearch在日志存储领域的优势及缺陷。

理解日志并不是一件容易的事,开发人员在编写代码之时往往会纠结在某处打印的日志是不是有意义的,而 SRE 在面对缺少日志的生产问题时往往一筹莫展,Ops 在对面海量日志时往往需要花费更多的精力来维护,而项目的实际管理者在面对毫无实际业务价值的日志时,往往不想花费太多的人力和财力去管理它。

因此,在开发应用程序时遵循良好的实践,在收集管理日志时选用成熟的方案,往往能让这些矛盾得以缓解,这也就有了这一篇的分享。

矛盾的开始

首先介绍的是为什么需要记录日志,日志的作用。其实关于日志的作用无需介绍太多,因为大多数的开发人员在调试代码问题时,解决不同环境的 Bug 时都有很明确的感受以及强烈的需求。日志作为调试的助手,生产环境的救星。笔者只见过嫌弃日志打的太少的,几乎没有见过嫌弃日志打的太多的开发和运维人员。通过查询日志的方式来确定代码的分支走向,API是否请求正确,核心业务的数据是否正确,是否有错误的堆栈信息,这些都构成开发和运维人员判断代码和生产问题的第一手段。笔者难以想象如果一个复杂庞大的系统没有记录任何日志,该如何排查生产环境的 Bug。

如果有如此强烈的需求,那每一行代码都打日志来记录上下文不就行了吗?这样无论什么环境有什么代码有问题,通过搜索日志都可以排查出来。理论上这样确实可行,但是有一些问题目前无法解决,一是日志存储量的问题,常见的中大型系统日志大概在 TB 级,超大型系统的日志大概在 PB 级,根据 Cloudflare提供的数据,它每秒大概处理 4 千万的请求,这对于存储的费用来讲是一个巨大的挑战。二是搜索的性能下降,像 Elasticsearch 数据库这类常见的日志存储方案,海量的日志会导致其所维护的映射关系爆炸式增长,即使划分不同的 Index,分布式管理不同的 ElasticSearch 集群,也很难做到搜索性能不随数据量的增加而下降。三是海量日志的生成会在峰期时拖慢系统性能,增大出故障的风险。

所以综上可以得出最简单的结论,即日志既不能打印太多导致存储和管理日志太难,也不应该因为打印太少导致运维人员无法排查问题,这听起来自相矛盾,但这就是关于日志的艺术!

日志级别

通常在我们打算记录日志前,我们需要判断打印日志的级别,在介绍如何确定日志级别前,我们先聊一聊日志级别的作用。首先第一点是大家所熟知的,日志级别可以帮助确定日志信息的优先级,可以有效的减少信息噪音和警报疲劳。二是在查询日志时,加到日志级别的过滤,能有效的查询到所需的日志。

常见的日志级别有以下几类,并且从低到高的顺序是:TRACEDEBUGINFOWARNERRORFATAL。通常在本地环境,我们打印日志所配置的级别是 Trace ,即打印出所有级别的日志,但是在生产环境,会随着存储的压力,查询的性能和磁盘 IO 的影响,将日志的级别调整到 INFO 级别甚至是 ERROR 级别。这就意味着你在代码中低于该级别的日志都不会打印。

那这是不是代表我们在代码中只需要打印和生产环境匹配的日志级别呢?例如生产环境只打印 INFO 级别的日志,我们在代码中写 DEBUG 级别的日志并不会打印,也没有必要写了。有的项目管理者为了省下日志管理的费用,只在生产环境只打印 ERROR 级别的日志,那是不是 INFO 级别的日志也可以不写呢?代码中只打印和生产环境匹配的日志级别日志,这确实是笔者在大多数项目时看到的现状,但是现实情况也往往更加复杂,例如以下几个场景。

场景一

某工程师在调查生产环境中某个创建资源的 API 性能较低问题时,发现是由于该 API 在保存资源前做了写 INFO 级别的日志,将资源对象都写到日志中,由于资源的对象属性很多,所以导致在业务峰期时,代码打印出海量日志,耗尽Buffer区内存,从而拖慢主线程,造成服务性能整体下降。因此该工程师将该业务日志打印操作删除,以降低生产环境磁盘 IO 损耗,解决性能问题。

但是某天由于修改了该 API 服务调用链路上的某服务代码,导致该 API 创建出来的对象有错误,并且由于缺少了生产环境保存该资源时的日志,无法排查出是 API 的请求参数有问题,还是后续的计算逻辑有问题。这时我们只能重新修改日志级别,重新构建发布上线吗?

场景二

假设将生产环境的日志设置为 ERROR 级别。某一时刻,依赖的下游服务故障,导致请求大量超时。又由于在业务峰期 QPS 非常高的时期,短时间内集中产生大量的错误日志,导致磁盘IO急剧提高,耗费大量CPU,进而导致整个服务瘫痪。我们应该如何处理?

场景三

某工程师在排查生产问题时,发现 INFO 级别的日志还无法满足排查 Root Cause,有一个 DEBUG 日志级别的日志是他需要的,但是生产环境只有 INFO 级别,这时只能修改级别然后重新启动服务吗?

日志级别规范与动态调整

解决以上问题的方法,一是需要我们在项目中,明确日志级别的规范,不为了调试方便和减少存储随意使用日志级别。二是给日志级别加上动态调整的功能。也就是需要解决线上问题时,调低线上日志输出级别,获取全面的Debug日志,帮助工程师提高定位问题的效率。在生产日志海量增加拖慢服务性能时,调高线上日志输出级别,减少日志的生成,缓解磁盘 IO 压力和提高服务性能。

以下是对于日志级别给出的建议:

记录日志

什么时候记录日志

什么时候记录日志并没有标准规范,需要开发人员根据业务和代码来自行判断,除了常规的记录事件,例如进行了哪些操作、发生了与预期不符的情况、运行期间出现未能处理的异常或警告、定期自动执行的任务外。笔者还建议在以下场景加上日志:

日志属性

除了在日志常规需要打印的 log leveltimestampmessageexceptionstack trace 外,排查问题往往还需要其它的字段来帮助定位和查找 Root Cause,常见的额外字段有以下几种:

日志安全

日志需要保证日志框架的安全和敏感信息处理。框架安全指的是使用成熟的,经过大量生产环境验证的日志框架库,而非自己造轮子。敏感信息处理是大部分公司的生命线,请牢记日志的安全性和合规性要求:

日志的坏味道

日志的可观测性

最近十年因为微服务和云原生的相继崛起,收集存储和分析日志领域发生了重大的变化。早期我们无需进行日志的收集,当时将单体服务的所有日志存储在文件当中,使用 tail、grep、awk 来从日志中挖掘信息。但是在系统日益复杂的今天,这种方式已经无法满足我们的需求。为了应对日益复杂的日志管理需求,开源社区和工业界也发展出一些列的方案,例如最为流行的 Elastic Stack 开源解决方案,云厂商提供的一站式解决方案像 AWS DataDogAzure Dynatrace

无论使用哪种方案,日志管理都已经不再是一个简单的话题。在我们有明确感知的打印日志和查询分析日志之间,还包含着对日志进行收集、缓冲、聚合、加工、索引、存储等若干个步骤,并且每一步都蕴含着艰难曲折。

日志的收集

最早我们使用 Elastic Stack 中的 Logstash 来进行日志的收集和加工。系统中不同的服务通过使用 tcp/udp 的协议,主动发送请求将日志推送到 Logstash 中,接着 Logstash 将日志进行转换加工(数据结构化)和输出。这种模式维持了很长一段时间,但是它也有比较严重的缺陷,那就是 Logstash 与它的插件是基于 JRuby 编写的,要跑在单独的 Java 虚拟机进程上,默认的堆大小就到了 1GB。如果需要部署成千上万个日志收集器,那么这种方案就显得太过沉重。所以后来 Elastic.co 公司使用 Golang 重写了一个功能较少,却更轻量高效的日志收集器 Filebeat 才缓解了这一矛盾。

除此之外 Fluentd 通常是配合 Kubernetes 时的首选开源日志收集器。它是 Kubernetes 原生的,可以使用 DaemonSet 的方式部署与 Kubernetes 无缝集成。它允许从不同的地方像 Kubernetes 集群、MySQL、Apache2 等收集日志,并解析发送到所需位置如 Elasticsearch、Amazon S3 等。Fluentd 用 Ruby 编写的,在低容量下运行良好,但当需要增加节点和应用程序的数量时,也会有会性能问题。

最后在日志收集时,有可能会因为业务峰期生成海量日志,影响服务稳定性和造成日志丢失。在这种情况下,我们还需要在 Logstash 或者存储日志数据库前加上一道缓冲层。在较小规模的系统中 Redis streams 是一个较好的选择,如果面对的规模更大的数据,那么 Kafka 集群或者云厂商提供的消息队列解决方案将是不二之选。

结构化日志

在收集完日志后,我们还需要进行结构化的处理。因为日志是非结构化数据,一行日志中通常会包含多项信息,如果不做处理,那只能以全文检索的原始方式去使用日志,这样既不利于统计对比,也不利于条件过滤。像下面这一行是 Nginx 服务器的 Access Log。

10.209.21.28 - - [04/Mar/2023:18:12:11 +0800] "GET /index.html HTTP/1.1" 200 1314 "https://guangzhengli.com" Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36

尽管我们已经习惯了默认的 Nginx 格式,但上面的示例仍然难以阅读和处理。我们可以通过 Logstash 或者其它工具将它转换成结构化的数据,例如 JSON 格式。

{ "RemoteIp": "10.209.21.28", "RemoteUser": null, "Datetime": "04/Mar/2023:10:49:21 +0800", "Method": "GET", "URL": "/index.html", "Protocol": "HTTP/1.1", "Status": 200, "Size": 1314, "Refer": "https://guangzhengli.com", "Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36"}

经过结构化后,例如像 ElasticSearch 这类倒排索引数据库可以针对不同的数据项建立索引,进行查询统计、聚合等操作。

除此之外还有一种工业界的做法像 Splunk 推荐将字段变为 key-value 对的形式放在同一个大的规范日志行中(logfmt),如将 Nginx 日志作为规范日志行将变成如下这样:

remote-ip=10.209.21.28 remote-user=null datetime="04/Mar/2023:10:49:21 +0800" method=GET url=https://guangzhengli.com/index.html protocol=HTTP/1.1 status=200 size=1314 refer=https://guangzhengli.com agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36"

这类数据经过 Splunk 存储后,可以通过使用内置的查询语言进行检索,像使用 method=get status=500 查询所有返回 500 响应的 GET 方法。使用 method=get method=get status=500 earliest=-7d | timechart count 查询语句得到过去 7 天返回 500 响应的 GET 方法总数量和图表。这种方式笔者使用的并不多,所以感兴趣的可以参考 Stripe canonical-log-lines 这篇文章获得更多细节。

日志的存储与查询

经过日志的数据结构化后,可以将数据存入数据库中并进行查询分析。在选择使用什么方案来存储和分析之前,我们先来看看日志数据的特点。

基于以上的存储和查询需求 Elasticsearch 在日志存储领域一骑绝尘,Elasticsearch 可以使用时间范围作为索引,例如根据实际数据量以按日索引的话,由于能准确地预知明天、后天的日期,因此全部索引都可以预先创建,这免去了动态创建的寻找节点、创建分片、在集群中广播变动信息等开销。其次 Elasticsearch 可以针对不同时间范围的数据,进行冷热数据处理,针对最近一段时间的日志数据,在硬件中搭配 SSD 和更强的处理器,针对时间更久的数据,可以使用 HDD 存储或者 AWS S3 进行归档。

最重要的是 Elasticsearch 的核心存储引擎使用倒排索引天生适配全文检索,并且与其搭配的 Kibana(只负责图形界面和展示) 可以完美适配数据检索、聚合、统计和定制形成各种图形、表格。下图为 Elastic Stack 官方图片:

当然 Elasticsearch 也不是没有缺陷的,Mapping Explosion 就是 Elasticsearch 已知的缺陷之一,Elasticsearch 维护着一个文档 (document) 和字段 (fields) 的映射表,当这个映射表拥有太多的 key 时,会占用大量的内存,导致频繁的垃圾回收。还有它的次冷热分层存储也有其缺陷之处,Elasticsearch 每天都会将数据从热存储移动到冷存储,这也会影响集群的读写性能。

所以 Elasticsearch 对于一些中小型集群和全文检索有很好的支持,但是对于超大型集群和面向分析的场景,使用面向列的数据库 ClickHouse 可能会是一个更好的选择。日志的不可变性完美的适合 ClickHouse 的存储和分析能力,搭配 ClickHouse 压缩编解码器可以大幅度减少存储需求,其线性可扩展性可以轻松扩展集群。关于更多使用 ClickHouse 存储日志可以看这篇文章:Log analytics using ClickHouse。商业开源公司 Signoz 也将 ClickHouse 作为其 OpenTelemetry 实现方案。下图展示的即 Signoz 的实时日志分析场景

总结

回顾整篇文章,我们主要涵盖了打印日志的最佳实践,管理日志的难题和应对挑战的方案。日志作为系统服务最重要的观测手段之一,其重要性不言而喻。在系统日益复杂的今天,开发和运维人员应遵循良好的日志实践,以此来应对日志管理的挑战。

由于笔者能力有限,以上内容如果有任何错误或者片面的观点,还请直接指出。如果有补充的地方,欢迎在评论或者仓库进行补充,非常感谢!

Reference

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

日志 日志级别 日志记录 日志安全 日志可观测性
相关文章