Docker 环境下 MySQL 读写分离实践:ProxySQL + 主从复制

0 阅读3分钟

Docker 环境下 MySQL 读写分离实践:ProxySQL + 主从复制

1. 要解决什么问题

典型场景:网关后面挂着 协议/业务服务(PRT)用户中心(User),二者共用同一个 MySQL 库(例如 PRT_DB),平时各连 prt-mysql:3306。读请求多了之后,希望:

  • (INSERT / UPDATE / DELETE)仍走主库,保证一致;
  • (SELECT)分流到从库,减轻主库压力;
  • 不拆掉 已有主库数据卷,避免「为了读写分离重装数据库」。

常见做法是:主从复制 + 中间件按 SQL 类型路由。本文采用 MySQL 8.0 主从 + ProxySQL 2.6


2. 目标架构

2.1 组件一览

组件角色端口(示例)
prt-mysql主库,历史数据卷保留3306
prt-mysql-slave从库,新卷3306(容器内)
prt-proxysql读写代理,应用连此6033(业务) / 6032(管理)
prt-django / uc-django应用,改 DATABASE_HOST 即可

2.2 拓扑图

flowchart TB
  subgraph apps["应用层"]
    GW[反向代理 / 网关]
    PRT[业务 Django]
    UC[用户中心 Django]
  end

  subgraph proxy["读写代理"]
    PS[ProxySQL<br/>6033]
  end

  subgraph mysql["MySQL 集群"]
    M[(主库 prt-mysql<br/>写 + binlog)]
    S[(从库 prt-mysql-slave<br/>只读副本)]
  end

  GW --> PRT & UC
  PRT --> PS
  UC --> PS
  PS -->|增删改| M
  PS -->|SELECT| S
  M -->|binlog 复制| S

2.3 一条 SQL 的路由

flowchart LR
  A[Django ORM 发 SQL] --> B[连接 prt-proxysql:6033]
  B --> C{ProxySQL 解析}
  C -->|SELECT| D[从库 hostgroup 1]
  C -->|其它| E[主库 hostgroup 0]
  D --> F[返回结果]
  E --> F

2.4 写后读与复制延迟

sequenceDiagram
  participant App as 应用
  participant PX as ProxySQL
  participant M as 主库
  participant S as 从库

  App->>PX: UPDATE 角色
  PX->>M: 写
  M-->>App: OK
  Note over M,S: binlog 异步复制

  App->>PX: SELECT 列表
  PX->>S: 读从库
  alt 从库未追上
    S-->>App: 短暂旧数据
  else 已同步
    S-->>App: 新数据
  end

列表类接口若不能接受延迟,可在后续迭代中为特定 SQL 强制走主库;首版通常先接受毫秒~秒级延迟。


3. 与单库部署的差异(两套 Compose)

很多人联调时会遇到:PRT 页面正常,User 登录页接口 1045。往往不是主库坏了,而是 两个项目各有一份 .env、各起一个 Django 容器

flowchart TB
  subgraph prt_stack["Compose 项目 prt"]
    E1[PRT/.env]
    D1[prt-django]
    E1 --> D1
  end

  subgraph uc_stack["Compose 项目 uc"]
    E2[User/backend/.env]
    D2[uc-django]
    E2 --> D2
  end

  D1 & D2 --> DB[(同一 PRT_DB)]

只改 PRT 的 DATABASE_HOST 不会自动改 User。后文切库与回滚都按 两边分别执行 来写。


4. 实施总流程

一键入口(推荐)

PRT 根目录(含 docker-compose.ymldocker-compose.rw-split.yml):

cd $PRT_ROOT
bash scripts/rw-split/fix-crlf.sh
bash scripts/rw-split/enable-rw-split.sh
  • 已有备份:enable-rw-split.sh --skip-backup
  • 只建基础设施、应用仍连主库:--no-cutover

从 Windows 开发机同步脚本到 Linux 部署机后,先执行 fix-crlf.sh(见 §8),再跑上述命令。


5. 分步说明(含联调中常见现象)

