手写性能测试工具——上篇

用户头像
Jerry Tse
关注
发布于: 2020 年 07 月 22 日

0. 前言

性能测试是对一个系统性能优化的前提,也是性能优化成果的检测和度量标准。性能测试有多个指标,这些指标相互关联互相影响。本文我们通过编写一个简单的形成测试工具,通过在不同场景下进行性能压测,来验证性能指标之间的关系和影响。



1. 性能测试指标



1.1 响应时间

是指应用系统发从请求开始到收到最后响应数据所需要的的时间。直观的反映了系统的“快慢”。响应时间越短,我们觉得系统反应越快,用户体验越好,反之亦然。

1.2 并发数

系统能够同时处理请求的数据,这个数字反映了系统的负载特性。并发数越大,系统负载能力越强。这里要注意,并发是系统每时每刻都要承载的,所以并发数没有时间维度,没有一秒钟并发数或一分钟并发数的说法。

1.3 吞吐量

指单位时间内系统处理的请求数量,体现系统的处理能力。



1.4 响应时间、并发数和吞吐量关系

吞吐量(每秒)=(1000 / 响应时间 ms)× 并发数

根据公式可知,在并发数不变的情况下,响应时间越短,系统吞吐量越大。

但是随着并发数增大,服务器资源占用率必然增加,系统响应时间变长,当达到系统自身瓶颈后(最大负载),继续增加访问,响应时长急剧增加。





1.5 性能计数器

性能计数器是描述服务器或操作系统性能的一些数据指标。包括System Load、对象和线程数、内存占用、CPU占用、磁盘和网络I/O等指标。这些也是系统监控的重要参数。



2. 代码实现

介绍了各种性能指标,接下来我们手写一个简单的性能测试工具,你可以通过这个工具压测自己的项目,看看响应时间、并发数和吞吐量关系三者的关系是否如我们预计那样变化。

源码下载地址:https://github.com/TmTse/architect_training_camp



  • 为了力求简单,代码只依赖原生JDK,不依赖任何三方Jar包。

  • 我们使用java.net.HttpURLConnection发起HTTP请求,也可以使用三方工具如HttpClient发起HTTP请求(可以应用异步HTTP提高性能),只要实现程序中HttpClient接口即可。

public interface HttpClient {
/**
* get
* @param url
* @param heads can be null
* @return
*/
Response get(String url, Map<String, String> heads);
/**
* normal form post
* @param url
* @param params can be null
* @param heads can be null
* @return
*/
Response post(String url, Map<String, String> params, Map<String, String> heads);
}



  • 程序中参数均在config.properties配置

url=https://www.baidu.com
total=100
concurrentNumber=10
HttpClientImplement=performance_testing_utils.HttpUrlConnection
MAX_QUEUE_SIZE=1000
  • 我们使用ExecutorService来实现并发访问,通过Executors.newFixedThreadPool(并发数)创建一个固定线程数线程池来具体并发数的并发访问。因为固定线程池的特点,能保证每时每刻都是10个并发访问,一旦有一个访问任务完成,线程空闲,就会有新的访问任务开始执行,直到所有任务执行完毕。

public class Client {
private static ExecutorService executorService;
private static final Properties config;
private static final String url;
private static final int total;
private static final int concurrent;
private static final ThreadPoolExecutor tpe;
private static final int MAX_QUEUE_SIZE;
static {
//load config
config = PropertiesLoadUtil.loadAsResource("config.properties");
url = (String) config.get("url");
total = Integer.valueOf(config.getProperty("total"));
concurrent = Integer.valueOf(config.getProperty("concurrentNumber"));
//init thread pool
executorService = Executors.newFixedThreadPool(concurrent);
tpe=(ThreadPoolExecutor)executorService;
MAX_QUEUE_SIZE=Integer.valueOf(config.getProperty("MAX_QUEUE_SIZE"));
}
public static void main(String[] args) {
if(total<=0){
throw new IllegalArgumentException("total must to greater 0");
}
if(concurrent<=0){
throw new IllegalArgumentException("concurrentNumber must to greater 0");
}
List<Future<Response>> futures = new ArrayList<>(total);
for (int i = 0; i < total; i++) {
Future<Response> future = executorService.submit(() -> HttpClientFactory.getHttpClient().get(url, null));
futures.add(future);
//forbid insert queue too large to out of memory
while (tpe.getQueue().size()>MAX_QUEUE_SIZE){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
executorService.shutdown();
long totalTime = 0;
long[] responseTimes = new long[total];
int i = 0;
for (Future<Response> future : futures) {
try {
long responseTime = future.get().getResponseTime();
totalTime += responseTime;
responseTimes[i++] = responseTime;
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
System.out.println(String.format("访问地址:%s",url));
System.out.println(String.format("并发数:%s,总访问次数:%s:", concurrent, total));
System.out.println(String.format("平均响应时间:%sms", totalTime / total));
Arrays.sort(responseTimes);
int offset = 95;
int index = (total * offset / 100 - 1) < 0 ? 0 : total * offset / 100 - 1;
System.out.println(String.format("%s%%响应时间:%sms", offset, responseTimes[index]));
System.out.println(String.format("吞吐量:%s/s",1000/(totalTime / total)*concurrent));
}
}



这里注意38行代码,如果线程池中未处理的队列数量超过MAX_QUEUE_SIZE,我们就让线程sleep一下,不要继续向线程池提交作业,直到队列数量降到MAX_QUEUE_SIZE以下。



通常情况下,在压测时候,我们会设置一个比较大的总数访问总数,以获取一个相对稳定的数据。所以访问总数远远大于并发数,一次性将所有任务都添加进线程池中,处理不完的任务如果都进入队列中可能将内存耗尽。所以我们限制任务提交的数量,避免以上问题的发生。MAX_QUEUE_SIZE可以根据运行中实际情况调整,如果发生OOM,可以将数值调小。



这个例子也可以改造成没有执行总数,而是一直执行,定期(一分钟,十分钟)输入性能结果,直到手动停止。感兴趣的同学可以自己试着改造一下。



  • 运行结果:

访问地址:https://www.baidu.com
并发数:10,总访问次数:100:
平均响应时间:31ms
95%响应时间:164ms
吞吐量:320/s



我们可以通过工具访问自己的项目(压测知名网站有可能被安全机制拦截),通过调整并发数(例如:10,20,30..)。看看是否像我们在1.4中预测的那样,响应时间随并发数增加而增加,并最终造成系统崩溃。在配合服务端资源监控程序,我们能更清晰的观察到服务器资源消耗和并发数的关系。



3. 性能问题

如果你的服务器性能较好,你就会发现,起初随着你调大并发数资源服务器资源占用率会提升,但是继续调大后使用率却不在提升。但此时资源并没有达到饱和状态。出现这种情况是因为你压测的客户端先于服务器达到了性能瓶颈。

为什么会这样呢?我们要从线程说起。我们知道线程并不是真的并发,线程通过获得CPU执行时间模拟并发执行,CPU核心数有限,单机能执行的合理线程数也是有限的,线程开的过多并不能达到并发的效果,反而会因为过于频繁的线程间切换造成性能下降。

程序中我们是通过线程模拟并发的,所以因为单机线程的限制,我们也只能模拟出有限的并发请求情况。所以很有可能无法压测出一个系统的真正性能情况。

如何解决呢?既然单机性能受限,我们通常的思路就是采用多机来解决。这些我们放到下次介绍。



4. 总结

本文中我们介绍了性能测试的重要指标和指标之间的关系。我们实现了一个简单的性能测试工具,可以用他对系统进行模拟压测,验证我们文中的结论。最后我们也指出了我们这个简单的性能测试工具并不完美,本身也有性能问题。关于这个性能工具的优化我们会留到下篇文章中继续介绍。

发布于: 2020 年 07 月 22 日 阅读数: 6
用户头像

Jerry Tse

关注

还未添加个人签名 2018.11.02 加入

还未添加个人简介

评论

发布
暂无评论
手写性能测试工具——上篇