借助 AOP 為 Java Web 應用記錄性能數據
來自: https://yq.aliyun.com/articles/4240
作為開發者,應用的性能始終是我們最感興趣的話題之一。然而,不是所有的開發者都對自己維護的應用的性能有所了解,更別說快速定位性能瓶頸并實施解決方案了。
2015 年北京 Velocity 的贊助商大多從事 APM 領域,提供性能剖析、可視化甚至優化的解決方案。這些廠商的產品看起來能夠很好地幫助中小企業的開發者解決應用性能上的缺陷,但是這些產品幾乎都有著一個致命的缺陷:極強的侵入性。
開發者需要在業務生產代碼中嵌入 APM 廠商提供的埋點代碼,才能夠使用 APM 廠商提供的 SaaS 服務。在瞬息萬變的技術大潮中,這種代碼級別的侵入和綁定,總是讓開發者憂心忡忡。如果我作為架構師,在自建 APM 還是使用 SaaS APM 上,也會謹慎考慮。
然而無論自建 APM 還是使用 SaaS 服務,其底層模型無非就是海量日志的實時處理,數據來源就是應用產生的性能日志了。
If we have data, let’s look at data. If all we have are opinions, let’s go with mine.
by Jim Barksdale
這是一個數據為王的時代,夸張一點說,數據可以指導一切!
言歸正傳,如果我們不希望使用 APM 廠商提供的強侵入的服務,我們就只能自建服務了,比如以 AOP 的方式采集線程內調用樹以及調用開銷并輸出日志,然后使用 ELK(Elasticsearch, Logstash, and Kibana) 去采集日志并提供搜索、可視化等功能。如果采集的日志僅作為離線計算使用,可以直接用 Flume 把日志寫入 HDFS。
隨著系統流量越來越大,上述的方案漸漸就扛不住了,然后就需要自己實現高性能的日志采集 Agent,把采集到的日志一股腦寫入 Kafka 之類的能扛大量堆積消息的 MQ 里面,然后使用 Storm/JStorm 做實時的流式計算。
前些日子我簡單搞了一個基于 AOP 來抓取調用樹和開銷的嘗試,感覺有點意思,分享一下。
抓取調用樹和時間開銷
在 Java 里面獲取代碼塊的時間開銷最常見的手段就是 System.currentTimeMillis() 。Apache 和 Guava 等流行類庫都有對獲取時間開銷這一功能的封裝類 StopWatch。
捕獲調用樹就沒有什么常見的封裝了。一種推薦的做法,是在一次調用中,給每個要剖析的代碼塊一個唯一的標記,這個標記要能夠體現代碼塊之間的嵌套、順序等關系。
舉個栗子,我們有如下調用關系。
func1
+- func2
| +- func3
| \- func4
\- func5
為了體現調用之間的嵌套和順序,我們給 func1 標記 0,給 func2 標記 0.1,給 func3 標記 0.1.1,給 func4 標記 0.1.2,給 func5 標記 0.2。如此一來,我們便能夠輕易地根據標記重建出調用樹。
我們可以把調用樹的抓取和記錄每個代碼塊的時間開銷的功能以線程安全的手法封裝起來,給這個封裝起一個類似于 Profiler 的名字。Profiler 提供 2 個靜態方法,enter 在進入代碼塊之前調用,exit 在代碼塊結束之后調用。
在實現 Profiler 的時候,需要給每個線程維護一個調用棧,以及剖析結果列表。基本上可以實現為 enter 壓棧,exit 退棧并把結果放入結果列表,當調用棧退空后,輸出完整的剖析結果。
AOP 與方法攔截器
Profiler 有一個需要嚴格執行的約定,就是 enter 和 exit 必須成對調用,就像 C++ 里面 new 和 delete 必須成對出現一樣,否則內存會被直接打爆,遠不是內存泄露這么簡單。
這種約定如果寫到業務代碼中,會死的很難看,各種 try finally 硬生生的把業務邏輯打斷,本來業務代碼就已經很惡心了,這么一搞簡直沒法維護。
所以我們需要一種比較科學的方式,以無入侵的方式實現對 Profiler 的正確調用。AOP 是一種合適的工具。
這里以 Spring AOP 為例,實現一個簡單的例子。
首先引入 Spring AOP 的依賴,或者包含 org.aopalliance.intercept.MethodInterceptor 的包。
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>2.5.6</version>
</dependency>
如果需要代碼能夠運行,還需要引入 cglib 的依賴。
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib-nodep</artifactId>
<version>2.2</version>
</dependency>
方法攔截器的參考實現如下,使用 try finally 這樣的 code pattern 去保證 Profiler 被正確使用。
public class Interceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
Class clazz = invocation.getMethod().getDeclaringClass();
String method = invocation.getMethod().getName();
String mark = clazz.getCanonicalName() + "#" + method;
Profiler.enter(mark);
try {
return invocation.proceed();
} finally {
String log = Profiler.exit();
if (log != null) {
System.out.println(log);
}
}
}
}
</code></code></code></div>