log4j2同步日誌引發的性能問題



一、問題回顧

1.1 問題描述

在項目的性能測試中,相關的接口的隨着併發數增加,接口的響應時間變長,接口吞吐不再增長,應用的CPU使用率較高。

1.2 分析思路

誰導致的CPU較高,阻塞接口TPS的增長?接口的響應時間的調用鏈分佈是什麼樣的,有沒有慢的點?
1)使用火焰圖分析應用的CPU如下,其中log4j2日誌佔了40%左右CPU,初步懷疑是log4j2的問題。
2)調用鏈的分析
通過pfinder查看調用鏈發現,接口總耗時78ms,沒有明顯慢的調用方法和慢sql等,先排除接口的本身的代碼問題。

1.3 初步結論

log4j2的問題,需詳細分析日誌的相關配置log4j2.xml。
上面可以看到日誌中Loggers節點下的 節點以及 節點都是打印的同步日誌。同步日誌是程序的業務邏輯和日誌輸出語句均在一個線程運行,當日志較多,在一定程度上阻塞了業務的運行效率。改成異步日誌試一下:
改成異步日誌配置:使用AsyncLogger

1.4 迴歸驗證

同步日誌改成異步日誌後。同樣10併發,接口的TP99由 51ms 降到 23ms,接口的吞吐TPS由 493提高到 1078,應用的CPU由 82%降到 57%。
完美end。問題解決了,但是log4j2的日誌我們還是要詳細研究學習一下。

1.5 結論

  • log4j2使用異步日誌將大幅提升性能,減少對應用本身的影響。
  • 從根本上減少不必要日誌的輸出
但是log4j2異步日誌是怎麼實現的和同步日誌有什麼區別?爲什麼異步日誌的的效率更高?引發我去學習一下log4j2的相關知識,下面和大家分享一下:


二、log4j2日誌

2.1 log4j2的優勢

log4j2是log4j 1.x 的升級版,參考了logback的一些優秀的設計,並且修復了一些問題,因此帶來了一些重大的提升,主要有:
  • 異常處理,在logback中,Appender中的異常不會被應用感知到,但是在log4j2中,提供了一些異常處理機制。
  • 性能提升, log4j2相較於log4j 1和logback都具有很明顯的性能提升,後面會有官方測試的數據。
  • 自動重載配置,參考了logback的設計,當然會提供自動刷新參數配置,最實用的就是我們在生產上可以動態的修改日誌的級別而不需要重啓應用——那對監控來說,是非常敏感的。
  • 無垃圾機制 ,log4j2在大部分情況下,都可以使用其設計的一套無垃圾機制,避免頻繁的日誌收集導致的jvm gc。

2.2 Log4J2日誌分類

Log4j2中記錄日誌的方式有同步日誌和異步日誌兩種方式,其中異步日誌又可分爲使用AsyncAppender和使用AsyncLogger兩種方式。使用LOG4J2有三種日誌模式,全異步日誌,混合模式,同步日誌,性能從高到低,線程越多效率越高,也可以避免日誌卡死線程情況發生。
同步和異步日誌的性能對比:

2.3 同步日誌

使用log4j2的同步日誌進行日誌輸出,日誌輸出語句與程序的業務邏輯語句將在同一個線程運行,即當輸出日誌時,必須等待日誌輸出語句執行完畢後,才能執行後面的業務邏輯語句。

2.4 異步日誌

而使用異步日誌進行輸出時,日誌輸出語句與業務邏輯語句並不是在同一個線程中運行,而是有專門的線程用於進行日誌輸出操作,處理業務邏輯的主線程不用等待即可執行後續業務邏輯。
log4j2最大的特點就是異步日誌,其性能的提升主要也是從異步日誌中受益,我們來看看如何使用log4j2的異步日誌
Log4j2中的異步日誌實現方式有AsyncAppender和AsyncLogger兩種。
其中:
  • AsyncAppender採用了ArrayBlockingQueue來保存需要異步輸出的日誌事件;
  • AsyncLogg er則使用了Disruptor框架來實現高吞吐。
