压力测试与瓶颈分析方案
压力测试方案
背景
之前做了一次压力测试,效果不理想,无论怎样增加扩展pod都无法达到压力测试极限。
制定新测试方案
常规测试是从入口发出请求,在网络畅通,较难分析出瓶颈。
本次制定的新方案是逆向测试,用渗透的方式,测试从末端开始请求,对用户链路所经历的节点不断渗透,向用户端进发。这种方案可用于分析,外部和内部因素产生的性能瓶颈
压力测试终端机
目的:
- 内网 ECS Windows:在阿里云上购买一个 windows 测试机,直接内网访问阿里云资源,可以用来排查 EIP/SLB/WAF等造成的瓶颈
- 外网 三台 Windows 压测机:主要用来压测 WAF/SLB 过去的流量
- Pod 测试节点:直接 Pod 对 Pod 压测,Pod 对 Service 压测。注意:最好将两个pod放在一个节点上。
服务器和服务优化
保障流量畅通达到数据库,再返回至用户。
以下列出优化的关键点:
- sysctl / ulimit 优化
- 网关连接数 Openresty
- Nginx 链接数
- Tomcat 链接数
- Springboot 链接数
- MySQL JDBC、Redis Jedis、Elasticsearch 服务连接数
阿里云优化
需要确认下面阿里云服务的链接数设置
RDS 链接数 . Redis 链接数 . Elasticsearch 链接数 . MongoDB 链接数
微服务链接池优化
php-fpm 链接数 . jdbc 链接池 . redis 链接池 . elasticsearch 链接池 . mongodb 链接池
数据库慢SQL和索引优化
根据上次压测结果,先将发现的慢SQL,优化掉
接口压测策略
注意:接口选择,不能选择有对接第三方的接口,第三方接口会影响到我们的 QPS
除此之外,还是注意服务器进程跟线程的启动原理,例如设置最大线程数8000,并不是服务器启动之后就会有8000个线程,而是最小可以只期待100个左右,随着链接数上升,系统不断开启新线程。启动线程的过程是很消耗资源的,会影响到测试结果。
所以一般,我们会去第二次压测数据为准。首次压测的目的是让服务器将该启动的线程和分配的资源,初始化好。
首先:选择接口
我们要准备几种接口:
- 没有业务逻辑的空接口,仅仅返回一个字符串之类的,可以使用 health 替代
- 有业务逻辑的,没有数据库访问的
- 有业务逻辑,有缓存访问
- 有业务逻辑,有缓存访问,有数据库访问
- 有业务逻辑,有缓存访问,有数据库访问,有 ES
- 有业务逻辑,有缓存访问,有数据库访问,有 ES,有 mongodb
然后:我们对服务器做一个摸底操作
第一轮:压 health
为什么压健康检查接口?因为它不会操作数据库和缓存也没有业务逻辑。压测该还接口对服务器资源消耗最小,同时能验证出服务器最大连接数设置是否又问题。
第二轮:压单服务接口,即该服务有业务逻辑也会访问数据库等资源,但是他不会链接其他接口。
对比两次压测结果,就知道带有业务逻辑之后,系统会下降多少性能
最后:混合压测,增加复杂度
第三轮:压综合服务接口,即该服务有业务逻辑也会访问数据库等资源,同时会链接其他接口完成一组业务逻辑。
逆向接口性能分析
首先通过 ARSM 找出慢接口,然后查看该接口都调用了哪些其他接口,最后分别对这些接口做压力测试,找出瓶颈原因。
接口压测实施流程
首先,使用内网 ECS windows 压测 K8S ingress IP 地址,然后再压测 K8S SLB 地址 . 然后,压测外网 使用外部三台 ECS windows 压测外网
最后,上面两组测试都为找到问题,我们启动一个 Pod 测试机,进行 pod 对 pod, pod 对 service 压测
瓶颈分析与优化方案
Nginx
链接复用
http 头
location /api/ {
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://gateway.netkiller.cn:8080/;
}
timeout
proxy_connect_timeout 120;
proxy_send_timeout 120;
proxy_read_timeout 120;
keepalive 优化
proxy_set_header Connection "";
upstream http_backend {
server 127.0.0.1:8080;
keepalive 16;
}
server {
...
location /http/ {
proxy_pass http://http_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
...
}
}
Springboot
/ # cat /proc/1/status | grep Threads
Threads: 61
另一中方式,记得数值要 -1 因为ls -l 第一行是 total 0 统计文件数量。
/ # ls -l /proc/1/task/ | wc -l
61
Redis 连接池不生效
压力测试中,系统出现瓶颈,流量始终无法达到数据库。
开发小伙伴这样配置 Redis 由于太久脱离一线,我也不清楚配置是否正确,只能验证一下。
spring.redis.host=172.18.200.5
spring.redis.port=6379
spring.redis.password=passw0rd
spring.redis.database=0
spring.redis.pool.max-active=1000
spring.redis.pool.max-idle=10
spring.redis.pool.max-wait=-1
spring.redis.pool.min-idle=5
spring.redis.pool.timeout=1000
查看客户端连接数是 2
127.0.0.1:6379> info Clients
# Clients
connected_clients:2
cluster_connections:0
maxclients:10000
client_recent_max_input_buffer:8
client_recent_max_output_buffer:0
blocked_clients:0
tracking_clients:0
clients_in_timeout_table:0
创建一个空的 Springboot 项目,写一个最简单的接口,接口中做 set/get 操作。
@Autowired
private RedisTemplate<String, String> redisTemplate;
@GetMapping("/redis")
public String redis() {
redisTemplate.opsForValue().set("name","neo",10, TimeUnit.SECONDS);
String name = (String) redisTemplate.opsForValue().get("name");
return name;
}
使用 ab 命令压测一下
ab -c 100 -n 10000 http://localhost:8080/redis
观看连接池的状态
127.0.0.1:6379> info Clients
# Clients
connected_clients:2
cluster_connections:0
maxclients:10000
client_recent_max_input_buffer:8
client_recent_max_output_buffer:0
blocked_clients:0
tracking_clients:0
clients_in_timeout_table:0
connected_clients:2 数值没有变化,我的猜测果然是对的,这种配置我记得是 Spring 1.5 之前的。
网上说 Springboot 默认使用 lettuce
spring.redis.host=172.18.200.5
spring.redis.port=6379
spring.redis.password=passw0rd
spring.redis.database=0
spring.redis.lettuce.pool.enabled=true
spring.redis.lettuce.pool.max-active=1000
spring.redis.lettuce.pool.max-idle=80
spring.redis.lettuce.pool.min-idle=20
spring.redis.lettuce.pool.max-wait=-1ms
spring.redis.lettuce.shutdown-timeout=100ms
spring.cache.redis.cache-null-values=false
配置后使用 ab 压测,connected_clients:2 没有任何变化。
127.0.0.1:6379> info Clients
# Clients
connected_clients:2
cluster_connections:0
maxclients:10000
client_recent_max_input_buffer:8
client_recent_max_output_buffer:0
blocked_clients:0
tracking_clients:0
clients_in_timeout_table:0
开启 Springboot 调试模式 debug=true,启动需要引入 commons-pool2
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
启动提示
Caused by: java.lang.NoClassDefFoundError: org/apache/commons/pool2/impl/GenericObjectPoolConfig
引入 commons-pool2 后启动成功
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
虽然启动成功,但是 ab 压测仍然 connected_clients:2
127.0.0.1:6379> info Clients
# Clients
connected_clients:2
cluster_connections:0
maxclients:10000
client_recent_max_input_buffer:20480
client_recent_max_output_buffer:0
blocked_clients:0
tracking_clients:0
clients_in_timeout_table:0
只能在debug信息中找线索,发现 JedisConnectionConfiguration
JedisConnectionConfiguration:
Did not match:
- @ConditionalOnClass did not find required classes 'org.apache.commons.pool2.impl.GenericObjectPool', 'redis.clients.jedis.Jedis' (OnClassCondition)
改为 jedis 试试,pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
配置文件做响应调整
spring.redis.jedis.pool.max-active=1000
spring.redis.jedis.pool.max-idle=80
spring.redis.jedis.pool.min-idle=20
spring.redis.jedis.pool.max-wait=-1
使用 ab 压测一轮,终于 connected_clients:190 上去了。
127.0.0.1:6379> info Clients
# Clients
connected_clients:190
cluster_connections:0
maxclients:10000
client_recent_max_input_buffer:20480
client_recent_max_output_buffer:0
blocked_clients:0
tracking_clients:0
clients_in_timeout_table:0
lettuce 连接池,始终还没有解决,目前先用 jedis,其实连接池是同质化产品,虽有性能差异,但差距非常小,对于项目整体而言可以说微乎其微。就如同 Tomcat 跟 Undertow 差距,有时我们需要整理考虑架构方案,并不是所有好工具组合后就一定是好产品。
数据库链接池
很多时候人们从网上找到 springboot 文章,文章中说这样配置连接池,于是就复制站台到自己的配置文件中,也没有去深究,最终流到生产环境。
spring.datasource.max-idle=10
spring.datasource.max-wait=10000
spring.datasource.min-idle=5
spring.datasource.initial-size=5
spring.datasource.validation-query=SELECT 1
spring.datasource.test-on-borrow=false
spring.datasource.test-while-idle=true
spring.datasource.time-between-eviction-runs-millis=18800
spring.datasource.jdbc-interceptors=ConnectionState;SlowQueryReport(threshold=0)
上面的配置已经作废,目前 Springboot 默认使用 hikari 链接池,他的正确配置如下
spring.datasource.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.maximum-pool-size=200
spring.datasource.hikari.auto-commit=true
spring.datasource.hikari.idle-timeout=30000
spring.datasource.hikari.pool-name=Hikari
spring.datasource.hikari.max-lifetime=55000
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.connection-test-query=SELECT 1
测试方法,写一个测试接口,里面运行一条 SQL 即可
@Autowired
private JdbcTemplate jdbcTemplate;
@GetMapping("/jdbc")
public String jdbc() {
String query = "SELECT * from test where id = 10";
return jdbcTemplate.queryForObject(query, (resultSet, i) -> {
System.out.println(resultSet.getString(1) + "," + resultSet.getString(2) + "," + resultSet.getString(3));
return ("OK");
});
}
然后压测这个接口,观察 show full processlist; 正常会不停的变化,如果没有任何变化,例如只启动了 8 个链接,就说明链接池配置没有生效。
mysql> show status like 'Threads%';
+-------------------+-------+
| Variable_name | Value |
+-------------------+-------+
| Threads_cached | 8 |
| Threads_connected | 40 |
| Threads_created | 111 |
| Threads_running | 1 |
+-------------------+-------+
4 rows in set (0.00 sec)
mysql> show full processlist;
+--------+------+-------------------+------+---------+------+----------+-----------------------+
| Id | User | Host | db | Command | Time | State | Info |
+--------+------+-------------------+------+---------+------+----------+-----------------------+
| 507077 | root | 172.20.0.1:50129 | NULL | Query | 0 | starting | show full processlist |
| 507638 | root | 172.18.5.89:49605 | test | Sleep | 0 | | NULL |
| 507639 | root | 172.18.5.89:49707 | test | Sleep | 0 | | NULL |
| 507640 | root | 172.18.5.89:49734 | test | Sleep | 0 | | NULL |
| 507641 | root | 172.18.5.89:49744 | test | Sleep | 0 | | NULL |
| 507642 | root | 172.18.5.89:49766 | test | Sleep | 0 | | NULL |
| 507643 | root | 172.18.5.89:49817 | test | Sleep | 0 | | NULL |
| 507644 | root | 172.18.5.89:49900 | test | Sleep | 0 | | NULL |
| 507645 | root | 172.18.5.89:49994 | test | Sleep | 0 | | NULL |
| 507647 | root | 172.18.5.89:50153 | test | Sleep | 0 | | NULL |
| 507648 | root | 172.18.5.89:50355 | test | Sleep | 0 | | NULL |
| 507649 | root | 172.18.5.89:50507 | test | Sleep | 0 | | NULL |
| 507650 | root | 172.18.5.89:50627 | test | Sleep | 0 | | NULL |
| 507651 | root | 172.18.5.89:50796 | test | Sleep | 0 | | NULL |
| 507652 | root | 172.18.5.89:51105 | test | Sleep | 0 | | NULL |
| 507653 | root | 172.18.5.89:51373 | test | Sleep | 0 | | NULL |
| 507654 | root | 172.18.5.89:51598 | test | Sleep | 0 | | NULL |
| 507655 | root | 172.18.5.89:52063 | test | Sleep | 0 | | NULL |
| 507656 | root | 172.18.5.89:52605 | test | Sleep | 0 | | NULL |
| 507657 | root | 172.18.5.89:53186 | test | Sleep | 0 | | NULL |
| 507658 | root | 172.18.5.89:53621 | test | Sleep | 0 | | NULL |
| 507659 | root | 172.18.5.89:53955 | test | Sleep | 0 | | NULL |
| 507660 | root | 172.18.5.89:54126 | test | Sleep | 0 | | NULL |
| 507661 | root | 172.18.5.89:54946 | test | Sleep | 0 | | NULL |
| 507662 | root | 172.18.5.89:55164 | test | Sleep | 0 | | NULL |
| 507663 | root | 172.18.5.89:55517 | test | Sleep | 0 | | NULL |
| 507666 | root | 172.18.5.89:56070 | test | Sleep | 0 | | NULL |
| 507667 | root | 172.18.5.89:56431 | test | Sleep | 0 | | NULL |
| 507668 | root | 172.18.5.89:56828 | test | Sleep | 0 | | NULL |
| 507669 | root | 172.18.5.89:57421 | test | Sleep | 0 | | NULL |
| 507670 | root | 172.18.5.89:57801 | test | Sleep | 0 | | NULL |
| 507671 | root | 172.18.5.89:58105 | test | Sleep | 0 | | NULL |
| 507672 | root | 172.18.5.89:58541 | test | Sleep | 0 | | NULL |
| 507675 | root | 172.18.5.89:59031 | test | Sleep | 0 | | NULL |
| 507676 | root | 172.18.5.89:59504 | test | Sleep | 0 | | NULL |
+--------+------+-------------------+------+---------+------+----------+-----------------------+
35 rows in set (0.00 sec)
Openfeign 瓶颈分析
在公司代码库中找到这样的配置,总觉得不对,我最后一次写代码是 2017 年,最开始叫 Feign 后来改为 Openfeign,为了分析出整个系统的瓶颈,有必要深挖一下 Feign/Openfeign。
spring:
feign:
httpclient:
enabled: true
大多数人的学习路径是看书或去网上找别人的文章,复制里面的代码和配置。这种学习方式有两个弊端,书上的内容知识更新太慢,版本比较旧。网上找文章也是如此,别人的例子就是编译不通过,配置也很多是过时或错误的,一股脑,不做验证的饮引用别人的代码和配置。我更习惯看官网文档和翻看官方源码。
上面的配置就很可能某小伙伴,在网上看到别人那样配置,然后复制过来,就上生产了,并且这段配置在公司已经存在很多年,所有人都没有质疑过,直到我来公司,解决系统瓶颈问题才发现。
下面再说 okhttp 的问题,okhttp 在 openfeign 项目中并不成熟,属于实验产品。配置项只有 feign.okhttp.enabled,没有 max-connections 配置。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
面临问题:如果使用 okhttp 怎么设置 max-connections,于是我便翻看 spring-cloud-starter-openfeign 源码,请看 openfeign 源码 https://github.com/spring-cloud/spring-cloud-openfeign/blob/main/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/FeignHttpClientProperties.java,明显 openfeign 源码没有体现 feign.okhttp.max-connections。如果强行使用 okhttp 只能这样设置
# 禁用 httpclient
feign.httpclient.enabled=false
# 启用 okhttp
feign.okhttp.enabled=true
# 因为没有提供 feign.okhttp.max-connections,只能用这种方式,从代码上看feign.httpclient.enabled=false并不影响这样使用
feign.httpclient.max-connections=1000
feign.httpclient.max-connections-per-route=50
另有新发现,重点看这里 @ConfigurationProperties(prefix = "spring.cloud.openfeign.httpclient") 如果是这样 spring.cloud.openfeign.httpclient
那我的配置应该必须是
spring.cloud.openfeign.httpclient.max-connections=200
可是官网手册上写:
feign.httpclient.max-connections=200
所以,官网手册也有滞后,我看的源码是 Openfeign 3.x 版本的,也就是说下一个版本 Openfeign 的配置文件将改为 spring.cloud.openfeign 前缀,同时 Openfeign 3.x 也没有解决 okhttp 的 max-connections 配置问题,还是这样使用 spring.cloud.openfeign.httpclient.max-connections=1000
spring.cloud.openfeign.httpclient.enabled=false
spring.cloud.openfeign.okhttp.enabled=true
spring.cloud.openfeign.httpclient.max-connections=1000
spring.cloud.openfeign.httpclient.max-connections-per-route=50
如果我年轻10年,以我的性格会吧这个缺陷修复了,然后 merge request 到官网,现在让给年轻人做吧。
常用瓶颈分析和系统指标监控命令
System Monitoring & Utility
User
last, lastb - show listing of last logged in users
[neo@linux ~]$ last reboot
reboot system boot 2.6.18-164.15.1. Wed Apr 28 23:43 (6+21:31)
reboot system boot 2.6.18-164.15.1. Fri Apr 16 04:07 (12+19:23)
reboot system boot 2.6.18-164.15.1. Fri Apr 16 02:19 (01:46)
reboot system boot 2.6.18-164.el5 Thu Apr 15 18:52 (07:25)
wtmp begins Thu Apr 15 18:52:15 2010
Memory
Memory
free - Display amount of free and used memory in the system
$ free
total used free shared buffers cached
Mem: 2053440 522028 1531412 0 87076 265952
-/+ buffers/cache: 169000 1884440
Swap: 2441840 0 2441840
5秒监控一次
neo@neo-OptiPlex-780:~/workspace/Document$ free -s 5
total used free shared buffers cached
Mem: 2054224 1708876 345348 0 58908 696404
-/+ buffers/cache: 953564 1100660
Swap: 2077692 81948 1995744
total used free shared buffers cached
Mem: 2054224 1708876 345348 0 58908 696404
-/+ buffers/cache: 953564 1100660
Swap: 2077692 81948 1995744
total used free shared buffers cached
Mem: 2054224 1709000 345224 0 58908 696404
-/+ buffers/cache: 953688 1100536
Swap: 2077692 81948 1995744
vmstat - Report virtual memory statistics
vmstat
# vmstat
procs -----------memory---------- ---swap-- -----io---- --system-- ----cpu----
r b swpd free buff cache si so bi bo in cs us sy id wa
0 0 0 203668 53352 2878928 0 0 0 2 4 6 0 0 100 0
procs:
r ;在运行队列中等待的进程数
b ;在等待io的进程数
w ;可以进入运行队列但被替换的进程
memoy
swap ;现时可用的交换内存(k表示)
free ;空闲的内存(k表示)
pages
re 回收的页面
mf 非严重错误的页面
pi 进入页面数(k表示)
po 出页面数(k表示)
fr 空余的页面数(k表示)
de 提前读入的页面中的未命中数
sr 通过时钟算法扫描的页面
disk 显示每秒的磁盘操作。 s表示scsi盘,0表示盘号
fault 显示每秒的中断数
in 设备中断
sy 系统中断
cy cpu交换
cpu 表示cpu的使用状态
cs 用户进程使用的时间
sy 系统进程使用的时间
id cpu空闲的时间
$ vmstat 1
procs -----------memory---------- ---swap-- -----io---- -system-- ----cpu----
r b swpd free buff cache si so bi bo in cs us sy id wa
2 0 0 2692472 347884 442576 0 0 0 54 11 7 99 1 0 0
2 0 0 2692420 347884 442600 0 0 0 0 6 87 100 0 0 0
2 1 0 2692320 347884 442600 0 0 0 2568 26 121 100 0 0 0
2 0 0 2687872 347884 442600 0 0 0 72 28 129 100 1 0 0
2 0 0 2684716 347884 442600 0 0 0 0 16 91 100 0 0 0
2 0 0 2680528 347884 442600 0 0 0 0 12 88 100 1 0 0
vmstat 参数详解
procs:
r-->在运行队列中等待的进程数
b-->在等待io的进程数
w-->可以进入运行队列但被替换的进程
memoy
swap-->现时可用的交换内存(k表示)
free-->空闲的内存(k表示)
pages
re--》回收的页面
m
版权归原作者 netkiller- 所有, 如有侵权,请联系我们删除。