藉助 AOP 為 Java Web 應用記錄效能資料

潘家邦發表於2015-09-04

作為開發者,應用的效能始終是我們最感興趣的話題之一。然而,不是所有的開發者都對自己維護的應用的效能有所瞭解,更別說快速定位效能瓶頸並實施解決方案了。

今年北京 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.

Jim Barksdale

這是一個資料為王的時代,誇張一點說,資料可以指導一切!

言歸正傳,如果我們不希望使用 APM 嘗試提供的強侵入的服務,我們就只能自建服務了,比如以 AOP 的方式採集執行緒內呼叫樹以及呼叫開銷並輸出日誌,然後使用 ELK(Elasticsearch, Logstash, and Kibana) 去採集日誌並提供搜尋、視覺化等功能。如果採集的日誌僅作為離線計算使用,可以直接用 Flume 把日誌寫入 HDFS。

隨著系統流量越來越大,上述的方案漸漸就扛不住了,然後就需要自己實現高效能的日誌採集 Agent,把採集到的日誌一股腦寫入 Kafuka 之類的能扛大量堆積訊息的 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);
            }
        }
    }
}

相關文章