注意這是兩種不同的實現方式,在設計和源碼上都是不同的體現。
2.4.1 AsyncAppender
AsyncAppender是通過引用別的Appender來實現的,當有日誌事件到達時,會開啓另外一個線程來處理它們。需要注意的是,如果在Appender的時候出現異常,對應用來說是無法感知的。AsyncAppender應該在它引用的Appender之後配置,默認使用 java.util.concurrent.ArrayBlockingQueue實現而不需要其它外部的類庫。當使用此Appender的時候,在多線程的環境下需要注意,阻塞隊列容易受到鎖爭用的影響,這可能會對性能產生影響。
<?xml version="1.0" encoding="UTF-8"?><Configuration status="warn"> <Appenders> <!--正常的Appender配置,此處配置的RollingFile會在下面AsyncAppender被通過name引用--> <RollingFile name="RollingFileError" fileName="${Log_Home}/error.${date:yyyy-MM-dd}.log" immediateFlush="true"filePattern="${Log_Home}/$${date:yyyy-MM}/error-%d{MM-dd-yyyy}-%i.log.gz">     <PatternLayout pattern="%d{yyyy-MM-dd 'at' HH:mm:ss z} %-5level %logger{36} : %msg%xEx%n"/>       <ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/>       <Policies>        <TimeBasedTriggeringPolicy modulate="true" interval="1"/>        <SizeBasedTriggeringPolicy size="10MB"/>        </Policies> </RollingFile> <!--一個Appender配置完畢--> <!--異步AsyncAppender進行配置直接引用上面的RollingFile的name--> <Async name="Async">        <AppenderRef ref="RollingFileError"/> </Async> <!--異步AsyncAppender配置完畢,需要幾個配置幾個--> </Appenders> <Loggers>         <Root level="error">          <!--此處如果引用異步AsyncAppender的name就是異步輸出日誌-->         <!--此處如果引用Appenders標籤中RollingFile的name就是同步輸出日誌-->          <AppenderRef ref="Async"/></Root> </Loggers></Configuration>
2.4.2 AsyncLogger
AsyncLogger纔是log4j2 的重頭戲,也是官方推薦的異步方式。它可以使得調用Logger.log返回的更快。Log4j2中的AsyncLogger的內部使用了Disruptor框架。
Disruptor簡介
Disruptor是英國外匯交易公司LMAX開發的一個高性能隊列,基於Disruptor開發的系統單線程能支撐每秒600萬訂單。
目前,包括Apache Strom、Log4j2在內的很多知名項目都應用了Disruptor來獲取高性能。
Disruptor框架內部核心數據結構爲RingBuffer,其爲無鎖環形隊列。
Disruptor爲什麼這麼快?
  • lock-free-使用了CAS來實現線程安全
  • 使用緩存行填 充解決僞共享問題
異步日誌可以有兩種選擇:全局異步和混合異步。
1)全局異步
全局異步就是,所有的日誌都異步的記錄,在配置文件上不用做任何改動,只需要在jvm啓動的時候增加一個參數;這是最簡單的配置,並提供最佳性能。
然後在src/java/resources目錄添加log4j2.component.properties配置文件

設置異步日誌系統屬性

log4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector
2)混合異步
混合異步就是,你可以在應用中同時使用同步日誌和異步日誌,這使得日誌的配置方式更加靈活。因爲Log4j文檔中也說了,雖然Log4j2提供一套異常處理機制,可以覆蓋大部分的狀態,但是還是會有一小部分的特殊情況是無法完全處理的,比如我們如果是記錄審計日誌,那麼官方就推薦使用同步日誌的方式,而對於其他的一些僅僅是記錄一個程序日誌的地方,使用異步日誌將大幅提升性能,減少對應用本身的影響。混合異步的方式需要通過修改配置文件來實現,使用AsyncLogger標記配置。
第一步:pom中添加相關jar包
<dependency>  <groupId>com.lmax</groupId>  <artifactId>disruptor</artifactId>  <version>3.4.2</version></dependency>
第二步:log4j2.xml同步日誌和異步日誌(配置AsyncLogger)混合配置的例子如下:
<?xml version="1.0" encoding="UTF-8"?><!--日誌級別以及優先級排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL --> <!--status="WARN" :用於設置log4j2自身內部日誌的信息輸出級別,默認是OFF--> <!--monitorInterval="30" :間隔秒數,自動檢測配置文件的變更和重新配置本身--><configuration status="WARN" monitorInterval="30">   <Properties>       <!--1、自定義一些常量,之後使用${變量名}引用-->       <Property name="logFilePath">log</Property>       <Property name="logFileName">test.log</Property>   </Properties>   <!--2、appenders:定義輸出內容,輸出格式,輸出方式,日誌保存策略等,常用其下三種標籤[console,File,RollingFile]-->   <!--Appenders中配置日誌輸出的目的地           console只的是控制檯 system.out.println           rollingFile 只的是文件大小達到指定尺寸的時候產生一個新的文件-->   <appenders>       <!--console :控制檯輸出的配置-->       <console name="Console" target="SYSTEM_OUT">           <!--PatternLayout :輸出日誌的格式,LOG4J2定義了輸出代碼,詳見第二部分 %p 輸出優先級,即DEBUG,INFO,WARN,ERROR,FATAL-->           <PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n"/>       </console>       <!--File :同步輸出日誌到本地文件-->       <!--append="false" :根據其下日誌策略,每次清空文件重新輸入日誌,可用於測試-->       <File name="log" fileName="${logFilePath}/${logFileName}" append="false">           <!-- 格式化輸出:           %d表示日期,%thread表示線程名,           %-5level:級別從左顯示5個字符寬度           %thred: 輸出產生該日誌事件的線程名           %class:是輸出的類           %L: 輸出代碼中的行號           %M:方法名           %msg:日誌消息,           %n是換行符           %c: 輸出日誌信息所屬的類目,通常就是所在類的全名            %t: 輸出產生該日誌事件的線程名            %l: 輸出日誌事件的發生位置,相當於%C.%M(%F:%L)的組合,包括類目名、發生的線程,以及在代碼中的行數。舉例:Testlog4.main(TestLog4.Java:10)            %p: 輸出日誌信息優先級,即DEBUG,INFO,WARN,ERROR,FATAL,           2020.02.06 at 11:19:54 CST INFOcom.example.redistest.controller.PersonController 40 setPerson - 添加成功1條數據           -->           <!-- %class{36} 表示 class 名字最長36個字符 -->           <PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %class{36} %L %M - %msg%xEx%n"/>       </File>       <!--       關鍵點在於 filePattern後的日期格式,以及TimeBasedTriggeringPolicy的interval,日期格式精確到哪一位,interval也精確到哪一個單位.       1) TimeBasedTriggeringPolicy需要和filePattern配套使用,由於filePattern配置的時間最小粒度如果設置是dd天,所以表示每一天新建一個文件保存日誌。       2) SizeBasedTriggeringPolicy表示當文件大小大於指定size時,生成新的文件保存日誌。與%i配合使用-->       <RollingFile name="RollingFileInfo" fileName="${sys:user.home}/logs/info.log"                    filePattern="${sys:user.home}/logs/$${date:yyyy-MM}/info-%d{yyyy-MM-dd}-%i.log">           <!--ThresholdFilter :日誌輸出過濾-->           <!--level="info" :日誌級別,onMatch="ACCEPT" :級別在info之上則接受,onMismatch="DENY" :級別在info之下則拒絕-->           <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>           <PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n"/>           <!-- Policies :日誌滾動策略-->           <Policies>               <!-- TimeBasedTriggeringPolicy :時間滾動策略,默認0點小時產生新的文件,interval="6" : 自定義文件滾動時間間隔,每隔6小時產生新文件, modulate="true" : 產生文件是否以0點偏移時間,即6點,12點,18點,0點-->               <TimeBasedTriggeringPolicyinterval="6" modulate="true"/>               <!-- SizeBasedTriggeringPolicy :文件大小滾動策略-->               <SizeBasedTriggeringPolicysize="100 MB"/>           </Policies>           <!-- DefaultRolloverStrategy屬性如不設置,則默認爲最多同一文件夾下7個文件,這裏設置了20 -->           <DefaultRolloverStrategy max="20"/>       </RollingFile>       <RollingFile name="RollingFileWarn" fileName="${sys:user.home}/logs/warn.log"                    filePattern="${sys:user.home}/logs/$${date:yyyy-MM}/warn-%d{yyyy-MM-dd}-%i.log">           <ThresholdFilter level="warn" onMatch="ACCEPT" onMismatch="DENY"/>           <PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n"/>           <Policies>               <TimeBasedTriggeringPolicy/>               <SizeBasedTriggeringPolicy size="100 MB"/>           </Policies>       </RollingFile>       <RollingFile name="RollingFileError" fileName="${sys:user.home}/logs/error.log"                    filePattern="${sys:user.home}/logs/$${date:yyyy-MM}/error-%d{yyyy-MM-dd}-%i.log">           <ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/>           <PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n"/>           <Policies>               <TimeBasedTriggeringPolicy/>               <SizeBasedTriggeringPolicy size="100 MB"/>           </Policies>       </RollingFile>   </appenders>   <!--3、然後定義logger,只有定義了logger並引入的appender,appender纔會生效-->   <loggers>       <!--過濾掉spring和mybatis的一些無用的DEBUG信息-->       <!--Logger節點用來單獨指定日誌的形式,name爲包路徑,比如要爲org.springframework包下所有日誌指定爲INFO級別等。 -->       <logger name="org.springframework" level="INFO"></logger>       <logger name="org.mybatis" level="INFO"></logger>       <!-- Root節點用來指定項目的根日誌,如果沒有單獨指定Logger,那麼就會默認使用該Root日誌輸出 -->       <root level="all">           <appender-ref ref="Console"/>           <appender-ref ref="RollingFileInfo"/>           <appender-ref ref="RollingFileWarn"/>           <appender-ref ref="RollingFileError"/>       </root>       <!--AsyncLogger :異步日誌,LOG4J有三種日誌模式,全異步日誌,混合模式,同步日誌,性能從高到底,線程越多效率越高,也可以避免日誌卡死線程情況發生-->       <!--additivity="false" : additivity設置事件是否在root logger輸出,爲了避免重複輸出,可以在Logger 標籤下設置additivity爲”false”只在自定義的Appender中進行輸出-->       <AsyncLogger name="AsyncLogger" level="trace" includeLocation="true" additivity="false">           <appender-ref ref="RollingFileError"/>       </AsyncLogger>   </loggers></configuration>
2.5 使用異步日誌的注意事項
在使用異步日誌的時候需要注意一些事項,如下:
  1. 不要同時使用AsyncAppender和AsyncLogger,也就是在配置中不要在配置 Appender的時候,使用Async標識的同時,又配置AsyncLogger,這不會報錯,但是對於性能提升沒有任何好處。
  2. 不要在開啓了全局同步的情況下,仍然使用AsyncAppender和AsyncLogger。這和上一條是同一個意思,也就是說,如果使用異步日誌,AsyncAppender、AsyncLogger和全局日誌,不要同時出現。
  3. 如果不是十分必須,不管是同步異步,都設置immediateFlush爲false,這會對性能提升有很大幫助。
  4. 如果不是 實需要,不要打印location信息,比如HTML的location,或者pattern模式裏的%C or $class, %F or %file, %l or %location, %L or %line, %M or %method, 等,因爲Log4j需要在打印日誌的時候做一次棧的快照才能獲取這些信息,這對於性能來說是個極大的損耗。


三、總結

在壓測的中,對於問題的根因盡最大能力探索挖掘,不斷沉澱總結實踐經驗。尤其是一些開源的組件使用,要詳細學習瞭解使用規範以及最佳實踐,必要時可以加入性能測試,確保滿足我們質量和性能要求。

參考資料

[1] https://www.yisu.com/zixun/623058.html

[2] https://www.jianshu.com/p/9f0c67facbe2

[3] https://blog.csdn.net/thinkwon/article/details/101625124

[4] https://zhuanlan.zhihu.com/p/386990511

-end-

本文分享自微信公衆號 - 京東雲開發者(JDT_Developers)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章