查看原文
其他

小记 | 从 0 到 1,看我玩弄千万日志于股掌

鱼皮 程序员鱼皮 2022-08-14

程序员的工作离不开日志。

日志就像一个笔记本,可以记录程序运行时的一些信息。

日志文件
通过日志,我们可以做很多事情。

日志的作用

1. 记录系统和接口的使用情况,比如请求日志
2. 记录和分析用户的行为,比如网站访问日志
3. 调试程序,和控制台的作用类似,但是控制台中的内容并不会保存到文件中,而日志可以长期保存。
4. 帮助我们排查和定位错误。比如在系统抛出异常时,将异常信息记录到日志,可以事后复盘。
5. 通过分析日志还能够优化代码逻辑、提升系统性能、稳定性等。
日志虽然有那么多的作用,但如果数量过多,也会让开发人员感到头疼。对于大型的系统,程序员们经常要看几千、几万行日志,常常看日志看到头晕眼花。
但是,其实处理日志是有很多技巧的,下面鱼皮分享自己和日志的故事。
故事分为 7 个阶段,从看日志看到怀疑人生,再到玩弄千万日志于股掌,鱼皮都做了哪些事情?

鱼皮和日志的爱恨情仇
第一阶段 无日志
刚开始搭建新的系统时,为了图个方便,鱼皮没有给系统接入任何的日志框架,也没有记录任何日志,整个项目非常的干净丝滑。需要调试时就直接用输出函数将信息打印在控制台,出了异常就直接打印堆栈。
// 输出调试System.out.println("value = " + value);// 出现异常catch(Exception e) { e.printStackTrace();}
真是无事一身轻,爽的不得了!
可惜,好景不长。在项目上线之后,突然有一天,系统出问题了,数据查不出来了,同事找上门来了。
鱼皮笑着说:“问题不大!”
然后登录服务器,进入项目目录,瞬间傻眼。
项目目录依旧干净丝滑,原来我特么根本没记日志啊!
这下好了,什么问题都查不出来。乖乖地去给项目加上日志功能吧。

第二阶段 引入日志类库
Java 语言有很多优秀的日志类库,比如 Logback、Log4j2 等,提供了很多记录和打印日志的方法,非常方便。可以直接使用其中一个类库,而无需自己实现。此处因为鱼皮的项目使用 Spring Boot 框架进行开发,直接使用其默认日志库 Logback 即可。
使用方式很简单,先添加 logback.xml 配置文件,主要配置了日志文件的存储路径和格式。Logback 框架还会自动将日志按天进行压缩,并且在一定天数后进行删除,以节约磁盘空间。最大存储天数也可以在配置文件中指定。
配置文件大概长这样:
<?xml version="1.0" encoding="UTF-8"?><configuration scan="true" scanPeriod="60 seconds"> <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>logs/application.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <!-- Daily rollover with compression --> <fileNamePattern>log/application-log-%d{yyyy-MM-dd}.gz</fileNamePattern> <maxHistory>30</maxHistory> </rollingPolicy> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level ${PID:- } ...</pattern> <charset>UTF-8</charset> </encoder> </appender> ...</configuration>
在要打印日志的类上创建一个日志对象:
Logger logger = LoggerFactory.getLogger(MyApp.class);
然后就可以使用该对象去记录日志啦:
catch(Exception e) { logger.error("app error", e);}
加上配置文件后,启动项目,就可以看见生成的日志文件了。欧耶,下次系统再出问题,就不怕缺乏信息来排错啦!
系统运行了一个小时之后,同事又找上门来了,这次鱼皮很有底气,笑着说:“问题不大!”
然后打开日志文件一看,傻眼了,有几千行日志,我怎么知道哪行日志是报错信息呢?
这你能秒了我?直接用 Linux 命令过滤出带 “ERROR” 字段的日志行就行了~
cat application.log | grep 'ERROR'
虽然解决了问题,但是后面每次报错,都要输一遍这个筛选命令,而且随着文件越来越大,命令执行的速度越来越慢了。
能不能把所有错误日志和正常日志区分开,放在不同的文件中呢?

第三阶段 日志分级
幸运的是,一般的日志框架都提供了日志分级存储功能,可以通过修改配置文件来实现。
修改 logback.xml 配置文件,将 ERROR(错误)级别的日志单独输出到 error.log 文件中,实现日志分级:
<configuration ...> <appender name="ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>logs/error.log</file> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>ERROR</level> <onMatch>ACCEPT</onMatch> <onMismatch>DENY</onMismatch> </filter> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level ${PID:- } [%15.15thread] %-50.50logger{50} - %msg%n</pattern> <charset>UTF-8</charset> </encoder> </appender></configuration>
启动项目,日志按预期分级写到了 application.log 和 error.log 两个文件。系统再出现异常时,鱼皮只需打开 error.log 文件,错误信息一览无遗!
系统运行一段时间后,鱼皮上线了一个很重要的服务,记录了相当多的业务日志。虽然目前错误日志可以单独查看,但是核心服务的日志和其他服务的正常日志都堆积在 application.log 中,想要仅查看核心服务的日志依旧要采用命令过滤的方式,比较麻烦。
有没有什么办法,把核心业务的日志单独记录到一个文件中呢?