5.1 备份与主库 binlog

主库叠加 master-rw.cnf 后需要 recreate prt-mysql 才能加载 log_bin / gtid_mode,但 不删除 mysql_data 卷。

bash scripts/rw-split/backup-before-rw-split.sh
bash scripts/rw-split/ensure-master-binlog.sh
bash scripts/rw-split/verify-master-mysql.sh

verify-master-mysql.sh 会检查:容器 healthy、SELECT 1、业务表行数、log_bin=ON。若这里失败,后面不必动从库卷。

5.2 从库首次启动与 root 密码

从库使用 新卷 mysql_slave_dataMYSQL_ROOT_PASSWORD 必须在 首次 docker compose up 就写在 PRT 的 .env 里。若第一次 up 时变量为空,日志里会出现 empty password,之后只改 .env 再 restart 不会重写卷内密码。

此时用 docker exec prt-mysql-slave mysql -uroot -e "SELECT 1"不要加 -h127.0.0.1)有时能进;用 -h127.0.0.1 可能报 1130(没有 root@'127.0.0.1'),容易误判成「密码错了」。

处理脚本:set-slave-root-password.sh(关 super_read_only 后只改 root@'localhost')。

5.3 建立复制(bootstrap)

bash scripts/rw-split/bootstrap-replication.sh
bash scripts/rw-split/verify-replication.sh

Replica_IO=Yes、Replica_SQL=No,常见原因是主库已开 GTID,但导库用了 SET-GTID-PURGED=OFFAUTO_POSITION 冲突。日志里 Last_Error 会提到 GTID 相关字样。此时跑 repair-replication.sh(在从库 RESET MASTER 后用 SET-GTID-PURGED=ON 重新导入)。

删从库卷若报 volume is in use,需要先 compose rm -f prt-mysql-slave,确认没有容器仍挂载该卷,再 volume rm

5.4 配置 ProxySQL

bash scripts/rw-split/configure-proxysql.sh

脚本会:

  • 写入后端 prt-mysql / prt-mysql-slave
  • 配置 mysql_users(root + 与 .env 一致的密码);
  • 规则:^SELECT → 从库,其余 → 主库;
  • 设置 mysql-default_authentication_plugin=mysql_native_password(见下)。

联调现象 A:6033 报 1045,主库直连却正常

  • mysql 客户端测 6033 失败,但 docker exec prt-mysql mysql -uroot -p... -e "SELECT 1" 成功。
  • 原因之一:ProxySQL 2.6 默认对客户端宣告 caching_sha2_password,与 mysql_users 里按 mysql_native_password 存的口令握手不一致。配置里显式改为 mysql_native_password 后,应用侧 PyMySQL 与带 --default-auth=mysql_native_password 的测试可通过。
  • 验证脚本:proxysql-test-6033.sh(同时尝试 PyMySQL,与 Django 一致)。

联调现象 B:.env 里肉眼是 Tractor,脚本却显示密码长度 7

  • grep MYSQL_PASSWORD .env | cat -A 显示 Tractor$^M,但 shell 变量长度仍为 7:多为历史隐藏字节或重复行;force-tractor-password.sh 会重写一行并统一主从与 ProxySQL。

5.5 应用切库(必须 force-recreate)

PRT:

bash scripts/rw-split/apply-app-database-env.sh prt-proxysql 6033
# 脚本内:改 PRT/.env → compose up --force-recreate prt-django prt-celery → restart prt-web

restart django 不会重新读 .env;只改 .env 不 recreate 容器也无效。切库后若 网关 502,常见是 prt-web 仍指向旧 django IP,需 restart prt-web(脚本已包含)。

User(单独目录):

cd $UC_BACKEND
bash scripts/fix-crlf.sh
bash scripts/fix-user-db-only.sh prt-mysql 3306   # 先确认主库通
# 确认 ProxySQL 6033 正常后再:
# bash scripts/fix-user-db-only.sh prt-proxysql 6033

