壓力測試不同於功能測試,軟體的正確性並不是它的測試重點。它所看重的是軟體的執行效率,尤其是短時間內訪問使用者數爆炸性增長時軟體的響應速度,壓力測試往往是在功能測試之後進行的。在實際的開發過程中,軟體潛在的效率瓶頸一般都是那些可能有多個使用者同時訪問的節點。
就目前 Java EE 的平台下開發的軟體來說,這種節點通常可能是:Web 服務器、資料庫伺服器和 JMS 伺服器。它們都是請求主要發生的地點,請求頻率較其它的節點要高,而且處於請求序列的關鍵路徑之上。如果它們效率無法提高的話,對於整個軟體的效率有致命的影響。而且在這些節點上一般都會發生較大規模的資料交換,有時其中還包含有商務邏輯處理,它們正是在進行壓力測試時首先需要考慮的。
本文以這三種節點為例,介紹如何使用 JMeter 來完成針對於它們的壓力測試。
Web 服務器
對於大多數的項目來說,並不會自行開發一個Web伺服器,因此Web伺服器壓力測試的對象實際就是--發布到Web伺服器中的軟體。最簡單的Web測試計劃只需要三個 JMeter 的測試元件,如:
其中:
- 線上程組中定義線程數、產生線程發生的時間和測試迴圈次數。
- 在http請求中定義伺服器、連接埠、協議和方法、請求路徑等。
- 表格監聽器負責收集和顯示結果。
這種設定對於包含了安全機制的 web 應用是不夠的,典型的 web 應用一般都會:
1. 有一個登入頁,它是整個應用的入口。當使用者登入之後,應用會將使用者相關的安全資訊放到 session 中。
2. 有一個 filter,它攔截請求,檢查每個請求相關的 session 中是否包含有使用者安全資訊。如果沒有,那麼請求被重新導向到登入頁,要求使用者提供安全資訊。
在這種配置下應用上面的測試計劃,那麼除了登入頁之外的其它請求都將因為缺少使用者安全資訊,而使請求實際定位到登入頁。如果不加斷言,那麼在監聽器看來所有的請求都是成功。而實際上,這些請求最終都沒有到達它們應該去的地方。顯然,這種測試結果不是我們所期望的。
為了成功的測試,至少有2種方法:
- 方法一,去掉程式的安全設定,如filter,使得不需要使用者安全資訊也能訪問受限內容;
- 方法二,不修改程式,使用JMeter提供的"Http URL重寫修飾符"或"Http Cookie管理器"。
對於第一種方法,有其局限性:
- 需要修改程式配置,如去掉web.xml中關於安全filter的設定。需要維護多個版本的web.xml,如壓力測試和功能測試分別各自的web.xml,增加了維護成本,而且有可能會在測試之後忘記將web.xml修改回來。
- 對於一些需要使用者安全資訊的頁面無能為力,如某些業務審計操作需要使用者安全資訊來記錄。因為缺少這樣的資訊,註定了測試的失敗。如果解決為了這個問題進一步的修改程式,那麼因為存在多個版本的程式,那麼其維護難度將大大增加。
雖然,第二種方法配置難度增加了,但是它不用修改程式。而且還可將測試計劃儲存成檔案,以便重複使用。因此,選用第二種方法是較為理想的做法。下面以一個簡化的例子說明使用方法二的配置步驟。
1. 例子由以下幾個檔案組成:
AuthorizenFilter.java,過濾器負責檢驗session中是否存在使用者資訊。如果沒有,那麼就轉向到 login.jsp。它的主要方法 doFilter 內容如下:
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest)request; HttpServletResponse res = (HttpServletResponse)response; HttpSession session= req.getSession(); User user = (User)session.getAttribute("user"); if(null == user){ String uri= req.getRequestURI(); //如果請求頁是登入頁,不轉向 if( uri.equalsIgnoreCase("/gWeb/login.jsp")){ chain.doFilter(request, response);} else{ res.sendRedirect("/gWeb/login.jsp");}}else{ chain.doFilter(request, response); } } |
User.java,使用者類負責記錄使用者的資訊。為了簡化,這裡的登入操作只允許指定使用者名稱和密碼。主要內容如下:
public class User {private String user;private String pwd;public User(String user, String pwd) {this.user = user;this.pwd = pwd;}public boolean login(){return user.equals("foxgem") && pwd.equals("12345678");}public String getUser() {return user;}public void setUser(String user) {this.user = user;}} |
Login.jsp 和welcome.jsp。其中 login.jsp 負責產生 User 對象,並調用 User 的login。當 login 返回為 true 時轉向到 welcome.jsp。其驗證部分的代碼:
<% if( request.getParameter("Submit") != null) { User ur= new User( request.getParameter("user"), request.getParameter("pwd")); if( ur.login()){ session.setAttribute("user", ur); response.sendRedirect("/gWeb/welcome.jsp"); } else{ session.setAttribute( "LOGIN_ERROR_MSG", "無效的使用者,可能原因:使用者不存在或被禁用。"); response.sendRedirect("/gWeb/index.jsp"); return; } }%> |
web.xml,配置 filter 攔截所有訪問 JSP 頁面的請求:
<filter> <filter-name>authorizen</filter-name> <filter-class>org.foxgem.jmeter.AuthorizenFilter</filter-class></filter><filter-mapping> <filter-name>authorizen</filter-name><url-pattern>*.jsp</url-pattern></filter-mapping> |
2. 建立如下結構的Web測試計劃:
其中主要測試元件說明如下:
- http請求預設值負責記錄請求的預設值,如伺服器、協議、連接埠等。
- 第一個http請求,請求login.jsp,並附加驗證所需要的參數(user=foxgem,pwd=12345678,Submit=Submit);其包含的響應斷言驗證url中包含"welcome.jsp",這一點可以從程式中反應。
- 第二個http請求,請求是welcome.jsp;其包含的響應斷言驗證響應文本中包含"foxgem",它是welcome.jsp頁面邏輯的一部分。
- http cookie管理器負責管理整個測試過程中使用的cookie,它不需要設定任何屬性。
- 迴圈控制器設定發送第二個請求的迴圈次數,表格監聽器負責收集和顯示第二個請求的測試結果。
啟動測試計劃之後,執行的順序是:首先,第一個請求登入頁進行登入;成功登入之後,使用迴圈控制器執行第二個請求。請求welcome.jsp時,響應斷言用來驗證是否確實是welocme.jsp來處理請求,而不是因為其它頁。在這個測試計劃中需要注意的是http cookie管理器。正是由於它的作用,使得第二個請求能順利的發送到welcome.jsp進行處理,而不是因為缺少使用者安全資訊轉寄到login.jsp。
在這個例子中,我們並沒有在程式中使用cookie(使用的是session),那麼http cookie管理器怎麼會起作用呢?這是因為在servlet/jsp規範中對於session的狀態跟蹤有2種方式:
- 使用cookie,保留和傳遞sessionid。它不要求程式對於url有什麼特殊的處理,但是要求瀏覽器允許cookie。在這個例子中,就是這種情形。
- 使用url重寫,每次顯式的在瀏覽器和伺服器之間傳遞sessionid。它要求程式對url進行編碼,對瀏覽器沒有要求。
對於第二種情形,可以使用JMeter前置管理器中的http url重寫修飾符來完成。對於Tomcat,Session參數是jsessionid,路徑擴充使用";"。使用url編碼時需要注意,必須將瀏覽器的cookie功能關閉。因為url編碼函數,如encodeURL,會判斷是否需要將sessionid編碼到url中。當瀏覽器允許cookie時,就不會進行編碼。
如果cookie而不是session來儲存使用者安全資訊,那麼直接使用http cookie管理器就行了。此時,需要將使用的cookie參數和值直接寫到管理器中,由它負責管理。對於其它的cookie使用,也是如此操作。
登入問題解決之後,對於 Web 服務器的測試就沒什麼痛點了。剩下的就是根據實際需要,靈活運用相關的測試組件搭建編寫的測試計劃。(當然,對於安全問題還有其它的使用情景。在使用時需要明確:JMeter 是否支援,如果支援使用哪種測試組件解決。)
資料庫伺服器
資料庫伺服器在大多數企業專案中是不可缺少的,對於它進行壓力測試是為了找出:資料庫物件是否可以有效地承受來自多個使用者的訪問。這些對象主要是:索引、觸發器、預存程序和鎖。通過對於SQL語句和預存程序的測試,JMeter 可以間接的反應資料庫物件是否需要最佳化。
JMeter 使用 JDBC 發送請求,完成對於資料庫的測試。一個資料庫測試計劃,建立如下結構即可:
其中:
- JDBC串連配置,負責設定資料庫串連相關的資訊。如:資料庫url、資料庫驅動類名、使用者名稱和密碼等等。在這些配置中,"綁定到池的變數名"(Variable Name Bound to Pool)是一個非常重要的屬性,這個屬性會在JDBC請求中被引用。通過它, JDBC請求和JDBC串連配置建立關聯。(測試前,請將所需要的資料庫驅動放到JMeter的classpath中)。
- JDBC請求,負責發送請求進行測試。
- 圖形結果,收集顯示測試結果。
在實際的項目中,至少有2種類型的JDBC請求需要關註:select語句和預存程序。前者反應了select語句是否高效,以及表的索引等是否需要最佳化;後者則是反應預存程序的演算法是否高效。它們如果效率低下,必然會帶來響應上的不盡如人意。對於這兩種請求,JDBC請求的配置略有區別:
如果對於Oracle,如果測試的是函數,那麼也可以使用select語句來進行配置,此時可以使用:select 函數(入參) from dual形式的語句來測試,其中dual是oracle的關鍵字,表示啞表。對於其它廠商的資料庫產品,請尋找手冊。
JMS伺服器
MOM 作為訊息資料交換的平台,也是影響應用執行效率的潛在環節。在 Java 程式中,是通過 JMS 與 MOM 進行互動的。作為 Java 實現的壓力測試工具,JMeter 也能使用 JMS 對應用的訊息交換和相關的資料處理能力進行測試。這一點應該不難理解,因為在整個測試過程中,JMeter 測試的重點應該是訊息的產生者和消費者的本身能力,而不是 MOM本身。
根據 JMS 規範,訊息交換有2種方式:發布/訂閱和點對點。JMeter針對這兩種情形,分別提供了不同的Sampler進行支援。以下MOM我們使用ActiveMQ 3.2.1,分別描述這兩種訊息交換方式是如何使用 JMeter 進行測試。
1. 測試前的準備(兩種情況都適用)
JMeter 雖然能使用 JMS 對 MOM 進行測試,但是它本身並沒有提供JMS需要使用的包。因此,在測試之前需要將這些包複製到 %JMETER_HOME%/lib 下。對於 ActiveMQ 來說,就是複製 %ACTIVEMQ_HOME%/lib。%ACTIVEMQ_HOME%/optional 是可選包,可根據實際情況來考慮是否複製。
JMeter 在測試時使用了 JNDI,為了提供 JNDI 提供者的資訊,需要提供 jndi.properties。同時需要將 jndi.properties 放到 JMeter 的 classpath 中,建議將它與 bin下的 ApacheJMeter.jar 打包在一起。對於 ActiveMQ,jndi.properties 的樣本內容如下:
java.naming.factory.initial = org.activemq.jndi.ActiveMQInitialContextFactoryjava.naming.provider.url = tcp://localhost:61616#指定connectionFactory的jndi名字,多個名字之間可以逗號分隔。#以下為例:#對於topic,使用(TopicConnectionFactory)context.lookup("connectionFactry")#對於queue,(QueueConnectionFactory)context.lookup("connectionFactory")connectionFactoryNames = connectionFactory#註冊queue,格式:#queue.[jndiName] = [physicalName]#使用時:(Queue)context.lookup("jndiName"),此處是MyQueuequeue.MyQueue = example.MyQueue#註冊topic,格式:# topic.[jndiName] = [physicalName]#使用時:(Topic)context.lookup("jndiName"),此處是MyTopictopic.MyTopic = example.MyTopic |
2. 發布/訂閱
在實際測試時,發行者和訂閱者並不是需要同時出現的。例如,有時我們可能想測試單位時間內訊息發行者的訊息產生量,此時就不需要訊息發行者,只需要訂閱者就可以了。本例為了說明這兩種Sampler的使用,因此建立如下的測試計劃:
其中JMS Publisher和JMS Subscriber的屬性:選擇"使用jndi.properties",串連工廠是connectionFactory,主題是MyTopic,其它使用預設配置。對於JMS Publisher,還需提供測試用的簡訊。
啟動ActiveMQ,運行測試計劃。如果配置正確,那麼與ActiveMQ成功串連之後,在JMeter的後台會列印出相關資訊。在測試過程中,JMeter 後台列印可能會出現java.lang.InterruptedException 資訊,這個是正常現象,不會影響測試過程和結果。這一點可以從 bin 下的 jmeter.log 看出。
3. 點對點
對於點對點,JMeter只提供了一種Sampler:JMS Point-to-Point。在例子中,建立如的測試計劃:
其中:Communication style是Request Only。對於另一種風格:Request Response,會驗證收到訊息的JMS Header中的JMSCorrelationID,以判斷是否是對請求訊息的響應。