logback異步輸出日志過程解讀
前言
logback應該是目前最流行的日志打印框架了,畢竟Spring Boot中默認的集成的日志框架也是logback。
在實際項目開發(fā)過程中,常常會遇到由于打印大量日志而導致程序并發(fā)降低,QPS降低的問題,而通過logback異步日志輸出則能很大程度上解決這個問題。
一、什么是Appender?
官方介紹:
Logback 將編寫日志事件的任務委托給名為 Appenders 的組件,Appenders 必須實現(xiàn)ch.qos.logback.core.Appender的接口。
簡單來說,Appender就是用來處理logback框架下日志輸出事件的組件。
- Appender接口的核心方法如下:
package ch.qos.logback.core; import ch.qos.logback.core.spi.ContextAware; import ch.qos.logback.core.spi.FilterAttachable; import ch.qos.logback.core.spi.LifeCycle; public interface Appender<E> extends LifeCycle, ContextAware, FilterAttachable { public String getName(); public void setName(String name); //核心方法:處理日志事件 void doAppend(E event); }
其中doAppend()方法是 logback 框架中最重要的方法。它負責將日志事件以適當?shù)母袷捷敵龅竭m當?shù)妮敵鲈O(shè)備。
二、Appender類圖
說明:
OutputStreamAppender 是另外三個附加程序的超類,即 ConsoleAppender 和 FileAppender,后者又是 RollingFileAppender 的超類。
下一個圖說明了 OutputStreamAppender 及其子類的類圖。
1、控制臺日志輸出 ConsoleAppender
- 配置示例:
<configuration> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%-4relative [%thread] %-5level %logger{35} - %msg %n</pattern> </encoder> </appender> <root level="DEBUG"> <appender-ref ref="STDOUT" /> </root> </configuration>
說明:
控制臺日志輸出主要是在開發(fā)環(huán)境采用,比如在IDEA中開發(fā)時,可以清楚直觀得在控制臺看到運行日志,更方便程序調(diào)試。
當應用發(fā)布到測試環(huán)境、生產(chǎn)環(huán)境時,建議關(guān)閉控制臺日志輸出,以提高日志輸出的吞吐量,減少不必要的性能開銷。
2、單日志文件輸出 FileAppender
- 配置示例:
<configuration> <appender name="FILE" class="ch.qos.logback.core.FileAppender"> <!-- 日志文件名稱 --> <file>testFile.log</file> <!-- 是否追加輸出 --> <append>true</append> <!-- 立即刷新,設(shè)置成false可以提高日志吞吐量 --> <immediateFlush>true</immediateFlush> <encoder> <!-- 日志輸出格式 --> <pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern> </encoder> </appender> <root level="DEBUG"> <appender-ref ref="FILE" /> </root> </configuration>
弊端:
采用單日志文件輸出日志,很容易導致日志文件的體積一直膨脹,不利于日志文件的管理和查看。
一般很少采用。
3、滾動日志文件輸出 RollingFileAppender
- 配置示例:
<configuration> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <!-- 日志文件名稱 --> <file>logFile.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <!-- 按天滾動生成歷史日志文件 --> <fileNamePattern>logFile.%d{yyyy-MM-dd}.log</fileNamePattern> <!-- 歷史日志文件保存的天數(shù)和容量大小--> <maxHistory>30</maxHistory> <totalSizeCap>3GB</totalSizeCap> </rollingPolicy> <encoder> <pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern> </encoder> </appender> <root level="DEBUG"> <appender-ref ref="FILE" /> </root> </configuration>
說明:
通過rollingPolicy 配置日志文件的滾動生成策略,以及歷史日志文件保存的天數(shù)和總?cè)萘看笮。?/p>
是測試環(huán)境和生產(chǎn)環(huán)境最推薦的日志輸出方式。
三、同步輸出和異步輸出比較
同步輸出
- 傳統(tǒng)的日志打印采用的是同步輸出的方式,所謂同步日志,即當輸出日志時,必須等待日志輸出語句執(zhí)行完畢后,才能執(zhí)行后面的業(yè)務邏輯語句。
- 使用logback的同步日志進行日志輸出,日志輸出語句與程序的業(yè)務邏輯語句將在同一個線程運行。
- 在高并發(fā)場景下,日志數(shù)量不但激增,作為磁盤IO來說,容易產(chǎn)生瓶頸,導致線程卡頓在生成日志過程中,會影響程序后續(xù)的主業(yè)務,降低程序的性能。
異步輸出
- 使用異步日志進行輸出時,日志輸出語句與業(yè)務邏輯語句并不是在同一個線程中運行,而是有專門的線程用于進行日志輸出操作,處理業(yè)務邏輯的主線程不用等待即可執(zhí)行后續(xù)業(yè)務邏輯。
- 這樣即使日志沒有完成輸出,也不會影響程序的主業(yè)務,從而提高了程序的性能。
四、異步日志實現(xiàn)原理AsyncAppender
logback異步輸出日志是通過AsyncAppender實現(xiàn)的。AsyncAppender可以異步的記錄 ILoggingEvents日志事件。
但是這里需要注意,AsyncAppender只充當事件分配器,它必須引用另一個Appender才能完成最終的日志輸出。
示意圖:
Logback的異步輸出采用生產(chǎn)者消費者的模式,將生成的日志放入消息隊列中,并將創(chuàng)建一個線程用于輸出日志事件,有效的解決了這個問題,提高了程序的性能。
logback中的異步輸出日志使用了AsyncAppender這個appender,通過看AsyncAppender源碼,跟到它的父類AsyncAppenderBase,可以看到它有幾個重要的成員變量:
AppenderAttachableImpl<E> aai = new AppenderAttachableImpl<E>(); BlockingQueue<E> blockingQueue; AsyncAppenderBase<E>.Worker worker = new AsyncAppenderBase.Worker();
lockingQueue是一個隊列,Worker是一個消費線程,基本可以判定是個生產(chǎn)者消費者模式。
- 再看消費者(work)的主要代碼:
while (parent.isStarted()) { try { E e = parent.blockingQueue.take(); //單條循環(huán) aai.appendLoopOnAppenders(e); } catch (InterruptedException ie) { break; } }
使用的是while單條循環(huán) ,即logback異步輸出是由一個消費者循環(huán)單條寫入日志文件,工作流程如下圖:
五、異步日志配置
配置示例:
配置異步輸出日志的方式很簡單,添加一個基于異步寫日志的 appender,并指向原先配置的 appender即可。
<configuration> <appender name="FILE" class="ch.qos.logback.core.FileAppender"> <file>myapp.log</file> <encoder> <pattern>%logger{35} - %msg%n</pattern> </encoder> </appender> <appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender"> <appender-ref ref="FILE" /> <!-- 設(shè)置異步阻塞隊列的大小,為了不丟失日志建議設(shè)置的大一些,單機壓測時100000是沒問題的,應該不用擔心OOM --> <queueSize>10000</queueSize> <!-- 設(shè)置丟棄DEBUG、TRACE、INFO日志的閥值,不丟失 --> <discardingThreshold>0</discardingThreshold> <!-- 設(shè)置隊列入隊時非阻塞,當隊列滿時會直接丟棄日志,但是對性能提升極大 --> <neverBlock>true</neverBlock> </appender> <root level="DEBUG"> <appender-ref ref="ASYNC" /> </root> </configuration>
核心配置參數(shù)說明:
屬性名 | 類型 | 描述 |
---|---|---|
queueSize | int | BlockingQueue的最大容量,默認情況下,大小為256。 |
discardingThreshold | int | 設(shè)置日志丟棄閾值, 默認情況下,當隊列還有20%容量,他將丟棄trace、debug和info級別的日志,只保留warn和error級別的日志。 |
includeCallerData | boolean | 提取調(diào)用方數(shù)據(jù)可能相當昂貴。若要提高性能,默認情況下,當事件添加到事件隊列時,不會提取與事件關(guān)聯(lián)的調(diào)用方數(shù)據(jù)。默認情況下,只復制線程名和 MDC 等“廉價”數(shù)據(jù)。通過將 includeecallerdata 屬性設(shè)置為 true,可以指示此附加程序包含調(diào)用方數(shù)據(jù)。 |
maxFlushTime | int | 根據(jù)被引用的 appender 的隊列深度和延遲,AsyncAppender 可能需要不可接受的時間來完全刷新隊列。當 LoggerContext 停止時,AsyncAppender stop 方法將等待工作線程完成直到超時。使用 maxFlushTime 指定最大隊列刷新超時(以毫秒為單位)。無法在此窗口內(nèi)處理的事件將被丟棄。此值的語義與 Thread.join (long)的語義相同。 |
neverBlock | boolean | 默認是false,代表在隊列放滿的情況下是否卡住線程。也就是說,如果配置neverBlock=true,當隊列滿了之后,后面阻塞的線程想要輸出的消息就直接被丟棄,從而線程不會阻塞。 |
默認情況下,event queue配置最大容量為256個events。如果隊列已經(jīng)滿了,那么應用程序線程將被阻塞,無法記錄新事件,直到工作線程有機會分派一個或多個事件。當隊列不再達到最大容量時,應用程序線程可以再次開始記錄事件。因此,當應用程序在其事件緩沖區(qū)的容量或附近運行時,異步日志記錄就變成了偽同步。
這未必是件壞事,AsyncAppender異步追加器設(shè)計目的是允許應用程序繼續(xù)運行,盡管需要稍微多一點的時間來記錄事件,直到附加緩沖區(qū)的壓力減輕。
優(yōu)化 appenders 事件隊列的大小以獲得最大的應用程序吞吐量取決于幾個因素。
下列任何或全部因素都可能導致出現(xiàn)偽同步行為:
- 大量的應用程序線程
- 每個應用程序調(diào)用都有大量的日志事件
- 每個日志事件都有大量數(shù)據(jù)
- 子級appenders的高延遲
為了保持事情的進展,增加隊列的大小通常會有所幫助,代價是減少應用程序可用的堆。
為了減少阻塞,在缺省情況下,當隊列容量保留不到20% 時,AsyncAppender 將丟失 TRACE、 DEBUG 和 INFO 級別的事件,只保留 WARN 和 ERROR 級別的事件。
這種策略確保了對日志事件的非阻塞處理(因此具有優(yōu)異的性能) ,同時在隊列容量小于20% 時減少 TRACE、 DEBUG 和 INFO 級別的事件。事件丟失可以通過將丟棄閾值屬性設(shè)置為0(零)來防止。
六、性能測試
這部分自己還沒時間做測試,引用網(wǎng)上的一些測試數(shù)據(jù)。
既然能提高性能的話,必須進行一次測試比對,同步和異步輸出日志性能到底能提升多少倍?
服務器硬件
- CPU 六核
- 內(nèi)存 8G
測試工具
- Apache Jmeter
1、同步輸出日志
- 線程數(shù):100
- Ramp-Up Loop(可以理解為啟動線程所用時間) :0 可以理解為100個線程同時啟用
- 測試結(jié)果:
重點關(guān)注指標 Throughput【TPS】 吞吐量:系統(tǒng)在單位時間內(nèi)處理請求的數(shù)量,在同步輸出日志中 TPS 為 44.2/sec
2、異步輸出日志
- 線程數(shù) 100
- Ramp-Up Loop:0
- 測試結(jié)果:
TPS 為 497.5/sec , 性能提升了10多倍?。?!
總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
IntelliJ?IDEA?2021.3永久最新激活至2099年(親測有效)
最新版idea2021.3已出來,很多網(wǎng)友迫不及待的要升級idea2021最新版,今天小編抽空給大家整理了一篇教程關(guān)于idea2021.3最新激活教程,本文以idea2021.2.3為例通過圖文并茂的形式給大家分享激活詳細過程,感興趣的朋友參考下吧2020-12-12淺析Java.IO輸入輸出流 過濾流 buffer流和data流
這篇文章主要介紹了Java.IO輸入輸出流 過濾流 buffer流和data流的相關(guān)資料,本文給大家介紹的非常詳細,具有參考借鑒價值,需要的朋友可以參考下2016-10-10JAVA不可變類(immutable)機制與String的不可變性(推薦)
這篇文章主要介紹了JAVA不可變類(immutable)機制與String的不可變性(推薦)的相關(guān)資料,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2016-08-08Spring Boot+Nginx實現(xiàn)大文件下載功能
相信很多小伙伴,在日常開放中都會遇到大文件下載的情況,大文件下載方式也有很多,比如非常流行的分片下載、斷點下載;當然也可以結(jié)合Nginx來實現(xiàn)大文件下載,在中小項目非常適合使用,這篇文章主要介紹了Spring Boot結(jié)合Nginx實現(xiàn)大文件下載,需要的朋友可以參考下2024-05-05使用Spring Boot+MyBatis框架做查詢操作的示例代碼
這篇文章主要介紹了使用Spring Boot+MyBatis框架做查詢操作的示例代碼,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-10-10