MySQL 连接池性能优化实战
MySQL 连接池性能优化实战
学习核心
学习资料
访问MySQL可以理解为最基础的一个MySQL连接池的应用场景。例如访问一个电商数据库生成秒杀订单、执行任务插入等一系列场景,因此连接池优化的这个切入点理论上是可以融入几乎所有后端业务场景的。
连接池实战(任务插入场景)
1.数据准备
数据构建
# 构建数据用于druid测试
CREATE TABLE `t_task_test_druid`
(
`id` int(11) NOT NULL AUTO_INCREMENT,
`task_id` varchar(256) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
`create_time` datetime,
`update_time` datetime,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_task_id` (`task_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_unicode_ci;
# 检索数据表
select * from t_task_test_druid;
2.项目搭建
项目配置(pom.xml、application.yml)
pom.xml:引入数据库连接相关配置(mysql连接、mybatis-plus框架、druid数据库连接池框架)
<!-- 引入数据库相关依赖(MySQL连接、MyBatis配置) -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- mybatis -->
<!--
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
-->
<!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-boot-starter -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
<!-- 引入druid相关依赖 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.15</version>
</dependency>
application.yml:项目核心配置
# 应用服务 WEB 访问端口
server:
port: 8080
servlet:
context-path: /api
# spring相关配置
spring:
profiles:
active: dev
redis:
host: localhost
port: 6379
# 数据库配置
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/db_asyncflow # db_mysql_base
username: root
password: 123456
# druid 数据库连接池配置
type: com.alibaba.druid.pool.DruidDataSource
druid:
min-idle: 1 # 最小连接数
max-active: 8 # 最大连接数(默认8)
max-wait: 1000 # 获取连接时的最大等待时间
min-evictable-idle-time-millis: 300000 # 一个连接在池中最小生存的时间,单位是毫秒
time-between-eviction-runs-millis: 60000 # 多久才进行一次检测需要关闭的空闲连接,单位是毫秒
# mybatis-plus 配置
mybatis-plus:
configuration:
map-underscore-to-camel-case: false
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
logic-delete-field: isDelete # 全局逻辑删除的实体字段名
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
model层
@TableName("t_task_test_druid")
@Data
public class Task {
@TableId(type = IdType.AUTO)
private String id;
@TableField("task_id")
private String taskId;
@TableField("create_time")
private Date createTime;
@TableField("update_time")
private Date updateTime;
}
mapper层
@Mapper
public interface TaskMapper extends BaseMapper<Task> {
}
service层
public interface TaskService extends IService<Task> {
}
@Service
public class TaskServiceImpl extends ServiceImpl<TaskMapper, Task> implements TaskService {
}
controller层
此处controller层可以简单构建返回操作结果,正常项目构建需要设定项目自定义响应数据格式
package com.noob.base.druid.controller;
import com.alibaba.fastjson.JSONObject;
import com.noob.base.druid.model.entity.Task;
import com.noob.base.druid.service.TaskService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Date;
import java.util.List;
import java.util.UUID;
@RestController
@RequestMapping("/task")
public class TaskController {
@Autowired
private TaskService taskService;
// 新增操作
@RequestMapping("/create")
public String create() {
Task task = new Task();
task.setTaskId(UUID.randomUUID().toString().replaceAll("-", ""));
Date now = new Date();
task.setCreateTime(now);
task.setUpdateTime(now);
boolean res = taskService.save(task);
if(res){
return "ok";
}
return "fail";
}
// 检索所有task
@GetMapping("/selectAll")
public String selectAll() {
List<Task> taskList = taskService.list();
return JSONObject.toJSONString(taskList);
}
}
接口测试
访问URL(检索所有任务信息:http://localhost:8080/api/task/selectAll)
访问URL(新建任务:http://localhost:8080/api/task/create),测试数据是否正常插入。随后可通过CURL指令进行数据插入性能测试:
curl -H "Trace-ID:holic-x" -H "Content-Type:application/json;charset=utf-8" -H "User-ID:888" http://localhost:8080/api/task/create -d '{"requestData":"xxx"}'
# output
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7707202f] was not registered for synchronization because synchronization is not active
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@6305303e] will not be managed by Spring
==> Preparing: INSERT INTO t_task_test_druid ( task_id, create_time, update_time ) VALUES ( ?, ?, ? )
==> Parameters: 91b898150e05490e9b887a522e9eee11(String), 2024-08-05 09:57:56.526(Timestamp), 2024-08-05 09:57:56.526(Timestamp)
<== Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7707202f]
3.压力测试
wrk-轻量级测试工具
macos 安装配置wrk
# 检索wrk版本信息
brew search wrk
# 使用homebrew安装配置wrk
brew install wrk
# 安装验证(output:wrk 4.2.0 [kqueue] Copyright (C) 2012 Will Glozer)
wrk -v
wrk 使用
# 访问百度测试(命令说明:使用8个线程、200个并发、持续10s 压测百度连接)
wrk -t8 -c200 -d10s --latency "http://www.baidu.com"
# output
wrk -t8 -c200 -d10s --latency "http://www.baidu.com"
Running 10s test @ http://www.baidu.com
# part 01
8 threads and 200 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 294.17ms 381.59ms 1.95s 81.02%
Req/Sec 64.48 35.85 260.00 76.64%
# part 02
Latency Distribution
50% 83.33ms
75% 312.75ms
90% 746.66ms
99% 1.59s
# part 03
5141 requests in 10.07s, 52.66MB read
Socket errors: connect 0, read 5, write 0, timeout 111
Requests/sec: 510.47
Transfer/sec: 5.23MB
响应参数解读(参考上述结果示例,拆分3部分进行解读)
part 01
- 表参数解读:Avg(平均值)、Max(最大值)、Stdev(标准差:表示统计数据的离散程度,标准差太大表示响应波动比较大)
- Latency(延迟):Avg(平均延迟)=》294.17ms、Max(最大延迟)=》1.95s
- 可以看出响应时间有不少波动,属于正常现象,一般最大延迟在1s内还属于可接受的范畴,可结合响应参数思考优化方案
- Req/Sec:Avg(平均值)=》64.48/s、Max(最大值)=》260/s、Stdev(标准差)
- 表参数解读:Avg(平均值)、Max(最大值)、Stdev(标准差:表示统计数据的离散程度,标准差太大表示响应波动比较大)
part 02(不同分位延迟数据)
- Latency Distribution:不同分位的延迟(可理解为按响应时间从小到大排序,排在某个百分比位置的数据):比如一共1000个请求,那么响应时间排名第990个请求的响应时间就是99%分位数值,对应到上述结果即99%的请求都在1.59s以内,只有1%的请求是在1.59以上。这些数值其实相对于平均值可以提供更多参考
part 03(总结性数据)
- 表示10s内发送了5141个请求,错误了0个(一般如果出现错误数errors都要进行重测,否则再快也没用)
- Requests/sec:510.47(QPS为510.47/s):此处对比part 01部分的Req/Sec(Avg),此处是其8倍左右(正好是开启线程的数量),因此part01部分的Req/Sec是单个线程的数据
任务插入场景压力测试(wrk)
(1)压力测试(MySQL-5.7.44)
配置说明
- MySQL:5.7.44
- MacOS:Apple M1 Pro、32GB
- 需注意每次压测要清空已有数据,尽量保证控制变量进行对比,确保数据的准确性
druid:
min-idle: 1 # 最小连接数
max-active: 8 # 最大连接数(默认8)
max-wait: 1000 # 获取连接时的最大等待时间
min-evictable-idle-time-millis: 300000 # 一个连接在池中最小生存的时间,单位是毫秒
time-between-eviction-runs-millis: 60000 # 多久才进行一次检测需要关闭的空闲连接,单位是毫秒
压测命令
使用轻量级压力测试工具wrk进行测试(测试参数:8个线程、50个连接、持续10s)
# wrk 性能压测:8个线程、50个并发、持续10s =》压测任务创建接口
wrk -t8 -c50 -d10s --latency "http://localhost:8080/api/task/create"
# 脚本测试
wrk -t8 -c50 -d10s --script=create_task.lua --latency "http://localhost:8080/api/task/create"
压测参数配置
application.yml:配置durid的max-active
参数:1、8、50、100、500、2000,通过上述wrk指令进行压测
# output:max-active = 1
Running 10s test @ http://localhost:8080/api/task/create
8 threads and 50 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 282.80ms 34.44ms 585.30ms 91.87%
Req/Sec 21.24 8.90 50.00 70.01%
Latency Distribution
50% 275.26ms
75% 284.22ms
90% 306.18ms
99% 481.92ms
1686 requests in 10.09s, 189.35KB read
Requests/sec: 167.03
Transfer/sec: 18.76KB
# output:max-active = 8
Running 10s test @ http://localhost:8080/api/task/create
8 threads and 50 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 112.09ms 28.51ms 448.57ms 95.63%
Req/Sec 54.88 9.53 80.00 83.61%
Latency Distribution
50% 108.10ms
75% 114.37ms
90% 121.15ms
99% 285.15ms
4332 requests in 10.08s, 486.50KB read
Requests/sec: 429.55
Transfer/sec: 48.24KB
# output:max-active = 50
Running 10s test @ http://localhost:8080/api/task/create
8 threads and 50 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 23.97ms 35.90ms 402.55ms 96.94%
Req/Sec 325.92 65.75 415.00 84.10%
Latency Distribution
50% 17.39ms
75% 19.86ms
90% 25.18ms
99% 254.06ms
25428 requests in 10.05s, 2.79MB read
Requests/sec: 2531.32
Transfer/sec: 284.72KB
# output:max-active = 100
8 threads and 50 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 23.33ms 30.35ms 359.51ms 97.07%
Req/Sec 317.45 59.92 425.00 84.40%
Latency Distribution
50% 17.85ms
75% 20.45ms
90% 25.46ms
99% 215.90ms
24839 requests in 10.05s, 2.73MB read
Requests/sec: 2470.57
Transfer/sec: 277.90KB
# output:max-active = 500
8 threads and 50 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 23.38ms 31.61ms 376.53ms 96.81%
Req/Sec 322.27 60.30 424.00 83.72%
Latency Distribution
50% 17.78ms
75% 19.97ms
90% 25.07ms
99% 225.12ms
25161 requests in 10.06s, 2.76MB read
Requests/sec: 2501.97
Transfer/sec: 281.43KB
# output:max-active = 2000
Running 10s test @ http://localhost:8080/api/task/create
8 threads and 50 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 23.79ms 33.58ms 386.14ms 97.03%
Req/Sec 320.02 62.65 420.00 84.67%
Latency Distribution
50% 17.68ms
75% 20.29ms
90% 25.34ms
99% 238.68ms
25054 requests in 10.05s, 2.75MB read
Requests/sec: 2493.78
Transfer/sec: 280.51KB
压测参数 | 连接池配置 | 测试结果(MySQL5.7.44) |
---|---|---|
8 线程、50并发、持续10s | 1 | QPS:167/s |
8 线程、50并发、持续10s | 8 | QPS:429/s |
8 线程、50并发、持续10s | 50 | QPS:2531/s |
8 线程、50并发、持续10s | 100 | QPS:2470/s |
8 线程、50并发、持续10s | 500 | QPS:2501/s |
8 线程、50并发、持续10s | 2000 | QPS:2493/s |
可以看到在同等压测条件下,连接池配置在100以内适当增加连接池个数可以提升QPS,但是当在100以上连接池放大的收益反而不高了。因此要结合实际案例场景调整连接池配置,以尽可能最大优化性能
(2)问题扩展
问题扩展:如果连接池不够用怎么办?
一个连接到达之前,如果发现druid连接池满了,则会等待连接释放(等待一段时间,超出最长等待时间则会抛出异常)。而对于GORM框架而言则会打开短连接。此处测试druid连接池不够用的场景(可以将参数调小:例如min-active调为1、max-wait调为10ms,进行压测,查看结果)
output: - druid 连接池不够用,抛出异常
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1584caab] was not registered for synchronization because synchronization is not active
2024-08-05 11:11:23.449 ERROR 16070 --- [io-8080-exec-10] o.a.c.c.C.[.[.[.[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [/api] threw exception [Request processing failed; nested exception is org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.PersistenceException:
### Error updating database. Cause: org.springframework.jdbc.CannotGetJdbcConnectionException: Failed to obtain JDBC Connection; nested exception is com.alibaba.druid.pool.GetConnectionTimeoutException: wait millis 10, active 1, maxActive 1, creating 0
### The error may exist in com/noob/base/druid/mapper/TaskMapper.java (best guess)
### The error may involve com.noob.base.druid.mapper.TaskMapper.insert
### The error occurred while executing an update
### Cause: org.springframework.jdbc.CannotGetJdbcConnectionException: Failed to obtain JDBC Connection; nested exception is com.alibaba.druid.pool.GetConnectionTimeoutException: wait millis 10, active 1, maxActive 1, creating 0] with root cause
com.alibaba.druid.pool.GetConnectionTimeoutException: wait millis 10, active 1, maxActive 1, creating 0
at com.alibaba.druid.pool.DruidDataSource.getConnectionInternal(DruidDataSource.java:1807) ~[druid-1.2.15.jar:na]
at com.alibaba.druid.pool.DruidDataSource.getConnectionDirect(DruidDataSource.java:1459) ~[druid-1.2.15.jar:na]
at com.alibaba.druid.pool.DruidDataSource.getConnection(DruidDataSource.java:1439) ~[druid-1.2.15.jar:na]
对于druid框架而言,如果连接到来时发现druid连接池已满,则会等待连接释放(如果超出最大等待时间,则抛出异常)
对于GORM框架而言,如果连接池不够用了则会开启短连接,用完就关掉,直到达到最大连接数,如果最大连接数比较大,就会开很多很多短连接,最终导致过多的TIME_WAIT,报错是端口不能打开(因为短连接随机占用了端口,如果短连接过多未得到及时释放,就会报错),要测试到这个现象需要把压测时间调长一点(例如:参数:连接池大小调成1,wrk压测时间调到120s
)异常现象:can't assign requested address
问题扩展:为什么同样的压测参数,QPS差异这么大?
一开始以为是mysql最大连接数配置问题,后面通过测试发现性能提高并不大(因为业务场景并不太复杂,占用连接的时长并不多,所以就算提高max_connections对性能提升并不大)
# 查看mysql默认的最大连接数
show variables like 'max_connections'; -- MySQL5.7.44:默认为151
# 修改max_connections(上限为16384)
set global max_connections = 256;
# 重启mysql服务后确认修改是否成功,然后进行压测
但如果max_connections设置得太小,会。MySQL里的max_connections参数代表mysql数据库的最大连接数,参数默认是151,显然不适用于生产,如果请求大于默认连接数,就会出现无法连接数据库的错误,会遇到too many connections的报错信息。max connections需要设置的一个合理的值,并需要做好监控,避免连接数满引发业务故障(生产环境建议5000-10000左右,没有固定的值,根据各自业务情况来定)。另外如果数据库连接数过高,需要分析业务端是否存在大量刷MySQL连接的情况,比如大量的短连接,连接没有正常关闭,代码逻辑问题等
java.sql.SQLNonTransientConnectionException: Data source rejected establishment of connection, message from server: "Too many connections"
at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:110) ~[mysql-connector-j-8.0.31.jar:8.0.31]
at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122) ~[mysql-connector-j-8.0.31.jar:8.0.31]
at com.mysql.cj.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:828) ~[mysql-connector-j-8.0.31.jar:8.0.31]
一般来说要设置合适的mysql的max_connections和druid的数据库连接池最大连接数,尽量避免因连接数不足影响正常的业务请求,因为它不仅会影响性能,还会直接报错爆出异常影响业务进行,尽量不要让其成为性能瓶颈,结合实际生产场景分析:
- 设置太小:数据库连接成为性能瓶颈,会影响正常业务的执行
- 设置太大:数据库连接占用比较大的内存资源,但实际上可能性能没有多大提升,就会造成资源浪费,甚至可能因为内存不足导致OOM
在这个过程中发现macos中的mysql@5.7版本过期,升级到mysql9.0.1版本,然后重置相关的内容(max_connections=151),用同样的压测配置进行测试,查看每个配置下的响应结果如下:
# output:max-active = 1
Running 10s test @ http://localhost:8080/api/task/create
8 threads and 50 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 27.24ms 27.00ms 334.65ms 97.24%
Req/Sec 254.81 49.91 363.00 83.04%
Latency Distribution
50% 22.55ms
75% 26.17ms
90% 28.83ms
99% 199.45ms
19999 requests in 10.06s, 2.20MB read
Requests/sec: 1988.46
Transfer/sec: 223.67KB
# output:max-active = 8
Running 10s test @ http://localhost:8080/api/task/create
8 threads and 50 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 9.15ms 25.18ms 293.52ms 97.63%
Req/Sec 1.08k 166.05 1.28k 91.45%
Latency Distribution
50% 5.26ms
75% 5.80ms
90% 7.45ms
99% 171.63ms
84380 requests in 10.03s, 9.27MB read
Requests/sec: 8413.15
Transfer/sec: 0.92MB
# output:max-active = 50
unning 10s test @ http://localhost:8080/api/task/create
8 threads and 50 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 7.63ms 24.10ms 305.35ms 97.74%
Req/Sec 1.41k 239.49 1.73k 89.29%
Latency Distribution
50% 4.02ms
75% 4.84ms
90% 6.20ms
99% 162.59ms
110260 requests in 10.02s, 12.11MB read
Requests/sec: 11003.63
Transfer/sec: 1.21MB
# output:max-active = 100
Running 10s test @ http://localhost:8080/api/task/create
8 threads and 50 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 8.78ms 29.81ms 329.69ms 97.49%
Req/Sec 1.38k 238.49 1.70k 90.04%
Latency Distribution
50% 4.11ms
75% 4.96ms
90% 6.42ms
99% 203.99ms
107474 requests in 10.02s, 11.81MB read
Requests/sec: 10723.79
Transfer/sec: 1.18MB
# output:max-active = 500
Running 10s test @ http://localhost:8080/api/task/create
8 threads and 50 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 4.34ms 2.55ms 57.69ms 94.73%
Req/Sec 1.43k 160.11 1.99k 82.38%
Latency Distribution
50% 3.98ms
75% 4.74ms
90% 5.73ms
99% 11.38ms
114255 requests in 10.02s, 12.55MB read
Requests/sec: 11403.18
Transfer/sec: 1.25MB
# output:max-active = 2000
Running 10s test @ http://localhost:8080/api/task/create
8 threads and 50 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 8.06ms 26.79ms 306.94ms 97.64%
Req/Sec 1.42k 234.81 1.76k 90.43%
Latency Distribution
50% 4.01ms
75% 4.83ms
90% 6.10ms
99% 182.72ms
111114 requests in 10.02s, 12.21MB read
Requests/sec: 11093.14
Transfer/sec: 1.22MB
压测参数 | 连接池配置 | 测试结果(MySQL-9.0.1) |
---|---|---|
8 线程、50并发、持续10s | 1 | QPS:1988/s |
8 线程、50并发、持续10s | 8 | QPS:8413/s |
8 线程、50并发、持续10s | 50 | QPS:11003/s |
8 线程、50并发、持续10s | 100 | QPS:10723/s |
8 线程、50并发、持续10s | 500 | QPS:11403/s |
8 线程、50并发、持续10s | 2000 | QPS:11093/s |
对于数据库连接池性能优化场景,可以结合实际业务场景说明性能提升的问题,虽然可能实际业务量并没有达到太大,但在测试环境能抗住高并发性能测试,结合数据说明性能提升体现即可。虽然现阶段业务量不太多,但也为后续业务扩增做相应的应对方案,考虑投入和产出比去说明。
对于请求响应的影响因素有很多,例如服务器资源配置、网络连接状况(如果是连接远程数据库的话)、数据库连接池配置(并发处理能力)、MySQL版本(并发处理能力)、MySQL的最大连接数配置等
- 服务器资源配置:服务器资源配置
- 网络连接状况:如果是连接远程数据库需考虑网络资源或者需要接口调用场景等都需要考虑网络
- 数据库连接池配置:druid框架参数配置(最大连接数、空闲时间 核心参数)
- MySQL版本:MySQL版本差异对并发处理能力的支持度不同
- MySQL的最大连接数配置:max_connections是MySQL服务端的参数配置,表示最多能开多少个连接,超过这个连接数就会抛出too many connections异常,对于实际业务场景这个值一般会设置得很大,尽量避免其因连接数不足而导致业务异常(即尽量不要让数据库连接成为性能瓶颈)
测试会受各种环境影响,所以测试数据其实没有一个标准的,只要数字不太离谱、亲自实践过心中有底即可,比如MySQL 比较常见的就是 2000-6000。参考MySQL5.6、macbook M2(1:600、8:2500、100:6442、2000:6554)
连接池扩展问题
1.不同的数据库连接池问题?为什么客户端的端口出现TIME_WAIT可以定位到MySQL的连接池?(GoLang数据库连接池??druid不会有,因为其不会创建短连接?)
todo:和mysql建立连接是使用tcp连接,所以tcp的状态适用。timewait是主动方断开链接的状态,大量timewait说明大量的主动方断开连接,从而联想到使用了大量tcp短连接。连接池是维护tcp长连接从而使sql查询可以共用连接不必新开连接,那么出现大量tcp短连接说明这个机制貌似失效了,从而推导出连接池的配置有问题
至于怎么发现timewait,可以使用netstat命令 看mysql的3306端口