第四阶段 按类隔离
幸运的是,Logback 日志框架支持将不同的类产生的日志记录到不同的文件中,修改配置文件即可。比如将所有 RequestAOP 类产生的请求日志记录到 request.log 中:
<appender name="REQUEST_HANDLER" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>logs/request.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> ... </rollingPolicy> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level ${PID:- } [%15.15thread] %-50.50logger{50} - %msg%n</pattern> <charset>UTF-8</charset> </encoder></appender><!-- logger 中配置类名 --><logger name="com.yupi.RequestAOP" level="INFO" additivity="false"> <appender-ref ref="REQUEST_HANDLER"/></logger>
启动项目,自动生成了 request.log 文件,打开这个文件,就可以查看所有的请求日志,可以用于流控分析等等,真爽死了!
后来,随着系统的访问量越来越大,单台服务器已经不能满足对并发的需求,因此鱼皮又加了三台机器,共同提供服务。
有一天,系统又出问题了,同事找上门来,鱼皮心想:信不信分分钟给你解决 bug!
一顿操作猛如虎,登录一台服务器查看日志,结果错误日志空空如也,比鱼皮的兜儿都干净。
奇怪了,怎么找不到错误信息?
对啊,现在有四台机器,请求可能落在了其他机器上,因此错误日志也可能在别的机器上!
哎,没办法,一台一台登录服务器去找错误信息吧。
其实四台机器还能忍,但是后来随着并发量的增大,鱼皮负责的系统已经有十台机器了,每次查看日志要依次登录十台机器去找!而且单个日志数据的量已经达到几十万行,无论怎么切分看起来都太累了。
哦,乔治,这太难受了!有没有什么办法,能让我在一个地方集中看日志啊!
要不直接把日志记录到数据库中?
不行不行,日志数据量太大了,数据库肯定存不下。而且写入数据库的速度受到网络传输等限制,比较缓慢。
怎么办啊?算了,先睡一觉。
第五阶段 日志上报与集中式管理
“嘿,少年,你想要力量么?”
“废话,谁不想要!”
“听说过 ELK 么?他会指引你前进的方向。”
鱼皮从梦中惊醒,对啊,可以用 ELK 搭建一个分布式日志收集系统啊!
ELK 是 Elasticsearch、Logstash 和 Kibana 的简称,不是单独的一个软件,而是一整套问题的解决方案
Elasticsearch(简称 ES)是全文搜索引擎,能够对海量数据进行存储和高效的搜索。
Logstash 是一个数据管道,能够从各种数据源(比如 MySQL 数据库)收集数据,将数据从一处传输到另一处,并加以解析和转换。
Kibana 是数据可视化平台,可以将 Elasticsearch 中存储的数据进行展示。在 Kibana 上,我们不仅可以看到所有原始的日志信息,还能够自定义各种精美直观的可视化图表。

通常使用 Logstash 统一收集各个机器上的数据,并传输至 Elasticsearch 进行存储,最后通过 Kibana 进行数据展示,之后就可以利用 Kibana 轻松地查看和分析所有的数据了。
既然可以解决问题,那就接入 ELK 吧~
但是使用 ELK 相当于为系统引入了三个新组件,考虑到系统使用的组件越多,复杂度越高,就越难维护;而且 Logstash 比较重,对 CPU 和内存的占用较高。因此,鱼皮灵机一动,干脆舍弃掉 Logstash,直接将 Elasticsearch 当成数据库来使用。
先在 Spring Boot 中整合 Elasticsearch,然后将日志数据通过依赖包提供的 API 接口存储到 Elasticsearch,最后接入 Kibana 进行展示。
Maven 引入依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-elasticsearch</artifactId> <version>2.3.4.RELEASE</version></dependency>
访问 ES 的接口:
@Repositorypublic interface UserRepository extends ElasticsearchRepository<HouseIndexTemplate, Long> {
}
想法是美好的,现实却是贼特么残酷的。
虽然 Spring Boot 接入 Elasticsearch 的确很方便,但是要把整个项目中的记日志代码全部替换成写入 ES 的代码,对项目的改动和侵入性太大了。而且将日志存入 ES 的耗时远远大于原来异步写入文件的耗时,并发量很大时,偶尔出现日志写入失败的情况。因此改代码改到一半时,鱼皮就抓狂放弃了,直接把改了的代码全部还原。
鱼皮再次陷入沉思,有没有一种方式,可以在不改动一行代码的情况下,将日志写入 ES 呢?

