程序员的工作离不开日志。
日志就像一个笔记本,可以记录程序运行时的一些信息。
日志的作用
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 是 Elasticsearch、Logstash 和 Kibana 的简称,不是单独的一个软件,而是一整套问题的解决方案。Elasticsearch(简称 ES)是全文搜索引擎,能够对海量数据进行存储和高效的搜索。Logstash 是一个数据管道,能够从各种数据源(比如 MySQL 数据库)收集数据,将数据从一处传输到另一处,并加以解析和转换。Kibana 是数据可视化平台,可以将 Elasticsearch 中存储的数据进行展示。在 Kibana 上,我们不仅可以看到所有原始的日志信息,还能够自定义各种精美直观的可视化图表。通常使用 Logstash 统一收集各个机器上的数据,并传输至 Elasticsearch 进行存储,最后通过 Kibana 进行数据展示,之后就可以利用 Kibana 轻松地查看和分析所有的数据了。但是使用 ELK 相当于为系统引入了三个新组件,考虑到系统使用的组件越多,复杂度越高,就越难维护;而且 Logstash 比较重,对 CPU 和内存的占用较高。因此,鱼皮灵机一动,干脆舍弃掉 Logstash,直接将 Elasticsearch 当成数据库来使用。先在 Spring Boot 中整合 Elasticsearch,然后将日志数据通过依赖包提供的 API 接口存储到 Elasticsearch,最后接入 Kibana 进行展示。<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
<version>2.3.4.RELEASE</version>
</dependency>
@Repository
public 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 进行存储。这样就完全不用修改项目的代码!其实非常简单,直接将 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盘、笔记本等
感谢大家支持,鱼皮会继续努力创作优质内容!