揭穿迷思:代理影響效能

工程 | Alef Arendsen | 2007 年 7 月 19 日 | ...

在最近的一篇部落格文章中,Marc Logemann 提到了代理效能的話題。在他的文章中,他請求 Spring 方面的 '夥計們' 提供一份白皮書。我不想花很多篇幅討論代理和位元組碼織入機制之間精確到納秒的差異,但我確實認為重申一下這些差異以及這場討論是否重要是很有價值的。

什麼是代理,我們為何使用它們?

讓我們首先簡要回顧一下代理的用途(通常意義上,以及在 Spring 中)。根據 Gang of Four (GoF) 關於設計模式書籍的說法,代理是另一個物件的替身或佔位符,用於控制對該物件的訪問。因為代理位於物件的呼叫者和實際物件之間,所以它可以決定阻止呼叫實際(或目標)物件,或者在呼叫目標物件之前做一些事情。prox.jpg

換句話說,代理可以作為真實物件的替身,為這些物件應用額外的行為——無論是安全相關的行為、快取還是效能度量。

許多現代框架使用代理來實現原本不可能實現的功能。許多物件關係對映器使用代理來實現一種行為,即在實際需要資料之前阻止資料載入(這有時稱為延遲載入)。Spring 也使用代理來實現其部分功能,例如其遠端處理能力、事務管理能力和 AOP 框架。

代理的替代方案是位元組碼織入。使用位元組碼織入機制時,永遠不會有第二個物件(即代理)。相反,如果需要應用行為(例如事務管理或安全性),它會被“織入”現有程式碼中,而不是“圍繞”程式碼。進行織入過程的一種方法是使用 Java5 的 -javaagent 標誌。也有其他方法可用。

換句話說:使用代理時,你最終會得到一個位於目標物件前面的代理物件,而使用位元組碼織入方法時,不會有需要委託呼叫的代理。

殘酷的事實

好的,讓我們直面這個問題:代理會給普通方法呼叫增加開銷……而且是顯著的開銷。在我看來,這完全不足為奇。在中間放置一個代理是完全自然的。一般來說,可以說中間層總是會增加開銷。現在問題是:**我們為代理增加的開銷換來了什麼?**

請注意,我在這裡不打算提供具體數字。正如 Stefan Hansel 在他在 Marc 部落格上的評論中正確指出的那樣,測量普通目標呼叫與中間有代理之間的差異的微基準測試(或任何微基準測試)並沒有太大意義,因為還需要考慮其他許多因素。

好的,但你**確實**想要數字?

好的,那我們來看吧。讓我們考慮以下這段程式碼,我們有兩個物件,一個被代理,一個沒有。假設目標物件本身(`doIt()` 方法)沒有做任何特別的事情。我們也假設代理本身沒有做任何特別的事情(它只是委託給目標物件)。

如果我在我的筆記型電腦 (MacBook) 上使用普通的 JDK 動態代理(稍後會詳細介紹)執行此程式碼,那麼對 *myRealObject* 的一次方法呼叫需要 9 納秒 (10-9)。對被代理物件的一次呼叫需要 500 納秒(大約慢了 50 倍)。


// real object
MyInterface myRealObject;
myRealObject.doIt();

// proxied object
MyInterface myProxiedObject;
myProxiedObject.doIt();

相比之下,如果我使用位元組碼織入方法(在這種情況下,我使用 AspectJ 來模擬相同的設定),我的呼叫只增加了大約 2 納秒。

所以總結來說,我無法美化事實:代理會給普通方法呼叫帶來顯著的開銷。

在我們繼續之前,先認識到這裡增加的開銷是**固定的**。如果 `doIt()` 方法本身需要 5 秒,被代理的呼叫**絕對不會**花費 50 倍的時間。不,相反,呼叫會花費 5 秒 + 約 500 納秒。

結合實際情況來看(或者:你需要在乎嗎?)

好的,現在我們知道代理並非那種能神奇地工作而不會產生副作用的超快速物件,問題是:“我們需要擔心開銷嗎?”答案很簡單:“不,你不需要” ;-)。我會解釋原因。

我們使用代理來透明地為物件新增行為。也許是為了用安全規則裝飾一個物件(管理員可以訪問它,但普通使用者不能),或者也許是因為我們想啟用延遲載入,只在第一次訪問時從資料庫載入資料。另一個原因可能是為我們的物件啟用透明的事務管理。

事務管理

讓我們看看事務管理的例子。以下序列圖大致描繪了(簡化檢視)在呼叫服務時發生的情況,其中事先啟動一個事務,並在成功完成後提交事務。seq.jpg

現在呼叫服務本身確實會涉及一定的開銷(我們之前已經討論過的開銷)。然而問題是,我們為這些開銷換來了什麼?

實現的益處

如果我們繼續看上面的例子,我們已經實現了幾個益處。

**程式碼簡化** 透過在中間放置一個代理,我們極大地簡化了程式碼。如果我們使用 Spring 提供的 `@Transactional` 註解,我們只需要做以下事情


public class Service {

  @Transactional 
  public void executeService() { }

}

以及


<tx:annotation-driven/>

<bean class="com.mycompany.Service"/>

另一種(程式設計方式的)方法將涉及顯著修改客戶端(呼叫者)或服務類本身。

**集中式事務管理** 事務管理現在由一箇中心設施負責,這使得最佳化和事務管理方法更加一致。如果我們在服務或呼叫者本身實現了事務管理程式碼,這是不可能實現的。

