MySQL数据库读写分离+分库分表

环境搭建指南

从零搭建 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_0orders_1)

1. 系统准备 (所有 MySQL 节点)

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
 

2. 配置主从复制

2.1 Master (node1)

编辑 /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 |
+------------------+----------+
 

2.2 Slave (node2, node3)

编辑 /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
 

2.3 测试主从复制

在 Master (node1):

CREATE DATABASE testdb;
USE testdb;
CREATE TABLE orders (
  id BIGINT PRIMARY KEY,
  name VARCHAR(50)
);
 

在 Slave (node2, node3) 验证:

SHOW DATABASES;  -- 应能看到 testdb
 

3. 部署分片节点

3.1 Shard1 (node4)

CREATE DATABASE shard1;
 

3.2 Shard2 (node5)

CREATE DATABASE shard2;
 

注: 不需要手动建表, ShardingSphere Proxy 会通过 DDL 自动在各 shard 上创建分片表。


4. 创建 ShardingSphere 连接用户

在 node1、node4、node5 上都执行:

CREATE USER 'shardingsphere'@'%' IDENTIFIED WITH mysql_native_password BY 'Password';
GRANT ALL PRIVILEGES ON *.* TO 'shardingsphere'@'%';
FLUSH PRIVILEGES;
 

在 node2、node3 上不需要手动创建 — 该用户会通过主从复制自动同步。


5. 安装 ShardingSphere Proxy (node6)

5.1 安装 Java 21

curl -s https://raw.githubusercontent.com/power721/repo/refs/heads/master/jdk21.sh | sh
 

5.2 下载 ShardingSphere Proxy

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
 

5.3 下载 MySQL JDBC 驱动

wget -P lib/ https://repo1.maven.org/maven2/com/mysql/mysql-connector-j/9.6.0/mysql-connector-j-9.6.0.jar
 

5.4 删除冲突依赖

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
 

6. 配置 ShardingSphere Proxy

6.1 全局配置 conf/server.yaml

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
 

6.2 分片 + 读写分离配置 conf/config-sharding.yaml

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

7. 启动 Proxy

cd shardingsphere-proxy
bin/start.sh
 

默认端口 3307, 连接:

mysql -h 192.168.0.218 -P 3307 -u root -proot
 

8. 建表验证

通过 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
 

9. 测试读写分离

通过 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 日志中看到路由目标。


10. 测试分库分表

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_0orders_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 默认连接池太小
  • 无健康检查

优化

  1. 3 台 App 节点 + Nginx 负载均衡
  2. HikariCP 连接池max=80min-idle=20connection-timeout=10s
  3. Tomcat 线程池max-threads=400accept-count=200
  4. 健康检查端点GET /health

关键配置 (application.properties)

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 + Locust + JVM

问题

  • ShardingSphere Proxy 默认配置未调优
  • Locust 并发用户数不足,未充分加压
  • JVM 堆内存偏小

优化

  1. ShardingSphere Proxy (server.yaml):

    • proxy-frontend-flush-threshold: 128 (减少 Netty flush 次数)
    • proxy-netty-backlog: 1024 (突发连接队列)
    • 后端连接池: 每个 datasource maxPoolSize=60minPoolSize=20
    • PreparedStatement 缓存: cachePrepStmts=trueprepStmtCacheSize=250
  2. Locust 加压:

    • 用户数 1000 → 3000, spawn-rate 50 → 100
    • 5 个 Worker 节点, 每个 600 用户
  3. JVM-Xms1g -Xmx2g -XX:+UseG1GC -XX:MaxGCPauseMillis=50

  4. Nginxkeepalive 256access_log off

关键配置 (config-sharding.yaml datasource 示例)

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 过大, 实际并发请求数不足

优化

  1. Page<T> → Slice<T>: 消除所有 COUNT(*) 查询
  2. HttpUser → FastHttpUser: 基于 geventhttpclient, 性能 ~5x
  3. 减少 think time: ReadHeavy 0.1-0.5s, WriteHeavy 0.2-0.8s

结果

RPS ~2,000 → ~2,500


第四轮: 去 ORM 开销

问题

  • Hibernate/JPA 的对象映射、脏检查、一级缓存在高并发下开销显著
  • 每次 SQL 都经过 Hibernate Session → Proxy → MySQL, 中间层越多延迟越高

