写点什么

架构师训练营第 0 期第 7 周作业

用户头像
Arthur
关注
发布于: 2020 年 07 月 21 日

1、性能压测的时候,随着并发压力的增加,系统响应时间和吞吐量如何变化,为什么?

性能压测的时候,随着并发压力的增加,系统的响应时间变长,吞吐量降低;

原因:

首先先来看 吞吐量 、响应时间 和 并发数 的定义,以及它们之间的关系;

1-1、响应时间

所谓响应时间,就是指 系统 【从发出请求开始】 到 【收到最后响应数据】 需要的时间。响应时间是系统最重要的性能指标,直观的反映系统的快慢。

比如 用户在电商购买商品,从发出支付请求到返回支付结果,中间消耗的时间就是响应时间。



1-2、并发数

系统能够【同时处理】请求的数目,这个数字也反映了系统的负载特性。对于网站而言,并发数即系统并发用户数,指同时提交请求的用户数目。

这里的【同时请求】,是指用户发送请求后,请求到达系统不用等待,系统可以直接处理;

并发数一般与系统硬件资源,如CPU、内存、硬盘、网络IO有关;



1-3、吞吐量

【单位时间】内系统处理的请求数量,体现系统的处理能力。对于网站,可以用“请求数/秒”或者“页面数/秒”来衡量,也可以用“访问人数/天” 或者 “处理的业务数/小时” 等来衡量。

TPS(每秒事务数)也是吞吐量的一个指标,还有 HPS(每秒HTTP请求数),QPS(每秒查询数)等。



1-4、响应时间、并发数 和 吞吐量 之间的关系

从上面的定义中,可以举例,单位时间用【秒】统计:

  • 系统处理的【并发数】为1,【响应时间】为1秒,那么1秒钟可以处理的请求数量就是1,即吞吐量为1;

  • 系统处理的【并发数】为1,【响应时间】为0.5秒,一秒钟可以响应2个请求,系统的吞吐量为2;

  • 系统处理的【并发数】为2,【响应时间】为0.5秒,一秒钟可以响应4个请求,系统的吞吐量为4;

按照上面的推理,得到 响应时间、并发数 和 吞吐量的 关系:

从公式看,系统【响应时间】不变的情况下,【吞吐量】会随着【并发数量】增加而增加;



但是服务器的资源是有限制的,特别是CPU的核数,通常CPU的一个核只能处理一个线程,而当并发请求的线程数量超过CPU核数,其他线程将等待【占用CPU】的线程释放资源,这时才能继续执行。这里要介绍一个重要指标,就是System Load;



所谓 System Load,就是系统负载,指当前【正在被CPU执行】和【等待被CPU执行】的进程数目总和,是反映系统忙闲程度的重要指标。

多核CPU情况下,完美情况是所有CPU都在使用,没有进程在等待处理,所以 System Load的理想值是CPU的数目

当load值低于CPU数目时,表示CPU有空闲,资源存在浪费;当load值高于CPU数目时,表示进程在排队等待CPU,表示系统资源不足,影响应用程序的执行性能



回到上面的问题,为什么随着【并发】压力增加,系统【响应时间】会变成,【吞吐量】会降低?

当 System Load数量 高于 CPU数目,此时 请求就需要等待 CPU释放资源,比如原来一个请求处理时间需要100ms,因为资源等待问题,一个请求的【响应时间】变为 200ms,随着【并发数】增加,等待的请求数只能排队,【响应时间】越来越长,从而导致【吞吐量】降低;



下面两张图,反映了随着【并发数】增加,系统【响应时间】和【吞吐量】的变化







查看上图还有个问题,作为架构师,为了保证系统稳定,一般将系统的状态维持在上面 a、b、c 三个点的什么部分?

这看似是设计问题,其实是【经济】问题,当一个系统已经使用解决高并发设计的方法,如 缓存、异步消息队列、数据库分片 等技术,优化空间有限,或者优化需要付出非常昂贵代价时,应该如何选择;

如果公司的资金成本比较充裕,为了保证系统的问题,最好的方式就是水平扩容服务器或者纵向提升服务器性能,也就是提升硬件资源,将系统维持在b点的左边,这样即使系统的请求增加,也有一段缓冲时间;

而如果公司的资金成本不是很宽裕,硬件升级对于公司的支出是很大的负担,维持在b点的右侧,虽然缓冲的余地变少,但是不至于系统立马就挂的情况;



2、用你熟悉的编程语言写一个 web 性能压测工具,输入参数:URL,请求总次数,并发数。输出参数:平均响应时间,95% 响应时间。用这个测试工具以 10 并发、100 次请求压测 www.baidu.com。

题目分析:

1、输入参数:URL、请求总次数、并发数,这里并发数就是网站一次收到并发的数量,在Java中首先想到多线程,10并发,对应10个线程同时发送请求,可以用【线程池】实现;

2、请求总数是100次,并发10,也就是分10批发送,最后记录100次请求的响应时间;

具体实现如下:

  • 首先,实现一个Http工具,用来发送Http请求,这里需要主要,为了避免重复创建连接带来的性能损耗,在初始化压测时创建【http连接池】,避免每次请求都要创建http连接;其次,考虑到请求超时的问题,要设置建立连接的超时和处理请求的超时时间;

import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.util.EntityUtils;
import java.io.IOException;
public class HttpConnectionPool {
/**
* 最大连接数
*/
private static final int MAX_CONNECTION_NUMBER = 100;
/**
* 连接超时,3000ms
*/
private static final int CONNECT_TIMEOUT = 3000;
/**
* 请求处理超时,3000ms
*/
private static final int SOCKET_TIMEOUT = 3000;
private static final RequestConfig REQUEST_CONFIG = RequestConfig.custom()
.setConnectTimeout(CONNECT_TIMEOUT)
.setConnectionRequestTimeout(CONNECT_TIMEOUT)
.setSocketTimeout(SOCKET_TIMEOUT)
.build();
private static PoolingHttpClientConnectionManager httpClientConnectionManager;
private final CloseableHttpClient httpClient;
public HttpConnectionPool(int concurrentNumber) {
httpClientConnectionManager = new PoolingHttpClientConnectionManager();
//设置连接参数
// 最大连接数
httpClientConnectionManager.setMaxTotal(MAX_CONNECTION_NUMBER);
// 路由最大连接数
httpClientConnectionManager.setDefaultMaxPerRoute(concurrentNumber);
httpClient = HttpClients.custom().setConnectionManager(httpClientConnectionManager).build();
}
public CloseableHttpClient getHttpClient() {
return httpClient;
}
/**
* get请求
*
* @param requestUrl 请求URL
* @return 响应结果
*/
public String doGet(String requestUrl) {
HttpGet httpGet = new HttpGet(requestUrl);
httpGet.setConfig(REQUEST_CONFIG);
CloseableHttpResponse response = null;
try {
response = httpClient.execute(httpGet);
return EntityUtils.toString(response.getEntity(), "UTF-8");
} catch (IOException e) {
System.out.println(e.getMessage());
}
return null;
}
/**
* 测试完成,要记得关掉连接
*/
public void close() {
if (httpClient != null) {
try {
httpClient.close();
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
if (httpClientConnectionManager != null) {
httpClientConnectionManager.close();
}
}
}



  • 其次,因为使用Java的多线程实现并发,这时创建一个Task,用来发送请求,并且记录单个请求的耗时,并且将耗时作为结果返回,用来最终的性能测试统计,所以这里实现Callable接口;因为一次并发为10,这里CountDownLatch用来记录次数,每请求一次减一;

import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
public class ResponseTimeTest implements Callable<Long> {
/**
* Http连接池
*/
private final HttpConnectionPool httpConnectionPool;
/**
* 请求URL
*/
private final String requestUrl;
/**
* CountDownLatch,用来记录次数
*/
private final CountDownLatch countDownLatch;
public ResponseTimeTest(CountDownLatch countDownLatch,
HttpConnectionPool httpConnectionPool,
String requestUrl) {
this.countDownLatch = countDownLatch;
this.httpConnectionPool = httpConnectionPool;
this.requestUrl = requestUrl;
}
@Override
public Long call() throws Exception {
long startTime = LocalDateTime.now().toInstant(ZoneOffset.of("+8")).toEpochMilli();
httpConnectionPool.doGet(requestUrl);
long responseTime = LocalDateTime.now().toInstant(ZoneOffset.of("+8")).toEpochMilli() - startTime;
countDownLatch.countDown();
return responseTime;
}
}



  • 最后,就是总的性能测试调度代码,构造函数使用【请求URL】,【请求次数】和【并发数】,在构造函数中【初始化线程池】和【Http连接池】,创建记录请求响应时间的列表,定义用来计算【平均时间】和【TP95】的方法,程序如下:

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.stream.Collectors;
public class PerformanceTestTool {
/**
* 请求URL
*/
private final String requestUrl;
/**
* 请求次数
*/
private final Integer requestTotal;
/**
* 并发数量
*/
private final Integer concurrent;
private final HttpConnectionPool httpConnectionPool;
private final ExecutorService executorService;
private final List<Long> responseTimes = new ArrayList<>();
public PerformanceTestTool(String requestUrl, Integer requestTotal, Integer concurrent) {
this.requestUrl = requestUrl;
this.requestTotal = requestTotal;
this.concurrent = concurrent;
this.httpConnectionPool = new HttpConnectionPool(concurrent);
this.executorService = Executors.newFixedThreadPool(concurrent);
}
public void test() throws Exception {
int requestTime = requestTotal / concurrent;
for (int i = 0; i < requestTime; i++) {
concurrentTest();
}
httpConnectionPool.close();
// 压测完成连接池需要关闭
executorService.shutdown();
}
private void concurrentTest() throws Exception {
// 每次并发为10
CountDownLatch countDownLatch = new CountDownLatch(concurrent);
for (int i = 0; i < concurrent; i++) {
Future<Long> future = executorService.submit(new ResponseTimeTest(countDownLatch, httpConnectionPool, requestUrl));
responseTimes.add(future.get());
}
countDownLatch.await();
}
/**
* 获取本次性能测试平均时间
*
* @return 平均时间
*/
public double getAverageTime() {
double averageTime = responseTimes.stream().mapToLong(Long::longValue).average().orElse(0);
return BigDecimal.valueOf(averageTime).setScale(2, RoundingMode.FLOOR).doubleValue();
}
/**
* 获取本次性能测试TP95时间
* TP95 就是把请求时间 从小到大 排序,在排序中95%的值,
* 如果请求100次,TP95就是排序95的值;果请求1000次,TP95就是排序950的值
*
* @return 平均时间
*/
public Long getTp95() {
List<Long> sortResponseTimes = responseTimes.stream().sorted(Comparator.comparing(Long::longValue)).collect(Collectors.toList());
int percent95 = BigDecimal.valueOf(sortResponseTimes.size() * 0.95).setScale(0, RoundingMode.FLOOR).intValue();
return sortResponseTimes.get(percent95);
}
public static void main(String[] args) throws Exception {
PerformanceTestTool performanceTestTool =
new PerformanceTestTool("https://www.baidu.com", 100, 10);
performanceTestTool.test();
System.out.println("Request Total : " + performanceTestTool.responseTimes.size());
System.out.println("Request Times : " + performanceTestTool.responseTimes);
System.out.println("Average Time : " + performanceTestTool.getAverageTime());
System.out.println("TP95 : " + performanceTestTool.getTp95());
}
}



输出结果:



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

Arthur

关注

还未添加个人签名 2018.08.31 加入

还未添加个人简介

评论

发布
暂无评论
架构师训练营第 0 期第 7 周作业