从零搭建 MySQL 主从复制 + 分库分表 + ShardingSphere Proxy 的完整步骤。
11 台 VM (初始搭建只需前 7 台):
| 节点 | IP | 角色 | 规格 |
|---|---|---|---|
| node1 | 192.168.0.226 | MySQL Master | 4C/8G |
| node2 | 192.168.0.225 | MySQL Slave1 | 4C/8G |
| node3 | 192.168.0.224 | MySQL Slave2 | 4C/8G |
| node4 | 192.168.0.220 | MySQL Shard1 | 4C/8G |
| node5 | 192.168.0.219 | MySQL Shard2 | 4C/8G |
| node6 | 192.168.0.218 | ShardingSphere Proxy | 16C/8G |
| node7 | 192.168.0.217 | Application (App1) | 4C/8G |
| node8 | 192.168.0.215 | Application (App2) | 4C/8G |
| node9 | 192.168.0.216 | Application (App3) | 4C/8G |
| node10 | 192.168.0.207 | Nginx LB | 4C |
| node11 | 192.168.0.206 | (备用) | - |
架构图:
node7/8/9
Application ×3
│
│
ShardingSphere Proxy
node6 (.218:3307)
↓ demo_db
│
┌──────────────┼──────────────┐
│ │ │
Master Shard1 Shard2
node1 node4 node5
.226 → testdb .220 → shard1 .219 → shard2
│
┌────┴────┐
│ │
Slave1 Slave2
node2 node3
.225 .224
数据分布:
user表 → Master 写, Slave1/Slave2 读 (读写分离)orders表 → 按user_id分片到 Shard1/Shard2, 每个 shard 2 张表 (orders_0,orders_1)
node1 ~ node5 都执行:
sudo apt update
sudo apt install -y mysql-server
# 允许远程连接
sudo sed -i 's/^bind-address.*/bind-address = 0.0.0.0/' /etc/mysql/mysql.conf.d/mysqld.cnf
sudo systemctl restart mysql
编辑 /etc/mysql/mysql.conf.d/mysqld.cnf, 添加:
server-id = 1
log_bin = /var/log/mysql/mysql-bin.log
binlog_format = row
sudo systemctl restart mysql
创建复制用户:
CREATE USER 'repl'@'%' IDENTIFIED WITH mysql_native_password BY 'Password';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';
FLUSH PRIVILEGES;
查看 binlog 位置 (记录 File 和 Position, 后面 Slave 要用):
SHOW MASTER STATUS;
+------------------+----------+
| File | Position |
+------------------+----------+
| mysql-bin.000001 | 856 |
+------------------+----------+
编辑 /etc/mysql/mysql.conf.d/mysqld.cnf:
# node2
server-id = 2
# node3
server-id = 3
sudo systemctl restart mysql
配置复制 (将 MASTER_LOG_FILE 和 MASTER_LOG_POS 替换为上一步记录的值):
CHANGE MASTER TO
MASTER_HOST='192.168.0.226',
MASTER_USER='repl',
MASTER_PASSWORD='Password',
MASTER_LOG_FILE='mysql-bin.000001',
MASTER_LOG_POS=856;
START SLAVE;
验证:
SHOW SLAVE STATUS\G
看到以下两行都为 Yes 说明成功:
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
在 Master (node1):
CREATE DATABASE testdb;
USE testdb;
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
name VARCHAR(50)
);
在 Slave (node2, node3) 验证:
SHOW DATABASES; -- 应能看到 testdb
CREATE DATABASE shard1;
CREATE DATABASE shard2;
注: 不需要手动建表, ShardingSphere Proxy 会通过 DDL 自动在各 shard 上创建分片表。
在 node1、node4、node5 上都执行:
CREATE USER 'shardingsphere'@'%' IDENTIFIED WITH mysql_native_password BY 'Password';
GRANT ALL PRIVILEGES ON *.* TO 'shardingsphere'@'%';
FLUSH PRIVILEGES;
在 node2、node3 上不需要手动创建 — 该用户会通过主从复制自动同步。
curl -s https://raw.githubusercontent.com/power721/repo/refs/heads/master/jdk21.sh | sh
wget https://archive.apache.org/dist/shardingsphere/5.5.3/apache-shardingsphere-5.5.3-shardingsphere-proxy-bin.tar.gz
tar xf apache-shardingsphere-5.5.3-shardingsphere-proxy-bin.tar.gz
ln -sf apache-shardingsphere-5.5.3-shardingsphere-proxy-bin shardingsphere-proxy
cd shardingsphere-proxy
wget -P lib/ https://repo1.maven.org/maven2/com/mysql/mysql-connector-j/9.6.0/mysql-connector-j-9.6.0.jar
5.5.3 自带的 Seata 和 Hive 相关 jar 会因缺少传递依赖导致 ClassNotFoundException, 必须删除:
rm -f lib/shardingsphere-transaction-base-seata-at-5.5.3.jar
rm -f lib/shardingsphere-database-connector-hive-5.5.3.jar
rm -f lib/shardingsphere-parser-sql-engine-hive-5.5.3.jar
rm -f lib/shardingsphere-parser-sql-statement-hive-5.5.3.jar
rm -f lib/shardingsphere-proxy-dialect-hive-5.5.3.jar
mode:
type: Standalone
authority:
users:
- user: root@%
password: root
privilege:
type: ALL_PERMITTED
props:
proxy-frontend-executor-size: 0 # 自动检测 CPU 核数
max-connections-size-per-query: 1
sql-show: true # 调试时开启, 生产关闭
check-table-metadata-enabled: false
proxy-frontend-flush-threshold: 128
proxy-netty-backlog: 1024
proxy-default-port: 3307
databaseName: demo_db
dataSources:
master_ds:
url: jdbc:mysql://192.168.0.226:3306/testdb?allowPublicKeyRetrieval=true&useSSL=false
username: shardingsphere
password: Password
slave1:
url: jdbc:mysql://192.168.0.225:3306/testdb?allowPublicKeyRetrieval=true&useSSL=false
username: shardingsphere
password: Password
slave2:
url: jdbc:mysql://192.168.0.224:3306/testdb?allowPublicKeyRetrieval=true&useSSL=false
username: shardingsphere
password: Password
shard1:
url: jdbc:mysql://192.168.0.220:3306/shard1?allowPublicKeyRetrieval=true&useSSL=false
username: shardingsphere
password: Password
shard2:
url: jdbc:mysql://192.168.0.219:3306/shard2?allowPublicKeyRetrieval=true&useSSL=false
username: shardingsphere
password: Password
rules:
# 1. 分片规则 — orders 表按 user_id 分到 shard1/shard2
- !SHARDING
tables:
orders:
actualDataNodes: shard${1..2}.orders_${0..1}
keyGenerateStrategy:
column: id
keyGeneratorName: snowflake
databaseStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: db_by_user_id
tableStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: table_by_user_id
shardingAlgorithms:
db_by_user_id:
type: INLINE
props:
# user_id % 2 + 1 → shard1 或 shard2
algorithm-expression: shard${user_id % 2 + 1}
table_by_user_id:
type: INLINE
props:
# user_id / 2 % 2 → orders_0 或 orders_1
algorithm-expression: orders_${user_id.longValue().intdiv(2) % 2}
keyGenerators:
snowflake:
type: SNOWFLAKE
# 2. 读写分离 — user 表及其他未分片表走这里
- !READWRITE_SPLITTING
dataSourceGroups:
master_slave:
writeDataSourceName: master_ds
readDataSourceNames:
- slave1
- slave2
loadBalancerName: round_robin
loadBalancers:
round_robin:
type: ROUND_ROBIN
# 3. 单表规则 — 未分片表默认路由到读写分离组
- !SINGLE
tables:
- "*.*"
defaultDataSource: master_slave
分片逻辑说明:
- 库级分片:
user_id % 2 + 1→ 奇数 id 去 shard2, 偶数 id 去 shard1 - 表级分片:
user_id / 2 % 2→ 进一步拆分到orders_0或orders_1 - 读写分离:
user等不分片的表, 写走 master_ds, 读轮询 slave1/slave2
cd shardingsphere-proxy
bin/start.sh
默认端口 3307, 连接:
mysql -h 192.168.0.218 -P 3307 -u root -proot
通过 Proxy 连接后执行:
USE demo_db;
-- user 表 (不分片, 走读写分离)
CREATE TABLE user (
id BIGINT PRIMARY KEY,
name VARCHAR(50),
age INT
);
-- orders 表 (分片, 自动分到 shard1/shard2)
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
user_id BIGINT,
amount DECIMAL(10,2),
created_at DATETIME
);
验证物理表:
-- node4 (shard1)
USE shard1;
SHOW TABLES;
-- 应看到: orders_0, orders_1
-- node5 (shard2)
USE shard2;
SHOW TABLES;
-- 应看到: orders_0, orders_1
-- node1 (master)
USE testdb;
SHOW TABLES;
-- 应看到: user
通过 Proxy 插入:
USE demo_db;
INSERT INTO user(id, name, age) VALUES (1, 'Alice', 20);
写入走 Master (node1), 查询走 Slave (node2/node3):
SELECT * FROM user; -- 路由到 slave1 或 slave2
开启 sql-show: true 后可在 Proxy 日志中看到路由目标。
USE demo_db;
INSERT INTO orders(id, user_id, amount) VALUES (1, 1, 100.00); -- → shard2
INSERT INTO orders(id, user_id, amount) VALUES (2, 2, 200.00); -- → shard1
INSERT INTO orders(id, user_id, amount) VALUES (3, 3, 300.00); -- → shard2
INSERT INTO orders(id, user_id, amount) VALUES (4, 4, 400.00); -- → shard1
通过 Proxy 查询会自动聚合所有分片结果:
SELECT * FROM orders; -- 返回全部 4 条
Locust (10 workers)
│
▼
┌─────────────────┐
│ Nginx LB │
│ 10.198.27.207 │
│ HTTP:80 │
└───────┬─────────┘
│
┌─────────────┼─────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ App 1 │ │ App 2 │ │ App 3 │
│ .217:8080│ │ .215:8080│ │ .216:8080│
│ 4C/8G │ │ 4C/8G │ │ 4C/8G │
└─────┬────┘ └────┬────┘ └────┬────┘
└────────────┼───────────┘
│
┌──────────────────┐
│ ShardingSphere │
│ Proxy .218:3307│
│ 16C/8G │
└───────┬─────────┘
│
┌──────────────┼──────────────┐
│ │ │
┌─────┴─────┐ ┌─────┴─────┐ ┌────┴──────┐
│ Master │ │ Shard1 │ │ Shard2 │
│ .226:3306 │ │ .220:3306 │ │ .219:3306 │
│ testdb │ │ shard1 │ │ shard2 │
│ 4C/8G │ │ 4C/8G │ │ 4C/8G │
└─────┬─────┘ └───────────┘ └───────────┘
repl │
┌─────┴──────────────┐
│ │
┌───┴─────┐ ┌────┴────┐
│ Slave1 │ │ Slave2 │
│ .225 │ │ .224 │
│ 4C/8G │ │ 4C/8G │
└─────────┘ └─────────┘
数据分布:
user表 → Master (写) + Slave1/Slave2 (读), 读写分离orders表 → 按user_id分片到 Shard1/Shard2, 每个 shard 2 张表 (orders_0,orders_1)
| 阶段 | 主要优化 | RPS | 提升 |
|---|---|---|---|
| 初始 | 默认配置,单 App 节点 | ~1,100 | 基线 |
| 第一轮 | 多 App 节点 + Nginx LB + 连接池调优 | ~1,100 | 无提升 (瓶颈不在 App) |
| 第二轮 | ShardingSphere 线程/连接池 + Locust 加压 + JVM 调优 | ~2,000 | +82% |
| 第三轮 | Page→Slice (去 COUNT) + FastHttpUser + 减 think time | ~2,500 | +25% |
| 第四轮 | JPA/Hibernate → JdbcTemplate (去 ORM 开销) | ~3,256 | +30% |
| 第五轮 | MySQL 内核参数调优 | ~5,600 | +72% |
| 第六轮 | ShardingSphere Proxy 扩 CPU 8C→16C | ~8,400 | +50% |
| 第七轮 | 游标分页 + 复合索引 + 消除跨分片查询 | ~9,000 | +7% |
| 第八轮 | Nginx TCP 调优 (TIME_WAIT 端口耗尽) | ~9,000 | 消除瓶颈 |
| 第九轮 | Locust 加压: 6000 用户 + 缩短 wait time | ~16,000 | +78% |
| 第十轮 | 双 SS Proxy (已回滚) | ~12,800 | 无提升, 瓶颈在 App 层 |
- 单 App 节点无法利用多机资源
- HikariCP 默认连接池太小
- 无健康检查
- 3 台 App 节点 + Nginx 负载均衡
- HikariCP 连接池:
max=80,min-idle=20,connection-timeout=10s - Tomcat 线程池:
max-threads=400,accept-count=200 - 健康检查端点:
GET /health
spring.datasource.hikari.maximum-pool-size=80
spring.datasource.hikari.minimum-idle=20
spring.datasource.hikari.connection-timeout=10000
server.tomcat.threads.max=400
server.tomcat.accept-count=200
RPS 未提升,说明瓶颈不在 App 层。
- ShardingSphere Proxy 默认配置未调优
- Locust 并发用户数不足,未充分加压
- JVM 堆内存偏小
-
ShardingSphere Proxy (
server.yaml):proxy-frontend-flush-threshold: 128(减少 Netty flush 次数)proxy-netty-backlog: 1024(突发连接队列)- 后端连接池: 每个 datasource
maxPoolSize=60,minPoolSize=20 - PreparedStatement 缓存:
cachePrepStmts=true,prepStmtCacheSize=250
-
Locust 加压:
- 用户数 1000 → 3000, spawn-rate 50 → 100
- 5 个 Worker 节点, 每个 600 用户
-
JVM:
-Xms1g -Xmx2g -XX:+UseG1GC -XX:MaxGCPauseMillis=50 -
Nginx:
keepalive 256,access_log off
maxPoolSize: 60
minPoolSize: 20
connectionTimeoutMilliseconds: 30000
# JDBC URL 参数
cachePrepStmts=true&prepStmtCacheSize=250&prepStmtCacheSqlLimit=2048&useServerPrepStmts=true
RPS ~1,100 → ~2,000
- 分页查询用
Page<T>每次都执行SELECT COUNT(*), 跨分片时代价极高 - Locust 使用
HttpUser(基于 requests 库), Python GIL 限制吞吐 - think time 过大, 实际并发请求数不足
Page<T>→Slice<T>: 消除所有COUNT(*)查询HttpUser→FastHttpUser: 基于 geventhttpclient, 性能 ~5x- 减少 think time: ReadHeavy 0.1-0.5s, WriteHeavy 0.2-0.8s
RPS ~2,000 → ~2,500
- Hibernate/JPA 的对象映射、脏检查、一级缓存在高并发下开销显著
- 每次 SQL 都经过 Hibernate Session → Proxy → MySQL, 中间层越多延迟越高
- JPA → JdbcTemplate: 所有 CRUD 操作直接用 SQL
- 删除
spring-boot-starter-data-jpa, 改用spring-boot-starter-jdbc - 创建
UserJdbcRepository/OrderJdbcRepository, 用JdbcTemplate+RowMapper - Entity 从 JPA 注解实体简化为纯 POJO
- 删除
- Snowflake ID 生成: 从 Hibernate IdentifierGenerator 改为 Spring
@Bean
@Repository
public class OrderJdbcRepository {
private final JdbcTemplate jdbc;
public Order save(Order order) {
jdbc.update("INSERT INTO orders (id, user_id, product, quantity, price, status, ...) VALUES (?,?,?,?,?,?,...)",
order.getId(), order.getUserId(), order.getProduct(), ...);
return order;
}
public Slice<Order> findAll(Pageable pageable) {
List<Order> list = jdbc.query(
"SELECT * FROM orders ORDER BY id DESC LIMIT ? OFFSET ?",
ORDER_ROW_MAPPER, pageable.getPageSize() + 1, pageable.getOffset());
boolean hasNext = list.size() > pageable.getPageSize();
if (hasNext) list = list.subList(0, pageable.getPageSize());
return new SliceImpl<>(list, pageable, hasNext);
}
}
RPS ~2,500 → ~3,256
MySQL 全部使用默认参数, 在 HDD 上严重低效:
innodb_buffer_pool_size = 128MB(8G 内存机器只用了 1.5%)innodb_flush_log_at_trx_commit = 1(每次 commit fsync, HDD 只能 ~200 TPS)sync_binlog = 1(每次 binlog fsync)innodb_flush_method = fsync(OS page cache 双重缓冲)innodb_redo_log_capacity = 100MB(频繁 checkpoint)performance_schema = ON(消耗 CPU 和内存)max_connections = 151(不够用)
[mysqld]
# Buffer Pool: 8G 机器给 5G (占 63%)
innodb_buffer_pool_size = 5G
innodb_buffer_pool_instances = 4
# 刷盘策略 — HDD 关键优化, 单项带来最大提升
# =2: 每次 commit flush 到 OS buffer, 每秒 fsync (丢失风险 ≤1s 数据)
innodb_flush_log_at_trx_commit = 2
sync_binlog = 0
# 绕过 OS page cache, 避免双重缓冲
innodb_flush_method = O_DIRECT
# Redo Log 加大, 减少 checkpoint 频率
innodb_redo_log_capacity = 1G
innodb_log_buffer_size = 64M
# I/O 容量 (HDD)
innodb_io_capacity = 200
innodb_io_capacity_max = 400
# 连接数
max_connections = 300
thread_cache_size = 64
# 关闭非必要功能
performance_schema = OFF
skip-name-resolve
# 一键部署到所有 MySQL 节点
cd deploy/mysql
export SSH_PASS="xxx"
./tune-mysql.sh
# 或手动: 复制到 /etc/mysql/conf.d/perf-tuning.cnf, 重启 mysql
innodb_buffer_pool_size = 5GB (was 128MB, ×40)
innodb_flush_log_at_trx_commit = 2 (was 1)
sync_binlog = 0 (was 1)
innodb_flush_method = O_DIRECT (was fsync)
innodb_redo_log_capacity = 1GB (was 100MB, ×10)
max_connections = 300 (was 151)
performance_schema = OFF (was ON)
- Buffer Pool 命中率: 99.97% (7,583,479 请求 / 仅 1,936 次磁盘读)
- 磁盘 IO %util: < 1% (之前被 fsync 打满)
- MySQL CPU: ~15-25%, 非常轻松
- 主从复制: 正常, 延迟 0 秒
RPS ~3,256 → ~5,600+ (提升 72%)
最大功臣: innodb_flush_log_at_trx_commit=2, 消除了 HDD 每次 commit fsync 的瓶颈。
| 节点 | CPU 使用率 | IO %util | 状态 |
|---|---|---|---|
| ShardingSphere Proxy (8C) | 89% | - | 瓶颈 (load avg=110) |
| Master MySQL | ~25% | 0.6% | 轻松 |
| Shard1 MySQL | ~15% | 0.8% | 轻松 |
| Shard2 MySQL | ~18% | 0.1% | 轻松 |
| App1 | ~15% | - | 轻松 |
| App2 | ~15% | - | 轻松 |
| App3 | ~15% | - | 轻松 |
结论: ShardingSphere Proxy 是唯一瓶颈, 8C 被 SQL 解析/路由打满。MySQL、App、IO 全部空闲。
- Proxy 8C 被打满 (89% CPU, load avg=110)
- 所有 SQL 解析、分片路由、结果合并都在单节点完成
- MySQL、App 全部空闲等活
- Proxy 节点扩容: 8C → 16C
proxy-frontend-executor-size: 0: 自动检测 CPU 核数,扩容后无需改配置
RPS ~5,600 → ~8,400 (提升 50%)
- 跨分片广播查询:
GET /api/orders?page=0&size=20不带userId, ShardingSphere 需要查询全部 4 张物理表 (shard1.orders_0, shard1.orders_1, shard2.orders_0, shard2.orders_1) 然后合并排序 - OFFSET 分页:
LIMIT 20 OFFSET 1000需要扫描并丢弃前 1000 行, 深分页性能差 - 单列索引:
idx_user_id(user_id)不覆盖 ORDER BY, 需回表排序
-- 在所有 4 张物理表上执行
ALTER TABLE orders_0 DROP INDEX idx_user_id, ADD INDEX idx_user_id_id (user_id, id);
ALTER TABLE orders_1 DROP INDEX idx_user_id, ADD INDEX idx_user_id_id (user_id, id);
效果: WHERE + ORDER BY 同时走索引,无回表排序
-- 优化前
Extra: NULL (需要 filesort)
-- 优化后
Extra: Using index condition (索引覆盖)
新增 API: GET /api/orders?userId=123&cursor=<lastId>&size=20
// O(limit) 替代 O(offset + limit)
public List<Order> findByUserIdAfterCursor(Long userId, long cursor, int limit) {
return jdbc.query(
"SELECT * FROM orders WHERE user_id = ? AND id > ? ORDER BY id LIMIT ?",
ROW_MAPPER, userId, cursor, limit + 1);
}
返回格式:
{
"content": [...],
"hasNext": true,
"nextCursor": 309262275351556096
}
客户端用 nextCursor 请求下一页,无论翻到第几页,性能恒定。
压测脚本中所有 order 列表查询都改为带 userId:
# 优化前: 广播到 4 张表
GET /api/orders?page=0&size=20
# 优化后: 精确路由到 1 张表
GET /api/orders?userId=127648&cursor=0&size=20
ShardingSphere 根据 user_id 精确路由到唯一的 shard + table,减少:
- 路由计算量: 1 次 vs 4 次
- 后端连接占用: 1 个 vs 4 个
- 结果合并排序: 无 vs 4 路归并
RPS ~8,400 → ~9,000 (提升 7%)
提升幅度不大因为 Proxy 此时 CPU 已从 89% 降到 ~53%, 不再是瓶颈。但该优化对延迟改善显著, 尤其是深分页场景。
9,000 RPS 时 Nginx LB 节点出现严重的 TCP TIME_WAIT 端口耗尽:
TCP: 35,672 (estab 2839, closed 32766, timewait 32755)
- 端口范围
32768-60999= 仅 28,232 个端口 - TIME_WAIT 已达 32,755 (超过端口总数!)
worker_connections 768远低于实际连接需求- 新连接无法分配端口 → 请求失败 → RPS 被卡住
虽然 Nginx upstream 配置了 keepalive 256, 但:
worker_connections 768限制了每个 worker 的并发连接数- 当 keepalive 池满时, 新请求创建短连接, 关闭后进入 TIME_WAIT (等 60 秒释放)
- 在 9000 RPS 下, TIME_WAIT 积累速度远超释放速度
worker_rlimit_nofile 65535; # 文件描述符上限
events {
worker_connections 10240; # 原 768, 提升 13x
}
# 扩大临时端口范围: 28K → 64K
sysctl -w net.ipv4.ip_local_port_range='1024 65535'
# 允许复用 TIME_WAIT 端口 (关键!)
sysctl -w net.ipv4.tcp_tw_reuse=1
# 缩短 TIME_WAIT 存活时间: 60s → 15s
sysctl -w net.ipv4.tcp_fin_timeout=15
# 允许更多 TIME_WAIT bucket
sysctl -w net.ipv4.tcp_max_tw_buckets=65536
# 网络设备队列
sysctl -w net.core.netdev_max_backlog=5000
# TCP 调优已集成到 setup-nginx-lb.sh, 重新部署即可:
cd deploy && ./setup-nginx-lb.sh
# 或手动在各节点执行 sysctl 命令
| 节点 | CPU | IO %util | 状态 |
|---|---|---|---|
| SS Proxy (16C) | ~53% | - | 有余量 |
| Master (4C) | ~45% | 0.5% | OK |
| Shard1 (4C) | ~58% | 0.8% | 偏高 |
| Shard2 (4C) | ~32% | 0.2% | OK |
| Slave1/2 (4C) | ~5-17% | - | 空闲 |
| App ×3 (4C) | ~15-23% | - | OK |
| Nginx LB (4C) | ~33% | - | TIME_WAIT 修复中 |
| Locust ×5 | ~17% | - | OK |
消除端口耗尽瓶颈, RPS 可继续上升
| 优先级 | 方案 | 预期效果 |
|---|---|---|
| P0 | App 节点 4C→8C (当前 load/核 4x, 严重超载) | 消除 App 层瓶颈 |
| P1 | Master DB 4C→8C (当前 load/核 3x) | 消除写入瓶颈 |
| P2 | 双 ShardingSphere Proxy (已验证可行, 瓶颈转移后可重新启用) | 水平扩展路由层 |
| P3 | 去 Proxy, 应用层分片 (直连 MySQL) | 消除中间层, 上限大幅提高 |
9,000 RPS 时服务端全部空闲 (Proxy 53%, App ~20%, DB ~50%), 客户端是瓶颈:
- 3000 用户 × avg wait 0.3-0.6s ≈ 9,000 RPS (客户端上限)
- 服务端还有大量 CPU 余量
- 并发用户数: 3,000 → 6,000 (每 worker 1200)
- Spawn rate: 100 → 200
- 缩短 wait time:
- ReadHeavyUser:
0.1-0.5s→0.02-0.1s - MixedWorkloadUser:
0.2-1.0s→0.05-0.2s - WriteHeavyUser:
0.2-0.8s→0.05-0.2s
- ReadHeavyUser:
- Worker 数量: 5 → 10
RPS ~9,000 → ~16,000 (提升 78%)
| 节点 | CPU | Load Avg | 状态 |
|---|---|---|---|
| SS Proxy (16C) | 91% | 58 (3.6x 超载) | 主瓶颈 |
| App1 .217 (4C) | ~70% | 15 | 偏高 |
| App2 .215 (4C) | ~66% | 9 | 偏高 |
| App3 .216 (4C) | ~79% | 11 | 偏高 |
| Master .226 (4C) | ~58% | 5.5 | 中等 |
| Shard1 .220 (4C) | ~64% | 3.2 | 中等 |
| Shard2 .219 (4C) | ~55% | 5.7 | 中等 |
| Slave1 .225 (4C) | ~33% | 1.7 | OK |
| Slave2 .224 (4C) | ~12% | 0.6 | 空闲 |
| Nginx LB (4C) | ~50% | 1.7 | OK (TW=263) |
| Locust ×10 | ~30% | - | OK |
结论:
- 主瓶颈: SS Proxy 再次打满 91%, load avg 58 (16C 的 3.6 倍)
- 次瓶颈: App 节点 66-79%, load avg 远超核数 (主因: Tomcat 线程等 Proxy 响应堆积)
- 延迟飙升: avg 6ms → 269ms (Proxy 排队导致)
单个 SS Proxy (16C) 在 16K RPS 时 CPU 91%, 是唯一单点瓶颈。垂直扩容 (加 CPU) 有上限, 需要水平扩展。
最初设计: 在 Nginx LB 上用 stream 模块做 TCP 代理:
App ×3 → Nginx LB:3307 (TCP stream) → Proxy1 + Proxy2 → MySQL
踩坑记录:
- stream 模块未安装: 默认 nginx 包不含 stream, 需要
apt install libnginx-mod-stream - 配置路径冲突:
stream {}块不能放在/etc/nginx/conf.d/(该目录在http {}块内被 include), 必须放/etc/nginx/stream-proxy.conf并在nginx.conf末尾单独 include - LB 成为新瓶颈: 部署后 RPS 从 16K 下降到 12K!
Nginx LB (4C) 同时处理 HTTP LB + TCP proxy, CPU 飙到 72%:
%Cpu: 11.6 us, 34.9 sy, 25.6 si → 72% 使用率
ESTAB connections: 6,197
每条 SQL 都要经过 LB 转发 (App→LB→Proxy), 网络中断处理 (si=26%) 成为瓶颈。
MySQL Connector/J 原生支持 loadbalance:// 协议, App 直连两个 Proxy, 零额外开销:
App ×3 → Proxy1 (.218:3307) + Proxy2 (.206:3307) → MySQL
(JDBC 客户端随机分配连接)
# 阶段1: 直连单 Proxy
jdbc:mysql://10.198.27.218:3307/demo_db
# 阶段2: Nginx TCP LB (失败, LB 成为瓶颈)
jdbc:mysql://10.198.27.207:3307/demo_db
# 阶段3: JDBC 客户端负载均衡 (最终方案)
jdbc:mysql:loadbalance://10.198.27.218:3307,10.198.27.206:3307/demo_db?loadBalanceAutoCommitStatementThreshold=0&loadBalanceConnectionGroup=demo
工作原理:
- HikariCP 创建连接时, MySQL Connector/J 随机选择一个 Proxy 节点
- 连接池中的 80 个连接大致均匀分布在两个 Proxy 上
- 无需中间代理层, 无额外网络开销
两个 Proxy 使用完全相同的配置文件 (server.yaml + config-sharding.yaml):
- 都是 Standalone 模式, 独立运行
- 每个 Proxy 各自维护到 MySQL 的后端连接池
# 一键安装 + 部署到所有 Proxy 节点
cd deploy/shardingsphere
SSH_PASS=xxx ./deploy-shardingsphere.sh --install
--install 模式自动完成:
- 下载 ShardingSphere Proxy 5.5.3
- 解压并创建软链接
- 下载 MySQL JDBC 驱动 (mysql-connector-j-9.6.0.jar)
- 删除不需要的依赖 (避免启动报错):
shardingsphere-transaction-base-seata-at-5.5.3.jarshardingsphere-database-connector-hive-5.5.3.jarshardingsphere-parser-sql-engine-hive-5.5.3.jarshardingsphere-parser-sql-statement-hive-5.5.3.jarshardingsphere-proxy-dialect-hive-5.5.3.jar
- 上传配置文件并启动
| 问题 | 原因 | 解决 |
|---|---|---|
ClassNotFoundException: ...seata... |
5.5.3 自带 seata 事务 jar, 缺少 seata 依赖 | 删除 lib/shardingsphere-transaction-base-seata-at-5.5.3.jar |
ClassNotFoundException: ...ZooKeeperHiveClientException |
5.5.3 自带 Hive 连接器, 缺少 Hive JDBC 依赖 | 删除全部 4 个 *hive*5.5.3.jar |
unadapted java version 警告 |
Java 21, SS Proxy 脚本未适配 | 可忽略, 实际运行正常 |
# Step 1: 在 app-cluster.conf 填入 Proxy2 IP
# PROXY_NODES=("10.198.27.218" "10.198.27.206")
# Step 2: 安装 + 部署双 Proxy
cd deploy/shardingsphere && SSH_PASS=xxx ./deploy-shardingsphere.sh --install
# Step 3: 重建 App (JDBC URL 改为 loadbalance://)
cd deploy && SSH_PASS=xxx ./deploy-app.sh
# (可选) Step 4: 如果要保留 Nginx TCP LB 作为备用
SSH_PASS=xxx ./setup-nginx-lb.sh
RPS 12,800 — 反而下降, 已回滚至单 Proxy
双 Proxy 成功分担了路由层负载, 但暴露了更深层的瓶颈:
| 节点 | 核数 | Load Avg | Load/核 | 状态 |
|---|---|---|---|---|
| App1 .217 | 4C | 16 | 4.0x | 严重超载 |
| App2 .215 | 4C | 15.5 | 3.9x | 严重超载 |
| App3 .216 | 4C | 15.4 | 3.9x | 严重超载 |
| Master .226 | 4C | 12.4 | 3.1x | 超载 |
| Proxy1 .218 | 16C | 10.4 | 0.65x | OK |
| Proxy2 .206 | 16C | 9.5 | 0.59x | OK |
| Shard1 .220 | 4C | 3.1 | 0.78x | OK |
| Shard2 .219 | 4C | 3.2 | 0.80x | OK |
| Nginx .207 | 4C | 1.7 | 0.43x | OK |
分析:
- 双 Proxy 生效: 各约 60% 负载, 不再是瓶颈
- 但 RPS 从 16K 降到 12.8K, 平均延迟从 269ms 升到 436ms
loadbalance://JDBC 协议可能引入额外开销 (连接验证、故障转移检测)- 真正瓶颈: App 节点 (4C) load avg 15-16, 严重超载; Master DB (4C) load avg 12, 也超载
- 结论: 当前架构瓶颈在 App 层和 Master DB, 而非 Proxy 层。单 Proxy (16C) 足够
回滚决定: 恢复单 Proxy 直连, 避免 loadbalance:// 额外开销。下一步应优先扩容 App 节点 (4C→8C) 和 Master DB (4C→8C)。
| 脚本 | 用途 |
|---|---|
deploy/deploy-app.sh |
构建 + 部署 Spring Boot 到 3 台 App 节点 |
deploy/setup-nginx-lb.sh |
部署 Nginx HTTP LB (+ 可选 TCP stream LB) |
deploy/shardingsphere/deploy-shardingsphere.sh |
部署 SS Proxy 到所有节点 (支持 --install) |
deploy/mysql/tune-mysql.sh |
部署 MySQL 调优配置到所有 DB 节点 |
stress-test/deploy-all.sh |
部署 Locust 压测集群 |
deploy/
├── app-cluster.conf # 集群 IP、SSH、JVM、Proxy 节点列表
├── deploy-app.sh # App 部署脚本
├── nginx-lb.conf # Nginx 负载均衡配置
├── setup-nginx-lb.sh # Nginx 部署脚本 (HTTP LB + 可选 TCP stream)
├── mysql/
│ ├── perf-tuning.cnf # MySQL 调优 (8G RAM 节点)
│ ├── perf-tuning-16g.cnf # MySQL 调优 (16G RAM 节点)
│ └── tune-mysql.sh # MySQL 调优部署脚本
└── shardingsphere/
├── server.yaml # Proxy 全局配置 (线程/连接)
├── config-sharding.yaml # 分片 + 读写分离规则
└── deploy-shardingsphere.sh # Proxy 多节点部署 (支持 --install)
innodb_flush_log_at_trx_commit=2: 适用于压测和可接受 ≤1s 数据丢失的场景。生产环境如需强一致性, 应改回=1并使用 SSD。sync_binlog=0: 压测环境设置。生产环境建议=1。performance_schema=OFF: 关闭后无法使用sysschema 做性能分析。如需排查慢查询, 临时开启。- Snowflake ID: 每个 App 节点的
worker-id必须唯一 (0-31), 通过-Dsnowflake.worker-id=N传入。 - ShardingSphere
proxy-frontend-executor-size=0: 自动检测 CPU 核数, 扩容后无需改配置。 - ShardingSphere 5.5.3 安装: 必须删除 seata + hive 相关 jar, 否则启动时 ClassNotFoundException。
- 双 Proxy 架构 (已回滚): 第十轮验证双 Proxy 可行 (各 60% 负载), 但
loadbalance://协议有额外开销, 且当前瓶颈在 App 层和 Master DB, 非 Proxy。扩容 App/Master 后可重新启用。 - Nginx TCP stream: 如果仍需使用, 注意
stream {}块不能放在conf.d/(被http {}include), 需要libnginx-mod-stream包。但在高 RPS 下 Nginx 同时做 HTTP + TCP 代理会成为瓶颈, 推荐使用 JDBC 客户端负载均衡。