揭穿迷思:代理 (Proxy) 會影響效能

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

在最近的一篇部落格文章中,Marc Logemann 探討了代理效能的主題。在他的文章中,他要求「Spring 團隊」提供一份白皮書。我不想花費 (頁) 又 (頁) 的篇幅來討論代理和位元組碼編織機制之間奈秒級的差異,但我確實認為再次重申這些差異是什麼,以及這個討論是否重要是有價值的。

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

讓我們先簡短地回顧一下代理的用途(一般而言,以及在 Spring 中)。根據四人幫 (Gang of Four, GoF) 的 設計模式書籍,代理是一個替代對象或另一個對象的佔位符,用於控制對它的訪問。因為代理位於對象的調用者和實際對象本身之間,它可以決定阻止調用實際(或目標)對象,或在調用目標對象之前執行某些操作。 prox.jpg

換句話說,代理可以用作實際對象的替身,以便將額外的行為應用於這些對象——無論是與安全性相關的行為、快取,還是效能測量。

許多現代框架使用代理來實現原本不可能實現的功能。許多物件關聯對應器使用代理來實現防止在實際需要資料之前載入資料的行為(這有時稱為延遲載入)。Spring 也使用代理來實現其某些功能,例如其遠端處理工具、其交易管理工具和 AOP 框架。

代理的替代方案是位元組碼編織。當使用位元組碼編織機制時,永遠不會有第二個物件(又名代理)。相反,如果需要應用行為(例如交易管理或安全性),它會被編織「到」現有程式碼中,而不是「圍繞它」。執行編織過程的一種方法是使用 Java 5 的 -javaagent 標誌。還有其他方法可用。

換句話說:使用代理,您最終會得到一個位於目標對象之前的代理對象,而使用位元組碼編織方法,則不會有必須委派調用的代理。

殘酷的真相

好的,讓我們結束它:代理會為純方法調用增加額外負擔... 顯著的額外負擔。在我看來,這絕對不足為奇。在中間放置一個代理是完全自然的。一般來說,可以說中間媒介總是會增加額外負擔。現在的問題是:為了代理增加的額外負擔,我們得到了什麼回報?

請注意,我不打算在這裡提供數字。正如 Stefan Hansel 在 他在 Marc 部落格上的評論 中正確指出的那樣,測量純目標調用與中間有代理之間的差異的微基準測試(或任何微基準測試)實際上沒有意義,因為您還必須考慮到一大堆其他因素。

好的,但您確實想要數字?

好的,那我們開始吧。讓我們考慮以下程式碼片段,其中我們有兩個物件,一個被代理,另一個沒有。讓我們假設目標物件本身(dotIt() 方法)沒有做任何特別的事情。讓我們也假設代理也沒有做任何特別的事情(它只是委派給目標物件)。

如果我在我的筆記型電腦 (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 社群中所有即將到來的活動。

查看全部