Docker 容器化 MySQL 8.0 GTID 主从复制实战与排错

发布时间:2026/9/18 13:26:00
Docker 容器化 MySQL 8.0 GTID 主从复制实战与排错 1. 我为什么坚持用 Docker 跑 MySQL 主从而不是装两台虚拟机前阵子帮同事在测试环境里搭一套 MySQL 主从复制他原本的计划是开两台虚拟机各自装一遍 MySQL 8.0再手动改配置文件、开防火墙端口、配账号。我看了眼他那台 16G 内存的开发机直接把他拦下来了。用一个自定义 Docker 网络加两个容器一主一从的复制链路跑起来只花了十几分钟删掉也只需要两条docker rm -f。这就是我用 Docker 做 MySQL 主从复制最直接的理由环境的创建和销毁成本被压到了极低。主从复制本身涉及的东西不少——server-id、binlog 格式、复制账号、初始数据快照、位点或者 GTID任何一个环节出错都会导致Replica_IO_Running显示No。如果每次排查都要重装一遍数据库光是等初始化就够喝一壶的。容器化之后配置全部落在宿主机目录的my.cnf里数据落在挂载卷里重建实例就是删容器再起容器配置文件不动思路清晰得多。这套方案适合谁我的判断是这样正在学 MySQL 复制原理、想动手验证binlog和relay log到底怎么流转的同学需要给项目搭一套本地读写分离验证环境的后端开发还有准备面试、想把主从延迟、GTID、半同步这些概念从纸面拉到实操层面的人。如果你是要上生产环境容器跑数据库这件事需要更谨慎地评估存储性能和运维体系但作为学习、测试、预发验证Docker 这条路我走过很多次靠谱。接下来的内容我会把整套流程拆开讲为什么这么选参数、每一步在干什么、报错了怎么查。所有命令和配置都是我在实际机器上跑通过的你可以直接抄但建议先看完原理那一段再动手。2. 动手前的环境准备与镜像选型2.1 Docker 环境检查与那几个经典的启动失败第一步永远是确认 Docker 本身是活的。Linux 上执行docker version和docker info能看到 Server 段的信息就说明守护进程在跑。Windows 和 macOS 上用的是 Docker Desktop这里有个特别高频的坑启动时报 virtualization support not detected一堆人卡在这一步以为要重装。这个报错的意思是宿主机的硬件虚拟化没打开进 BIOS 或 UEFI 把 Intel VT-x 或者 AMD-V 打开就行。还有一种情况是 Windows 上 Hyper-V、WSL2 和某些虚拟机软件抢虚拟化层关掉冲突的那一方再重启。另外提醒一句Windows 家庭版早期是不支持 Hyper-V 的现在走 WSL2 后端基本都能用但 BIOS 那一步躲不掉。确认环境没问题后建议把 Docker 的镜像存储位置调到大一点的盘两个 MySQL 8.0 容器加上数据卷几个 G 是起步的量。需要开放的端口也要提前想好。我一般把主库映射成3307从库映射成3308避开宿主机上可能已经存在的3306。容器之间通信走 Docker 内部网络用的是容器自己的3306所以外网映射成什么端口完全不影响复制配置。这个区分很重要后面配CHANGE REPLICATION SOURCE TO时SOURCE_HOST要填容器名或者内网 IP不是127.0.0.1:3307。2.2 MySQL 镜像版本到底该挑哪个镜像 tag 的选择直接决定了配置文件里哪些参数能用、哪些会报错。我把常见的几个版本列一下方便你按需选镜像 tag特点适合场景mysql:8.0长期支持caching_sha2_password默认认证插件生态成熟大多数学习和测试场景我的首选mysql:8.4LTS 版本默认移除mysql_native_password插件想提前踩新版本坑的人mysql:5.7老项目兼容utf8mb4排序规则不同复现遗留系统问题mysql:latest指向当前最新可能带来意外的行为变化我个人不建议在复现环境用为什么不建议用latest因为主从复制对版本一致性有要求从库版本低于主库基本没法玩而latest会在你不知情的时候变成一个新的大版本。你可能今天搭好的环境明天docker pull一下就变成另一个东西了。我通常写死mysql:8.0需要升级时显式改 tag这样出问题时可回溯。还有个细节MySQL 8.0 默认的认证插件是caching_sha2_password。用位点方式复制时从库连主库如果没配置 TLS可能报认证相关的错误。解决办法有两个一是复制账号显式指定IDENTIFIED WITH mysql_native_password二是在CHANGE REPLICATION SOURCE TO里加上SOURCE_SSL1和GET_SOURCE_PUBLIC_KEY1。我在测试环境习惯用第一种简单直接生产环境更倾向走 TLS。2.3 目录规划与端口分配我习惯在宿主机上建一个统一的工作目录结构大致是这样mkdir -p /opt/mysql-cluster/{master/{conf,data,logs},slave/{conf,data,logs}}分成master和slave两棵树各自的conf放my.cnfdata挂载到容器的/var/lib/mysqllogs放错误日志方便排查。这样做的核心考虑是数据和配置必须落在容器外部。如果只写在镜像层或者容器可写层里容器一删数据就没了而且容器重建之后server-id、log-bin这些配置如果靠docker exec进去临时改下次重建又要再来一遍非常难受。权限问题这里要特别注意。MySQL 容器内以mysql用户运行UID 通常是 999。宿主机上data目录如果归属 root容器启动时会报权限错误然后退出日志里能看到Permission denied。解决办法是把宿主机的data目录chown 999:999或者干脆用命名卷让 Docker 自己管。我偏向 bind mount 加改权限因为想随时ls一下数据文件看看情况。端口分配上主库对外用3307从库用3308两个容器内部都是3306。容器之间在自定义网络里可以直接用容器名互相解析这是后面配复制主机的关键能绕开容器 IP 变化的问题。3. 主从复制原理先啃透配置才不是背口诀3.1 binlog、relay log 和那三个线程的流转过程很多人配主从就是背命令主库开 binlog从库 change masterstart slave。能跑通但一报错就懵。我想先把流转链路讲清楚因为后面所有报错几乎都能对应到这条链路上的某个环节。主库上任何一次数据变更会被写进binlog二进制日志。这个写入是在事务提交阶段完成的所以顺序很关键。主库同时会维护一个dump 线程从库连上来之后主库为每个从库启动一个 dump 线程负责把 binlog 事件推给从库。从库这边有两个线程IO 线程负责接收主库推过来的事件写进本地的relay log中继日志SQL 线程负责读 relay log把事件在从库上重放一遍最终让从库的数据和主库一致。所以从库本质上是个重放机。它不关心主库现在有什么数据它只关心从某个起点开始主库发生的每一条变更我按顺序执行一遍。这也是为什么搭建时必须解决两个问题起点在哪位点或 GTID起点之前的历史数据怎么补齐初始快照。搞不清这两点就会出现从库连上了但数据对不上的情况。还有一个常被忽略的点log_replica_updates老版本叫log_slave_updates这个参数。从库默认要不要把自己重放产生的事件再写进自己的 binlog如果你的架构是级联复制从库再挂从库必须打开如果是纯粹的一主一从打开与否不影响当前链路但打开之后从库也能当其他实例的数据源做后续扩展会方便。我在从库配置里一律打开。3.2 位点复制和 GTID 复制新手到底选哪个两种复制方式的差别用一句话概括位点复制靠文件名 偏移量定位GTID 复制靠全局事务编号定位。位点复制是这样的你在主库上执行SHOW MASTER STATUS得到类似mysql-bin.000003和157这样的结果然后告诉从库从 mysql-bin.000003 的第 157 字节开始追。这种方式直观但脆得很。主库重启、日志切割、做了日志清理位点就可能失效或者对不上。更麻烦的是主从切换场景你手动去算位点很容易算错。GTID全局事务标识符是给每个事务发一个唯一编号形如3f9b8e5a-...-0000000000a:1-25。从库只需要告诉主库我要所有我还没执行过的事务主库自动算出该从哪里开始发。主从切换、故障恢复时不用再纠结位点直接SOURCE_AUTO_POSITION1就行。代价是配置上多两个参数gtid_modeON和enforce_gtid_consistencyON而且 GTID 模式下不支持某些语句比如事务里混用临时表和 InnoDB 表的老写法。我的建议是新搭环境直接用 GTID。它把最容易出错的一块交给了数据库自己处理学习成本略高一点但省下来的排查时间远超这点投入。本文后面的实操也走 GTID 路线同时我会说明位点方式怎么写方便你看老文档时对照。3.3 server-id、binlog 格式这些参数为什么非改不可server-id是复制拓扑里每个实例的唯一身份证。主库、从库的 server-id 绝对不能相同相同的话从库连上来会直接被拒绝错误日志里会明确说这是同一个 server id。我一般用 1、2、3 这样递增规划一眼能看出角色。binlog_format我选ROW。三种格式里STATEMENT记录的是 SQL 语句原文遇到NOW()、UUID()、RAND()这类非确定性函数主从执行结果可能不一致MIXED是在两者之间自动切换但切换逻辑对使用者是个黑盒ROW记录的是每一行数据变更前后的镜像结果确定绝大多数场景下更安全。配合binlog_row_imageFULL更新时把整行前后镜像都记下来方便排查代价是日志体积大一些。还有一组关于持久化的参数sync_binlog1表示每次事务提交都把 binlog 刷盘innodb_flush_log_at_trx_commit1表示每次提交都把 redo log 刷盘。两个都设成 1 就是常说的双 1 配置数据安全性最高性能有一定损耗。学习环境我建议双 1 开着先把数据一致性这件事保证住如果做压测可以调整后再观察差异这种对比本身也是有价值的经验。字符集统一设成utf8mb4排序规则utf8mb4_0900_ai_ci8.0 的默认。字符集不一致是复制报错的一个隐蔽来源尤其是中文和 emoji 场景主库能写进去从库写不进去Replica_SQL_Running就会变成No。4. 一个字符一个字符敲出主库和从库4.1 主库的 my.cnf 与启动配置主库配置文件放在/opt/mysql-cluster/master/conf/my.cnf内容如下[mysqld] server-id1 log-binmysql-bin binlog_formatROW binlog_row_imageFULL gtid_modeON enforce_gtid_consistencyON log_replica_updatesON binlog_expire_logs_seconds604800 max_binlog_size512M sync_binlog1 innodb_flush_log_at_trx_commit1 character-set-serverutf8mb4 collation-serverutf8mb4_0900_ai_ci default_authentication_pluginmysql_native_password max_connections500 innodb_buffer_pool_size1G [client] default-character-setutf8mb4逐个说下我为什么这么写。binlog_expire_logs_seconds604800是 7 天8.0 之后推荐用这个参数而不是老的expire_logs_days单位是秒604800 7 * 24 * 3600。日志不清理会一直吃磁盘测试环境尤其容易堆到几十个 G所以我默认设了 7 天。max_binlog_size512M控制单个 binlog 文件大小到这个值就滚动新文件。innodb_buffer_pool_size1G这个值不是拍脑袋来的。经验做法是给到物理内存的 50% 到 70%但容器里要留够给系统和其他进程的空间。宿主机 8G 内存、跑两个容器的情况下各给 1G 是比较稳妥的。如果你机器内存宽裕可以往上加这个参数对读性能影响最大。default_authentication_pluginmysql_native_password是给复制账号铺路避免caching_sha2_password带来的认证麻烦。注意这个参数在 8.4 已被移除用 8.4 的话得改用mysql_native_passwordON之类的写法这也是我前面建议写死mysql:8.0的原因之一。启动主库容器docker run -d \ --name mysql-master \ --network mysql-cluster-net \ -p 3307:3306 \ -v /opt/mysql-cluster/master/conf/my.cnf:/etc/mysql/conf.d/my.cnf \ -v /opt/mysql-cluster/master/data:/var/lib/mysql \ -v /opt/mysql-cluster/master/logs:/var/log/mysql \ -e MYSQL_ROOT_PASSWORDMaster2024 \ mysql:8.0注意网络这里需要先建一个自定义网络docker network create mysql-cluster-net。用自定义网络的好处是容器之间可以通过容器名互访从库配SOURCE_HOSTmysql-master就能解析到不用管 IP 变成多少。这一点后面讲容器重启问题时会再强调。4.2 从库配置的差异点在哪从库的my.cnf大部分和主库一致差异集中在几行[mysqld] server-id2 relay_logrelay-bin log_replica_updatesON read_onlyON super_read_onlyON gtid_modeON enforce_gtid_consistencyONserver-id2不重复这是硬性要求。relay_log指定中继日志的前缀方便在数据目录里辨认。read_onlyON让普通用户无法在从库写入防止误操作破坏一致性super_read_onlyON更进一步连有 SUPER 权限的账号也写不了。这两个一起开能挡住绝大多数手滑在从库改数据导致主从冲突的事故。但要提醒一句super_read_onlyON会把复制线程之外的写入全部锁死包括你用 root 账号做维护操作。搭环境阶段如果需要临时导入初始数据得先把它关掉导完再打开。我有次忘了这一步mysqldump导入一直报The MySQL server is running with the --super-read-only option排查了十分钟才反应过来。从库容器的启动命令和主库类似改一下名字、端口和挂载路径即可docker run -d \ --name mysql-slave \ --network mysql-cluster-net \ -p 3308:3306 \ -v /opt/mysql-cluster/slave/conf/my.cnf:/etc/mysql/conf.d/my.cnf \ -v /opt/mysql-cluster/slave/data:/var/lib/mysql \ -v /opt/mysql-cluster/slave/logs:/var/log/mysql \ -e MYSQL_ROOT_PASSWORDSlave2024 \ mysql:8.04.3 启动之后先验证连通性再往下走两个容器起来后先看状态docker ps --filter namemysql-如果某个容器状态是Restarting或者直接退出了马上看日志别急着往下配docker logs mysql-master --tail 100最常见的两种失败一是配置文件语法错误MySQL 直接起不来日志里会明确指出是my.cnf的第几行二是数据目录权限问题日志里是Permission denied。前者改配置后者执行chown -R 999:999 /opt/mysql-cluster/master/data。容器都正常后验证从库能不能访问主库。进从库容器docker exec -it mysql-slave bash mysql -uroot -pSlave2024 -h mysql-master -P 3306 -e SELECT 1;这一步能通说明网络和账号都没问题可以进入下一阶段。如果卡住或者报Unknown MySQL server host说明两个容器不在同一个网络或者名字写错了。这里先解决掉别等到配复制时才发现因为那时的报错信息会绕一层排查成本更高。5. 建立复制链路并验证数据真的同步了5.1 主库创建复制账号并导出初始快照复制账号只干一件事让从库能连上来读 binlog。权限给REPLICATION SLAVE就够了不要图省事给ALL PRIVILEGES账号权限越大泄露后的风险越高。CREATE USER repl% IDENTIFIED WITH mysql_native_password BY Repl2024; GRANT REPLICATION SLAVE ON *.* TO repl%; FLUSH PRIVILEGES;repl%里的%表示允许任意主机连接。在容器网络里从库的 IP 是不固定的所以用%比较省事。如果是生产环境建议限定具体网段。接下来做初始数据快照。GTID 模式下我推荐用mysqldump加--single-transaction的方式它在 InnoDB 上能拿到一致性快照又不锁表docker exec mysql-master mysqldump \ -uroot -pMaster2024 \ --single-transaction \ --master-data2 \ --flush-logs \ --all-databases \ --set-gtid-purgedON \ /opt/mysql-cluster/init.sql几个参数的意思--single-transaction开启可重复读事务保证备份期间数据一致--master-data2会把当时的 GTID 信息以注释形式写进备份文件方便对照--flush-logs在备份开始时切一个新 binlog让位点更清晰--set-gtid-purgedON会把 GTID 信息写进备份导入从库后能正确对应复制起点。这个参数很关键漏了它导入后可能报 GTID 相关错误。备份文件如果很大注意宿主机的磁盘余量。默认导出路径选在挂载目录里容器删了文件还在这点比导到容器内部好。5.2 从库导入数据并开启复制把备份导入从库。前面提到super_read_only会挡住写入先临时关掉SET GLOBAL super_read_only 0; SET GLOBAL read_only 0;然后导入docker exec -i mysql-slave mysql -uroot -pSlave2024 /opt/mysql-cluster/init.sql导入完成后在从库上执行复制配置。GTID 模式下命令很简洁CHANGE REPLICATION SOURCE TO SOURCE_HOSTmysql-master, SOURCE_PORT3306, SOURCE_USERrepl, SOURCE_PASSWORDRepl2024, SOURCE_AUTO_POSITION1, GET_SOURCE_PUBLIC_KEY1; START REPLICA;SOURCE_AUTO_POSITION1就是 GTID 模式的核心从库不再需要手算位点。GET_SOURCE_PUBLIC_KEY1是为了兼容caching_sha2_password认证加了它即使账号不是mysql_native_password也能连上属于兜底。如果是位点模式写法是这样CHANGE REPLICATION SOURCE TO SOURCE_HOSTmysql-master, SOURCE_PORT3306, SOURCE_USERrepl, SOURCE_PASSWORDRepl2024, SOURCE_LOG_FILEmysql-bin.000003, SOURCE_LOG_POS157;日志文件名和偏移量来自SHOW MASTER STATUS的输出必须在锁表或者一致性快照那一刻获取否则会漏数据。这也是位点模式更麻烦的地方。导入之后记得把只读打开回来SET GLOBAL read_only 1; SET GLOBAL super_read_only 1;5.3 状态怎么看字段怎么读执行SHOW REPLICA STATUS\G在 8.0.22 之前这个命令叫SHOW SLAVE STATUS两个都能用但新命令是趋势。输出字段很多重点看这几个字段正常值含义Replica_IO_RunningYesIO 线程正常能从主库拉事件Replica_SQL_RunningYesSQL 线程正常能重放事件Seconds_Behind_Source0或接近 0复制延迟空闲时通常是 0Last_IO_Error空有内容就是 IO 环节出问题了Last_SQL_Error空有内容就是重放环节出问题了Retrieved_Gtid_Set一串 GTID 区间已经从主库拉到的 GTIDExecuted_Gtid_Set和上面一致或更全已经在从库执行完的 GTIDReplica_IO_Running和Replica_SQL_Running有一个不是Yes复制就是断的。这两个字段的组合能快速定位问题层次IO 挂了是连接层面的问题SQL 挂了是数据或语句层面的问题。Seconds_Behind_Source这个值在空闲时可能显示 0但主库持续写入时它反映的是真实延迟压测时可以观察它是否飙升。验证同步最直接的办法是造数据-- 在主库执行 CREATE DATABASE IF NOT EXISTS demo_db; CREATE TABLE demo_db.t_user (id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50)); INSERT INTO demo_db.t_user (name) VALUES (alice), (bob);然后在从库查SELECT * FROM demo_db.t_user;能看到两条记录就说明链路通了。再回从库SHOW REPLICA STATUS看Executed_Gtid_Set有没有变化两边对照着看心里更有底。6. 我踩过的坑和排查思路实录6.1 连接失败的三类报错怎么区分复制配置完SHOW REPLICA STATUS一看Last_IO_Error有内容先从错误码入手。下面这张表是我遇到频率最高的几种错误码报错内容关键词常见原因处理方式1045Access denied账号密码错或主机名不匹配检查repl%是否存在密码是否一致2003Cant connect网络不通容器名解析失败确认两个容器在同一自定义网络1236Could not find first log file位点失效、日志被清理GTID 模式改用AUTO_POSITION11062Duplicate entry从库已有数据重放冲突重新做快照或按 GTID 跳过1032Cant find record从库数据被误删或缺失确认从库只读是否被绕过1045 这个最坑的地方在于账号密码都对但报错依旧。原因往往是主库上存在多个同名账号比如repllocalhost和repl%从库连过来匹配到的是权限更小的那个。用SELECT user, host FROM mysql.user WHERE userrepl;看一眼就清楚了。6.2 数据不一致了怎么安全地恢复主从数据不一致的表现通常是Replica_SQL_RunningNoLast_SQL_Error报主键冲突或者找不到记录。原因可能是有人在从库手动写了数据也可能是主从切换时的边界问题。处理冲突最省事的做法是重建停掉复制重新在主库做一次一致性快照导入从库覆盖再重新配置复制。数据量小的时候十分钟搞定比一点点修数据可靠得多。如果数据量很大、重做代价高可以考虑跳过出问题的事务。GTID 模式下这样操作STOP REPLICA; SET GTID_NEXT出问题事务的GTID; BEGIN; COMMIT; SET GTID_NEXTAUTOMATIC; START REPLICA;这里的 GTID 从Last_SQL_Error或者Retrieved_Gtid_Set里找。必须强调跳过事务意味着从库永远缺了这条变更只在确认这条变更对数据完整性无影响时使用比如重复插入一条已经存在的配置记录。跳过之后要做数据校验别跳完就当没事了。6.3 容器重启后 IP 变了复制直接断这是我早期用 Docker 搭主从最容易翻车的地方。刚开始图省事配置里写的是主库容器的内网 IP类似SOURCE_HOST172.18.0.2。容器重启一次IP 可能变成172.18.0.3复制立刻报 2003 连不上。而且这个错很难第一时间想到因为昨天还好好的。解决办法就是用自定义网络加容器名。创建网络docker network create mysql-cluster-net启动容器时加--network mysql-cluster-net配置里主机名写mysql-master。Docker 内置的 DNS 会把容器名解析成当前 IPIP 怎么变都不影响。这个改动看起来小但省下的排查时间非常多。另一种情况是容器被删了重建数据目录挂载还在但server-id变了。比如重建从库时网络配置没问题但配置文件挂载路径写错容器用了默认的server-id1和主库撞车复制直接拒绝。所以每次重建之后进容器SELECT server_id;确认一下是个成本极低的习惯。6.4 日常该盯哪些指标搭好只是开始长期跑起来要盯几个点。我一般看两类东西。第一类是复制状态本身。写个小脚本定时跑SHOW REPLICA STATUS把Replica_IO_Running、Replica_SQL_Running、Seconds_Behind_Source三个值抓出来任一异常就告警。这么简单的检查能挡住 90% 以上的复制中断事故。第二类是资源水位。SHOW BINARY LOGS;看 binlog 占了多少SHOW REPLICA STATUS里看 relay log 有没有堆积。测试环境磁盘被日志吃满是常事尤其有人跑压测写了几百万行几天就几十个 G。前面配的binlog_expire_logs_seconds就是干这个的但也要确认清理策略真的生效了。还有一条经验不要在主库上跑FLUSH LOGS之后马上做位点操作。这个命令会滚动 binlog如果你用的不是 GTID 模式正在复制的从库可能正好在一个日志切换的边界上出现短暂的状态异常。GTID 模式下这个问题基本不存在这也是我推荐 GTID 的原因之一。最后分享一个我经常用的小技巧。搭这套东西之前我会先写一个简单的docker-compose.yml把两个服务、网络、卷、端口全写进去。这样每次重建就是docker compose down docker compose up -d比手动敲两条docker run更省事也更容易分享给同事。配置如下可以直接拿走用services: master: image: mysql:8.0 container_name: mysql-master networks: - mysql-net ports: - 3307:3306 volumes: - ./master/conf/my.cnf:/etc/mysql/conf.d/my.cnf - ./master/data:/var/lib/mysql environment: MYSQL_ROOT_PASSWORD: Master2024 slave: image: mysql:8.0 container_name: mysql-slave networks: - mysql-net ports: - 3308:3306 volumes: - ./slave/conf/my.cnf:/etc/mysql/conf.d/my.cnf - ./slave/data:/var/lib/mysql environment: MYSQL_ROOT_PASSWORD: Slave2024 networks: mysql-net: driver: bridge用compose的时候要注意卷路径是相对docker-compose.yml所在目录的所以文件得放在/opt/mysql-cluster/下。另外container_name在同一台宿主机上不能重复这点和用docker run是一样的。