在任何系統(tǒng)中,日志都是非常重要的組成部分,它是反映系統(tǒng)運行情況的重要依據(jù)辉巡,也是排查問題時的必要線索。絕大多數(shù)人都認可日志的重要性蕊退,但是又有多少人仔細想過該怎么打日志郊楣,日志對性能的影響究竟有多大呢?今天就讓我們來聊聊Java日志性能那些事瓤荔。
說到Java日志净蚤,大家肯定都會說要選擇合理的日志級別、合理控制日志內(nèi)容输硝,但是這僅是萬里長征第一步……哪怕一些DEBUG級別的日志在生產(chǎn)環(huán)境中不會輸出到文件中今瀑,也可能帶來不小的開銷。我們撇開判斷和方法調(diào)用的開銷点把,在Log4J 2.x的性能文檔中有這樣一組對比:
logger.debug("Entry number: " + i + " is " + String.valueOf(entry[i])); logger.debug("Entry number: {} is {}", i, entry[i]);
上面兩條語句在日志輸出上的效果是一樣的橘荠,但是在關(guān)閉DEBUG日志時,它們的開銷就不一樣了愉粤,主要的影響在于字符串轉(zhuǎn)換和字符串拼接上砾医,無論是否生效拿撩,前者都會將變量轉(zhuǎn)換為字符串并進行拼接衣厘,而后者則只會在需要時執(zhí)行這些操作。Log4J官方的測試結(jié)論是兩者在性能上能相差兩個數(shù)量級。試想一下影暴,如果某個對象的toString()方法里用了ToStringBuilder來反射輸出幾十個屬性時错邦,這時能省下多少資源。
因此型宙,某些仍在使用Log4J 1.x或Apache Commons Logging(它們不支持{}模板的寫法)的公司都會有相應(yīng)的編碼規(guī)范撬呢,要求在一定級別的日志(比如DEBUG和INFO)輸出前增加判斷:
if (logger.isDebugEnabled()) { logger.debug("Entry number: " + i + " is " + String.valueOf(entry[i])); }
除了日志級別和日志消息,通常在日志中還會包含一些其他信息妆兑,比如日期魂拦、線程名、類信息搁嗓、MDC變量等等芯勘,根據(jù)Takipi的測試,如果在日志中加入class腺逛,性能會急劇下降荷愕,比起LogBack的默認配置,吞吐量的降幅在6成左右棍矛。如果一定要打印類信息安疗,可以考慮用類名來命名Logger。
在分布式系統(tǒng)中够委,一個請求可能會經(jīng)過多個不同的子系統(tǒng)荐类,這時最好生成一個UUID附在請求中,每個子系統(tǒng)在打印日志時都將該UUID放在MDC里慨绳,便于后續(xù)查詢相關(guān)的日志掉冶。《The Ultimate Guide: 5 Methods For Debugging Production Servers At Scale》一文中就如何在生產(chǎn)環(huán)境中進行調(diào)試給出了不少建議脐雪,當中好幾條是關(guān)于日志的厌小,這就是其中之一。另一條建議是記錄下所有未被捕獲的日志战秋,其實拋出異常有開銷璧亚,記錄異常同樣會帶來一定的開銷,主要原因是Throwable類的fillInStackTrace方法默認是同步的:
public synchronized native Throwable fillInStackTrace();
一般使用logger.error都會打出異常的堆棧脂信,如果對吞吐量有一定要求癣蟋,在情況運行時可以考慮覆蓋該方法,去掉synchronized native狰闪,直接返回實例本身疯搅。
聊完日志內(nèi)容,再來看看Appender埋泵。在Java中幔欧,說起IO操作大家都會想起NIO罪治,到了JDK 7還有了AIO,至少都知道讀寫加個Buffer礁蔗,日志也是如此觉义,同步寫的Appender在高并發(fā)大流量的系統(tǒng)里多少有些力不從心,這時就該使用AsyncAppender了浴井,同樣是使用LogBack:
在10線程并發(fā)下晒骇,輸出200字符的INFO日志,AsyncAppender的吞吐量最高能是FileAppender的3.7倍磺浙。在不丟失日志的情況下洪囤,同樣使用AsyncAppender,隊列長度對性能也會有一定影響撕氧。
如果使用Log4J 2.x箍鼓,那么除了有AsyncAppender,還可以考慮性能更高的異步Logger呵曹,由于底層用了Disruptor款咖,沒有鎖的開銷,性能更為驚人奄喂。根據(jù)Log4J 2.x的官方測試铐殃,同樣使用Log4J 2.x:
64線程下,異步Logger比異步Appender快12倍跨新,比同步Logger快68倍富腊。
同樣是異步,不同的庫之間也會有差異:
同等硬件環(huán)境下域帐,Log4J 2.x全部使用異步Logger會比LogBack的AsyncAppender快12倍赘被,比Log4J 1.x的異步Appender快19倍。
Log4J 2.x的異步Logger性能強悍肖揣,但也有不同的聲音民假,覺得這只是個看上去很優(yōu)雅,只能當成一個玩具龙优。關(guān)于這個問題羊异,還是留給讀者自己來思考吧。
如果一定要用同步的Appender彤断,那么可以考慮使用ConsoleAppender野舶,然后將STDOUT重定向到文件里,這樣大約也能有10%左右的性能提升宰衙。
大部分生產(chǎn)系統(tǒng)都是集群部署平道,對于分布在不同服務(wù)器上的日志,用Logstash之類的工具收集就好了供炼。很多時候還會在單機上部署多實例以便充分利用服務(wù)器資源一屋,這時千萬不要貪圖日志監(jiān)控或者日志查詢方便句伶,將多個實例的日志寫到同一個日志文件中,雖然LogBack提供了prudent模式陆淀,能夠讓多個JVM往同一個文件里寫日志,但此種方式對性能同樣也有影響先嬉,大約會使性能降低10%轧苫。
如果對同一個日志文件有大量的寫需求,可以考慮拆分日志到不同的文件疫蔓,做法之一是添加多個Appender含懊,同時修改代碼,不同的情況使用不同Logger衅胀;LogBack提供了SiftingAppender岔乔,可以直接根據(jù)MDC的內(nèi)容拆分日志,Jetty的教程中就有根據(jù)host來拆分日志的范例滚躯,而根據(jù)Takipi的測試雏门,SiftingAppender的性能會隨著拆分文件數(shù)的增長一同提升,當拆分為4個文件時掸掏,10并發(fā)下SiftingAppender的吞吐量約是FileAppender的3倍多茁影。
看了上面這么多的數(shù)據(jù),不知您是否覺得自己的日志有不少改進的余地丧凤,您還沒有把系統(tǒng)優(yōu)化到極致募闲,亦或者您還有其他日志優(yōu)化的方法,不妨分享給大家愿待。