本文介紹如何提升 Java Web 服務效能,主要介紹了三種方法:一是採用 Web 服務的非同步呼叫,二是引入 Web 服務批處理模式,三是壓縮 SOAP 訊息。重點介紹在編程過程中如何使用非同步 Web 服務以及非同步呼叫和同步調用的差異點。本文還示範了如何在項目中使用以上三種方法,以及各種方法所適合的應用情境。
Java Web 服務簡介
Web 服務是一種面向服務架構的技術,通過標準的 Web 協議提供服務,目的是保證不同平台的應用服務可以互操作。Web 服務(Web Service)是基於 XML 和 HTTP 通訊的一種服務,其通訊協定主要基於 SOAP,服務的描述通過 WSDL、UDDI 來發現和獲得服務的中繼資料。 這種建立在 XML 標準和 網際網路通訊協定 (IP)基礎上的 Web 服務是分散式運算的下一步發展方向,Web 服務為那些由不同資源構建的商務應用程式之間的通訊和協作帶來了光明的前景,從而使它們可以彼此協作,而不受各自底層實現方案的影響。
JAX-RPC 1.0 是 Java 方面的 Web 服務的原始標準 , 但是由於 JAX-RPC 1.0 對 Web 服務功能的認識有一定的局限,於是 JAX-WS 2.0 應用而生。JAX-WS 2.0 開發工作的主要目標是對各項標準進行更新,成功實現了業界對 JAX-RPC 1.X 的各種期望。此外,JAX-WS 2.0 直接支援 XOP/MTOM,提高了系統附件傳送能力以及系統之間的互通性。
執行個體剖析 Web 服務效能瓶頸
通過以上簡述不難體會到,Web 服務以其 XML + HTTP 的松耦合、平台無關的特性,集萬般寵愛於一身,必將成為未來資料共用的基礎。但與此同時我們也應當認識到世間完事萬物均有其矛盾的兩面性:有優點,必將存在缺點,Web 服務亦是如此。就像當初 JAVA 大行其道的時候效能成為其致命詬病一樣,Web 服務也同樣面臨效能問題,似乎“效能問題”天生就是“平台無關”揮之不去的冤家。但問題終歸要解決,實踐是檢驗和分析問題的唯一途徑,讓我們先來建立一個簡單的 Web 服務再來審視和分析隱含其中的效能問題。
建立服務
建立服務 Java Bean: 首先我們建立一個儘可能簡單的書店服務 Bean,服務的內容只有一個 qryBooksByAuthor,即根據作者 (Author) 查詢其名下的書籍 (List)。
圖 1. 書店服務 Bean(BookStoreSrvBean)
服務 Input- 作者 (Author) 的實體類 :
圖 2. 作者實體類 (Author)
服務出參 Output- 書籍 (Book) 列表的實體類:
圖 3. 書籍實體類 (Book)
至此我們的服務代碼已經完成,我們不在此討論此服務的業務合理性,建立此服務的目的只是舉一個儘可能簡單的執行個體以分析 web 服務的效能。
下面的任務就是開發 Web 服務了,手工編寫及發布符合規範的 Web 服務過程極為繁瑣,在此使用 IBM 的 Rational Software Architect(後面簡稱 RSA)來進行 Web 服務的伺服器端以及用戶端的開發。
發布 Web 服務
建立動態 Web 項目 : 發布 Web 服務的前提當然需要一個 J2EE 的 Web 項目,開啟 RSA->File->New->Dynamic Web Project, 項目名稱為 testWebService, 其餘選項根據需要進行選擇 ( 注意需要選擇加入 Web 項目到 EAR)。建立好的 Web 項目和 EAR 項目效果如下 :
圖 4. Web 項目以及應用項目的結構
建立 Web 服務: 選中匯入的 com.ibm.test.ws.srv.BookStoreSrvBean,右鍵 New->Other->Web Service 來建立並發布 Web 服務。建立的時候選擇常用的 JAX-WS 標準 , 並選擇產生 WSDL 檔案。由於 Web 服務的建立不是本文重點,此部分內容暫且省略。服務建立完成之後就發行就緒到上一步建好的 Web 項目中了。
建立用戶端
使用 RSA,用戶端的建立工作將會非常簡單:右鍵點擊上面產生的 WSDL 檔案 ->Web Services->Generate Client
圖 5. 建立用戶端介面
在此介面,根據實際情況選擇 server,JAX-WS 標準以及 Client 代碼的目標項目,然後點擊下一步。
圖 6. 輸入用戶端資訊
此介面暫時使用預設配置,某些特殊選項將在後面章節進行描述。
用戶端調用
由於 JAX-WS 規範大部分的 stub 調用代碼是即時產生的,我們只需要修改用戶端 WSDL 的 port 就可以用以下代碼進行 Web 服務的調用。這裡修改 WSDL 連接埠的目的是讓用戶端調用 RSA 提供的 TCP/IP Monitor 的虛擬連接埠,這樣我們就可以很輕易地看到 Web 服務實際的調用以及返回的 SOAP 訊息了。
用戶端調用代碼如下 :
圖 7. 用戶端調用代碼
使用 TCP/IP Monitor 看到的 SOAP 訊息如下 :
圖 8. Web 服務調用產生的 SOAP 訊息
Java Web 服務效能分析
從以上執行個體我們可以看到,Web 服務的調用與傳統的 RPC 還是有較大差異的。最大的特點是調用雙方使用 XML 格式的 SOAP 規範訊息進行傳輸,這樣以文本進行傳輸的好處是拋棄了私人協議,無論調用雙方是何種平台,只要能夠構造以及解析 XML 文本,並且存在雙方都支援的傳輸協議,那麼調用就成為了可能。而 XML 的日益規範以及 HTTP 協議的普及更是給這兩個必要條件提供了堅強的後盾,Web 服務成為未來通用的服務提供標準已是不爭的事實。
但是相信使用過 Web 服務的人都曾經經受過其效能不佳的窘境,原因為何我們結合剛才的執行個體可以分析出以下幾點:
● SOAP 簡訊轉化導致效率低下
從剛才的 TCP/IP Monitor 監測到的 request 以及 response 的訊息我們可以看到,在發送訊息時,我們傳入了 Author 對象,在實際的調用發生時,這個 Author 對象會被轉化成 XML 格式的 SOAP 訊息,此訊息在到達 Server 端會被解析並重新構造成 Server 端的 Author 對象。Response 也是同理,Books List 也會經曆 XML 序列化和還原序列化的過程。最糟糕的是,這種過程會在每一次調用的時候都會發生,這種構造以及解析的過程都會極大地消耗 CPU,造成資源的消耗。
●SOAP 簡訊傳輸導致傳輸內容膨脹
以 request 參數 Author 為例,必要的資訊僅僅是”Bruce Eckel”這幾個位元組,但轉化成 XML 訊息後,可以從 SOAP 訊息看到,多了很多 SOAP 規範的標籤,這些資訊會導致需要傳輸的內容急劇增大,幾個位元組很可能會變成幾KB。當調用頻度和參數內容增多的時候,這種傳輸內容的膨脹將不是一個可以忽略的影響,它不但會吃掉網路的頻寬,還會給 Server 的資料吞吐能力造成負擔,後果可想而知。
●同步阻塞調用在某些情況下導致效能低下
同步阻塞調用是指用戶端在調用 Web 服務發送 request 後一直處於阻塞狀態,用戶端線程就會掛起,一直處於等待狀態,不能進行其他任務的處理。這樣就會造成線程的浪費,如果相應線程佔用了一些資源,也不能夠及時釋放。
這個問題在純用戶端訪問 Server 端的情況下並不明顯,但如果是兩個 Server 端之間進行 Web 服務調用的話,阻塞模式就會成為調用 Server 端的效能瓶頸。
Web 服務效能最佳化實踐
使用非同步方式調用 web 服務
先需要強調一點的是,這裡的非同步方式指的是用戶端的非同步,無論用戶端是同步還是非同步,都對服務端沒有任何影響。我們期望的理想結果是:當用戶端發送了調用請求後不必阻塞等待 server 端的返回結果。最新的 JAX-WS 標準中增加了這一非同步呼叫的特性,更好的訊息是,RSA 工具中也對 JAX-WS 的這一特性進行了支援,這樣就極大地方便了我們進行非同步呼叫用戶端的建立。
其實講用戶端配置為非同步模式極其簡單,只要在 RSA 產生 Client 端代碼時將‘ Enable asynchronous invocation for generated client ’ 選中即可 , 如 :
圖 9. 非同步用戶端建立選項
這樣在產生的用戶端的 BookStoreSrvBeanService 中就會多了 qryBooksByAuthorAsync 的非同步方法呼叫。既然是非同步方法呼叫,回調 (Call Back) 就是必不可少的,在下面的非同步用戶端測試代碼中可以看到匿名內部類作為回調 handler 的具體使用方法 :
圖 10. 非同步用戶端調用範例程式碼
測試代碼的輸出結果如下:
圖 11. 非同步呼叫控制台輸出
可以看到,當 Web 服務沒有返回時,用戶端仍然有機會做自己的輸出 :“not done yet, can do something else…”。有些人可能會認為作為用戶端此處的輸出並無實際意義,但試想如果一個 server 作為用戶端去訪問一個 Web 服務,如果在服務等待期間能夠有機會脫離阻塞狀態執行自己需要的代碼,甚至可以使用 wait 等方法釋放被當前線程佔用的資源,那麼對於此 server 來說這將是一個對效能提升起到本質作用的因素。
使 web 服務支援批處理模式
● 批處理模式簡介
批處理顧名思義是採用一次性處理多條事務的方式來取代一次一條事務的傳統處理方式。Java Database Connectivty (JDBC) 中提供了大量的批處理 API 用於最佳化資料庫操作效能,例如 Statement.executeBatch() 可以一次性接收並執行多條 SQL 語句。批處理思想可以方便的移植到 Web 服務調用情境以達到最佳化 Web 服務調用響應的目的。通過實際 Web 服務調用時間戳記分析不難看出網路通訊是 Web 服務效能的瓶頸之一,因此通過減少網路通訊開銷來最佳化 Web 服務效能,批處理模式是其中較為直接的一種實現方式。
●批處理模式適應性
批處理模式雖然作用顯著,但是也不適合所有情境。使用批處理模式處理 Web 服務請求時需要考慮一下幾點:
1.不同 Web 服務執行時間差異性
不同 Web 服務執行時間不盡相同,因此在同時處理多 Web 服務請求時需要考慮這種時間差異性。一般情況下是等待最長處理時間的 Web 服務執行完畢後匯總所有 Web 服務執行結果從而返回到用戶端,因此存在批處理多 Web 服務反而比順序單次調用 Web 服務消耗更長時間可能性。需要在採用批處理模式前對 Web 服務效能有清晰的瞭解,儘可能將績效參數相似的 Web 服務納入批處理,而分別處理執行時間差異較大的 Web 服務。一般建議將效能差異在 30% 以內的多 Web 服務可以考慮納入批處理。比方說 AccountWebService 中有一個擷取使用者賬戶列表的 Web 服務 getUserAccounts,這個 Web 服務執行需要 15 秒,另外 UserWebService 中有一個擷取使用者目前 pending 的待處理通知 getUserPendingNotifications,這個 Web 服務執行需要 2 秒時間,我們可以看到這兩個 Web 服務執行時間差異較大,因此在這種情況下我們不建議將這兩個 Web 服務納入批處理。而 AccountWebService 中有一個增加第三方使用者帳號的 Web 服務 addThirdPartyNonHostAccount,該 Web 服務執行需要 3 秒,此時就就可以考慮能將 getUserPendingNotifications Web 服務和 addThirdPartyNonHostAccount 放在一個批處理中一次性調用處理。
2.不同 Web 服務業務相關性
一般情況下建議考慮將存在業務相關性的多 Web 服務放入批處理中,只有業務存在相關性的多 Web 服務才會涉及到減少調用次數以提高應用系統效能的需求。比方說使用者在增加第三方帳號 addThirdPartyNonHostAccount 以後會預設自動發送一條 pending 的 notification 給使用者用以提示使用者來啟用增加的帳號,因此這種情境下可以完美的將 addThirdPartyNonHostAccount Web 服務和 getUserPendingNotifications Web 服務放入一個批處理中,在使用者增加完三方帳號後系統自動重新整理 pending notification 地區以提示使用者啟用帳號。UserWebService 中有一個擷取使用者主帳號的 Web 服務 getUserHostAccounts 和擷取使用者三方帳號的 Web 服務 getUserNonHostAccounts,MetaDataService 中有一個擷取國家金融機構假期資料的 Web 服務 getFinacialAgencyHolidays,該 Web 服務明顯和 getUserHostAccounts,getUserNonHostAccounts 不存在業務上相關性,因此不應該將它們納入批處理。
3.盡量避免將存在依賴關係的多 Web 服務放入同一個批處理中
將多個存在依賴關係的多 Web 服務放入同一批處理中需要專門考慮、處理多 Web 服務彼此間的依賴關係,進而無法將方便的這些 Web 服務並發執行而不得不串列執行有依賴關係的 Web 服務,最悲觀情況下批處理回應時間將是批處理中所有 Web 服務串列執行時間和。原則上即使批處理中 Web 服務間存在依賴關係,通過動態指定依賴關係也可以實現多 Web 服務的批處理調用。但是這樣將大大增加批處理實現的技術複雜性,因此不建議如此操作。
4.多線程方式處理批處理 Web 服務請求
批處理模式在服務實現端一般通過多執行緒方法來並發處理多個 Web 服務調用請求。通過集中的解析器解析批處理模式請求,之後針對每一個 Web 服務調用會啟動一個單獨的線程來處理此 Web 請求,同時會有一個總的線程管理器來調度不同 Web 服務執行線程,監控線程執行進度等。在所有線程執行完成後匯總 Web 服務執行結果返回用戶端。
●批處理實現方式
批處理實現方式一般有兩種:靜態批處理模式,動態批處理模式:
靜態批處理模式實現較為簡單,但是相對缺乏靈活性。靜態批處理的核心思想就是在已有 Web 服務的基礎上通過組合封裝的方式來得到批處理的目的。舉例來說將系統中已有的 Web 服務請求結構組合成一個新的資料物件模型作為 Web 服務批處理請求結構,在用戶端進行批處理調用時通過初始化批處理請求資料對象,並將特定的 Web 服務要求對象賦值給批處理請求對象屬性的方式。同理在服務實現端在產生批處理響應資料對象時也是通過將具體 Web 服務的回應群組合起來產生並返回用戶端。
動態批處理模式實現較為複雜,但也能提供更大的操作靈活性。動態批處理模式一般需要應用採用 Java 反射 API 開發具有容器功能的批處理實現架構。用戶端可以動態向容器中增加 Web 服務調用請求,比方說用戶端可以動態將 addThirdPartyNonHostAccount,getUserPendingNotifications 兩個 Web 服務加入到這個容器中然後發起一個架構提供的批處理 Web 服務調用請求。該批處理 Web 服務在實現端將解析容器並將其中的各個 Web 服務要求抽取解析並啟動獨立的線程來處理。
壓縮 SOAP
當 Web Service SOAP 訊息體比較大的時候,我們可以通過壓縮 soap 來提高網路傳輸效能。通過 GZIP 壓縮 SOAP 訊息,得到位元據,然後把位元據作為附件傳輸。以前常規方法是把位元據 Base 64 編碼,但是 Base 64 編碼後的大小是位元據的 1.33 倍。辛苦壓縮的,被 Base64 給抵消差不多了。是否可以直接傳輸位元據呢? JAX-WS 的 MTOM 是可以的,通過 HTTP 的 MIME 規範, SOAP message 可以字元,二進位混合。我們在 client 和 server 端各註冊一個 handler 來處理壓縮和解壓。 由於壓縮後的 SOAP 訊息附件與訊息體中的部分不是基於 MTOM 自動關聯的,需要單獨處理附件。在產生 client 端和 server 端代碼的時候需要 enable MTOM。 Handler 具體代碼在本文代碼附件中, test.TestClientHanlder, test.TestServerHanlder。 寫好了 handler 了之後還要為 service 註冊 handler。
用戶端 handler 範例代碼如下:
用戶端代碼
public boolean handleMessage(MessageContext arg0) {
SOAPMessageContext ct = (SOAPMessageContext) arg0;
boolean isRequestFlag = (Boolean) arg0
.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY);
SOAPMessage msg = ct.getMessage();
if (isRequestFlag) {
try {
SOAPBody body = msg.getSOAPBody();
Node port = body.getChildNodes().item(0);
String portContent = port.toString();
NodeList list = port.getChildNodes();
for (int i = 0; i < list.getLength(); i++) {
port.removeChild(list.item(i));
}
ByteArrayOutputStream outArr = new ByteArrayOutputStream();
GZIPOutputStream zip = new GZIPOutputStream(outArr);
zip.write(portContent.getBytes());
zip.flush();
zip.close();
byte[] arr = outArr.toByteArray();
TestDataSource ds = new TestDataSource(arr);
AttachmentPart attPart = msg.createAttachmentPart();
attPart.setDataHandler(new DataHandler(ds));
msg.addAttachmentPart(attPart);
} catch (SOAPException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
return true;
}
Web 服務端 handler 範例代碼如下:
服務端代碼
public boolean handleMessage(MessageContext arg0) {
SOAPMessageContext ct = (SOAPMessageContext) arg0;
boolean isRequestFlag = (Boolean) arg0
.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY);
SOAPMessage msg = ct.getMessage();
if (!isRequestFlag) {
try {
Object obj = ct.get(“Attachments”);
Attachments atts = (Attachments) obj;
List list = atts.getContentIDList();
for (int i = 1; i < list.size(); i++) {
String id = (String) list.get(i);
DataHandler d = atts.getDataHandler(id);
InputStream in = d.getInputStream();
ByteArrayOutputStream out = new ByteArrayOutputStream();
GZIPInputStream zip = new GZIPInputStream(in);
byte[] arr = new byte[1024];
int n = 0;
while ((n = zip.read(arr)) > 0) {
out.write(arr, 0, n);
}
Document doc = DocumentBuilderFactory.newInstance()
.newDocumentBuilder()
.parse(new ByteArrayInputStream(out.toByteArray()));
SOAPBody body = msg.getSOAPBody();
Node port = body.getChildNodes().item(0);
port.appendChild(doc.getFirstChild().getFirstChild());
}
} catch (SOAPException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (SAXException e) {
e.printStackTrace();
} catch (ParserConfigurationException e) {
e.printStackTrace();
}
}
return true;
}
在 web.xml 中 service-ref 部分添加 handler. Server 端 handler 也是同樣添加。
配置代碼
<handler-chains>
<handler-chain>
<handler>
<handler-name>TestClientHandler</handler-name>
<handler-class>test.TestClientHandler
</handler-class>
</handler>
</handler-chain>
</handler-chains>
結束語
以上三種解決方案是根據筆者的經驗和分析,針對 Web 服務當前所面臨的效能瓶頸進行提出的。並且,這幾種解決方案在實際項目使用中都取得了比較好的效果。 綜上所述, 在實際項目中,根據不同的需求採用上述方法中一個或者多個組合,可以使 Web 服務效能更加最佳化。
文章來源:伯樂線上