架构性能瓶颈
制定架构并不只是画个图, 装个软件然后就开工. 最重要的是分析找出这个架构的性能瓶颈, 然后或者用硬件, 或者用优化代码的方式加以改善提高, 最终达到整个架构支持高并发的目标.
这是上一篇博客写到的秒杀架构草稿图:
那么它的性能瓶颈在哪呢? 大家可以注意到几个中间件的颜色是不一样的, 分别表示.
绿色: 作为流量入口的负载均衡器, 性能是最优越最不容易出问题的. 软件的代表自然是大名鼎鼎的nginx, 基本上一台4核16G的服务器, 承担个数万并发没有问题. 硬件的代表有F5, 小肥爬爬曾经在一个项目用过F5, 对它支撑20多万用户的表现非常震惊....
土黄色: 微服务的接口层实现有好多方式, java系的protocalbuf/dubbo/feign, go系的gin, python的tornado / falcon.... 理论上当然是go语言最好, java系的pb/dubbo最好. 但我习惯了用feign, 所以要实测nacos+feign的性能, 同时也给团队内部一个量化数据頂心丸.
红色: 应用系统的性能瓶颈最终都会是mysql, 确切地说, 是一切OLTP系统. 这其实是可以理解的, 写操作, 特别是数据库的操作涉及到事务同步的概念, 在开发语言的层面就是资源的竞争, 加锁, 等待... 同时这些系统一般都是SOCKET程序, 还记得操作系统概念的同学都知道, 资源竞态和IO场景效率都不会太高. 所以mysql 会成为架构的性能瓶颈, 应该是程序员的一个普遍从业经验.
我们的计划是除nginx外, 先单独对某个部件组合进行测试, 结合各方面的测试数据, 然后再制定业务工程的划分和代码级别的优化.
搭建微服务工程
工程代码在这里(记得选择分支) , 如下:
git clone https://gitee.com/xiaofeipapa/springcloud-adv -b high-concurrency
建议先下载代码, 然后对照代码看博客.
spring boot 版本选择
出于习惯, 我还是使用alibaba全家桶来搭建微服务工程. 核心的版本号如下:
<!-- spring / spring cloud / spring boot-->
<!-- https://github.com/alibaba/spring-cloud-alibaba/wiki/版本说明 -->
<spring-boot.version>2.6.13</spring-boot.version>
<spring.version>5.3.25</spring.version>
<spring-cloud-alibaba.version>2021.0.5.0</spring-cloud-alibaba.version>
<spring-cloud.version>2021.0.4</spring-cloud.version>
<nacos.version>2.2.0</nacos.version>
feign的实现
feign工程代码不复杂, 里面只有2个方法: 无参的和带1个参数的. 带参数方法主要用于后面的jmeter 测试.
/**
* 作者: 小肥爬爬
* 简书: https://www.jianshu.com/u/db796a501972
* 邮箱: imyunshi@163.com
* <p>
* 您可以任意转载, 恳请保留我作为原作者, 原创不易, 跪求, 谢谢.
**/
@FeignClient(name = "sample-feign")
public interface ITestFeign {
@GetMapping("/test/time")
String time() throws UnknownHostException;
@GetMapping("/test/hello")
String hello(@RequestParam String name);
}
/**
* 作者: 小肥爬爬
* 简书: https://www.jianshu.com/u/db796a501972
* 邮箱: imyunshi@163.com
* <p>
* 您可以任意转载, 恳请保留我作为原作者, 原创不易, 跪求, 谢谢.
*
* 秒杀入口
**/
@RestController
@Slf4j
public class SampleFeignController implements ITestFeign {
@Value("${server.port}")
int port;
@Override
public String time() throws UnknownHostException {
InetAddress localHost = InetAddress.getLocalHost();
return "from: " + localHost.getHostAddress() + ":" + port + " , at: " + LocalDateTime.now();
}
@Override
public String hello(String name) {
return "hello, " + name;
}
}
主要配置文件:
server:
port: 17000
tomcat:
threads:
max: 4000
# ---------- springboot 旧版本配置
# # 最大工作线程数, 默认200
# max-threads: 2000
# # 最大连接数 从 100 变成 200
# max-connections: 2200
# # 等待队列
# accept-count: 100
# min-spare-threads: 500
这里把tomcat的并发设成4000.
gateway 工程
配置gateway工程的主要要点是: 引入gateway的包, 排除 spring-boot-starter-web . 因为springcloud gateway 用的是 reactor 写法, 和web 写法不兼容.
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
下载nacos 并启动
nacos下载地址在github, 经常出现访问不畅. 所以我上传了一个2.2.2 版本到百度云盘, 请自由下载:
链接: https://pan.baidu.com/s/1Jp7OOKAoOBWZTVBADx6hTg?pwd=27x8 提取码: 27x8
启动命令如下:
cd nacos目录/bin
./startup.sh -m standalone
启动gateway 工程和feign工程
依次启动gatway 工程 和feign 工程. 如果想形成feign工程的第二个部署实例怎么办? 有两个办法: 要么用docker部署, 要么用idea 直接启动.
作为一篇博客不想引入太多其他技术, 这里介绍下怎么用idea 启动第二个实例. 右上角点一下启动命令旁边的下拉小箭头, 然后选择 Edit Configurations
在打开的界面选择 StartFeignSample, 然后点击上面的小工具图标, 其中有一个是复制命令. 将命令改名, 然后在 Environment variables 设置环境变量为 server.port=17001, 如下:
再增加一个实例, 端口是17002. 即最后这个服务总共有3个实例. 如nacos的下图:
用ab进行简单测试
接下来我们用ab测试nacos+feign集群的性能. 测试命令如下:
# 让操作系统打开更多句柄
ulimit -n 65536
ab -n 20000 -c 2000 http://localhost:10000/sample-feign/test/time/
以上命令应该在mac / linux 都能正常运行. 如果你是用windows 系统..... 咳咳, 可能要找找其他帖子看看怎么运行.
-n: 表示ab 总共发送 2万个请求.
-c: 表示并发数量2000.
最终在我的电脑的表现如下:
Concurrency Level: 2000
Time taken for tests: 0.977 seconds
Complete requests: 20000
Failed requests: 70
(Connect: 0, Receive: 0, Length: 70, Exceptions: 0)
Total transferred: 3719720 bytes
HTML transferred: 1019720 bytes
Requests per second: 20462.68 [#/sec] (mean)
Time per request: 97.739 [ms] (mean)
Time per request: 0.049 [ms] (mean, across all concurrent requests)
Transfer rate: 3716.57 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 39 5.3 40 50
Processing: 11 54 11.4 51 107
Waiting: 1 39 9.7 37 99
Total: 56 94 10.7 92 138
Percentage of the requests served within a certain time (ms)
50% 92
66% 99
75% 102
80% 103
90% 107
95% 110
98% 114
99% 119
100% 138 (longest request)
从上数据可以看到, 所有请求都在146ms内完成, 可以看到即使是单机, 轻松抗住2000个并发毫无问题. 事实上即使n再加大, 性能也是刚刚的. 这是我电脑的数据:
(-n 50000 )
-c | 完成所有请求的总时间(s) | 请求的最长时间(ms) |
---|---|---|
3000 | 2.323 | 187 |
4000 | 2.367 | 260 |
5000 | 2.369 | 332 |
6000 | 2.345 | 409 |
7000 | 2.406 | 500 |
8000 | 2.378 | 561 |
9000 | 2.368 | 569 |
10000 | 2.467 | 666 |
我们怎么来解读这个数据? 大家可以回想下个人购买产品的体验, 当点击"下单购买"按钮的时候, 一般会看到个loading框在转转转, 然后会提示你购买成功或者失败, 这个过程一般在2-4秒. 也就是说只要处理时间在2-4秒, 都是可以接受的.
所以这个数据的结论就是: 所有请求都能在3秒内完成. 并且耗费时间最多的请求也不足1秒, 我们的并发测试达到了效果. 当然这个方法非常简单, 没有实际方法, 没有调用数据库. 但这足以说明性能刚刚的了.
用go性能会更刚. 不过这个性能指标对大多数公司已经够用了.
feign vs dubbo
feign仅是http请求的包装, 模型更简单, 很多老项目的代码都用得上. dubbo的性能大概比feign强一倍. 但我个人不怎么喜欢用. 读者也可以换成dubbo 进行自己的测试.
用jmeter 进行带参数的链接测试
jmeter 和 ab的测试结论其实差不多, 但jmeter 的测试功能更多更强大. 接下来我们看看怎么用jmeter 测试带参数的链接和返回测试报告.
增加测试方法
// feign的测试方法:
@FeignClient(name = "sample-feign")
public interface ITestFeign {
// ... 其他方法
@GetMapping("/test/hello")
String hello(@RequestParam String phone, @RequestParam Integer actId, @RequestParam Integer num);
}
// controller的方法
@Override
public String hello(String phone, Integer actId, Integer num) {
log.info("---- 活动id: {}, 手机号: {}, 购买数量: {}" , actId, phone, num);
return "hello, " + phone;
}
这个方法表示会接受3个参数: 秒杀活动的id, 用户的手机号, 以及用户购买商品的数量.
生成模拟数据文件
我一般把一些小功能程序扔到junit执行, 以下是代码:
/**
* 作者: 小肥爬爬
* 简书: https://www.jianshu.com/u/db796a501972
* 邮箱: imyunshi@163.com
* <p>
* 您可以任意转载, 恳请保留我作为原作者, 原创不易, 跪求, 谢谢.
**/
@Slf4j
public class GenPhoneDataTest extends BaseFeignSampleTests{
ConcurrentHashMap<String, Integer> cache = new ConcurrentHashMap<>();
// 生成1w个用户并保存到csv
@Test
public void doTest() throws Exception {
int max = 10000;
int start = 0;
int activityId = 1000; // 活动id
while (start < max){
// 生成随机手机号
String phone = PersonInfoSource.getInstance().randomChineseMobile();
// 保证不重复
while (cache.contains(phone)){
phone = PersonInfoSource.getInstance().randomChineseMobile();
}
// 随机购买数量, 不超过3件
int productCount = NumberSource.getInstance().randomInt(1, 4);
cache.put(phone, productCount);
start++;
}
this.writeFile(activityId);
log.info("---- 已保存 10000个手机号");
}
private void writeFile(int activityId) throws IOException {
String path = "/tmp/phone_data.txt";
File file = new File(path);
//如果文件不存在,创建文件
if (!file.exists()) {
file.createNewFile();
}
FileWriter writer = new FileWriter(file);
for (String name : cache.keySet()){
writer.write(name + "," + cache.get(name) + "," + activityId);
writer.write("\n");
}
writer.flush();
writer.close();
}
}
以上代码会生成10000行数据, 保存到 /tmp/phone_data.txt 文件, 数据格式类似:
17349473799,3,1000
15863934940,2,1000
19601404246,1,1000
19419729371,3,1000
这个文件是为jmeter预备的, 接下来看看jmeter 的操作.
下载执行jmeter
下载jmeter, 我打包了一个在百度云盘: https://pan.baidu.com/s/1HmMfHKoLeJyRXeKNJrImrA?pwd=f6if 提取码: f6if
解压之后, 在bin目录下 打开 jmeter 即可.
增加线程组
在Test Plan那里打开鼠标右键:
这表示我们用4000个线程进行测试, 即模拟4000个并发用户, 循环10次, 即总共4万个请求.
增加参数文件
这表示每个发出的http请求都会从 phone_data.txt 文件读取参数. 第一个参数名是phone, 第二个参数名是num, 第三个是actId, 它们用,分隔. 参数的顺序不能乱, 要和txt文件里的内容一致.
增加http请求
以上是http请求的设置, 它的链接是 http://localhost:10000/sample-feign/test/hello , 会带上3个参数. 格式是: 参数名: xxx , 值: ${xxx} . 这些参数名对应着上一节的 Config data set .
增加报告
View result tree 和 summary report 都要点上.
最终看起来是这样的:
启动工程, 点击 jmeter工具栏那个绿色的按钮, 然后静待测试结果即可.
观察测试结果
测试结论和ab 测试差不多. 但是jmeter 可以设置参数, 保存测试报告, 返回每一个请求的结果. 这就很有用了.
建议大家都熟练掌握jmeter, 通过它测试掌握自己代码每个接口的性能.
总结
在这一节我们动手测试了nacos+feign的性能, 证明足以满足大多数项目的并发请求. 下一章我们会测mysql, 大多数项目的真正性能瓶颈.