一 背景
在一次活動前的壓測中,發現一個服務(平響為250ms左右)存在併發瓶頸,單例項的QPS壓力從20升高到40後服務就雪崩了(平響急劇升高)。
通過<jstack -F>命令檢視執行緒資訊,發現很多執行緒BLOCKED在列印日誌的地方:
該服務使用log4j-2.7列印日誌,當時做了下面三個嘗試:
- 從Logger改成asyncLogger,無效果;
- 減少日誌量(只列印com.xxx.xxx包路徑下的日誌),單例項QPS壓力升高到48後服務雪崩;
- 不列印info級別日誌,單例項QPS壓力到80服務依然正常;
很疑惑,為什麼日誌列印對服務效能的影響如此大?而且單例項的QPS壓力只有20也太小了(併發數只有5 = 20 / 1000ms / 250ms)!
二 排查
分析<jstack -F>命令查出的執行緒資訊,類Throwable的653行,printStackTrace()方法會對標準錯誤輸出流(System.err)加同步鎖(synchronized)。非常順利,併發瓶頸的原因找到了!
但是,為什麼logger.info會進入到Throwable.printStackTrace()呢?
錯判1、jstack
懷疑<jstack -F>命令查出的執行緒資訊有問題,嘗試用<jstack -l>命令,提示錯誤資訊"well-known file is not secure",搜了下是由於<pid>程式的所有者與執行jstack命令的使用者不一致,使用sudo未成功(機器許可權問題,阻塞未解決)。
錯判2、GC
分析Throwable.printStackTrace()的上一行堆疊資訊(類Logger的144行、類AbstractLogger的1992/1998行),懷疑是GC導致(歷史經驗,但講不通),檢視服務雪崩時的GC日誌,發現確實GC頻繁,搜了下CMS GC的相關文章,嘗試修改JVM引數(記憶體大小、GC演算法等),無效果。
錯判3、log4j的bug
Remote debug到測試環境上,在Throwable.printStackTrace()處斷點,發現必現異常(ArrayIndexOutOfBoundsException:4)。於是使用關鍵字log4j+ArrayIndexOutOfBoundsException搜了下,找到log4j2的官方issue(https://issues.apache.org/jira/browse/LOG4J2-1542),不太對,繼續瀏覽該關鍵字其他的bug issue,沒有找到答案,想著要不提一個bug?但升級log4j的版本到2.13後無效果。
柳暗花明
再次Remote debug到測試環境上,一步一步除錯,發現會進入一些非本工程的程式碼且出現單詞trace,想起來之前看到的通過位元組碼注入方式(jar包)列印trace日誌的方案,懷疑是trace包內陣列越界後catch異常同時e.printStackTrace()。最後找到trace包的提供者驗證了該懷疑:
三 結論
通過位元組碼注入方式列印trace日誌的jar包有一個陣列越界的bug: