性能測試的目標
性能測試不同於功能測試,不是對與錯的檢驗,而是快與慢的衡量.在進行真正的性能測試之前要先搞清楚目標:
1. 在確定的硬體條件下,可以支持的併發數越大越好,響應時間越快越好.具體需要達到的併發數是多大,要求的響應時間是多快,由產品經理來提出.
2. 在確定的硬體條件下,測試得到最大併發數和相應的響應時間之後.如果增加硬體投入,可以得到怎樣的性能提升回報? (系統擴展性和伸縮性測試,Scalability)
這裡的硬體條件包括:cpu,memery,I/O,network bandwidth.
性能測試中的基準測試 Benchmarking
與功能測試相似,性能測試也要設計測試用例,不同的是在正式開始你的業務測試用例之前你要先進行一下基準測試.為什麼呢?其實就是先要量一下你的硬體的能力,不然,如果你的測試結果不好,你怎麼知道是硬體慢還是你的軟體的問題.這些硬體測試包括:
1. 網路帶寬測試, 你可以通過copy大文件的方式測試你的網路的最大帶寬是多少.
2. cpu,你可以利用比較複雜的演算法來衡量cpu的快慢
3. memery,這個不用測試,你知道memery的大小
4. IO, 也可以通過copy大文件來測試
這些基準測試用例在後面的調優過程中,還可以用來衡量你修改之後真的變好了嗎.
設計你的業務測試用例
比較理想的測試用例就是要儘可能模模擬實世界的情況,這往往做不到,尤其是對於新產品來說.你可以先錄製一些用戶最常用,最典型的case作為起點.
另外,對於併發的概念需要搞清楚.併發用戶,通常是指同時在線的用戶,這些用戶可以能在用你的系統的不同的功能,注意並不是說大家都在做同一件事情.對某一個事務併發請求是指某一個request的併發調用.
對於后一種併發,你往往需要計算在用戶量最大的時候,大概大家都集中的在干哪一件事情,這個請求一定要夠快才好.
設計好這兩種測試用例以後,在後面的調優過程中,他們就成了衡量你的改進的成效的衡量的標尺.
性能調優
性能調優要從底層開始,基本上要從OS開始,到JVM,Cache,Buffer Pool, SQL,DB Schema, 演算法.
一次不要改的太多,改一點,測一下,這可是個慢功夫,需要有耐心.
在執行測試的時候還要注意,要遵循相同的過程,系統需要在重啟之後先熱身再開始真正的測試,不然你會發現你的測試結果很不一樣,琢磨不定.
還有,要注意你的客戶端的能力,比如JMeter,很需要內存,別因為客戶端不行,誤以為是你的系統的問題,那就太烏龍了.
在測試調優的時候,需要藉助一些監控工具比如JConsole,來監控系統的狀況,找到系統的瓶頸,所謂瓶頸,就是最慢的那個部分,也常表現為100%被佔滿.比如你的內存或者cpu被用盡了.如果cpu和內存還沒有用盡,說明他們在等某個資源.這時候需要用profile工具去尋找,比如JProfile,YourKit.
利用性能監控日誌
因為性能的問題不是很容易重現,當product環境中遇到性能問題的時候,如果是數據的問題,也許當你把product 數據copy到你的測試環境中,就能重現比較慢點查詢,加以改進.但是如果是併發用戶或者網路等運行時環境的問題,你就很難重現.這時,如果你能通過日誌看到那些關鍵的響應慢的方法,也許可以幫助你快點找到問題所在.下面的代碼可以幫你做到這一點,僅供參考:
import org.slf4j.Logger;
public class TraceUtil {
final Logger logger;
final long threshold = 1000;
private long begin;
private long offtime = 0;
private String threadInfo;
private String targetId;
public TraceUtil(Logger logger, Thread thread, String targetId, long begin) {
this.logger = logger;
this.threadInfo = thread.getId() "-" thread.toString();
this.targetId = targetId;
this.begin = begin;
}
public void trace(String targetEvent) {
long duration = System.currentTimeMillis() - begin;
long increment = duration - offtime;
offtime = duration;
float percentage = (float) increment / (float) duration * 100;
if (duration > threshold && percentage > 20) {
logger.error(
"Response time is too large: [{}], {}/{} ({}), {}, {}",
new String[] { threadInfo "", increment "",
duration "", percentage "%", targetEvent,
targetId });
}
}
}
利用JVM的MXBean找到blocked的點
當你發現JVM佔用的cpu很高,
響應時間比較慢,很可能是被IO或者網路等慢速設備拖住了.也有可能是你的方法中某個同步點(同步方法或者對象)成為性能的瓶頸.這時候你可以利用JVM提供的monitor API來監控:
<%@ page import="java.lang.management.*, java.util.*" %>
<%!
Map cpuTimes = new HashMap();
Map cpuTimeFetch = new HashMap();
%>
<%
out.println("Threads Monitoring");
long cpus = Runtime.getRuntime().availableProcessors();
ThreadMXBean threads = ManagementFactory.getThreadMXBean();
threads.setThreadContentionMonitoringEnabled(true);
long now = System.currentTimeMillis();
ThreadInfo[] t = threads.dumpAllThreads(false, false);
for (int i = 0; i < t.length; i ) {
long id = t[i].getThreadId();
Long idObj = new Long(id);
long current = 0;
if (cpuTimes.get(idObj) != null) {
long prev = ((Long) cpuTimes.get(idObj)).longValue();
current = threads.getThreadCpuTime(t[i].getThreadId());
long catchTime = ((Long) cpuTimeFetch.get(idObj)).longValue();
double percent = (double)(current - prev) / (double)((now - catchTime) * cpus * 1000);
if (percent > 0 && prev > 0) {
out.println("<li>" t[i].getThreadName() "#" t[i].getThreadId() " Time: " percent " (" prev ", " current ") ");
String locked = t[i].getLockInfo()==null?"":t[i].getLockInfo().getClassName();
out.println(" Blocked: (" t[i].getBlockedTime() ", " t[i].getBlockedCount() ", " locked ")</li>");
}
}
cpuTimes.put(idObj, new Long(current));
cpuTimeFetch.put(idObj, new Long(now));
}
%>
同步是性能的一大瓶頸
通過監控發現,大量線程block在一個同步方法上,這樣cpu也使不上勁.當你發現性能上不去,IO和網路等慢速設備也不是問題的時候,你就得檢查一下是否在某個關鍵點上使用了同步(synchronizae).有時候也許是你應用的第三方的jar裡面的某個方法是同步的,這種情況下,你就很難找到問題所在.只能在編寫代碼的時候看一下你引用的方法是否是同步的.