Java Web 服務性能優化實踐


來源:IBM developerworks

簡介: 本文介紹如何提升 Java Web 服務性能,主要介紹了三種方法:一是採用 Web 服務的異步調用,二是引入 Web 服務批處理模式,三是壓縮 SOAP 消息。重點介紹在編程過程中如何使用異步 Web 服務以及異步調用和同步調用的差異點。本文還示範瞭如何在項目中使用以上三種方法,以及各種方法所適合的應用場景。

Java Web 服務簡介

Web 服務是一種面向服務架構的技術,通過標準的 Web 協議提供服務,目的是保證不同平臺的應用服務可以互操作。Web 服務(Web Service)是基於 XML 和 HTTP 通訊的一種服務,其通信協議主要基於 SOAP,服務的描述通過 WSDL、UDDI 來發現和獲得服務的元數據。 這種建立在 XML 標準和 Internet 協議基礎上的 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<Book>)。

圖 1. 書店服務 Bean(BookStoreSrvBean)

Java Web 服務性能優化實踐

服務 Input- 作者 (Author) 的實體類 :

圖 2. 作者實體類 (Author)

Java Web 服務性能優化實踐

服務出參 Output- 書籍 (Book) 列表的實體類:

圖 3. 書籍實體類 (Book)

Java Web 服務性能優化實踐

至此我們的服務代碼已經完成,我們不在此討論此服務的業務合理性,創建此服務的目的只是舉一個儘可能簡單的實例以分析 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 項目以及應用項目的結構

Java 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. 創建客戶端界面

Java Web 服務性能優化實踐

在此界面,根據實際情況選擇 server,JAX-WS 標準以及 Client 代碼的目標項目,然後點擊下一步。

圖 6. 輸入客戶端信息

Java Web 服務性能優化實踐

此界面暫時使用默認配置,某些特殊選項將在後面章節進行描述。

客戶端調用

由於 JAX-WS 規範大部分的 stub 調用代碼是實時生成的,我們只需要修改客戶端 WSDL 的 port 就可以用以下代碼進行 Web 服務的調用。這裏修改 WSDL 端口的目的是讓客戶端調用 RSA 提供的 TCP/IP Monitor 的虛擬端口,這樣我們就可以很輕易地看到 Web 服務實際的調用以及返回的 SOAP 消息了。

客戶端調用代碼如下 :

圖 7. 客戶端調用代碼

Java Web 服務性能優化實踐

使用 TCP/IP Monitor 看到的 SOAP 消息如下 :

圖 8. Web 服務調用產生的 SOAP 消息

Java Web 服務性能優化實踐

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 規範的標籤,這些信息會導致需要傳輸的內容急劇增大,幾個字節很可能會變成幾千字節。當調用頻度和參數內容增多的時候,這種傳輸內容的膨脹將不是一個可以忽略的影響,它不但會吃掉網絡的帶寬,還會給 Server 的數據吞吐能力造成負擔,後果可想而知。

●同步阻塞調用在某些情況下導致性能低下

同步阻塞調用是指客戶端在調用 Web 服務發送 request 後一直處於阻塞狀態,客戶端線程就會掛起,一直處於等待狀態,不能進行其他任務的處理。這樣就會造成線程的浪費,如果相應線程佔用了一些資源,也不能夠及時釋放。

這個問題在純客戶端訪問 Server 端的情況下並不明顯,但如果是兩個 Server 端之間進行 Web 服務調用的話,阻塞模式就會成爲調用 Server 端的性能瓶頸。

Web 服務性能優化實踐

使用異步方式調用 web 服務

先需要強調一點的是,這裏的異步方式指的是客戶端的異步,無論客戶端是同步還是異步,都對服務端沒有任何影響。我們期望的理想結果是:當客戶端發送了調用請求後不必阻塞等待 server 端的返回結果。最新的 JAX-WS 標準中增加了這一異步調用的特性,更好的消息是,RSA 工具中也對 JAX-WS 的這一特性進行了支持,這樣就極大地方便了我們進行異步調用客戶端的創建。

其實講客戶端配置爲異步模式極其簡單,只要在 RSA 生成 Client 端代碼時將‘ Enable asynchronous invocation for generated client ’ 選中即可 , 如下圖 :

圖 9. 異步客戶端創建選項

Java Web 服務性能優化實踐

這樣在生成的客戶端的 BookStoreSrvBeanService 中就會多了 qryBooksByAuthorAsync 的異步方法。既然是異步方法,回調 (Call Back) 就是必不可少的,在下面的異步客戶端測試代碼中可以看到匿名內部類作爲回調 handler 的具體使用方法 :

圖 10. 異步客戶端調用示例代碼

Java Web 服務性能優化實踐

測試代碼的輸出結果如下:

圖 11. 異步調用控制檯輸出

Java Web 服務性能優化實踐

可以看到,當 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 消息,得到二進制數據,然後把二進制數據作爲附件傳輸。以前常規方法是把二進制數據 Base64 編碼,但是 Base64 編碼後的大小是二進制數據的 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 樣例代碼如下:

客戶端代碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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 樣例代碼如下:

服務端代碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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 也是同樣添加。

配置代碼

1
2
3
4
5
6
7
8
9
<handler-chains>
            <handler-chain>
                <handler>
                    <handler-name>TestClientHandler</handler-name>
                    <handler-class>test.TestClientHandler
 </handler-class>
                </handler>
            </handler-chain>
        </handler-chains>

結束語

以上三種解決方案是根據筆者的經驗和分析,針對 Web 服務當前所面臨的性能瓶頸進行提出的。並且,這幾種解決方案在實際項目使用中都取得了比較好的效果。 綜上所述, 在實際項目中,根據不同的需求採用上述方法中一個或者多個組合,可以使 Web 服務性能更加優化。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章