Log4j2中的同步日誌與異步日誌

1.背景

Log4j 2中記錄日誌的方式有同步日誌和異步日誌兩種方式,其中異步日誌又可分爲使用AsyncAppender和使用AsyncLogger兩種方式。html

2.Log4j2中的同步日誌

所謂同步日誌,即當輸出日誌時,必須等待日誌輸出語句執行完畢後,才能執行後面的業務邏輯語句。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]
logger.info運行時到底發生了什麼?日誌信息是如何輸出到app.log中的?

Log4j2中日誌輸出的詳細過程以下:

1.首先使用全局Filter對日誌事件進行過濾。

Log4j2中的日誌Level分爲8個級別,優先級從高到低依次爲OFF、FATAL、ERROR、WARN、INFO、DEBUG、TRACE、 ALL。安全

全局Filter的Level爲ALL,表示容許輸出全部級別的日誌。logger.info()請求輸出INFO級別的日誌,經過。數據結構

2.使用Logger的Level對日誌事件進行過濾。

Logger的Level爲TRACE,表示容許輸出TRACE級別及以上級別的日誌。logger.info()請求輸出INFO級別的日誌,經過。併發

3.生成日誌輸出內容Message。

使用佔位符的方式輸出日誌,輸出語句爲logger.info("increase {} from {} to {}", arg1, arg2, arg3)的形式,最終輸出時{}佔位符處的內容將用arg1,arg2,arg3的字符串填充。app

log4j2用Object[]保存參數信息,在這一階段會將Object[]轉換爲String[],生成含有輸出模式串"increase {} from {} to {}"和參數數組String[]的Message,爲後續日誌格式化輸出作準備。

4.生成LogEvent。

LogEvent中含有loggerName(日誌的輸出者),level(日誌級別),timeMillis(日誌的輸出時間),message(日誌輸出內容),threadName(線程名稱)等信息。

在上述程序中,生成的LogEvent的屬性值爲loggerName=com.meituan.Main,Level=INFO,timeMillis=1505659461759,message爲步驟3中建立的Message,threadNama=main。

5.使用Logger配置的Filter對日誌事件進行過濾。

Logger配置的Filter的Level爲DEBUG,表示容許輸出DEBUG及以上級別的日誌。logger.info()請求輸出INFO級別的日誌,經過。

6.使用Logger對應的Appender配置的Filter對日誌事件進行過濾。

Appender配置的Filter配置的INFO級別日誌onMatch=ACCEPT,表示容許輸出INFO級別的日誌。logger.info()請求輸出INFO級別的日誌,經過。

7.判斷是否須要觸發rollover。

此步驟不是日誌輸出的必須步驟,如配置的Appender爲無需進行rollover的Appender,則無此步驟。

由於使用RollingFileAppender,且配置了基於文件大小的rollover觸發策略,在此階段會判斷是否須要觸發rollover。判斷方式爲當前的文件大小是否達到了指定的size,若是達到了,觸發rollover操做。關於Log4j2中的RollingFileAppender的rollover,可參見Log4j2中RollingFile的文件滾動更新機制

8.PatternLayout對LogEvent進行格式化,生成可輸出的字符串。

上述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的信息被格式化爲指定格式的字符串。

9.使用OutputStream,將日誌輸出到文件。

將日誌字符串序列化爲字節數組,使用字節流OutoutStream將日誌輸出到文件中。若是配置了immediateFlush爲true,打開app.log就可觀察到輸出的日誌了。

3.Log4j2中的異步日誌

使用log4j2的同步日誌進行日誌輸出,日誌輸出語句與程序的業務邏輯語句將在同一個線程運行,如上面的例子,打印的日誌中顯示的線程名稱爲main,與業務邏輯語句在同一個線程中(此句有誤,LogEvent中的線程名稱不是輸出LogEvent的線程,是生成LogEvent的線程,輸出LogEvent的線程和生成LogEvent可能不是一個線程!)

而使用異步日誌進行輸出時,日誌輸出語句與業務邏輯語句並非在同一個線程中運行,而是有專門的線程用於進行日誌輸出操做,處理業務邏輯的主線程不用等待便可執行後續業務邏輯。

Log4j2中的異步日誌實現方式有AsyncAppender和AsyncLogger兩種。

其中,AsyncAppender採用了ArrayBlockingQueue來保存須要異步輸出的日誌事件;AsyncLogger則使用了Disruptor框架來實現高吞吐。

3.1 AsyncAppender

<?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進行日誌輸出。

3.2 AsyncLogger

Log4j2中的AsyncLogger的內部使用了Disruptor框架。

Disruptor簡介

Disruptor是英國外匯交易公司LMAX開發的一個高性能隊列,基於Disruptor開發的系統單線程能支撐每秒600萬訂單。

目前,包括Apache Strom、Log4j2在內的不少知名項目都應用了Disruptor來獲取高性能。

Disruptor框架內部核心數據結構爲RingBuffer,其爲無鎖環形隊列。

 

單線程每秒可以處理600萬訂單,Disruptor爲何這麼快?

a.lock-free-使用了CAS來實現線程安全

ArrayBlockingQueue使用鎖實現併發控制,當get或put時,當前訪問線程將上鎖,當多生產者、多消費者的大量併發情形下,因爲鎖競爭、線程切換等,會有性能損失。

Disruptor經過CAS實現多生產者、多消費者對RingBuffer的併發訪問。CAS至關於樂觀鎖,其性能優於Lock的性能。

b.使用緩存行填充解決僞共享問題

計算機體系結構中,內存的訪問速度遠遠低於CPU的運行速度,在內存和CPU之間,加入Cache,CPU首先訪問Cache中的數據,CaChe未命中,才訪問內存中的數據。

僞共享:Cache是以緩存行(cache line)爲單位存儲的,當多個線程修改互相獨立的變量時,若是這些變量共享同一個緩存行,就會無心中影響彼此的性能。

 

關於僞共享的深度分析,可參考《僞共享,併發編程的性能殺手》這篇文章。

AsyncLogger

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線程進行日誌的異步處理。

4.總結

 
日誌輸出方式
sync 同步打印日誌,日誌輸出與業務邏輯在同一線程內,當日志輸出完畢,才能進行後續業務邏輯操做
Async Appender 異步打印日誌,內部採用ArrayBlockingQueue,對每一個AsyncAppender建立一個線程用於處理日誌輸出。
Async Logger 異步打印日誌,採用了高性能併發框架Disruptor,建立一個線程用於處理日誌輸出。
相關文章
相關標籤/搜索