談一談Java開發中的坑(一) -- log4j2在SaaS項目中的應用

背景介紹:
做server端開發有些年頭了,特別是開始做SaaS類型的項目時,深知log的重要性,特別是半夜三更用戶(大都是美國那邊的)遇到問題的電話打過來,這個時候沒有一個強大log,那只有抓瞎的份了。以前都是走的微軟系的開發工具,log的框架也是別人早就寫好的,所以對於log性能這一塊也沒有什麼關注,覺得它就應該是輕量級的,不應該耗費太多的資源。
後來換公司以後,開始學習用java編程了,緊跟着也就是各種開源類項目的引用。這次就跟大家說說,我在log這一塊遇到的問題和解決方案。

遇到的問題:
log框架的選型上比較順利,直接選用了比較常用的log4j2。開發工程中,沒有考慮太多,直接採用了常用的同步logger,appender採用的RollingFileAppender。類似如下配置方法:
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="warn" name="MyApp" packages="">
<Appenders>
<RollingFile name="RollingFile" fileName="logs/app.log"
filePattern="logs/$${date:yyyy-MM}/app-%d{MM-dd-yyyy}-%i.log.gz">
<PatternLayout>
<Pattern>%d %p %c{1.} [%t] %m%n</Pattern>
</PatternLayout>
<Policies>
<TimeBasedTriggeringPolicy interval="1" modulate="true" />
<SizeBasedTriggeringPolicy size="250 MB"/>
</Policies>
</RollingFile>
</Appenders>
<Loggers>
<Root level="error">
<AppenderRef ref="RollingFile"/>
</Root>
</Loggers>
</Configuration>
開發工程中一切安好,可是在打壓測試時發現,service的響應性能隨着線程的增加會變得很差(初期目標8000個用戶線程)。後來抓了dump(jstack)發現了好多線程都停留在執行log的方法裏。這纔開始看看爲什麼log的性能爲什麼這麼差。

解決方案:
後來上網搜索了一下,發現對於像這種SaaS,log需求量比較大的應用(多線程併發打log),需要採用異步的log輸出方式。log4j2對於異步log有很好的支持。這裏就把我學到的做一個小的總結。
  1. 異步log的歷史
起初log4j 1.x中引入了async appender,例如在RollingFileAppender中,設置一個屬性immediateFlush=false,就可以實現異步log,其後臺通過一個ArrayBlockingQueue,來實現log的緩存機制,其I/O操作使用的是BufferedOutputStream。
其後的log4j2中引入了AsyncLogger的概念,其中引用到了LMAX Disprutor的概念,性能明顯優化於ArrayBlockingQueue;同時Log4j2還引入了一個新的RandomAccessFileAppender,其使用ByteBuffer + RandomAccessFile的方式,比BufferedOutputStream在性能方面有明顯的提升。
  1. 最終的解決方案
AsyncLogger + RandomAccessFileAppender,示例如下:
<Configuration status="WARN">
<Appenders>
<!-- Async Loggers will auto-flush in batches, so switch off immediateFlush. -->
<RandomAccessFile name="RandomAccessFile" fileName="async.log"
immediateFlush="false" append="false">
<PatternLayout>
<Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [ThdID=%t] %-level: %m%n</Pattern>
</PatternLayout>
<Policies>
<TimeBasedTriggeringPolicy interval="1" modulate="true" />
</Policies>
</RandomAccessFile>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="RandomAccessFile"/>
</Root>
</Loggers>
  1. 一個小建議:
在使用log的方式上,不要採取這樣的方式,例如:myLogger.logDebug("aa" + i + " bb" + j),而是要採用myLogger.logDebug("aa {} bb {}", i, j);的方式。這樣做的好處,可以把字符串的拼接操作,放在需要輸出之前,這樣可以在你想要按level控制輸出時,減少那些不必要的字符操作,例如:
public void logDebug(String logMsg, Object... params){
if(currentLevel > DEBUG){
// 當決定不輸出debug level的數據時,這裏沒有發生字符串的拼接操作
return;
}

// 在這裏做實際的輸出
}

以下是幾篇非常不錯的關於AsyncLogger和LMAX Disprutor的文章,供大家深入的學習:
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章