log4j 和 log4j2 在springboot中的性能对比
文章链接: https://pengcheng.site/2019/11/17/log4j-he-log4j2-zai-springboot-zhong-de-xing-neng-dui-bi/
前言
在 java 项目中最常用的三大日志框架是logback
,log4j
,log4j2
。其中logback
是 springboot 的默认框架。由于历史原因,我手上有个springboot项目的日志框架是用的log4j
。在对某个接口进行压测和性能优化的时候发现,打印同步日志会导致接口的性能缩水,所以当时直接把性能要求高的接口的日志关闭掉了。随着系统的逐渐复杂,重要性越来越高,定位问题的难度越来越大,我逐渐感觉到日志的的重要性。所以优化日志势在必行。
我是从两个方面来考虑优化日志这个问题的。
- 更换性能更高的日志框架;
- 使用异步日志或者延时刷盘的日志配置。
对第一个问题,很容易就可以Google到,目前性能表现最好的日志框架是log4j2
,所以直接把日志框架替换成log4j2
就可以了。
对于第二个问题,因为当前日志是同步的,需要把“打印日志”这个操作完成之后才会运行接下来的业务代码,而“打印日志”通常是要输出到控制台或者文件中的,IO开销很大,如果把这个过程变成异步的应该能从一定程度上提高性能表现。
不知道在哪听到的两句话,想在这里分享一下:
- 没有日志的系统就像在裸奔
- 没有uid的日志是没有灵魂的
PS:这里没有uid的本来是指是调用方的id,更一般的说法就是不能定位问题的日志是没有存在价值的。
环境
- springboot 2.1.5
- java 8
- MacBook Pro 2017
log4j日志
log4j 在 springboot 中的配置
在 springboot 中使用 log4j 需要引入其依赖并把 springboot 自带的日志框架 logback 排除掉:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j</artifactId>
<version>1.3.8.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
然后编写配置文件log4j.properties
:
log4j.rootLogger=INFO,stdout,file
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} [%p] %c:[%L] %m%n
log4j.appender.file=org.apache.log4j.DailyRollingFileAppender
log4j.appender.file.File=logs/seller_penalty_api_server.log
log4j.appender.file.Append=true
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} [%p] %c:[%L] %m%n
性能测试
知己知彼,百战不殆。
要对比性能,首先就要知道原来的log4j
框架的性能表现如何。
这里我使用了一种简单的方式来进行对比。在 springboot 中实现两个简单的接口,一个打印一行日志,一个不打印日志,然后用wrk
工具对接口进行压测,看两个接口能够达到的QPS分别是多少。
// 不打印日志
@GetMapping("/ping")
public String ping() {
return "pang";
}
// 打印日志
@GetMapping("pingLog")
public String pingWithLog() {
log.info("calling /pingLog");
return "pangLog";
}
压测配置如下,分别对两个接口进行多次压测,取三次作为采样结果。
wrk -t10 -c250 -d30s http://localhost:8080/ping
采样次数 | 1 | 2 | 3 |
---|---|---|---|
不打印日志 | 21937.40 | 23570.31 | 22950.31 |
打印日志 | 7825.40 | 7848.33 | 7788.54 |
可以看到两种情况基本上有将近三倍的差距。我是用nohup后台运行的,如果直接使用java -jar
运行,日志输出到文件的同时还会输出到控制台,这种情况QPS将进一步降低,大概在5k+左右。这也正是我们最开始为了接口性能选择关闭日志的原因
log4j2
引入log4j2(同log4j一样,要排除 springboot 自带的日志框架):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<!-- 增加识别yaml格式依赖,加上这个才能辨认到log4j2.yml文件 -->
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
</dependency>
然后在src/main/resources/
路径中编写log4j2
的配置文件。默认配置文件名是log4j2
,支持xml、properties、json、yaml多种文件格式,这里我选了yml,所以需要额外引入jackson-dataformat-yaml
的包。
Configuration:
status: debug
appenders: # 包含控制台和文件两个appender
Console:
name: LogToConsole
PatternLayout:
Pattern: "%d{yyyy-MM-dd HH:mm:ss} [%p] %c:[%L] %m%n"
RollingFile:
- name: LogToRollingFile
fileName: logs/seller_penalty_api_server.log
filePattern: "logs/seller_penalty_api_server.log.%d{yyyy-MM-dd}.gz" # 带有后缀名会自动压缩
PatternLayout:
pattern: "%d{yyyy-MM-dd HH:mm:ss} [%p] %c:[%L] %m%n"
Policies:
TimeBasedTriggeringPolicy: # 基于时间的trigger,每天一个文件
interval: 1
Loggers:
logger:
- name: io.shopee.bigdata.penalty.server
level: debug
additivity: false
AppenderRef:
- ref: LogToConsole
- ref: LogToRollingFile
Root:
level: error
AppenderRef:
ref: LogToConsole
使用log4j2
之后,使用wrk
和同样的参数压测(关掉控制台输入),三次测试的QPS分别是:
采样次 | 1 | 2 | 3 |
---|---|---|---|
QPS | 13992.50 | 15562.27 | 15161.16 |
可以看到在这个简单的场景下,性能几乎是log4j的2到3倍之多!所以很明显,首先替换日至框架是一个明智的选择。
log4j2 异步日志
到目前为止,上述的测试都是针对同步日志进行的测试。在确定了log4j2
作为日志框架之后,我想看一下,使用异步日志能不能进一步提高性能表现。
AsyncAppender 和 Asynchronous Loggers
在log4j2
中异步日志有两种实现AsyncAppender
和 Asynchronous Loggers
前者是 log4j2
最开始的异步日志实现,它把其他 Appender 作为输入,然后把产生 logEvent
输出到默认的容器ArrayBlockingQueue
中,然后使用另外一个线程中来输出日志以实现异步。但是官方文档也指出:
在这种多线程应用的实践中需要主要:阻塞队列很容易发生锁争用,测试表明当大量线程并发写日志的时候,性能甚至会变得更糟糕。所以应该考虑使用无锁的Asyn Loggers进行优化。
上述的Asyn Loggers
是log42
团队后来才加入的异步实现,连官方文档都推荐它了,那还有什么好犹豫的呢。它需要引入一个额外的依赖:
<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<version>3.4.2</version>
</dependency>
Disruptor
通过CAS而不是锁实现多生产者、多消费者对RingBuffer的并发访问,实现高吞吐和高性能。
它们两者的具体区别可以参考参考资料2
全异步和混合异步
Log4j2的异步日志的使用又可以分为全异步和混合异步,所谓全异步就是所有的日志都以异步方式输出;混合异步就是只有指定的代码文件用异步方式输出,其他日志使用同步方式输出;前者能够获得更高的性能,也是官方推荐的方式,后者具有更好的灵活性,可以进行针对性配置。
本文中我们直接使用全异步日志。
在springboot中开启全异步的方式很简单,只需要在配置文件中加如下配置(yml):
log4j2:
contextSelector: org.apache.logging.log4j.core.async.AsyncLoggerContextSelector
或者在用java -jar
启动的时候加入系统参数:
-DLog4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector
应用启动之后按照上述的方法测试接口性能:
采样次 | 1 | 2 | 3 |
---|---|---|---|
QPS | 16238.76 | 16006.78 | 16322.12 |
可以看到把日志从同步改成异步之后,性能有一定提升,但是并没有想象中那么大。
总结
- 同步日志和业务逻辑在同一个线程中,当日志输出完毕才能执行接下来的代码,涉及到IO,非常影响性能;
log4j2
在本文的简单场景中性能要远远高于log4j
,这次替换是值得的;log4j2
异步日志应该使用Asynchronous Loggers
而不是AsyncAppender
- 本次测试是基于一个很简单的场景,在官方文档中,异步日志的性能要远远超过同步日志的,但是在本次测试中并没有体现出来;日志在 springboot 中的性能可能受多个方面的影响,比如每个调用链中日志的条数、springboot本身的配置,日志的level、硬件环境等。
- 项目代码见Github
彩蛋1-性能测试需要注意的点
关于性能测试,在 log4j2 的官方文档中提到的几点我觉得是比较通用的:
- 获取性能的采样结果之前需要 warm up the JVM;
- 重复热身多次,然后等待IO线程赶上并释放缓冲区;
- 测试的时候重复多次取平均数。
总结一下就是系统刚启动的一段时间的测试数据不能作为参考,需要压测一段时间,取稳定之后的结果。就像运动前的热身一样,不光JVM,各种线程池或连接池都需要一段时间的请求才能启动起来进入正常工作状态。
彩蛋2-springboot 中多环境日志配置
在springboot中我们通常针对dev,test,live等多种环境会有多套配置文件,比如application-dev.yml
,application-test.yml
, application-live.yml
。然后可以在启动的时候可以通过在全局配置文件application.yml
指定某个配置文件来启用配置:
sping:
profiles:
active: live
日志同样有这样的需求,不同环境使用不同的日志配置,比如dev或test环境需要开启的log level为DEBUG
,而在live环境中却不需要。这种时候我们只需要写多套日志配置,比如log4j2-dev.yml
,log4j2-test.yml
,log4j2-live.yml
,然后在其对应的配置文件中启用日志配置就可以了:
logging:
config: classpath:log4j2-dev.yml
参考资料
- Spring Boot Log4j 2 example
- Log4j2中的同步日志与异步日志
- Difference between Asynclogger and AsyncAppender in Log4j2
- https://www.callicoder.com/spring-boot-log4j-2-example/
原文发表于:https://pengcheng.site/2019/10/28/springboot-zi-dong-pei-zhi-qian-xi/
log4j 和 log4j2 在springboot中的性能对比的更多相关文章
- lua、groovy嵌入到java中的性能对比(转)
lua和groovy都是可以嵌入到java中的脚本语言.lua以高性能著称,与C/C++在游戏开放中有较多使用,groovy是一个基于Java虚拟机(JVM)的敏捷动态语言,在jvm下有着不错的性能. ...
- springboot中动态修改log4j2日志级别
springboot中动态修改log4j2日志级别 在spring boot中使用log4j2日志时,项目运行中,想要修改日志级别. 1.pom.xml依赖: <dependency> & ...
- Slf4j与log4j及log4j2的关系及使用方法
Slf4j与log4j及log4j2的关系及使用方法 slf4j slf4j仅仅是一个为Java程序提供日志输出的统一接口,并不是一个具体的日志实现方案,就比如JDBC一样,只是一种规则而已,所以单独 ...
- log4j、log4j2和slf4j的基本使用
一.什么是log4j.log4j2和slf4j Log4j是Apache的一个开源项目,通过配置来控制日志的输出.主要是控制日志的输出级别.输出位置和输出内容格式. Log4j2是在log4j框架的基 ...
- Java logger组件:slf4j, jcl, jul, log4j, logback, log4j2
先说结论 建议优先使用logback 或 log4j2.log4j2 不建议和 slf4j 配合使用,因为格式转换会浪费性能. 名词:jcl 和 jul 标题中的 jcl 是 apache Jakar ...
- 转:Java logger组件:slf4j, jcl, jul, log4j, logback, log4j2
先说结论 建议优先使用logback 或 log4j2.log4j2 不建议和 slf4j 配合使用,因为格式转换会浪费性能. 名词:jcl 和 jul 标题中的 jcl 是 apache Jakar ...
- Slf4j与log4j及log4j2、logbak的关系及使用方法
Slf4j与log4j及log4j2的关系及使用方法 slf4j slf4j仅仅是一个为Java程序提供日志输出的统一接口,并不是一个具体的日志实现方案,就比如JDBC一样,只是一种规则而已,所以单独 ...
- JCL、SLF4J、Log4J、Log4J2、LogBack和JUL之间的关系,你搞清楚了吗?
写在前面 日志组件是我们平时开发过程中必然会用到的组件.在系统中正确的打印日志至少有下面的这些好处: 调试:在程序的开发过程中,必然需要我们不断的调试以达到程序能正确执行的状态 .记录日志可以让开发人 ...
- 一文讲尽门面日志slf4j和log4j、log4j2、logback依赖jar引用关系
公众号Mac代码分割阅读链接 前言 之前都是使用SparkStreaming开发,最近打算学习一下Flink,就从官网下载了Flink 1.11,打算搞一个客户端,将程序提交在yarn上.因为Flin ...
随机推荐
- HDU 1577 WisKey的眼神
WisKey的眼神 Time Limit: 1000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others) Total Sub ...
- 40.Unique Binary Search Trees(不同的二叉搜索树)
Level: Medium 题目描述: Given n, how many structurally unique BST's (binary search trees) that store v ...
- 背包九讲(Orz)
P01: 01背包问题 题目 有\(N\)件物品和一个容量为\(V\)的背包.第\(i\)件物品的费用是\(c[i]\),价值是\(w[i]\).求解将哪些物品装入背包可使这些物品的费用总和不超过背包 ...
- linux的CentOS、Ubuntu、Debian三个比较异同
Linux有非常多的发行版本,从性质上划分,大体分为由商业公司维护的商业版本与由开源社区维护的免费发行版本.商业版本以Redhat为代表,开源社区版本则以debian为代表.这些版本各有不同的特点,在 ...
- NSQ消息队列
前面的总结中提到过这个玩意,所以简单说说,win上面的测试验证 网上有比较合适的博文,我先推荐几篇 https://blog.csdn.net/a2247889821/article/details/ ...
- NULL合并操作符??
参考官方手册: /** * NULL合并操作符 ?? */ // $a, $b, $c都未声明和定义 var_dump($a??$b??$c); // NULL // $a为数组,$b为100,$c为 ...
- SQL查询连续年份
有这样一个问题,给出一个表格记录了夺冠球队的名称和年份,我们要做的就是写出一条SQL语句,查询再次期间连续夺冠的有哪些,起止时间是什么 下边是代码 create table #t(TEAM vaarc ...
- 四、附加到进程调试(.NET Framework)
附加到进程调试: 1.需要在IIS配置环境并可运行即通过浏览器可打开. 2.找到项目w3wp.exe进程并附加到进程调试,点击项目添加断点,直接访问浏览器即可. 优点:w3wp.exe是已经运行的,调 ...
- go语言从例子开始之Example1.helloworld
Example: package main import "fmt" func main() { fmt.Println("hello world") } Re ...
- Hybrid App技术解析 — 原理篇
Hybrid App技术解析 — 原理篇 原文出处: https://segmentfault.com/a/1190000015678155 引言 随着 Web 技术和移动设备的快速发展,Hybr ...