那這一切又有什麼關係呢?

如果這還不夠,我們隨時可以開始研究代理機制帶來的實際效能下降,並將其與啟動和/或提交事務所需的實際時間進行比較。我沒有任何具體的數字,但我可以向你保證,在 JDBC 事務上提交一個事務所需的時間肯定比 491 納秒要長。

但是如果代理執行的是非常細粒度的操作呢?

啊哈!那是完全不同的情況。當然,你可以透明地新增不同類別的行為(無論使用代理還是位元組碼織入方法)。我通常區分細粒度行為和粗粒度行為。在我看來,粗粒度行為應用於服務級別或僅應用於我們應用程式中的某個有限的操作集。更細粒度的行為集例如包括記錄系統中每個方法的日誌。我絕對不會選擇基於代理的方法來處理這種細粒度的場景。

經驗法則

綜上所述,我們可以得出以下結論
  • 首先,代理會增加開銷,但如果應用於被代理物件的行為與耗時較長的操作(例如資料庫或檔案訪問或事務管理)有關,那麼這種開銷可以忽略不計。
  • 我們還可以說,如果你需要非常細粒度的行為,並希望將其應用於大量物件,那麼選擇位元組碼織入方法(例如 AspectJ)可能更安全。
  • 如果這些還不夠,那麼仍然可以說,代理(除非應用於系統中數千個或更多物件)絕不會是你在系統性能下降時首先需要檢視的地方。
  • 另一個可能的經驗法則是,系統中的任何請求可能不應涉及(呼叫)超過 10 個(左右)被代理方法。**10 次代理操作 * 每次代理操作 500 納秒 = 5 微秒**(我認為這仍然可以忽略不計),但**100,000 次代理操作 * 每次代理操作 500 納秒 = 50 毫秒**(在我看來這就不再可以忽略不計了)。

不同型別的代理

除了關於代理是否會增加開銷的討論之外,簡要討論不同型別的代理也很重要。存在幾種不同型別的代理。在我做的小型基準測試中,我使用了 JDK 動態代理基礎設施(來自 `java.lang.reflect` 包),它只能為介面建立代理。另一種代理機制是 CGLIB,它使用了略微不同的代理方法。上次我在兩者之間進行小型效能基準測試時,並沒有發現顯著差異,坦率地說,我對此不太在意。重要的是已建立代理的內部工作原理。如果你自己開始實現代理,有很多事情可能會出錯。例如,如果你比較以下兩段程式碼,你可能不會預料到兩者之間在效能上會存在**巨大**差異。當我說巨大時,我是指大約 10 倍的差距……

public Object invoke(Object proxy, Method proxyMethod, Object[] args)
throws Throwable {
	Method targetMethod = null;
	if (!cachedMethodMap.containsKey(proxyMethod)) {
		targetMethod = target.getClass().getMethod(proxyMethod.getName(), 
			proxyMethod.getParameterTypes());
		cachedMethodMap.put(proxyMethod, targetMethod);
	} else {
		targetMethod = cachedMethodMap.get(proxyMethod);
	}
	Ojbect retVal = targetMethod.invoke(target, args);
	return retVal;
}

public Object invoke(Object proxy, Method proxyMethod, Object[] args)
throws Throwable {
	Method targetMethod = target.getClass().getMethod(proxyMethod.getName(), 
			proxyMethod.getParameterTypes());
	Ojbect retVal = targetMethod.invoke(target, args);
	return retVal;
}

換句話說,將代理的生成或建立留給知道自己在做什麼的人或框架來做。幸運的是,我沒有參與代理的設計,而且 Rob、Juergen、Rod 等人在這方面比我做得好得多,所以不用擔心 ;-)。

那位元組碼織入呢?

總的來說,可以說位元組碼織入方法需要根據你的環境花費更多時間來設定。在某些場景中,你需要設定 Java Agent,在其他情況下,你可能需要修改編譯過程,其他框架可能需要使用不同的類載入器。換句話說,位元組碼織入更難設定一些。根據我的經驗,(和往常一樣)80/20 法則也適用於這裡。80% 的需求可能可以使用基於代理的系統來解決。對於最後的部分,或者剩下的 20%,選擇位元組碼織入方法可能是一個不錯的選擇。

與 AOP 的關係

你可能想知道我為什麼還沒有觸及 AOP 這個話題。代理和位元組碼織入與 AOP 有很強的關係。或者也許反過來。無論如何,Spring 的 AOP 框架*使用*代理來實現其功能。在我看來,代理只是一個實現細節(雖然是一個相當重要的細節),與 AOP 和整個 Spring 框架緊密相連。

結論

總而言之,我們可以說代理確實會給對其代理的物件呼叫帶來一點開銷,但在大多數情況下,關於這個開銷的討論並不重要。原因部分在於代理帶來的巨大益處(例如由於程式碼簡化和集中控制帶來的程式碼維護性大大提高),也在於我們使用代理執行的操作(例如事務管理或快取)通常對效能的影響遠大於代理機制本身。

訂閱 Spring 新聞通訊

透過 Spring 新聞通訊保持聯絡

訂閱

領先一步

VMware 提供培訓和認證,助您快速進步。

瞭解更多

獲取支援

Tanzu Spring 在一個簡單的訂閱中提供對 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位制檔案。

瞭解更多

即將舉行的活動

檢視 Spring 社群中所有即將舉行的活動。

檢視全部