联调现象 C:PRT .env 已是 prt-mysqlprintenv 仍显示 prt-proxysql

  • 原因:部署脚本先 source .env 再改文件,docker compose 继承了 shell 里旧的 DATABASE_HOST。现版 apply-app-database-env.sh 已改为 先改文件、unset 变量、再 source

5.6 浏览器控制台与后端错误如何区分

控制台内容来源是否阻塞业务
tabs-xxxxx.js + No tab with id: …浏览器扩展(标签页类插件)否,可忽略
/api/user/... + (1045, Access denied for user 'root'@'172.x.x.x')uc-django 连库失败是,修 User .env + recreate
/PRT/api/... 502多为 nginx → django 上游与 ProxySQL 无必然关系

PRT 能打开首页而 User 登录失败,优先对比两个容器的 DATABASE_HOST / DATABASE_PORT,不要先重建从库。


6. 从零重建从库与代理(不碰主库卷)

当从库卷状态混乱、ProxySQL 用户表反复试错后,可在 验收主库通过 的前提下删掉 从库卷 + ProxySQL 卷 重来:

cd $PRT_ROOT
bash scripts/rw-split/rebuild-rw-split-from-scratch.sh

默认 只重建基础设施,PRT 应用仍保持 prt-mysql:3306;6033 验证通过后再:

bash scripts/rw-split/rebuild-rw-split-from-scratch.sh --cutover

或分步 apply-app-database-env.sh prt-proxysql 6033 与 User 侧 fix-user-db-only.sh

flowchart TD
  V[verify-master-mysql 通过] --> R[停 slave + proxysql]
  R --> RM[删除 mysql_slave_data / proxysql_data 卷]
  RM --> UP[up 新容器]
  UP --> B[bootstrap + configure]
  B --> T[proxysql-test-6033]

7. 回滚

PRT:

bash scripts/rw-split/rollback-app-database.sh
# 或 apply-app-database-env.sh prt-mysql 3306

User:

cd $UC_BACKEND
bash scripts/fix-user-db-only.sh prt-mysql 3306

可选停止从库与代理(主库数据仍在):

docker compose -p prt -f docker-compose.yml -f docker-compose.rw-split.yml \
  stop prt-mysql-slave prt-proxysql

8. Windows 开发机 → Linux 部署脚本

在 Windows 上编辑 .sh 后,部署机执行时若出现:

  • set: pipefail\r: 无效的选项名 → 脚本带 CRLF 或误用 pipefail(本项目脚本统一 set -eu);
  • sed: -e 表达式 #1,字符 8:"s"的未知选项sed 与路径粘在一起,应为 sed -i 's/\r$//' .env有空格);
  • 脚本只打印标题就退出 → 常为脚本自身 CRLF,需在脚本头做自修复或先 fix-crlf.sh

PRT:

cd $PRT_ROOT
sed -i 's/\r$//' .env
sed -i 's/\r$//' scripts/rw-split/*.sh
bash scripts/rw-split/fix-crlf.sh

User:

cd $UC_BACKEND
sed -i 's/\r$//' .env
sed -i 's/\r$//' scripts/*.sh
bash scripts/fix-crlf.sh

9. 验收清单

flowchart LR
  subgraph check["上线前检查"]
    A1[主库 SELECT 1 + 业务表]
    A2[SHOW REPLICA STATUS Yes/Yes]
    A3[6033 SELECT 1]
    A4[PRT captcha HTTP 200]
    A5[User captcha HTTP 200]
    A6[改角色后刷新列表]
  end
  A1 --> A2 --> A3 --> A4 --> A5 --> A6

10. 小结

  • 读写分离在本方案中是:主库卷保留 + 新从库 + ProxySQL 路由,应用只改连接地址与端口。
  • 两个 Django 服务必须各自改 .envforce-recreate,不能指望改一处两边生效。
  • 联调中的 1045、502、复制 SQL 线程停止、从库空密码、6033 认证失败,多与 环境变量、卷初始化时机、GTID 导库方式、ProxySQL 认证插件、nginx 上游 相关,按流程图顺序排查即可。