神器 JMH + Arthas 性能监控
點擊上方“朱小廝的博客”,選擇“設為星標”
后臺回復"書",獲取
后臺回復“k8s”,可領取k8s資料
來源 |?bryantchang.github.io/2019/12/08/java-profile-tools/
問題描述
JMH簡介
Arthas 我的代碼在運行時到底做了什么
實際操作
監(jiān)控方法調用
trace命令 & jad命令
watch命令
最近的工作日并不算太平,各種大大小小的case和解case,發(fā)現已經有好久好久沒有靜下心來專心寫點東西了。不過倒還是堅持利用業(yè)余時間學習了不少微課上的東西,發(fā)現大佬們總結的東西還是不一樣,相比于大學時的那些枯燥的課本,大佬們總結出來的內容更活,更加容易理解。自己后面也會把大佬們的東西好好消化吸收,變成自己的東西用文字性的東西表達出來。
今天想總結的東西是最近工作中使用到的測試工具JMH以及Java運行時監(jiān)控工具Arthas。他們在我的實際工作中也算是幫了大忙。所以在這里拋磚引玉一下這些工具的使用方法。同時也加深一下自己對這些工具的熟悉程度。對這兩個工具,我都會首先簡單介紹一下這些工具的大致使用場景,然后會使用一個在工作中真正遇到的性能問題排查為例詳細講解這兩個工具的實際用法。話不多說,直奔主題。
問題描述
為了能夠讓我后面的實例能夠貫穿這兩個工具的使用,我首先簡單描述下我們在開發(fā)中遇到的實際的性能問題。然后再引出這兩個性能工具的實際使用,看我們如何使用這兩個工具成功定位到性能瓶頸的。
問題如下:為了能夠支持丟失率,我們將原先log4j2 的Async+自定義Appender的方式進行了修正,把異步的邏輯放到了自己改版后的Appender中。但我們發(fā)現修改后日志性能要比之前Async+自定義Appender的方式下降不少。這里由于隱私原因我并沒有用實際公司中的實例,這里我用了一種其他同樣能夠體現問題的方式。我們暫時先不給出具體的配置文件,先給出性能測試代碼和結果
代碼
package?com.bryantchang.appendertest;import?org.slf4j.Logger; import?org.slf4j.LoggerFactory;public?class?AppenderTest?{private?static?final?String?LOGGER_NAME_DEFAULT?=?"defaultLogger";private?static?final?String?LOGGER_NAME_INCLUDE?=?"includeLocationLogger";private?static?final?Logger?LOGGER?=?LoggerFactory.getLogger(LOGGER_NAME_INCLUDE);public?static?final?long?BATCH?=?10000;public?static?void?main(String[]?args)?throws?InterruptedException?{while(true)?{long?start,?end;start?=?System.currentTimeMillis();for?(int?i?=?0;?i?<?BATCH;?i++)?{LOGGER.info("msg?is?{}",?i);}end?=?System.currentTimeMillis();System.out.println("duration?of?"?+?LOGGER_NAME_INCLUDE?+??"?is?"?+?(end?-?start)?+?"ms");Thread.sleep(1000);}} }代碼邏輯及其簡單,就是調用logger.info每次打印10000條日志,然后記錄耗時。兩者的對比如下
從這兩張圖片中我們能夠看到同樣的邏輯,兩個程序的耗時差距相差了數十倍,但看圖片,貌似僅僅是logger的名稱不一樣。對上面的實驗結果進行分析,我們可能會有兩個疑問
上面的代碼測試是否標準,規(guī)范
如果真的是性能問題,那么這兩個代碼到底在哪個方法上有了這么大的差距導致了最終的性能差異
下面這兩個工具就分別來回答這兩個問題
JMH簡介
第一個問題就是,測試的方法是否標準。我們在編寫代碼時用的是死循環(huán)+前后“掐秒表”的方式。假如我們要再加個多線程的測試,我們還需要搞一個線程池,除了代碼本身的邏輯還要關心測試的邏輯。我們會想,有沒有一款工具能夠將我們從測試邏輯中徹底解放出來,只需要關心我們需要測試的代碼邏輯。JMH就是這樣一款Java的測試框架。下面是JMH的官方定義
JMH?是一個面向?Java?語言或者其他?Java?虛擬機語言的性能基準測試框架這里面我們需要注意的是,JMH所測試的方法約簡單越好,依賴越少越好,最適合的場景就是,測試兩個集合put,get性能,例如ArrayList與LinkedList的對比等,這里我們需要測試的是批量打一批日志所需要的時間,也基本符合使用JMH的測試場景。下面是測試代碼,bench框架代碼以及主函數。
待測試方法
可以看到待測試方法非常簡單,就是單批次一次性打印10000條日志的操作,所以并沒有需要額外說明的部分。下面我們再來看benchmark部分。
public?class?LogBenchMarkMain?{@Benchmark@BenchmarkMode(Mode.AverageTime)@Fork(value?=?1)@Threads(1)public?void?testLog1()?{LogBenchMarkWorker.getInstance().logBatch();}@Benchmark@BenchmarkMode(Mode.AverageTime)@Fork(value?=?1)@Threads(4)public?void?testLog4()?{LogBenchMarkWorker.getInstance().logBatch();}@Benchmark@BenchmarkMode(Mode.AverageTime)@Fork(value?=?1)@Threads(8)public?void?testLog8()?{LogBenchMarkWorker.getInstance().logBatch();}@Benchmark@BenchmarkMode(Mode.AverageTime)@Fork(value?=?1)@Threads(16)public?void?testLog16()?{LogBenchMarkWorker.getInstance().logBatch();}在這段代碼中,我們就會發(fā)現有了一些JMH中特有的東西,我下面進行簡要介紹。
Benchmark注解:標識在某個具體方法上,表示這個方法將是一個被測試的最小方法,在JMH中成為一個OPS BenmarkMode:測試類型,JMH提供了幾種不同的ModeThroughput:整體吞吐量AverageTime:調用的平均時間,每次OPS執(zhí)行的時間SampleTime:取樣,給出不同比例的ops時間,例如99%的ops時間,99.99%的ops時間 fork:fork的次數,如果設置為2,JMH會fork出兩個進程來測試 Threads:很容易理解,這個參數表示這個方法同時被多少個線程執(zhí)行在上面的代碼中,我定義了4個待測試的方法,方法的Fork,BenchmarkMode均為測試單次OPS的平均時間,但4個方法的線程數不同。除了這幾個參數,還有幾個參數,我會在main函數里面來講,main代碼如下所示
public?class?Main?{public?static?void?main(String[]?args)?throws?RunnerException?{Options?options?=?new?OptionsBuilder().include(LogBenchMarkMain.class.getSimpleName()).warmupIterations(5).measurementIterations(5).output("logs/BenchmarkCommon.log").build();new?Runner(options).run();} }我們可以看到,在main函數中,我們就是要設置JMH的基礎配置,這里面的幾個配置參數含義如下:
include:benchmark所在類的名字,可以使用正則表達 warmupIteration:預熱的迭代次數,這里為什么要預熱的原因是由于JIT的存在,隨著代碼的運行,會動態(tài)對代碼的運行進行優(yōu)化。因此在測試過程中需要先預熱幾輪,讓代碼運行穩(wěn)定后再實際進行測試 measurementIterations:實際測試輪次 output:測試報告輸出位置我分別用兩種logger運行一下測試,查看性能測試報告對比
使用普通logger
LogBenchMarkMain.testLog1???avgt????5??0.006?±?0.001???s/op LogBenchMarkMain.testLog16??avgt????5??0.062?±?0.026???s/op LogBenchMarkMain.testLog4???avgt????5??0.006?±?0.002???s/op LogBenchMarkMain.testLog8???avgt????5??0.040?±?0.004???s/op使用了INCLUDE_LOCATION的logger
LogBenchMarkMain.testLog1???avgt????5??0.379?±?0.007???s/op LogBenchMarkMain.testLog16??avgt????5??1.784?±?0.670???s/op LogBenchMarkMain.testLog4???avgt????5??0.378?±?0.003???s/op LogBenchMarkMain.testLog8???avgt????5??0.776?±?0.070???s/op這里我們看到,性能差距立現。使用INCLUDE_LOCATION的性能要明顯低于使用普通logger的性能。這是我們一定很好奇,并且問一句“到底慢在哪”!!
Arthas 我的代碼在運行時到底做了什么
Arthas是阿里開源的一款java調試神器,與greys類似,不過有著比greys更加強大的功能,例如,可以直接繪制java方法調用的火焰圖等。這兩個工具的原理都是使用了Java強大的字節(jié)碼技術。畢竟我也不是JVM大佬,所以具體的實現細節(jié)沒法展開,我們要做的就是站在巨人的肩膀上,接受并用熟練的使用好這些好用的性能監(jiān)控工具。
實際操作
talk is cheap, show me your code,既然是工具,我們直接進行實際操作。我們在本機運行我們一開始的程序,然后講解arthas的使用方法。
我們首先通過jps找到程序的進程號,然后直接通過arthas給到的as.sh對我們運行的程序進行字節(jié)碼增強,然后啟動arthas,命令如下
./as.sh?pid可以看到,arthas支持查看當前jvm的狀態(tài),查看當前的線程狀態(tài),監(jiān)控某些方法的調用時間,trace,profile生成火焰圖等,功能一應俱全 我們這里也只將幾個比較常用的命令,其他的命令如果大家感興趣可以詳見官網arthas官網。這篇文章主要介紹下面幾個功能
反編譯代碼
監(jiān)控某個方法的調用
查看某個方法的調用和返回值
trace某個方法
監(jiān)控方法調用
這個主要的命令為monitor,根據官網的介紹,常用的使用方法為
monitor?-c?duration?className?methodName其中duration代表每隔幾秒展示一次統計結果,即單次的統計周期,className就是類的全限定名,methodname就是方法的名字,這里面我們查看的方法是Logger類的info方法,我們分別對使用兩種不同logger的info方法。這里面的類是org.slf4j.Logger,方法時info,我們的監(jiān)控語句為
monitor?-c?5?org.slf4j.Logger?info監(jiān)控結果如下
使用普通appender
我們可以看到,使用include appeder的打印日志方法要比普通的appender高出了3倍,這就不禁讓我們有了疑問,究竟這兩個方法各個步驟耗時如何呢。下面我們就介紹第二條命令,trace方法。
trace命令 & jad命令
這兩個程序的log4j2配置文件如下
<?xml?version="1.0"?encoding="UTF-8"?> <!--status:日志等級?? monitorInterval:更新配置文件的時間間隔,單位秒--> <configuration?status="warn"?monitorInterval="30"><!--定義所有的appender?--><appenders><!--這個輸出控制臺的配置?--><Console?name="console"?target="SYSTEM_OUT"><!--控制臺只輸出level及以上級別的信息(onMatch),其他的直接拒絕(onMismatch)?--><ThresholdFilter?level="warn"?onMatch="ACCEPT"?onMismatch="DENY"/><!--日志打印格式?--><PatternLayout?pattern="[%d{HH:mm:ss.SSS}]?[%-5p]?%l?-?%m%n"/></Console><Async?name="AsyncDefault"?blocking="false"?includeLocation="false"><AppenderRef?ref="fileAppender"/></Async><Async?name="AsyncIncludeLocation"?blocking="false"?includeLocation="true"><AppenderRef?ref="fileAppender"/></Async><!--文件會打印出所有信息,這個log每次運行程序會自動清空,由append屬性決定,這個也挺有用的,適合臨時測試用?--><!--append為TRUE表示消息增加到指定文件中,false表示消息覆蓋指定的文件內容,默認值是true?--><File?name="fileAppender"?fileName="log/test.log"?append="false"><PatternLayout?pattern="[%d{HH:mm:ss.SSS}]?[%-5p]?%l?-?%m%n"/></File><!--添加過濾器ThresholdFilter,可以有選擇的輸出某個級別以上的類別??onMatch="ACCEPT"?onMismatch="DENY"意思是匹配就接受,否則直接拒絕??--><File?name="ERROR"?fileName="logs/error.log"><ThresholdFilter?level="error"?onMatch="ACCEPT"?onMismatch="DENY"/><PatternLayout?pattern="[%d{yyyy.MM.dd?'at'?HH:mm:ss?z}]?[%-5p]?%l?-?%m%n"/></File><!--這個會打印出所有的信息,每次大小超過size,則這size大小的日志會自動存入按年份-月份建立的文件夾下面并進行壓縮,作為存檔?--><RollingFile?name="rollingFile"?fileName="logs/app.log"filePattern="logs/$${date:yyyy-MM}/web-%d{yyyy-MM-dd}.log.gz"><PatternLayout?pattern="[%d{yyyy-MM-dd?HH:mm:ss}]?[%-5p]?%l?-?%m%n"/><Policies><TimeBasedTriggeringPolicy?modulate="true"?interval="1"/></Policies><DefaultRolloverStrategy><Delete?basePath="logs"?maxDepth="2"><IfFileName?glob="*/*.log.gz"?/><IfLastModified?age="7d"?/></Delete></DefaultRolloverStrategy></RollingFile></appenders><!--然后定義logger,只有定義了logger并引入的appender,appender才會生效?--><loggers><!--過濾掉spring和mybatis的一些無用的DEBUG信息--><logger?name="defaultLogger"?additivity="false"><appender-ref?ref="AsyncDefault"></appender-ref></logger><logger?name="includeLocationLogger"?additivity="false"><appender-ref?ref="AsyncIncludeLocation"></appender-ref></logger><!--建立一個默認的root的logger?--><root?level="INFO"></root></loggers></configuration>我們都是用了一個AsyncAppender套用了一個FileAppender。查看fileAppender,發(fā)現二者相同完全沒區(qū)別,只有asyncAppender中的一個選項有區(qū)別,這就是includeLocation,一個是false,另一個是true。至于這個參數的含義,我們暫時不討論,我們現在知道問題可能出在AsyncAppender里面,但是我們該如何驗證呢。trace命令就有了大用場。
trace命令的基本用法與monitor類似,其中主要的一個參數是-n則是代表trace多少次的意思
trace?-n?trace_times?className?methodName我在之前Log4j2的相關博客里面講到過,任何一個appender,最核心的方法就是他的append方法。所以我們分別trace兩個程序的append方法。
trace?-n?5?org.apache.logging.log4j.core.appender.AsyncAppender?appendtrace結果如下
使用普通appender
我們立刻可以發(fā)現,兩個trace的熱點方法不一樣,在使用include的appender中,耗時最長的方法時org.apache.logging.log4j.core.impl.Log4jLogEvent類中的createMemento方法,那么怎么才能知道這個方法到底做了啥呢,那就請出我們下一個常用命令jad,這個命令能夠反編譯出對應方法的代碼。這里我們jad一下上面說的那個createMemento方法,命令很簡單
jad?org.apache.logging.log4j.core.impl.Log4jLogEvent?createMemento結果如下
watch命令
watch命令能夠觀察到某個特定方法的入參,返回值等信息,我們使用這個命令查看一下這個createMemento方法的入參,如果兩個程序的入參不同,那基本可以斷定是這個原因引起命令如下
watch?org.apache.logging.log4j.core.impl.Log4jLogEvent?createMemento?"params"?-x?2?-n?5?-b?-f這里面的參數含義如下
-x?參數展開層次 -n?執(zhí)行次數 -b?查看方法調用前狀態(tài) -f?方法調用后其中的param代表查看方法的調用參數列表,還有其他的監(jiān)控項詳見官網官網
最終watch結果如下
使用普通logger
果不其然,這兩個參數果然是一個true一個false,我們簡單看下這個參數是如何傳進來的,我們jad一下AsyncAppender的append方法
不過為了一探究竟,我還是靜態(tài)跟了一下這段代碼
這個includeLocation會在event的createMemento中被用到,在序列化生成對象時會創(chuàng)建一個LogEventProxy,代碼如下
public?LogEventProxy(final?LogEvent?event,?final?boolean?includeLocation)?{this.loggerFQCN?=?event.getLoggerFqcn();this.marker?=?event.getMarker();this.level?=?event.getLevel();this.loggerName?=?event.getLoggerName();final?Message?msg?=?event.getMessage();this.message?=?msg?instanceof?ReusableMessage??memento((ReusableMessage)?msg):?msg;this.timeMillis?=?event.getTimeMillis();this.thrown?=?event.getThrown();this.thrownProxy?=?event.getThrownProxy();this.contextData?=?memento(event.getContextData());this.contextStack?=?event.getContextStack();this.source?=?includeLocation???event.getSource()?:?null;this.threadId?=?event.getThreadId();this.threadName?=?event.getThreadName();this.threadPriority?=?event.getThreadPriority();this.isLocationRequired?=?includeLocation;this.isEndOfBatch?=?event.isEndOfBatch();this.nanoTime?=?event.getNanoTime(); }如果includeLocation為true,那么就會調用getSource函數,跟進去查看,代碼如下
public?StackTraceElement?getSource()?{if?(source?!=?null)?{return?source;}if?(loggerFqcn?==?null?||?!includeLocation)?{return?null;}source?=?Log4jLogEvent.calcLocation(loggerFqcn);return?source;}public?static?StackTraceElement?calcLocation(final?String?fqcnOfLogger)?{if?(fqcnOfLogger?==?null)?{return?null;}//?LOG4J2-1029?new?Throwable().getStackTrace?is?faster?than?Thread.currentThread().getStackTrace().final?StackTraceElement[]?stackTrace?=?new?Throwable().getStackTrace();StackTraceElement?last?=?null;for?(int?i?=?stackTrace.length?-?1;?i?>?0;?i--)?{final?String?className?=?stackTrace[i].getClassName();if?(fqcnOfLogger.equals(className))?{return?last;}last?=?stackTrace[i];}return?null;}我們看到他會從整個的調用棧中去尋找調用這個方法的代碼行,其性能可想而知。我們用arthas監(jiān)控一下,驗證一下。
首先我們trace crateMemento方法
trace?-n?5?org.apache.logging.log4j.core.impl.Log4jLogEvent?createMementotrace?-n?5?org.apache.logging.log4j.core.impl.Log4jLogEvent?serializetrace?-n?5?org.apache.logging.log4j.core.impl.Log4jLogEvent$LogEventProxy?<init>trace?-n?5?trace?-n?5?org.apache.logging.log4j.core.LogEvent?getSource至此我們通過結合JMH和arthas共同定位出了一個線上的性能問題。不過我介紹的只是冰山一角,更多常用的命令還希望大家通過官網自己了解和實踐,有了幾次親身實踐之后,這個工具也就玩熟了。
想知道更多?掃描下面的二維碼關注我后臺回復"技術",加入技術群 后臺回復“k8s”,可領取k8s資料總結
以上是生活随笔為你收集整理的神器 JMH + Arthas 性能监控的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 你对JVM三色标记的理解嘛?
- 下一篇: 搞定 Go 语言,不会这些可不行