Log4j 2中記錄日誌的方式有同步日誌和異步日誌兩種方式,其中異步日誌又可分爲使用AsyncAppender和使用AsyncLogger兩種方式。html
所謂同步日誌,即當輸出日誌時,必須等待日誌輸出語句執行完畢後,才能執行後面的業務邏輯語句。java
下面經過一個例子來了解Log4j2中的同步日誌,並藉此來探究整個日誌輸出過程。apache
log4j2.xml配置以下:編程
<?xml version="1.0" encoding="UTF-8"?> <Configuration status="debug" name="MyApp" packages=""> <!--全局Filter--> <ThresholdFilter level="ALL"/> <Appenders> <Console name="Console" target="SYSTEM_OUT"> <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" /> </Console> <RollingFile name="RollingFile" fileName="logs/app.log" filePattern="logs/app-%d{yyyy-MM-dd HH}.log"> <!--Appender的Filter--> <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/> <PatternLayout> <Pattern>%d %p %c{1.} [%t] %m%n</Pattern> </PatternLayout> <Policies> <SizeBasedTriggeringPolicy size="500MB"/> </Policies> </RollingFile> </Appenders> <Loggers> <Logger name="com.meituan.Main" level="trace" additivity="false"> <!--Logger的Filter--> <ThresholdFilter level="debug"/> <appender-ref ref="RollingFile"/> </Logger> <Root level="debug"> <AppenderRef ref="Console"/> </Root> </Loggers> </Configuration>
java代碼以下:數組
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; public class Main { public static void main(String args[]) { Logger logger = LogManager.getLogger(Main.class); Person person = new Person("Li", "lei"); logger.info("hello, {}", person); } private static class Person { private String firstName; private String lastName; public Person(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } public String toString() { return "Person[" + firstName + "," + lastName + "]"; } } }
使用以上的配置,當咱們運行程序後,如下log將被添加到logs/app.log中。緩存
2017-09-13 19:41:00,889 INFO c.m.Main [main] hello, Person[Li,lei]
Log4j2中的日誌Level分爲8個級別,優先級從高到低依次爲OFF、FATAL、ERROR、WARN、INFO、DEBUG、TRACE、 ALL。安全
全局Filter的Level爲ALL,表示容許輸出全部級別的日誌。logger.info()請求輸出INFO級別的日誌,經過。數據結構
Logger的Level爲TRACE,表示容許輸出TRACE級別及以上級別的日誌。logger.info()請求輸出INFO級別的日誌,經過。併發
使用佔位符的方式輸出日誌,輸出語句爲logger.info("increase {} from {} to {}", arg1, arg2, arg3)的形式,最終輸出時{}佔位符處的內容將用arg1,arg2,arg3的字符串填充。app
log4j2用Object[]保存參數信息,在這一階段會將Object[]轉換爲String[],生成含有輸出模式串"increase {} from {} to {}"和參數數組String[]的Message,爲後續日誌格式化輸出作準備。
LogEvent中含有loggerName(日誌的輸出者),level(日誌級別),timeMillis(日誌的輸出時間),message(日誌輸出內容),threadName(線程名稱)等信息。
在上述程序中,生成的LogEvent的屬性值爲loggerName=com.meituan.Main,Level=INFO,timeMillis=1505659461759,message爲步驟3中建立的Message,threadNama=main。
Logger配置的Filter的Level爲DEBUG,表示容許輸出DEBUG及以上級別的日誌。logger.info()請求輸出INFO級別的日誌,經過。
Appender配置的Filter配置的INFO級別日誌onMatch=ACCEPT,表示容許輸出INFO級別的日誌。logger.info()請求輸出INFO級別的日誌,經過。
此步驟不是日誌輸出的必須步驟,如配置的Appender爲無需進行rollover的Appender,則無此步驟。
由於使用RollingFileAppender,且配置了基於文件大小的rollover觸發策略,在此階段會判斷是否須要觸發rollover。判斷方式爲當前的文件大小是否達到了指定的size,若是達到了,觸發rollover操做。關於Log4j2中的RollingFileAppender的rollover,可參見Log4j2中RollingFile的文件滾動更新機制。
上述log4j2.xml文件中配置的Pattern及各個參數的意義以下:
<Pattern>%d %p %c{1.} [%t] %m%n</Pattern>
參數
|
意義
|
---|---|
%d |
日期格式,默認形式爲2012-11-02 14:34:02,781 |
%p |
日誌級別 |
%c{1.} |
%c表示Logger名字,{1.}表示精確度。若Logger名字爲org.apache.commons.Foo,則輸出o.a.c.Foo。 |
%t |
處理LogEvent的線程的名字 |
%m |
日誌內容 |
%n |
行分隔符。"\n"或"\r\n"。 |
在此步驟,PatternLayout將根據Pattern的模式,利用各類Converter對LogEvent的相關信息進行轉換,最終拼接成可輸出的日誌字符串。
如DatePatternConverter對LogEvent的日誌輸出時間進行格式化轉換;LevelPatternConverter對LogEvent的日誌級別信息進行格式化轉換;LoggerPatternConverter對LogEvent的Logger的名字進行格式化轉換;MessagePatternConverter對LogEvent的日誌輸出內容進行格式化轉換等。
經各類Converter轉換後,LogEvent的信息被格式化爲指定格式的字符串。
將日誌字符串序列化爲字節數組,使用字節流OutoutStream將日誌輸出到文件中。若是配置了immediateFlush爲true,打開app.log就可觀察到輸出的日誌了。
使用log4j2的同步日誌進行日誌輸出,日誌輸出語句與程序的業務邏輯語句將在同一個線程運行,如上面的例子,打印的日誌中顯示的線程名稱爲main,與業務邏輯語句在同一個線程中(此句有誤,LogEvent中的線程名稱不是輸出LogEvent的線程,是生成LogEvent的線程,輸出LogEvent的線程和生成LogEvent可能不是一個線程!)。
而使用異步日誌進行輸出時,日誌輸出語句與業務邏輯語句並非在同一個線程中運行,而是有專門的線程用於進行日誌輸出操做,處理業務邏輯的主線程不用等待便可執行後續業務邏輯。
Log4j2中的異步日誌實現方式有AsyncAppender和AsyncLogger兩種。
其中,AsyncAppender採用了ArrayBlockingQueue來保存須要異步輸出的日誌事件;AsyncLogger則使用了Disruptor框架來實現高吞吐。
<?xml version="1.0" encoding="UTF-8"?> <Configuration status="warn"> <Appenders> <RollingFile name="MyFile" fileName="logs/app.log"> <PatternLayout> <Pattern>%d %p %c{1.} [%t] %m%n</Pattern> </PatternLayout> <SizeBasedTriggeringPolicy size="500MB"/> </RollingFile> <Async name="Async"> <AppenderRef ref="MyFile"/> </Async> </Appenders> <Loggers> <Root level="error"> <AppenderRef ref="Async"/> </Root> </Loggers> </Configuration>
上面就是一個使用AsyncAppender的典型配置,配置AsyncAppender後,日誌事件寫入文件的操做將在單獨的線程中執行。
AsyncAppender的經常使用參數
參數名
|
類型
|
說明
|
---|---|---|
name | String | Async Appender的名字。 |
AppenderRef | String | 異步調用的Appender的名字,能夠配置多個。 |
blocking | boolean | 默認爲true。若是爲true,appender將一直等待直到queue中有空閒;若是爲false,當隊列滿的時候,日誌事件將被丟棄。(若是配置了error appender,要丟棄的日誌事件將由error appender處理) |
bufferSize | integer | 隊列中可存儲的日誌事件的最大數量,默認爲128。(源碼中爲128,Log4j2官網爲1024,官網信息有誤) |
關於AsyncAppender的其餘參數,可參考Log4j2對AsyncAppender的詳細介紹。
每一個Async Appender,內部維護了一個ArrayBlockingQueue,並將建立一個線程用於輸出日誌事件,若是配置了多個AppenderRef,將分別使用對應的Appender進行日誌輸出。
Log4j2中的AsyncLogger的內部使用了Disruptor框架。
Disruptor是英國外匯交易公司LMAX開發的一個高性能隊列,基於Disruptor開發的系統單線程能支撐每秒600萬訂單。
目前,包括Apache Strom、Log4j2在內的不少知名項目都應用了Disruptor來獲取高性能。
Disruptor框架內部核心數據結構爲RingBuffer,其爲無鎖環形隊列。
ArrayBlockingQueue使用鎖實現併發控制,當get或put時,當前訪問線程將上鎖,當多生產者、多消費者的大量併發情形下,因爲鎖競爭、線程切換等,會有性能損失。
Disruptor經過CAS實現多生產者、多消費者對RingBuffer的併發訪問。CAS至關於樂觀鎖,其性能優於Lock的性能。
計算機體系結構中,內存的訪問速度遠遠低於CPU的運行速度,在內存和CPU之間,加入Cache,CPU首先訪問Cache中的數據,CaChe未命中,才訪問內存中的數據。
僞共享:Cache是以緩存行(cache line)爲單位存儲的,當多個線程修改互相獨立的變量時,若是這些變量共享同一個緩存行,就會無心中影響彼此的性能。
關於僞共享的深度分析,可參考《僞共享,併發編程的性能殺手》這篇文章。
Log4j2異步日誌如何進行日誌輸出,咱們一樣從一個例子出發來探究Log4j2的異步日誌。
log4j2.xml配置以下:
<?xml version="1.0" encoding="UTF-8"?> <Configuration status="debug" name="MyApp" packages=""> <Appenders> <Console name="Console" target="SYSTEM_OUT"> <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" /> </Console> <RollingFile name="RollingFile" fileName="logs/app.log" filePattern="logs/app-%d{yyyy-MM-dd HH}.log"> <PatternLayout> <Pattern>%d %p %c{1.} [%t] %m%n</Pattern> </PatternLayout> <Policies> <SizeBasedTriggeringPolicy size="500MB"/> </Policies> </RollingFile> <RollingFile name="RollingFile2" fileName="logs/app2.log" filePattern="logs/app2-%d{yyyy-MM-dd HH}.log"> <PatternLayout> <Pattern>%d %p %c{1.} [%t] %m%n</Pattern> </PatternLayout> <Policies> <SizeBasedTriggeringPolicy size="500MB"/> </Policies> </RollingFile> </Appenders> <Loggers> <AsyncLogger name="com.meituan.Main" level="trace" additivity="false"> <appender-ref ref="RollingFile"/> </AsyncLogger> <AsyncLogger name="RollingFile2" level="trace" additivity="false"> <appender-ref ref="RollingFile2"/> </AsyncLogger> <Root level="debug"> <AppenderRef ref="Console"/> <AppenderRef ref="RollingFile"/> </Root> </Loggers> </Configuration>
java代碼以下:
public class Main { public static void main(String args[]) { Logger logger = LogManager.getLogger(Main.class); Logger logger2 = LogManager.getLogger("RollingFile2"); Person person = new Person("Li", "lei"); logger.info("hello, {}", person); logger2.info("good bye, {}", person); }
上述log4j2.xml中配置了兩個AsyncLogger,名字分別爲com.meituan.Main和RollingFile2。
而且,在main方法中分別使用兩個logger來輸出兩條日誌。
在加載log4j2.xml的啓動階段,若是檢測到配置了AsyncRoot或AsyncLogger,將啓動一個disruptor實例。
上述程序中,main線程做爲生產者,EventProcessor線程做爲消費者。
當運行到相似於logger.info、logger.debug的輸出語句時,將生成的LogEvent放入RingBuffer中。
若是RingBuffer中有LogEvent須要處理,EventProcessor線程從RingBuffer中取出LogEvent,調用Logger相關聯的Appender輸出LogEvent(具體輸出過程與同步過程相同,一樣須要過濾器過濾、PatternLayout格式化等步驟)。
若是RingBuffer中沒有LogEvent須要處理,EventProcessor線程將處於等待阻塞狀態(默認策略)。
須要注意的是,雖然在log4j2.xml中配置了多個AsyncLogger,可是並非每一個AsyncLogger對應着一個處理線程,而是僅僅有一個EventProcessor線程進行日誌的異步處理。
|
日誌輸出方式
|
---|---|
sync | 同步打印日誌,日誌輸出與業務邏輯在同一線程內,當日志輸出完畢,才能進行後續業務邏輯操做 |
Async Appender | 異步打印日誌,內部採用ArrayBlockingQueue,對每一個AsyncAppender建立一個線程用於處理日誌輸出。 |
Async Logger | 異步打印日誌,採用了高性能併發框架Disruptor,建立一個線程用於處理日誌輸出。 |