揭穿神話:代理影響效能

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

在最近的一篇部落格文章中,Marc Logemann 談到了代理效能的主題。在他的文章中,他要求“Spring 開發者”提供一份白皮書。我不想花費大量篇幅討論代理和位元組碼織入機制之間精確到納秒的差異,但我確實認為重申這些差異以及這種討論是否重要是有價值的。

什麼是代理以及我們為什麼使用它們?

讓我們首先簡要回顧一下代理的用途(通常以及在 Spring 中)。根據 GoF(Gang of Four)關於 設計模式 的書,代理是另一個物件的替身或佔位符,用於控制對其的訪問。因為代理位於物件的呼叫者和實際物件之間,所以它可以決定阻止實際(或目標)物件被呼叫,或者在目標物件被呼叫之前做一些事情。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代理;在其他情況下,您可能需要修改編譯過程;其他框架可能需要使用不同的類載入器。換句話說,位元組碼織入設定起來稍微困難一些。根據我的經驗,(一如既往)80-20法則在這裡也適用。80%的需求可能可以使用基於代理的系統解決。對於最後一部分,或者說剩下的20%,選擇位元組碼織入方法可能是一個不錯的選擇。

與AOP的關係

您可能想知道我為什麼還沒有談到AOP的話題。代理和位元組碼織入與AOP有很強的關係。或者也許是反過來。無論如何,Spring的AOP框架使用代理來實現其功能。在我看來,代理只是一個實現細節(儘管是一個非常重要的細節),與AOP和Spring總體上密切相關。

結論

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

獲取 Spring 新聞通訊

透過 Spring 新聞通訊保持聯絡

訂閱

領先一步

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

瞭解更多

獲得支援

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位制檔案,只需一份簡單的訂閱。

瞭解更多

即將舉行的活動

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

檢視所有