优化

  1. JPA → JdbcTemplate: 所有 CRUD 操作直接用 SQL
    • 删除 spring-boot-starter-data-jpa, 改用 spring-boot-starter-jdbc
    • 创建 UserJdbcRepository / OrderJdbcRepository, 用 JdbcTemplate + RowMapper
    • Entity 从 JPA 注解实体简化为纯 POJO
  2. Snowflake ID 生成: 从 Hibernate IdentifierGenerator 改为 Spring @Bean

关键代码 (OrderJdbcRepository.java 示例)

@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 内核参数调优 ⭐

问题

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 (不够用)

优化 (deploy/mysql/perf-tuning.cnf)

[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
 

调优后 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 的瓶颈。


当前瓶颈分析 (5,600 RPS 时)

节点 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 全部空闲。


第六轮: ShardingSphere Proxy 扩 CPU

问题

  • Proxy 8C 被打满 (89% CPU, load avg=110)
  • 所有 SQL 解析、分片路由、结果合并都在单节点完成
  • MySQL、App 全部空闲等活

优化

  1. Proxy 节点扩容: 8C → 16C
  2. proxy-frontend-executor-size: 0: 自动检测 CPU 核数,扩容后无需改配置

结果

RPS ~5,600 → ~8,400 (提升 50%)


第七轮: 游标分页 + 复合索引 + 消除跨分片查询 ⭐

问题

  1. 跨分片广播查询GET /api/orders?page=0&size=20 不带 userId, ShardingSphere 需要查询全部 4 张物理表 (shard1.orders_0, shard1.orders_1, shard2.orders_0, shard2.orders_1) 然后合并排序
  2. OFFSET 分页LIMIT 20 OFFSET 1000 需要扫描并丢弃前 1000 行, 深分页性能差
  3. 单列索引idx_user_id(user_id) 不覆盖 ORDER BY, 需回表排序

优化

1. 复合索引 (user_id, id)

-- 在所有 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 (索引覆盖)
 

2. 游标分页 (Keyset Pagination)

新增 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 请求下一页,无论翻到第几页,性能恒定。

3. 消除跨分片广播查询

压测脚本中所有 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%, 不再是瓶颈。但该优化对延迟改善显著, 尤其是深分页场景。


第八轮: Nginx TCP 调优 (TIME_WAIT 端口耗尽) ⭐

问题

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, 但:

  1. worker_connections 768 限制了每个 worker 的并发连接数
  2. 当 keepalive 池满时, 新请求创建短连接, 关闭后进入 TIME_WAIT (等 60 秒释放)
  3. 在 9000 RPS 下, TIME_WAIT 积累速度远超释放速度

优化

Nginx 主配置 (/etc/nginx/nginx.conf)

worker_rlimit_nofile 65535;   # 文件描述符上限

events {
    worker_connections 10240;  # 原 768, 提升 13x
}
 

TCP 内核参数 (Nginx LB + App + Proxy 节点)

# 扩大临时端口范围: 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 命令
 

9,000 RPS 时各节点负载

节点 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 可继续上升


继续提升方向 (16,000 RPS 时)

优先级 方案 预期效果
P0 App 节点 4C→8C (当前 load/核 4x, 严重超载) 消除 App 层瓶颈
P1 Master DB 4C→8C (当前 load/核 3x) 消除写入瓶颈
P2 双 ShardingSphere Proxy (已验证可行, 瓶颈转移后可重新启用) 水平扩展路由层
P3 去 Proxy, 应用层分片 (直连 MySQL) 消除中间层, 上限大幅提高

第九轮: Locust 加压

问题

9,000 RPS 时服务端全部空闲 (Proxy 53%, App ~20%, DB ~50%), 客户端是瓶颈:

  • 3000 用户 × avg wait 0.3-0.6s ≈ 9,000 RPS (客户端上限)
  • 服务端还有大量 CPU 余量

优化

  1. 并发用户数: 3,000 → 6,000 (每 worker 1200)
  2. Spawn rate: 100 → 200
  3. 缩短 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
  4. Worker 数量: 5 → 10

结果

RPS ~9,000 → ~16,000 (提升 78%)

16,000 RPS 时瓶颈分析

节点 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

结论:

  1. 主瓶颈: SS Proxy 再次打满 91%, load avg 58 (16C 的 3.6 倍)
  2. 次瓶颈: App 节点 66-79%, load avg 远超核数 (主因: Tomcat 线程等 Proxy 响应堆积)
  3. 延迟飙升: avg 6ms → 269ms (Proxy 排队导致)

第十轮: 双 ShardingSphere Proxy ⭐

问题

单个 SS Proxy (16C) 在 16K RPS 时 CPU 91%, 是唯一单点瓶颈。垂直扩容 (加 CPU) 有上限, 需要水平扩展。

方案演进

方案 A: Nginx TCP stream LB (已废弃)

最初设计: 在 Nginx LB 上用 stream 模块做 TCP 代理:

App ×3 → Nginx LB:3307 (TCP stream) → Proxy1 + Proxy2 → MySQL
 

踩坑记录:

  1. stream 模块未安装: 默认 nginx 包不含 stream, 需要 apt install libnginx-mod-stream
  2. 配置路径冲突stream {} 块不能放在 /etc/nginx/conf.d/ (该目录在 http {} 块内被 include), 必须放 /etc/nginx/stream-proxy.conf 并在 nginx.conf 末尾单独 include
  3. 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%) 成为瓶颈。

