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

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):

bash 复制代码
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 复制代码
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 复制代码
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 复制代码
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 复制代码
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(单独目录):

bash 复制代码
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 卷 重来:

bash 复制代码
cd $PRT_ROOT
bash scripts/rw-split/rebuild-rw-split-from-scratch.sh

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

bash 复制代码
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 复制代码
bash scripts/rw-split/rollback-app-database.sh
# 或 apply-app-database-env.sh prt-mysql 3306

User:

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

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

bash 复制代码
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:

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

User:

bash 复制代码
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 上游 相关,按流程图顺序排查即可。
相关推荐
用户298698530141 小时前
Java 操作 Word 文档:常见编辑功能实现
java·后端
XovH1 小时前
第 40 篇 k8s之Helm:编写自定义 Helm Chart
后端
曾几何时`1 小时前
Go(四)Channel
开发语言·后端·golang
橘子星2 小时前
前端薅数据神器 Fetch:不用翻墙,在线拿捏后端与 AI 接口
前端·后端
用户925807911482 小时前
画图理解mysql日志机制
java·后端
huzhongqiang2 小时前
120行代码实现一个极简 Agent
后端·agent
XIAOHEZIcode2 小时前
进程、会话与终端——一次真实的 Linux Session 解剖
linux·后端·命令行
枕星而眠2 小时前
【数据结构】树与二叉树基础知识点总结
数据结构·c++·后端·算法·运维开发
极光技术熊2 小时前
从零构建在线Excel:一个Java全栈工程师的实战记录
前端·后端