很多成功的企業應用程序都是使用 Java EE 平台構建的。但是,Java EE 的設計原理並不能夠有效地支持 Web 2.0 應用程序。深入了解 Java EE 和 Web 2.0 原理之間的脫節可幫助您制定明智的決策,從而使用各種方法和工具在一定程度上解決這種脫節。本文將解答 Web 2.0 和標准 Java EE 平台緣何成為失敗的組合,並演示為何由事件驅動的異步架構更適合 Web 2.0 應用程序。本文還介紹了一些框架和 API,它們通過支持異步設計使得 Java 平台更加適合 Web 2.0。
Java EE 原理和設想
Java EE 平台的創建目的就是為企業到客戶(B2C)和企業到企業(B2B)應用程序提供支持。企業發現了 Internet 的價值之後就開始使用它增強與合作伙伴和客戶之間的現有業務流程。這些應用程序通常要與一個現有企業集成系統(EIS)進行交互。大多數常見基准測試(測試 Java EE 服務器的性能和可伸縮性)— ECperf 1.1、SPECjbb2005 和 SPECjAppServer2004— 的用例都將這一點反映到了 B2C、B2B 和 EIS 中。類似地,標准的 Java PetStore 演示也是一個典型的電子商務應用程序。
很多有關 Java EE 架構可伸縮性的明顯和暗含的設想都反映在基准測試中:
從客戶機角度來看,請求吞吐量是影響性能的最重要特性。
事務持續時間是最重要的性能因素,並且,縮減所有個體事務的持續時間將改善應用程序的總體性能。
事務之間通常都是彼此獨立的。
除長期執行的事務以外,只有少數業務對象會受事務影響。
應用服務器的性能和部署在同一管理域的 EIS 會限制事務的持續時間。
通過使用連接池可以抵消一定的網絡通信成本(在處理本地資源時產生)
通過對網絡配置、硬件和軟件進行優化,可以縮短事務持續時間。
應用程序所有者可以控制內容和數據。在不依賴外部服務的前提下,向用戶提供內容的最重要限制因素是帶寬。
性能和可伸縮性問題
Java EE 平台最初的設計目的是使用部署在單個管理域中的資源操作服務。其設想的前提是 EIS 事務生存期較短並且請求處理較快,從而使平台能夠支持較高的事務負載。
很多新興架構方法和模式 — 例如對等(P2P)、面向服務架構和統稱(非正式地)為 Web 2.0 的新型 Web 應用程序 — 不滿足這些假設。在這些應用程序的使用場景中,請求處理將占用更長的時間。因此,當使用 Java EE 方法開發 Web 2.0 應用程序時,將出現嚴重的性能和可伸縮性問題。
這些設想產生了以下 Java EE API 構建原理:
同步 API。Java EE 在很多應用中都需要使用同步 API(重量級並且繁瑣的 Java Message Service (JMS) API 基本上是惟一的例外)。這種需求更多地源於可用性的需要,而非性能需求。同步 API 易於使用並且開銷較低。但需要處理大型多線程時,則會出現嚴重問題,因此 Java EE 嚴格限制未受控制的多線程處理。
有限的線程池。人們很快發現線程是種重要的資源,並且當線程數量超過某一界限後,應用服務器的性能將顯著下降。然而,根據每個操作都很短暫的設想,這些操作可以分配到一組有限的線程中,從而維持較高的請求吞吐量。
有限的連接池。如果只使用一個數據庫連接,則很難獲得最優的數據庫性能。雖然一些數據庫操作可以並行執行,但是增加額外的數據庫連接只能將應用程序提速到某一點。當連接數達到某一值後,數據庫性能將開始下滑。通常,數據庫連接的數量要小於 servlet 線程池中可用線程的數量。因此,連接池在創建時允許向服務器組件 — 例如 servlet 和 Enterprise JavaBeans (EJB) — 分配一個連接並在以後返回給連接池。如果連接不可用,組件將等待阻塞當前線程的連接。因為其他組件只對連接占用很短的時間,因此這種延遲通常較短。
固定的資源連接。應用程序被假設只使用很少一些外部資源。與各個資源的連接工廠通過 Java Naming and Directory Interface (JNDI)(或 EJB 3.0 的依賴性注入)獲得。實際上,支持與不同 EIS 資源進行連接的主要 Java EE API 只有企業 Web 服務 API。其他 API 多數都假設資源是固定的並且只有諸如用戶憑證這樣的額外數據應該提供給開放連接操作。
在 Web 1.0 中,這些原理玩轉得非常好。可以將一些獨特的應用程序設計為遵守這些規則。但是,這些原理不能有效支持 Web 2.0。
Web 2.0 帶來的巨變
Java EE 迎合 SOA
SOA 的引入首先對 Java EE 提出了挑戰。在 SOA 中,交互通常產生很高的吞吐量,並且由於要跨多個域到達服務端點,因此很可能會產生較高的延遲。一些交互可能還需要得到操作人員的允許,而這種批准流程可能會產生幾小時到幾周的延遲。各種中間流程通常會使延遲情況進一步惡化,SOA 的出現就是為了支持這些中間流程。
通過利用事務消息傳遞 API 並引入業務流程概念,Java EE 平台已經解決了延遲帶來的難題。SOAP-over-HTTP Web 服務調用模型和諸如 JMS 之類的消息傳遞服務之間一直不太匹配。HTTP 使用同步請求/響應模型並且沒有提供任何內置的可靠特性。諸如 WS-Notification、WS-Reliability、WS-ReliableMessaging 和 WS-ASAP 這些規范試圖針對部署在 B2B 環境中的 Web 服務解決這種錯誤匹配。但是對於 B2C 場景,通常部署的是富應用程序客戶機,因為這種客戶機可以使用特定於場景的交互模式(相對於 Web 應用程序)處理高延遲。
Web 2.0 應用程序具有很多獨特需求,因此,不適合將 Java EE 用於 Web 2.0 實現。其中一個需求就是,Web 2.0 應用程序更多地通過服務 API 使用另一個 Web 2.0 應用程序,而不是使用 Web 1.0 應用程序。Web 2.0 應用程序的一個更為重要的因素是,極度傾向於用戶到用戶(C2C)交互:應用程序所有者只生成一小部分內容;用戶負責生成大部分內容。
SOA + B2C + Web 2.0 = 高延遲
在 Web 2.0 環境中,聚合應用程序經常使用通過 SOA 服務 API 公開的服務和提要。這些應用程序需要在 B2C 環境中使用服務。例如,一個聚合應用程序可能從三個不同的數據源提取數據,如天氣信息、交通信息和地圖。檢索這三種獨特數據所需的時間延長了總的請求處理時間。不管數據源和服務 API 的數量是否增加,用戶仍然期望得到具有高反應度的應用程序。
諸如緩存這類技術可以緩解延遲問題,但是不適用於所有場景。比如,可以緩存地圖數據來減少響應時間,但通常並不適合將搜索查詢結果或者實時交通信息進行緩存。
服務調用本來就是一種高延遲過程,在客戶機和服務器上通常只分配很小一部分 CPU 資源。Web 服務調用的持續時間很大一部分用於建立連接和傳輸數據。因此,通常來講,提升客戶端或服務器端的性能對於減少調用持續時間效果甚微。
更好的交互性
Web 2.0 對用戶參與的支持引發了另外一大挑戰,因為應用程序要處理來自每個活動用戶的更多數量的請求。下面這些理由證明了這一點:
因為大多數事件是由其他用戶的操作引起的,因此會引發更多相關事件,並且用戶具備更強大的能力來生成事件。這些事件通常使用戶能夠更加積極地使用 Web 應用程序。
應用程序為用戶提供了更多的用例。Web 1.0 用戶僅僅可以浏覽類別、購買商品並跟蹤他們的訂單處理狀態。現在,用戶可以通過論壇、聊天、聚合等等方法與其他用戶進行積極地交流,這將產生更高的通信負載。
如今的應用程序越來越多地使用 Ajax 改善用戶體驗。與普通 Web 應用程序的頁面相比,使用 Ajax 的 Web 頁面加載要慢一些,因為頁面是由一些靜態內容、腳本(可能會非常大)和一些發往服務器的請求組成。加載完成後,Ajax 頁面通常會向服務器生成一些短小的請求。
高延遲和低帶寬客戶機
以手機和其他限制帶寬的客戶機為目標的應用程序日趨流行。即使服務器可以為特定客戶機提供快速服務,客戶機仍然不能迅速地使用數據,這要歸咎於其低帶寬連接和設備本身的物理限制。雖然客戶機是通過低吞吐量連接加載數據,服務器在占用 servlet 線程時仍然未被充分利用或處於等待狀態。隨著越來越多的移動設備使用網絡服務以及無線頻段的充分利用,這類客戶機的吞吐量和延遲性將逐漸減少,除非開發出一種通信機制來提供更好的可伸縮性。
與典型的 Web 1.0 應用程序相比,這些因素往往會生成更多的服務器通信量和請求數。在高負載期間,這種通信量難於控制(然而,Ajax 也提供了更多的機會對通信量進行優化;與支持相同用例的簡單 Web 應用程序相比,Ajax 生成的通信量通常更少)。
更多內容
Web 2.0 的特征就是比上一代 Web 應用程序擁有更大量的內容和更大的規模。
在 Web 1.0 世界中,內容通常只有經過業務實體的明確允許後才被發布到公司網站。企業需要控制所顯示的文本的每個字符。因此,如果計劃發布的內容超出了框架的大小限制,則要對內容進行優化或將其分成幾個較小的部分。
Web 2.0 站點的一個特性就是不會限制內容的大小或創建。大部分 Web 2.0 內容由用戶和社區生成。組織和企業僅僅提供工具實現內容創建和發布。由於使用了大量圖像、音頻和視頻,內容的大小也相應增加。
持久連接
建立客戶機到服務器的新連接會耗費很多時間。如果某些交互在預期之中,則建立一次客戶機/服務器通信,然後重復使用該連接,這樣做會獲得更高的效率。持久連接對於發送客戶機通知也很有用。但是 Web 2.0 應用程序的客戶機通常位於防火牆之後,一般很難或不能直接建立服務器到客戶機的連接。Ajax 應用程序需要發送請求輪詢特定事件。要減少輪詢請求的數量,一些 Ajax 應用程序使用 Comet 模式:該服務器被設計為在某個事件發生以前保持等待狀態,然後發送應答,同時保持連接打開。
對等消息傳遞協議,如 SIP、BEEP 和 XMPP,逐漸使用持久連接。流式直播視頻也從持久連接中獲益良多。
更容易發生 Slashdot 效應
Web 2.0 應用程序擁有大量的訪客,這一點使某些站點更容易發生 “Slashdot 效應” — 如果某個流行的 blog、新聞站點或社會型網站提及某個站點時,該站點的通信量負載會猛增。所有 Web 站點都應該准備好處理比普通負載高幾個數量級的通信量。這種情況下更重要的一點是,站點在如此高的負載下不會發生崩潰。
延遲問題
與操作吞吐量相比,操作延遲對 Java EE 應用程序的影響更大。即使應用程序使用的服務可以處理大量操作,延遲仍然保持不變或者進一步惡化。目前的 Java EE API 還無法很好地處理這一情況,因為這種情況違背了這些 API 設計中暗含的延遲假設。
在使用同步 API 時,為論壇或 blog 中的大型頁面提供服務將開啟一個處理線程。如果每個頁面需要一秒鐘的服務時間(例如 LiveJournal 這類應用程序,包含很多較大的頁面),並且線程池包含 100 個線程,那麼一秒鐘的時間內無法為超過 100 個頁面提供服務 — 這種性能無法接受。增加線程池中的線程數量收效甚微,因為當線程池中的線程數量增加時,應用程序-服務器性能將開始降低。
Java EE 架構無法利用 SIP、BEEP 和 XMPP 這樣的消息傳遞協議,因為 Java EE 的同步 API 持續使用單個線程。由於應用服務器使用有限的線程池,持續使用一個線程將使應用服務器在使用這些協議發送和接收消息時無法處理其他請求。同樣要注意,使用這些協議發送的消息可長可短(尤其在使用 BEEP 時),並且要生成這些消息,還需要使用 Web 服務或其他方法訪問部署在其他組織內的資源。此外,BEEP 和 Stream Control Transmission Protocol (SCTP) 這樣的傳輸協議在 TCP/IP 連接之上還需要建立一些同步的邏輯連接,這使線程管理問題變得更加嚴重。
要實現流式場景,Web 應用程序必須摒棄標准的 Java EE 模式和 API。因此,Java EE 很少用於運行 P2P 應用程序或流視頻。對於經常使用 Java Connector Architecture (JCA) 連接器實現專有異步邏輯的協議,常常開發自定義組件來處理(正如後文將介紹的,新一代 servlet 引擎也支持使用非標准接口處理 Comet 模式。然而,就 API 和使用模式而言,這種支持與標准的 servlet 接口截然不同)。
最後,回想一下 Java EE 的一個基本原理,即對網絡基礎結構進行優化可以縮短事務持續時間。但是,對於現場直播的視頻提要,提升網絡基礎結構的速度絲毫不會減少請求的持續時間,因為視頻流是邊生成邊發送給客戶機的。對網絡基礎結構進行優化只會增加流的數量,從而支持更多的客戶機並以更高的分辨率處理流。
異步方法
要避免我們討論的這些問題,一個可行的方法就是在設計應用程序時將延遲納入到考慮事項中並使用由事件驅動的異步方法實現應用程序。如果應用程序處於空閒狀態,則不會占用線程這樣的有限資源。通過使用異步 API,應用程序將對外部事件進行輪詢並在事件發生後執行相應的操作。通常,這種應用程序被分為若干個事件循環,每個循環都有自己獨有的線程。
事件驅動異步設計的一個明顯優點就是,如果大量等待外部服務的操作之間沒有數據依賴關系,則可以並行執行這些操作。即使根本沒有發生並行操作,事件驅動的異步架構也提供了優於傳統同步設計的強大的可伸縮性。
異步 API 優點:概念證明模型
可以通過一個簡單的 servlet 流程模型演示異步 API 帶來的可伸縮性優勢(如果您已經確信異步設計能夠滿足 Web 2.0 應用程序的可伸縮性需求,那麼可以跳過本節內容,直接了解可用來解決 Web 2.0 / Java EE 問題的 解決方案討論)。
在我們的模型中,servlet 流程對到來的請求執行一些處理,對數據庫進行查詢,然後使用從數據庫獲取的信息調用 Web 服務。最後,根據 Web 服務的響應生成最終的響應。
模型的 servlet 使用兩種類型的資源,並伴有較高的延遲。在逐漸增加的負載之下,這些資源的特征和行為都互不相同:
數據庫連接。這種資源通常以 DataSource 的形式用於 Web 應用程序,它提供了數量有限的連接,通過這些連接實現同步處理。
網絡連接。這種資源用於編寫對客戶機的響應並調用 Web 服務。直到現在為止,這種資源在大多數應用服務器中都受到限制。然而,新一代應用服務器開始使用 nonblocking I/O (NIO) 實現這種資源,因此我們可以根據需要使用任意數量的同步網絡連接。模型 servlet 在以下幾種情形中使用這種資源:
調用 Web 服務。盡管目標服務器每秒可以處理的請求的數量是有限制的,但是這個數量通常都很高。調用持續時間取決於網絡通信量。
從客戶機讀取請求。我們的模型忽視了這一開銷,因為模型假定使用了一個 HTTP GET 請求。在這種情形下,從客戶機讀取請求所需的時間不會添加到 servlet 請求持續時間中。
向客戶機發送響應。我們的模型忽視了這一開銷,因為,對於較短的 servlet 響應來說,應用服務器可以在內存中緩沖該響應,然後再使用 NIO 將它發送給客戶機。並且我們假設這個響應非常短小。在這種情形下,向客戶機發送響應所需的時間不會添加到 servlet 請求持續時間中。
讓我們假設 servlet 執行時間被劃分為如表 1 所示的幾個階段:
表 1. Servlet 操作時限(以抽象單位表示持續時間)
階段 持續時間 操作 1 2 個單位 解析 servlet 請求信息 2 8 個單位 處理本地數據庫事務 3 2 個單位 處理數據庫請求結果並准備遠程調用 4 16 個單位 使用一個 Web 服務調用遠程服務器 5 4 個單位 創建響應 總用時: 32 個單位
圖 1 展示了執行期間業務邏輯、數據庫和 Web 服務之間的時間分布:
圖 1. 執行步驟的時間分布
這些選擇的時限提供了一個可讀的圖表。在實際中,大多數 Web 服務進行處理使用的時間遠遠超過圖表顯示的時間。可以這樣講,Web 服務的處理時間要比業務邏輯 Java 代碼的處理時間高出 100 到 300 倍。但是,為了演示同步調用模型,我們挑選了一些不太符合現實的參數,比如,Web 服務性能極其快,或者應用服務器速度很慢,或兩者兼有。
讓我們假設連接池的容量為 2。因此,同一時間內只能處理兩個數據庫事務。(對於真實的應用服務器,實際的線程數和連接數要比這個數大)。
我們還假設 Web 服務調用使用的時間相同並且全部可以並行處理。這一假設比較符合實際,因為 Web 服務交互過程包括來回發送數據。執行實際的處理只是 Web 服務調用的一小部分。
對於這種場景,同步和異步用例在低負載下表現相同。如果數據庫查詢和 Web 服務調用並行進行,異步用例表現更加良好。在發生超載時,比如訪問量忽然達到峰值,將看到一個有趣的結果。我們假設同一時刻有 9 個請求。對於同步用例,servlet 引擎線程池有三個線程。而對於異步用例,我們只使用一個線程。
注意,在這兩個用例中,所有 9 個連接在到達時全部被接受(大多數 servlet 引擎都會這樣做)。然而,在處理前三個連接時,同步用例沒有對接受的其他六個連接進行處理。
圖 2 和圖 3 是使用一個簡單的模擬程序創建的,它分別模擬同步和異步 API 用例:
圖 2. 同步用例
圖 2 中的每個矩形表示流程的一個步驟。矩形中的第一個數字是流程編號(1 到 9),第二個數字是流程內的階段編號。每個流程使用惟一的顏色標記。注意,數據庫和 Web 服務操作位於單獨的行中,因為它們分別由數據庫引擎和 Web 服務實現執行。servlet 引擎在等待結果期間不執行任何操作。淺灰色區域表示空閒(等待)狀態。
圖表底部的菱形標記表示在該點完成了一個或多個請求。標記的第一個數字表示以抽象單位計算的時間;第二個使用圓括號括起的可選數字表示在該點終止的請求數。在圖 2 中可以看到,前兩個請求在點 32 處完成,最後一個請求在點 104 處完成。
現在假設數據庫和 Web 服務客戶機運行時支持異步接口。並且假設所有異步 servlets 只使用一個線程(但是,如果提供了額外線程的話,異步接口非常適合使用額外線程)。圖 3 顯示了結果:
圖 3. 異步用例
圖 3 中有幾處需要注意。第一個請求要比同步用例中晚結束 23%。但是,最後一個請求則快了 26%。並且所使用的線程只是同步用例的三分之一。請求執行時間的分布更加有規律,因此用戶可以以更加有規律的速度接收頁面。第一個請求和最後一個請求的處理時間相差了 80%。在同步接口用例中,這個值達到了 225%。
現在假設我們對應用程序和數據庫服務器進行了升級,它們的性能提升了兩倍。表 2 展示了用時結果(使用與表 1 相關的單位):
表 2. 升級後的 Servlet 操作時限
階段 持續時間 操作 1 1 個單位 解析 servlet 請求信息 2 4 個單位 執行本地數據庫事務處理 3 1 個單位 處理數據庫請求結果並為遠程調用做准備 4 16 個單位 使用 Web 服務調用遠程服務器 5 2 個單位 創建響應 總用時: 24 個單位
可以看到,單個請求處理時間一般為 24 個時間單位,大概是原來的請求持續時間的 3/4。
圖 4 展示了業務邏輯、數據庫和 Web 服務之間的新的分布:
圖 4. 升級後的步驟時間分布
圖 5 展示了同步處理後的結果。可以看到,總體持續時間減少了 25%。但是,步驟的分布模式沒有發生很大變化,並且 servlet 線程處於等待狀態的時間更長了。
圖 5. 升級後的同步用例
圖 6 展示了異步 API 的處理結果:
圖 6. 升級後的異步用例
使用異步 API 得到的結果非常有趣。數據庫和應用服務器的性能提高時,處理可以很好地進行相應擴展。結論已經得到證明,並且最差和最佳請求處理時間相差只有 57%。總的處理時間(截至最後一個請求完成)是升級之間所使用時間的 57%。與同步用例的 75% 相比,這是一個很顯著的改進。最後一個請求(兩種情況中的第 9 個請求)要比同步用例中早完成 40%,而第一個請求僅僅比同步用例晚 14%。此外,在異步用例中,可以執行更多數量的並行 Web 服務操作。而使用同步則無法達到這種並行程度,因為 servlet 線程池中的線程數是有限制的。即使 Web 服務能夠處理更多的請求,servlet 也不會發送請求,因為它不處於活動狀態。
實際的測試結果表明,異步應用程序具有更好的可伸縮性並且可以更從容地應對超載情況。延遲問題非常棘手,並且 Moore 定律也幫不了什麼忙。大多數現代計算改進增加了所需的帶寬。多數情況下,延遲可能維持不變,甚至進一步惡化。正因為如此,開發人員才嘗試將異步接口引入到應用服務器中。
目前,可以使用很多方法實現異步系統,但是還未將其中任何一種方法確立為事實標准。每種方法都各有優缺點,並且它們在不同的情形中扮演不同的角色。本文後面的內容將對這些機制進行大致介紹,包括各種機制的優缺點,使您能夠使用 Java 平台構建事件驅動的異步應用程序。
一般解決方案
Ad-hoc 並發性和 NIO
自 Java 1.4 起,Java 語言提供了一個非阻塞網絡 I/O API(java.nio.*)。而從 Java SE 5 開始,Java 提供了更加標准的並發性工具(java.util.concurrent.*)。開發人員利用非阻塞 I/O 和並發性實現的應用程序能夠通過可用的 API 和框架支持大量同步連接。
然而,這些 API 仍然處於較低的級別,並且通常只有在無法使用其他方式解決性能問題時才得到使用。NIO 選擇器機制是一種非常低級的 API。使用它很難編寫任何比復制流更復雜的操作。編寫使用相同 NIO 選擇器的獨立模塊也很困難。需要開發一個框架來封裝 NIO 並簡化這種類型的開發。
鑒於這些原因,很少直接使用 NIO API。應用程序通常使用一個更可用的接口將 NIO API 封裝起來。和眾多 API 相比,NIO API 有其獨特的作用,但是不應該強制應用程序編程人員直接使用它。
使用並發性工具編寫的應用程序則很少發生由於多線程處理引發的故障,因為 Java 5 的並發性工具提供了更高級的操作。然而,很容易發生死鎖並且難於調試和查找錯誤根源
為了在 Java 平台上以通用的方式支持異步交互,人們作出了很多嘗試。所有這些嘗試都基於一個消息傳遞通信模型。大部分使用了 actor 模型的一個變體來定義對象。此外,這些框架在可用性、可用庫和方法方面各有不同。
階段式事件驅動架構
階段式事件驅動架構(SEDA)是一種有趣的框架,它將異步編程和自主計算的原理結合在一起。SEDA 是 J2SE 1.4 對 Java NIO API 引入的最大一項補充。該項目本身已經被中斷,但是 SEDA 為 Java 應用程序的可伸縮性和適應性設定了新的基准,並且其有關異步 API 的思想對其他項目也產生了影響。
SEDA 試圖將異步和同步 API 設計結合起來,產生有趣的結果。這個框架具有比 ad-hoc 並發性 更加良好的可用性,但它還無法達到用戶認可的程度。
使用 SEDA,應用程序被劃分為若干個階段。每個階段表示的組件包含一定數量的線程。請求被分配到一個階段然後進行處理。階段可以通過以下幾種方式管理自身的容量:
根據負載增加和減少使用線程的數量。這允許服務器動態適應組件的實際使用情況。如果某個組件的使用急劇上升,則會分配更多線程。如果為空閒狀態,則減少線程的數量。
根據負載更改行為。例如,可以根據負載生成更加簡單的頁面。避免對頁面使用圖像,使用更少的腳本,禁用不必要的功能等等。用戶仍然可以使用應用程序,但是生成的請求和通信量將變少。
對試圖納入請求或拒絕接受請求的階段進行阻塞。
前兩種方法非常不錯,采用了智能應用程序實現自主計算的思想。然而,第三種方法揭示了為什麼該框架至今無法得到廣泛應用的原因。除非在設計應用程序時加倍小心,否則這樣做會因為增加了死鎖風險而引入一個故障點。下面介紹了致使該框架難於使用的其他一些原因:
階段是一種非常粗粒度的組件。比如網絡接口和 HTTP 支持。在將網絡層作為整體處理時,很難解決諸如某些客戶機帶寬有限這樣的問題。
無法使用簡單的方法返回異步調用的結果。結果只是被分配給階段,寄希望於階段能自己找到相關的操作狀態。
目前,大多數可用的 Java 庫都是同步的。框架並沒有嘗試以一種一致的方式將同步代碼從異步代碼中分離開來,從而使編寫出的代碼很容易意外阻塞整個階段。
貫徹 SEDA 項目思想的實現中部署最多的可能是 Apache MINA 框架。它用於 OSFlash.org Red5 流服務器的實現、Apache Directory Project 和 Jive Software Openfire XMPP Server。
E 編程語言
嚴格來講,E 編程語言是一種動態輸入的函數性編程語言,而非一種框架。它強調提供安全的分布式計算,它還為異步編程提供了一些有趣的概念。在異步編程方面,該語言仿效了其前輩 Joule 和 Concurrent Prolog,但是其並發性支持和整體語法更加自然,而且對於擁有主流編程語言(例如 Java 語言、JavaScript 和 C#)背景的編程人員來說也十分友好。
該語言目前通過 Java 和 Common-Lisp 實現。可以通過 Java 應用程序使用。但是,要將其應用於高負荷的服務器端應用程序,仍然存在著一些障礙。大多數問題源於其早期開發,但將來很可能會得到解決。其他一些問題則是由該語言的動態特性引起的,但是這些問題大部分與該語言提供的並發性擴展並無關系。
E 提供了以下核心語言概念來支持異步編程:
vat 表示對象的容器。所有對象都保存在一些 vat 的上下文中,並且不能從其他 vat 同步訪問這些對象。
promise 變量用來表示某些異步操作的結果。它的初始狀態為未解決狀態,表示該操作還未結束。完成操作後,它會獲得一個值或者出現 錯誤。
任何對象都可以接收消息並進行本地調用。本地對象可以通過即時的調用操作進行同步調用,也可以通過最終的發送操作進行異步調用。只能使用最終的發送操作對遠程對象進行調用。最終的調用將生成一個 promise。. 操作符用於即時調用,而 <- 操作符用於最終調用。
Promise 也可通過顯式方式創建。此時,將提供對解析器對象的引用,並傳遞給其他 vat。這個對象有兩個方法:resolve 和 smash。
when 操作符允許您在 promise 執行 resolve 或 smash 時調用一些代碼。when 操作符中的代碼被處理為閉包,並且通過訪問外圍范圍中的定義執行。這種方式類似於匿名的內部 Java 類訪問一些方法范圍內的定義。
這幾個概念構成了一個功能強大的可用系統,允許輕松地創建異步組件。即使不是在生產環境中使用該語言,仍然可以使用它原型化復雜並發性問題。它執行一種消息傳遞規程並提供方便的語法來處理並發性問題。其操作符也十分簡單,並且可以在其他編程語言中進行模仿,雖然產生的代碼很可能不及原始代碼那麼優雅和簡單。
E 增強了異步編程的可用性。該語言提供的並發支持與其他語言特性毫不相關,並且它可能對現有語言進行了改進。在 Squeak、Python 和 Erlang 開發環境中已經對這些語言特性進行了討論。與更加特定於域的語言特性(如 C# 中的迭代器)相比,這種語言特性可能更為有用。
AsyncObjects 框架
AsyncObjects 框架項目側重於使用純 Java 代碼創建可用的異步組件框架。該框架嘗試將 SEDA 和 E 編程語言結合在一起。與 E 相同,它提供了基本的並發性機制。同樣,它也仿效 SEDA 來提供機制集成同步 Java API。該框架的第一個原型版本發行於 2002 年。自此之後,該框架的開發變得消極起來,但是最近,該項目重新開始活躍。E 已經展示了異步編程的可用性,而這個框架將試圖使用純 Java 代碼獲得同樣的可用性。
和 SEDA 相同,應用程序被分為若干個事件循環。但是,該項目目前還沒有實現任何類似 SEDA 的自管理特性。與 SEDA 不同的是,它對 I/O 使用了更加簡單的負載管理機制,因為組件更加細粒度化並且 promise 可用來接收操作結果。
框架實現了與 E 編程語言相同的異步組件概念、vat、和 promise。因為不能使用純 Java 代碼引入新的操作符,並不是任意一個對象都是異步組件。實現需要擴展某個基類,並且應該提供由框架實現的異步接口。由框架提供的異步接口實現將向組件的 vat 發送消息,而 vat 稍後將消息分配給組件。
該框架的當前版本(0.3.2)與 Java 5 兼容並且支持泛型。如果當前平台支持的話,也將使用 Java NIO。但是,框架能夠返回到普通套接字。
該框架最大的一個問題是類庫非常匮乏,因為很難實現與同步 Java API 的集成。目前,只實現了網絡 I/O 庫。但是,最近作出的一些改進 — 例如 Axis2 中的異步 Web 服務和前面描述的 Tomcat 6 的 Comet Servlet— 可以簡化這種集成。
Waterken 的 ref_send
Waterken 的 ref_send 框架是使用 Java 編程語言實現 E 思想的又一嘗試。它主要通過 Java 語言的一個子集(稱為 Joe-E)實現。
該庫支持最終的操作調用。然而,與 AsyncObjects 中的支持相比,這種支持的自主性較低。該框架的當前版本還存在線程安全問題。
它只發布了一些核心類和一些非常小的示例,並且沒有提供重要的應用程序和類庫。因此,關於如何在更大的范圍內實現框架思想仍不明確。框架構建者宣稱目前正在實現一個完整的 Web 服務器並且即將發布。等到發布之後在重新審視這個框架可能會更加有趣。
Frugal Mobile Objects
Frugal Mobile Objects 是另一種基於 actor 模型的框架。它以諸如 Java ME CLDC 1.1 這樣的資源受限環境為目標,使用有趣的設計模式減少資源使用量,同時保持接口具有適當的簡單性。
該框架表明應用程序在性能和可伸縮性方面會從異步設計中獲益 — 甚至在一個資源受限的環境中。
該框架提供的 API 看似非常繁瑣,但是框架的受限目標環境充分證明了這些 API 的有效性。
Scala actor
Scala 是另一種面向 Java 平台的編程語言。它提供了一個 Java 特性超集,但是卻使用了稍有不同的語法。與普通的 Java 編程語言相比,它提供了一些可用性增強。
其中一個有趣特性就是基於 actor 的並發性支持,這一點模擬了 Erlang 編程語言。它的設計似乎還沒有最終確定,但是這種特性基本可用並且得到該語言的語法支持。然而,與 E 的並發性支持相比,Scala 的跟 Erlang 類似的並發性支持的可用性和自主性較低。
Scala 模型還存在一些安全性問題,因為每條消息都傳遞對調用方的引用。這使得被調用的組件可以調用調用方組件的所有操作,而不僅僅是返回調用值。就這方面來說,E 的 promise 模型更具粒度化。這種機制用來與阻塞進行通信,目前還沒有完全開發完畢。
Scala 的優點在於它可以編譯為 JVM 字節碼。理論上講,它可以用於 Java SE 和 Java EE 應用程序,並且不會帶來性能損失。然而,對於商業開發的適用性則另當別論,因為 Scala 的 IDE 支持有限,並且,與 Java 語言不同的是,它尚不具備供應商支持。因此,只能用於生命周期較短的項目(如原型),但是,如果對生命周期較長的項目使用該語言,則會添加很多風險。
特定於 Servlet 或特定於 I/O 的 API
由於我們討論的這些問題只要針對 servlet、Web 服務和一般的 I/O 級別,因此使用了一些項目來專門解決這些問題。這些解決方案的最大缺陷就是它們只針對有限類別的應用程序解決問題。如果不能對本地和遠程資源進行異步調用,即使能夠實現異步 servlet 也毫無用處。它還應該能夠編寫一個異步模型和業務邏輯代碼。另一個常見問題是解決方案的可用性,通常要低於普通的解決方案。
然而,作為為實現異步組件而作出的努力,這些嘗試都值得關注。
JSR 203(NIO.2)
JSR 203 是 NIO API 的改進版。在撰寫本文時,它仍然處於初期的草案階段,在開發過程中可能發生了很多重大修改。其目標是將 API 納入到 Java 7 中。
JSR 203 引入了異步通道(asynchronous channel)概念。目的是解決眾多編程問題,但是似乎 API 仍然非常低級。它最終引入了以前版本所不具備的異步 File I/O API,並且 IoFuture 和 CompletionHandler 概念使它可以更輕松地使用其他框架中的類。一般來講,新的異步 NIO API 要比上一代 API 中基於選擇器的 API 更加易用。甚至可以將它直接用於簡單的任務,而不需要編寫自定義包裝器。
然而,這種 JSR 的一大缺點就是,它高度特定於文件和套接字 I/O。它沒有提供構建塊來創建更高級的異步組件。可能提供了高級的類,但是必須提供自己的方法來執行相同的任務。這看似是一個不錯的技術理念,因為在 Java 語言中仍然沒有出現標准的異步組件開發方法。
Glassfish Grizzly NIO
Glassfish Grizzly NIO 支持類似於 SEDA 框架,並且繼承了大部分 SEDA 問題。然而,它提供了對 I/O 任務的更加具體化的支持。所提供的 API 要比普通 NIO API 更加高級,但是使用起來仍然很枯燥。
Jetty 6 continuation
Jetty continuation 是一種與傳統方法截然不同的方法。甚至可以將之稱為一種快速補丁(quick hack)。servlet 可能會請求一個 continuation 對象並調用具有指定超時的 suspend() 方法。該操作將拋出一個異常。然後再對 continuation 調用一個恢復操作,或者 continuation 超過指定時間後自動重新開始執行。
因此 Jetty 嘗試實現一個具有異步語義的同步查找 API。然而,這種行為將打斷客戶機的預測,因為 servlet 將從頭執行方法,而不是從調用 suspend() 的位置執行。
Apache Tomcat 6 Comet API
Tomcat Comet API 專門為支持 Comet 交互模式而設計。servlet 引擎通知 servlet 關於其狀態轉換以及數據是否可讀的信息。與 Jetty 使用的方法相比,這種方法更加健全和簡單。它使用傳統的同步 API 對流執行寫入和讀取操作。通過使用這種方式實現,如果謹慎使用,則不會出現 API 阻塞的情況。
JAX WS 2.0 和 Apache Axis2 Asynchronous Web Service Client API
JAX WS 2.0 和 Axis2 為 Web 服務的非阻塞調用提供了 API 支持。當 Web 服務操作完成後,Web 服務引擎將通知提供的偵聽器。這為 Web 服務的使用提供了新的機會 — 即使來自 Web 客戶機。如果一個 servlet 中發生若干獨立的調用,它們將並行執行,因此客戶機中的總延遲將更低。
結束語
現在,我們已經認識到了異步 Java 組件的必要性,並且,異步應用程序目前正在積極開發之中。兩種大型的開源 servlet 引擎(Tomcat 和 Jetty)都至少針對最令開發人員頭痛的 servlet 提供了一些支持。盡管 Java 庫已開始提供異步接口,這些缺口還缺乏通用的結構,並且,由於線程管理和其他問題,彼此之間很難兼容。因此需要容器能夠托管由不同來源提供的各種異步組件。
目前,用戶面對著各種各樣的選擇,每種方法在不同情形下都各有優缺點。例如,Apache MINA 庫為一些流行的網絡協議提供了現成的支持,因此,在需要使用這些協議的情況下它將是一個不錯的選擇。Apache Tomcat 6 可以很好地支持 Comet 交互模式,如果要在這種模式中進行異步交互,那麼則可以選擇使用 Apache Tomcat 6。如果是從頭構建應用程序,並且現有庫明顯不能提供足夠支持,那麼可以使用 AsyncObjects 框架,因為它提供了各種各樣的可用接口。這種框架還可以用於圍繞現有異步組件庫創建包裝器。
現在,是時候為 Java 語言創建一個通用的異步編程框架了。然後,還需要花費很多精力將現有異步組件集成到這個框架中,並為現有同步接口創建一個異步版本。每實現一個步驟,企業 Java 應用程序的可伸縮性都會得到改善,並且我們將能夠應對比這更艱難的挑戰。持續發展的 Internet 以及不斷增生的各種網絡服務必定將為我們帶來更多這樣的挑戰。