方案 B: JDBC 客户端负载均衡 (最终方案)

MySQL Connector/J 原生支持 loadbalance:// 协议, App 直连两个 Proxy, 零额外开销:

App ×3 → Proxy1 (.218:3307) + Proxy2 (.206:3307) → MySQL
         (JDBC 客户端随机分配连接)
 

最终实现

JDBC URL 变更

# 阶段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 部署

两个 Proxy 使用完全相同的配置文件 (server.yaml + config-sharding.yaml):

  • 都是 Standalone 模式, 独立运行
  • 每个 Proxy 各自维护到 MySQL 的后端连接池
# 一键安装 + 部署到所有 Proxy 节点
cd deploy/shardingsphere
SSH_PASS=xxx ./deploy-shardingsphere.sh --install
 

--install 模式自动完成:

  1. 下载 ShardingSphere Proxy 5.5.3
  2. 解压并创建软链接
  3. 下载 MySQL JDBC 驱动 (mysql-connector-j-9.6.0.jar)
  4. 删除不需要的依赖 (避免启动报错):
    • shardingsphere-transaction-base-seata-at-5.5.3.jar
    • shardingsphere-database-connector-hive-5.5.3.jar
    • shardingsphere-parser-sql-engine-hive-5.5.3.jar
    • shardingsphere-parser-sql-statement-hive-5.5.3.jar
    • shardingsphere-proxy-dialect-hive-5.5.3.jar
  5. 上传配置文件并启动

ShardingSphere Proxy 5.5.3 安装踩坑

问题 原因 解决
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)
 

注意事项

  1. innodb_flush_log_at_trx_commit=2: 适用于压测和可接受 ≤1s 数据丢失的场景。生产环境如需强一致性, 应改回 =1 并使用 SSD。
  2. sync_binlog=0: 压测环境设置。生产环境建议 =1
  3. performance_schema=OFF: 关闭后无法使用 sys schema 做性能分析。如需排查慢查询, 临时开启。
  4. Snowflake ID: 每个 App 节点的 worker-id 必须唯一 (0-31), 通过 -Dsnowflake.worker-id=N 传入。
  5. ShardingSphere proxy-frontend-executor-size=0: 自动检测 CPU 核数, 扩容后无需改配置。
  6. ShardingSphere 5.5.3 安装: 必须删除 seata + hive 相关 jar, 否则启动时 ClassNotFoundException。
  7. 双 Proxy 架构 (已回滚): 第十轮验证双 Proxy 可行 (各 60% 负载), 但 loadbalance:// 协议有额外开销, 且当前瓶颈在 App 层和 Master DB, 非 Proxy。扩容 App/Master 后可重新启用。
  8. Nginx TCP stream: 如果仍需使用, 注意 stream {} 块不能放在 conf.d/ (被 http {} include), 需要 libnginx-mod-stream 包。但在高 RPS 下 Nginx 同时做 HTTP + TCP 代理会成为瓶颈, 推荐使用 JDBC 客户端负载均衡。