第六阶段 日志代
如果不改动任何代码,每台机器产生的日志仍然是独立记录到当前机器的日志文件中的,想要通过一个界面集中查看各机器上的日志非常麻烦。
那如果把日志文件中的数据自动同步到 ES 上,不就能通过 Kibana 方便地查看了么!
谁来做同步这件事呢?难道要我自己写个定时任务程序把日志文件上传到 ES 上?如果是那样,我还不如继续改原来项目的代码。
能不能找个代理来帮我做这件事呢?
就像我们每天丢辣鸡一样,把辣鸡丢到小区门口的辣鸡桶就行了,然后辣鸡车会帮我们把辣鸡运送至辣鸡站集中处理。
我们的日志文件相当于辣鸡,代理就相当于辣鸡车,而 ES 就相当于辣鸡站。
通过百度,发现 ELK 早就升级为 ElasticStack 了,除了上面说的三大组件外,还多了一个 Beats。
Beats 是轻量级数据采集器,针对不同的数据类型提供了不同的组件。
要将日志文件数据上传到 ES 进行存储,可以使用 Filebeat。Filebeat 是轻量型日志采集器,其提供了一种轻量型方法,用于转发和汇总日志与文件,让我们轻松面对成百上千、甚至成千上万的服务器、虚拟机和容器生成的日志。
Filebeat 就相当于一个代理(agent),可以帮助收集各个机器上的日志,然后传输给 Logstash 进行处理或者直接传输到 Elasticsearch 进行存储。这样就完全不用修改项目的代码!
ElasticStack 整体架构如下:

那怎么使用 Filebeat 呢?
其实非常简单,直接将 Filebeat 安装到日志文件所在的服务器上,然后在其配置文件中定义输入(要采集的日志文件路径)和输出(要将采集到的数据发送到哪里)即可。比如在下面的配置中,会采集 system 日志并传输给 Logstash:
filebeat.inputs:- type: log paths: - /var/log/system.log
output.logstash: hosts: ["127.0.0.1:5044"]
搞定,这下真是爽死了!经几千行日志就能将我淹没,不知所措。而现在,只需要打开 Kibana 控制台,动动手指,就能轻松地查看和分析几十万、几百万的日志。
感觉自己就像一个大将军,这些日志是我统治的小兵,都得乖乖听我号令,好不痛快!

第七阶段 完善日志架构
利用 ElasticStack,已经能够轻松地集中管理海量的日志,而且 Elasticsearch 支持水平扩容,可以应对日志量级的持续增大,存个千万条数据完全没有问题。
但是,当每秒产生的日志量过多时,ElasticStack 并不是无敌的,虽然 Filebeat、Elasticsearch、Kibana 都很强劲,但往往 Logstash 是那阿喀琉斯之踵(或者三娃的屁股)!
因为 Logstash 要同时接受多个 Filebeat 采集的日志,机器越多,部署的 Filebeat 也就越多,Logstash 的压力就会越大。虽然也可以像扩容 ES 一样增加 Logstash 的节点数,但是并不能从根本上解决问题,当日志量级增大到一定程度时,不仅是 Logstash,连 ES 集群都有可能撑不住!
因此,我们需要接入一些中间件来进行缓冲,首选的可靠且高性能的消息队列 Kafka(依赖分布式协调工具 Zookeeper)。
最终,完善的分布式日志收集系统架构如下:


至此,鱼皮终于将千万日志玩弄于股掌,这种感觉真的是太爽了。
如果你也正在被日志折磨,一定要试着搭建一套完善的日志系统。

最后分享自己记录日志的经验:
1. 不要过度依赖日志,什么都记,日志应当简洁明晰,具有实际价值。
2. 在保证可理解的同时适当减少日志的长度,比如把 this is an apple 简化为 apple。
3. 将日志进行分级和分类,仅在开发和测试环境输出 DEBUG 级别日志,不要在生产环境中使用。
4. 统一日志的格式,便于后续处理分析,通常在日志框架配置即可。
5. 不要把日志当成存储数据的工具!注意日志信息中不能出现敏感信息,也不要对外公开!

鱼皮从 0 到 1,经历了七个阶段,成功地玩弄千万日志于股掌。其实,无论是学习还是实际应用,我们都需要有这种持续实践、探索和优化的精神
会当凌绝顶,一览众山小。

点击下方阅读原文查看官方 ElasticStack 文档。

往期推荐

求职 | 秋招签了字节跳动,分享下经验

编程百晓生 | 啥是数据库?

小记 | 再次登上学生时代的个人博客,泪目

小记 | 一周上线百万级高并发系统

编程百晓生 | 我竟然被狗狗币勒索了?



鱼皮送福利啦

10 月 31 日起 15 天内,鱼皮将为文章阅读数、分享数、留言数最多的读者送上企鹅周边~ 并抽取几名粉丝送出随机小礼品!


阅读最多:超长鼠标垫

分享最多:QQ长鹅

留言最多:QQ小公仔

幸运奖:腾讯云U盘、笔记本等


感谢大家支持,鱼皮会继续努力创作优质内容!


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存