<form id="dlljd"></form>
        <address id="dlljd"><address id="dlljd"><listing id="dlljd"></listing></address></address>

        <em id="dlljd"><form id="dlljd"></form></em>

          <address id="dlljd"></address>
            <noframes id="dlljd">

              聯系我們 - 廣告服務 - 聯系電話:
              您的當前位置: > 關注 > > 正文

              Log4j2源碼解析:同步寫、異步寫原理、中間技術思考

              來源:CSDN 時間:2023-03-07 11:41:12

              Log4j2中的組件 從配置開始API基本使用小細節 寫日志的原理主要流程 同步寫異步寫 順便說一下ArrayBlockingQueue notFull 與 notEmpty 異步寫是怎么玩的巧妙的異步寫設計 ByteBuffer與RandomAccessFileGarbage-free避免創建多余對象異步Logger性能 寫在最后

              本文主要記錄我對Log4j2源碼閱讀后的一些個人理解,包括的內容有:Log4j2的組件、同步寫、異步寫原理、以及中間技術的個人思考。而log4j2的使用與配置稍顯簡易,可在很多地方找到說明,本文則不重點討論這部分的內容。


              【資料圖】

              Log4j2中的組件

              Log4j2以插件的方式來配置各個組件,在配置中可自由的插拔組件,并支持自動生效(配置monitorInterval)。

              從配置開始

              Log4j2支持多種格式配置文件,xml、json、yaml、properties。相應地,初始化時會逐個查找并加載這類文件。以最常用的xml配置為例,Log4j2默認的文件名稱為log4j2.xml,主要配置如下:

              target/rolling.log

              如上配置的對象有: 1. Configuration: 表示日志環境的一份配置描述,用于構建一份運行時的LoggerContext2. Logger表示日志記錄器,可有多個。示例中有2個普通記錄器,1個根記錄器。一個記錄器需要指定Appender,可指定多個,表示日志記錄到哪里。 3. Appenders表示日志的輸出源,可有多個。示例中有3個,Console表示通過標準輸出流輸出到控制臺,RollingRandomAccessFile表示可滾動的記錄日志,日志文件到達一定的大小后,會啟用壓縮。 Async表示一種異步的輸出端,通過異步線程與隊列的方式寫入日志到其關聯的實際Appender中。

              API基本使用

              // 獲取Logger private static final Logger logger = LogManager.getLogger("HelloWorld"); private static final Logger logger = LogManager.getLogger(Class.getName()); logger.info("Hello, World!");logger.debug("Logging in user {} with birthday {}", user.getName(), user.getBirthdayCalendar());logger.debug("Logging in user %s with birthday %s", user.getName(), user.getBirthdayCalendar());

              小細節

              Log4j2在對外的info,warn等API上在內置了logIfEnabled的判斷,而不用在程序中顯示的寫上:

              if (logger.isInfoEnabled()) {    logger.info();}if (logger.isWarnEnabled()) {    logger.warn();}

              所以在寫日志時,放心的直接用logger.info,logger.warn吧。

              寫日志的原理

              主要流程

              同步寫

              如上流程是典型的同步寫的過程,多線程并發寫是通過臨界區來實現。簡要過程如下:

              相關代碼: AbstractOutputStreamAppender#directEncodeEvent 以常用的基于PatternLayout的日志格式為例:

              private void encodeSynchronized(final CharsetEncoder charsetEncoder, final CharBuffer charBuffer,            final StringBuilder source, final ByteBufferDestination destination) {      // 臨界區阻塞式Encode        synchronized (destination) {            try {                TextEncoderHelper.encodeText(charsetEncoder, charBuffer, destination.getByteBuffer(), source,                        destination);            } catch (final Exception ex) {               ..            }        }    }// 根據不同的Appender用OutputStream.writeBytespublic synchronized void flush() {        flushBuffer(byteBuffer);        flushDestination();}

              異步寫

              如上配置的Async表示一種異步輸出器,對應的實現為AsyncAppender。 異步Appender使用異步線程加阻塞隊列(BlockingQueue)來實現異步寫的功能。默認情況下通過BlockingQueueFactory創建缺省的隊列類型為:ArrayBlockingQueue,隊列大小為128.

              順便說一下ArrayBlockingQueue

              notFull 與 notEmpty

              put和take阻塞調用線程是借用notFull和notEmpty兩個條件對象來實現的。

              所以在理解這兩個詞的意思時,看看Doug Lea給的注釋:

              等待take的條件    private final Condition notEmpty;    等待put的條件    private final Condition notFull;

              換言之,此處的notEmpty表示為:只有隊列在notEmpty的條件下才能take, 如果隊列為empty,那么當前take的線程則需要等待。類似的,當隊列在notFull時,才能put, 如果隊列為full, 那么當前put的線程則需要等待。

              異步寫是怎么玩的

              業務線程并發調用同一個Logger寫日志時,Log4j2內部把內容解析成LogEvent,然后投遞到隊列中,由異步的線程來負責消費。

              相關代碼: 投遞到隊列:

              public void append(final LogEvent logEvent) {         ...         獲取不可變的副本對象        final Log4jLogEvent memento = Log4jLogEvent.createMemento(logEvent, includeLocation);        if (!transfer(memento)) { // 投遞到隊列失敗,降級策略            if (blocking) { // 新版本默認為true                // 根據策略不同,可丟棄、可等待、可由當前線程執行                final EventRoute route = asyncQueueFullPolicy.getRoute(thread.getId(), memento.getLevel());                route.logMessage(this, memento);            } else {                // 交由error-appender處理                logToErrorAppenderIfNecessary(false, memento);            }        }    }// 投遞LogEvent到隊列,新版本可指定隊列為LinkedTransferQueueprivate boolean transfer(final LogEvent memento) {        return queue instanceof TransferQueue            ? ((TransferQueue) queue).tryTransfer(memento)            : queue.offer(memento);}

              消費寫LogEvent

              消費while (!queue.isEmpty()) {     try {           final LogEvent event = queue.take();           if (event instanceof Log4jLogEvent) {               final Log4jLogEvent logEvent = (Log4jLogEvent) event;               logEvent.setEndOfBatch(queue.isEmpty());               // 在異步線程中調用Appender寫日志               callAppenders(logEvent);           } else {               ...          }       } catch (final InterruptedException ex) {           不處理異常,繼續寫日志       } }

              巧妙的異步寫設計

              實際環境中,很多業務線程在并發的寫日志到隊列中,并由一條異步消費者線程負責消費寫。高并發的場景下,很容易造成生成的速度大于消費的速度。 為了不阻塞生成,投遞LogEvent時選擇的是隊列的offer方法,如果成功入隊,則執行馬上返回給應用。如果隊列滿了,則默認降級為在同步寫,這種設計能極大的提供性能 。而消費時選擇的隊列的take方法,如果生產的日志較少,則park消費者線程,讓出CPU。 注意到offer方法在入隊成功時,也會調用notEmpty.signal()方法,進而喚醒消費者,從而讓它繼續工作。

              ByteBuffer與RandomAccessFile

              項目中常常會用到RollingRandomAccessFile這種Appender,而在Log4j2中都是基于ByteBuffer來管理字節緩沖,并用于處理字節流與字符流的高效轉換。最后采用RandomAccessFile來寫Bytes到文件。Log4j2官方給出的性能測試報告提到相較于BufferedOutputStream,ByteBuffer + RandomAccessFile有20-200%的性能改善。

              private ByteBuffer getByteBuffer() {        ByteBuffer result = byteBufferThreadLocal.get();        if (result == null) {            result = ByteBuffer.wrap(new byte[byteBufferSize]);            byteBufferThreadLocal.set(result);        }        return result;}

              在輸出ByteBuffer時,根據Appender的不同選擇不同的輸出策略。基于RandomAccessFile的輸出為: randomAccessFile.write(byteBuffer.toArray(), 0, byteBuffer.limit());

              Garbage-free(避免創建多余對象)

              Log4j2提倡創建最少的對象做更多事,盡量避免創建多余的對象。在內部有很多細節代碼,這里分析2個例子。 1. API設計上避免創建變長數組 用void info(String message, Object p0, Object p1, Object p2, Object p3)這類參數明確的api替換帶變長數組的api.

              提供工具來避免基礎類型參數的自動裝箱. 自動裝箱會創建大量的對象,Log4j2提供Unbox工具類來轉換為StringBuilder.

              異步Logger

              事實上log4j2的異步logger才是性能改善最卓越的部分。異步Logger內部采用 Disruptor來實現,它是一個用于代替隊列的無鎖化線程間的通信的工具庫,可以明顯的提高吞吐量并降低延遲。 自己后續會對Disruptor做一個學習與分析。感謝關注。

              性能

              Log4j2的性能改善是明顯的,官方文檔:http://logging.apache.org/log4j/2.x/performance.html 提供了大量的場景對比結果,有興趣的可以去了解一下。

              寫在最后

              The log4j1.x amost has became End of Life。So upgrade it and learn about it.

              哦,對了,升級的時候可以用SLF4J來做統一的外觀,然后引入log4j-api,log4j-core, slf4j-api,還有slf4j到log4j2的橋接器log4j-slf4j-impl就行。但是需要注意的是: 咱們內部中間件大多默認依賴了slf4j-log4j12,因此需要把這類依賴全部排出, 可以用如下的tip:

              org.slf4jslf4j-log4j12999-not-exist

              Good luck. Tks.

              責任編輯:

              標簽:

              相關推薦:

              精彩放送:

              新聞聚焦
              Top 中文字幕在线观看亚洲日韩