一台机器Docker已经就绪镜像随便拉这样一个环境里要搭MySQL 8的主从复制慢的半小时快的十分钟。这个组合我反复搭过很多次第一遍各种踩坑后面熟了基本就是复制粘贴级别的操作。这次把完整流程、每个参数为什么这么配、以及你极大概率会撞上的问题一次性整理出来照着走就行。这套方案解决的是数据库“一台机器扛全部”的问题单库出故障、读写竞争、备份影响业务。通过Docker起两个MySQL 8实例一个主库一个从库主库写入的数据自动同步到从库读请求可以分流到从库主库挂了还能手动切到从库顶上。文章适合三类人刚接触Docker想练手的开发、要搭测试库做读写分离的运维、以及被“mysql容器起不来”反复折磨的临时救火队员。1. 主从复制在解决什么问题1.1 复制的基本原理与一主一从的角色分工主从复制这个机制说穿了就干一件事把主库上发生的每一次数据变更原样同步到从库上重放一遍。MySQL官方实现依赖的是binlog二进制日志主库每次执行写操作都会把变更记录写进binlog从库通过两个后台线程把这份日志搬到本地再执行完成复制。从库上有两个关键线程。IO线程负责连接主库把主库的binlog拉到本地存成relay log中继日志SQL线程负责读取relay log把里面的SQL操作在从库上重放一次。两个线程配合主库的数据变更就串行地在从库上复现了。整个过程是异步的从库的重放永远可能落后主库一点点这个落后程度就是SHOW SLAVE STATUS里的Seconds_Behind_Master字段单位秒。用生活里的例子类比一下主库就像总部的生产车间binlog就是车间里的“生产流水单”每生产一批货就往流水单上记一笔从库像是外地的分店分店的采购员IO线程定期把流水单抄回来仓库管理员SQL线程照着流水单把货补齐。分店永远不会比总部更早知道新货品但只要你接受这个“延迟”分店就能一直跟着总部的节奏走。1.2 哪些场景真正需要主从复制主从复制最常见的落地场景有四个读写分离、容灾备份、报表计算、滚动升级。读写分离是最大的一类需求。业务读多写少单库扛不住连接数和查询压力就搞一个主库专门写一个或多个从库专门读压力瞬间分散。容灾备份角度从库是主库的一份“活备份”主库数据盘坏了或者整个机器宕掉从库能立刻顶上配合VIP或者域名切换业务中断时间能压缩到分钟级。报表类需求也很适合从库查报表的SQL往往又慢又重放主库跑容易拖垮正常业务放到从库上跑完全不影响主库。另外做数据库升级或者大版本变更的灰度测试从库也是完美的试错环境。但也要泼一盆冷水主从复制不是万能药。如果你的业务就是单机几千数据量的小库读写压力都不大硬上主从只会增加运维成本和故障点没有任何收益。主从复制甚至不能替代备份它只是多了一份数据副本如果你的从库误删了数据主库也会照样同步误删这时候只能靠正式备份来恢复。1.3 为什么这次选择MySQL 8而不是5.7MySQL 8.0从2018年发布到现在经过了多个稳定版本打磨8.0.40这个版本早已是长期支持版中的成熟版本。相比5.78.0在性能上有明显提升特别是对多核CPU的利用、JSON能力、窗口函数、CTE这些新特性都是5.7没有的。如果你是从零开始搭建新环境没有老项目的兼容包袱直接选8.0是明确的选择。不过MySQL 8也有8自己的脾气。默认的认证插件换成了caching_sha2_password老客户端和5.7时代的工具连上去经常报认证失败这个坑在第4章会专门讲。配置文件的行为也有些变化但整体结构清晰。主从复制本身8.0的GTID复制机制比5.7成熟得多故障恢复时处理起来简单不少。2. Docker部署方案的设计思路2.1 Docker对比裸机部署的收益用Docker搭主从复制最直观的收益是环境隔离。同一台机器上裸机装两个MySQL需要处理端口冲突、路径冲突、不同版本的依赖库冲突还要担心系统库被覆盖而Docker容器天然隔离每个实例有自己的文件系统、网络栈、配置目录互不干扰。其次是可复制性。运行两个容器的docker run命令、两个配置文件加起来就这么点东西整个环境可以在任何一台有Docker的机器上重现。我测试过把配置文件和启动命令整理成一个部署包里换一台机器五分钟就能恢复整套环境。这在裸机上根本不敢想。第三个收益是清理简单。测试环境不要了docker rm -f mysql-master mysql-slave再删掉数据目录完事一点系统垃圾都不留。裸机卸载MySQL才是真正的噩梦各种残留目录、开机自启项目、环境变量清不干净。2.2 镜像版本与目录规划的核心考量镜像选mysql:8.0.40不选mysql:latest。latest标签看起来很香但它的指向会变今天拉是8.4明天拉可能就变成9.x了。数据库这种强一致性要求的服务版本漂移是大忌你配置好了主从某天重建容器发现镜像版本变了配置文件不兼容整个架构直接瘫痪。固定版本号是随手就能做的防御动作。目录规划上宿主机建一套目录结构比如/opt/mysql8/master和/opt/mysql8/slave各自下面再分conf、data两个子目录。容器内的/etc/mysql/conf.d挂载宿主机conf容器内的/var/lib/mysql挂载宿主机data。这样做的意义是容器本身只是一个执行环境配置和数据全部在宿主机上删容器不丢数据改配置直接改宿主机文件然后重启容器即可。生产实践里我还习惯把日志目录也挂出来方便排查但本文为了演示干净先不另做logs目录。2.3 网络规划自建Docker网络并固定IPDocker默认的bridge网络有个问题一个容器重启后IP地址可能变化而主从复制关系建立后从库是拿着主库IP去连接的如果主库IP漂移复制直接断掉。解决思路是自建一个docker network手动指定网段让主从两个容器用固定IP。我用的网络规划创建一个/24的独立网段比如172.20.0.0/24主库固定172.20.0.10从库固定172.20.0.11。这样一来容器之间互访走固定IP不受重启影响同时宿主机端口照常映射主库3306、从库3307外部客户端也能通过宿主机IP加映射端口连接对应实例。两条访问路径都通畅后续做验证和调试非常方便。这个“固定IP端口映射”的混合方案比“只映射端口靠localhost访问”或者“只有容器IP不映射端口”都更实用。后者只能从容器内部操作调试数据库还得反复exec进容器非常别扭前者可以拿着宿主机的mysql客户端或者可视化工具愉快地连接。3. 完整实操逐命令搭建一主一从3.1 拉取镜像并准备宿主机目录先确认Docker正常工作直接拉镜像这一步可能耗时取决于你的网络情况docker pull mysql:8.0.40拉好后确认一下本地确实有这个镜像docker images | grep mysql然后创建宿主机目录结构。我习惯把环境统一放在/opt/mysql8下面这样清楚也方便清整mkdir -p /opt/mysql8/master/conf /opt/mysql8/master/data mkdir -p /opt/mysql8/slave/conf /opt/mysql8/slave/datadata目录是MySQL的数据目录初始状态下是空的。挂载到容器后MySQL第一次启动时会在里面初始化数据所以权限必须对。如果是root创建的目录直接把目录属主改成当前Docker用户对应的UID或者干脆chmod 755。很多MySQL容器起不来的原因就是挂载目录权限有问题这个坑下面细讲。3.2 编写主从配置文件每个参数为什么这么配先写主库配置文件/opt/mysql8/master/conf/my.cnf[mysqld] server_id 10 log_bin mysql-bin binlog_format ROW gtid_mode ON enforce_gtid_consistency ON character_set_server utf8mb4 collation_server utf8mb4_unicode_ci innodb_buffer_pool_size 256M再写从库配置文件/opt/mysql8/slave/conf/my.cnf[mysqld] server_id 20 log_bin mysql-bin binlog_format ROW gtid_mode ON enforce_gtid_consistency ON relay_log relay-bin character_set_server utf8mb4 collation_server utf8mb4_unicode_ci innodb_buffer_pool_size 256M两个配置文件长得差不多但有两处必须不一样。server_id在主从集群里是节点身份标识每个实例必须唯一我直接用10和20方便后面看状态时一眼认出来。如果你把两个配置一模一样的文件分别挂到两个容器复制关系建立后从库会立刻报错因为无法区分日志来源。另一个点是主库的binlog是给从库拉取用的从库的binlog在本文场景下不是必需的但我也开着原因是同一个从库如果未来要作为更高一级主库的下游或者想接备份工具开日志是未雨绸缪。gtid_mode和enforce_gtid_consistency是GTID复制的开关。MySQL复制有两种方式老式的是靠binlog文件名加日志偏移量position定位复制点新式的是靠全局事务IDGTID自动定位。GTID方式最大的好处是故障恢复时不用手工去对“文件位置”只要集群里的GTID记录连续从库会自动找到正确的位置拉起复制。配置上必须在启动前就写在配置文件里如果等实例跑起来再用SET GLOBAL临时改GTID这类参数限制很多改起来一步三坎。binlog_format选ROW还是STATEMENT正常情况下选ROW不会错。STATEMENT格式记录的是SQL语句本身同样的语句在从库上重放可能因为环境差异产生不同结果ROW格式记录的是每一行的数据变更从库照着改行数据即可一致性最强唯一的缺点是日志体积大一点但对主从这种场景一致性比体积重要。同步MySQL官方工具比如canal也要求binlog_formatROW。外部的挂载点对应容器内的/etc/mysql/conf.d目录这是默认加载配置的地方。注意官方镜像里的/etc/mysql/my.cnf文件最后一行是!includedir /etc/mysql/conf.d/会把conf.d目录下所有.cnf文件自动include进来。所以我们把宿主机配置挂到conf.d下即可不用去覆盖原始my.cnf。3.3 启动主库并完成基础初始化先创建一个自定义网络规划好子网docker network create --subnet172.20.0.0/24 mysql-net再启动主库容器docker run -d \ --name mysql-master \ --net mysql-net \ --ip 172.20.0.10 \ -p 3306:3306 \ -v /opt/mysql8/master/conf:/etc/mysql/conf.d \ -v /opt/mysql8/master/data:/var/lib/mysql \ -e MYSQL_ROOT_PASSWORDRoot123456 \ mysql:8.0.40命令拆开解释--net指定刚创建的网络--ip固定容器IP-p 3306:3306把容器内MySQL端口映射到宿主机3306-v两段挂载分别是配置目录和数据目录-e MYSQL_ROOT_PASSWORD是初始化时设置root密码。容器启动后第一次运行会执行数据库初始化这个过程大约十几秒到几十秒依赖机器性能。等待一会儿后确认主库起来了docker ps | grep mysql-master看到状态是Up就说明容器活着。此时进入容器操作MySQLdocker exec -it mysql-master mysql -uroot -pRoot123456能登录就表示主库初始化成功。顺手在容器内看一次master日志状态SHOW MASTER STATUS\G这时能看到Executed_Gtid_Set字段还是空的因为还没有任何事务产生。这个命令等下创建完复制账号后再执行一次会得到我们需要的关键信息。3.4 在主库创建复制专用账号并获取同步起点复制账号原则上不应该用root权限最小化是数据库的基本素养。在MySQL命令行里执行CREATE USER repl% IDENTIFIED BY Repl123456; GRANT REPLICATION SLAVE ON *.* TO repl%; FLUSH PRIVILEGES;GRANT REPLICATION SLAVE是复制专用的最小权限这个权限只允许账号拉取binlog日志不能读写任何业务数据。生产环境里更精细的做法是把%改成从库的固定IP比如172.20.0.11让账号只在指定来源可连这能减少账号泄露后的风险面。我这里为了演示用%实际部署建议收窄。在8.0默认认证插件下这个账号是用caching_sha2_password方式创建的因为主从都是8.0从库连接完全没问题不需要特意指定mysql_native_password。只有从库或客户端是5.7或更早版本时才需要把账号改成老认证方式这个放第4章展开。创建完账号后重新执行一次SHOW MASTER STATUS\G你会看到类似这样的输出File: mysql-bin.000003 Position: 157 Binlog_Do_DB: Binlog_Ignore_DB: Executed_Gtid_Set:记录下File和Position的值。这是在GTID未开启时从库定位复制起点需要的信息。由于我们在配置中已经开启了GTID后面从库可以直接用MASTER_AUTO_POSITION1让MySQL自动定位File和Position实际上用不上了但知道它们是什么、代表什么对理解主从复制有好处。3.5 启动从库并配置主从关系启动从库容器命令基本同主库区别在名字、IP和映射端口docker run -d \ --name mysql-slave \ --net mysql-net \ --ip 172.20.0.11 \ -p 3307:3306 \ -v /opt/mysql8/slave/conf:/etc/mysql/conf.d \ -v /opt/mysql8/slave/data:/var/lib/mysql \ -e MYSQL_ROOT_PASSWORDRoot123456 \ mysql:8.0.40等一段时间容器的初始化完成后进入从库容器docker exec -it mysql-slave mysql -uroot -pRoot123456此时从库还是一个独立的空库没有任何业务数据。要建立主从复制先验证从库能连上主库。有个直接的办法在从库容器里直接用mysql客户端连接主库IP测试docker exec -it mysql-slave mysql -h172.20.0.10 -urepl -pRepl123456能连上、看到MySQL欢迎信息说明从库到主库的网络链路和账号权限都通了。这一步很有必要网络不通或者账号密码错误都能在这里暴露而不会等到配置主从后一脸茫然。测试完退出回到从库的mysql命令行执行CHANGE MASTER TO语句。因为我们在配置里开了GTID用GTID方式配置最干净不用去找binlog文件名和偏移量CHANGE MASTER TO MASTER_HOST172.20.0.10, MASTER_PORT3306, MASTER_USERrepl, MASTER_PASSWORDRepl123456, MASTER_AUTO_POSITION1;先解释一下参数MASTER_HOST是主库容器IPMASTER_PORT是主库容器内部的MySQL端口3306注意不是宿主机映射端口3306因为从库容器直接通过Docker网络访问主库容器走的是容器网络不是宿主机的端口映射。MASTER_USER和MASTER_PASSWORD就是刚才创建的复制账号。MASTER_AUTO_POSITION1告诉MySQL用GTID自动定位同步点。如果你的配置没开GTID那么就改用老式position方式配置CHANGE MASTER TO MASTER_HOST172.20.0.10, MASTER_PORT3306, MASTER_USERrepl, MASTER_PASSWORDRepl123456, MASTER_LOG_FILEmysql-bin.000003, MASTER_LOG_POS157;MASTER_LOG_FILE和MASTER_LOG_POS就取自第3.4节SHOW MASTER STATUS得到的File和Position。注意两个写法的区别虽然只是一行参数但背后的定位逻辑完全不同GTID方式是把定位动作交给系统position方式是手工指定从哪个日志文件的哪个偏移量开始拉。配置完成后启动复制线程并查看状态START SLAVE; SHOW SLAVE STATUS\G重点看几行Slave_IO_Running: Yes Slave_SQL_Running: Yes Seconds_Behind_Master: 0Slave_IO_Running和Slave_SQL_Running都必须是Yes这是复制健康的两个最基础指标。Seconds_Behind_Master显示从库落后主库多少秒刚启动时是0说明已经跟上了。这两个线程任何一个显示No都代表复制异常具体的错误信息在输出的Last_IO_Error和Last_SQL_Error字段里比配置文件日志排查效率高得多。3.6 数据同步验证从写入到读取的完整链路复制配置完成光看线程状态还不够得真刀真枪验证一遍数据链路。回到主库容器建一个测试库和表插几条数据CREATE DATABASE testdb; USE testdb; CREATE TABLE user_info ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, age INT DEFAULT 0 ); INSERT INTO user_info(name, age) VALUES (张三, 28), (李四, 32), (王五, 25);切换到从库容器命令行看数据到了没有SHOW DATABASES; USE testdb; SELECT * FROM user_info;如果能看到同样的库、同样的表、同样的三行数据核心复制链路就通了。再验证一下DDL和DELETE同样能同步-- 主库执行 ALTER TABLE user_info ADD COLUMN city VARCHAR(30); DELETE FROM user_info WHERE age 25; UPDATE user_info SET name 张三丰 WHERE id 1;再从库查一次表结构和数据都跟上变化说明复制机制工作正常。这里我建议把验证做完整不要只查一次SELECT就完事因为主从复制不仅同步INSERTDDL、DELETE、UPDATE同样走binlog全部验证一遍才算真正放心。至此Docker下的MySQL 8主从复制已经搭建完成。但从我实际经验看很多人卡在了第4章的某个环节下面把高频坑逐个拆开。4. 排错与避坑清单4.1 容器启动失败数据目录权限和配置文件问题MySQL容器最常见的启动失败原因就是数据目录权限不对。宿主机的/opt/mysql8/master/data如果是root创建的容器内MySQL进程以mysql用户运行往目录里写文件就会提示类似Cant create/write to file /var/lib/mysql/xxx然后容器反复退出重启。遇到容器起不来第一件事是看日志这是最快定位的手段docker logs mysql-master日志里如果有Permission denied相关信息直接修改目录权限chown -R 999:999 /opt/mysql8/master/data /opt/mysql8/master/confMySQL官方镜像里mysql用户的UID是999把宿主机目录直接chown给999简单有效。如果目录里已经残留了初始化的文件也可以清空目录再重启容器重新初始化测试环境这样处理最省事。还有一种情况是配置文件语法导致MySQL启动失败。比如[mysqld]小节点写成了[mysql]或者参数名拼错MySQL会在日志里报类似unknown variable的错。这种只要对照配置逐行检查即可特别注意[mysqld]标签下的参数才是服务端启动参数。4.2 报错caching_sha2_password认证插件兼容性问题执行CHANGE MASTER TO或者从库连接主库时如果报错说Authentication plugin caching_sha2_password无法加载或者认证失败原因在于账号的认证方式。MySQL 8.0默认用caching_sha2_password这个插件对客户端版本有要求MySQL 5.7及更早版本不支持部分老版本客户端工具也不兼容。这种情况的处理方式是让复制账号使用老式认证插件ALTER USER repl% IDENTIFIED WITH mysql_native_password BY Repl123456;改了之后重新连接测试即可。需要注意MySQL 8.0较新版本官方已经开始逐步弃用mysql_native_password如果你的主从都是8.0完全不需要走这一步。我见过有人从网上抄了老配置把所有账号都改成老认证反而引入了不必要的兼容隐患。原则是主从都是8.0什么都不用改只有跨版本场景才动认证插件。4.3 复制中断错误码1236和1045等常见报错复制线程挂掉是常见问题报错信息最常出现两个错误码。Got fatal error 1236表示从库拉取binlog时在主库找不到对应日志文件或位置本质上是主库binlog被清理或从库记录的同步点已经过期。在GTID模式下常见原因是主库开启了expire_logs_days之类的清理策略从库长期断开后主库早期binlog已删从库需要的GTID事务已不存在。解决办法是重建复制关系重新获取主库当前日志位置再CHANGE MASTER TO一次让从库从最新位置开始同步。代价是重建期间的数据需要自行处理所以监控复制状态很重要。错误码1045本质是Access denied账号密码或者权限问题。从库连主库时被拒绝排查顺序是先用mysql -h主库IP -urepl -p在从库容器里手动测试连接如果手动连接也是Access denied问题就在账号侧重新确认GRANT有没有生效如果手动连接成功但START SLAVE后还是1045检查CHANGE MASTER TO里的密码有没有打错尤其是密码里有特殊字符时很容易因为转义问题弄错。4.4 两个实例的server-uuid冲突如果你图省事直接把主库的data目录复制了一份给从库用启动后从库会报错说发现重复的server-uuid。MySQL每个实例启动时会生成一个server-uuid存在data目录的auto.cnf文件里复制data目录会同时复制这个文件两个实例就有了相同的UUID复制关系直接不可用。判断是不是这个问题的办法是分别查看两个容器的auto.cnfcat /opt/mysql8/master/data/auto.cnf cat /opt/mysql8/slave/data/auto.cnf如果server-uuid完全一样删除从库的auto.cnf再重启从库即可rm /opt/mysql8/slave/data/auto.cnf docker restart mysql-slaveMySQL启动时会检测到缺少auto.cnf自动生成一个新的UUID。这也是为什么我不建议直接复制data目录来搭从库正确做法是让每个实例自己完成初始化通过binlog或GTID做数据同步而不是手动拷贝数据文件。4.5 Docker环境本体的坑镜像慢、权限、网络搭建过程还没到MySQL层面Docker本身就可能给你使绊子。docker pull镜像下载特别慢这是很多人第一步就卡住的地方。官方源在国内网络环境下确实不稳定实际做法是在Docker Daemon配置里加registry-mirrors镜像加速地址。不同平台的加速地址变化比较快直接搜当前可用的配置方法几分钟就能改好。Linux环境里执行docker命令报permission denied while trying to connect to the docker api是典型的权限问题当前用户不在docker组里。root下执行没问题普通用户就报这个错。解决办法是把用户加入docker组然后重新登录sudo usermod -aG docker $USER改完需要重新进入会话让用户组生效。临时解决可以用sudo docker来跑命令但长期用sudo操作docker会让文件权限和容器行为变得乱糟糟不建议。Windows下Docker Desktop启动失败提示virtualization support not detected这是Windows虚拟化没开或者WSL2环境没配置好。去BIOS确认Intel VT-x或AMD-V已启用Windows功能里打开Windows Hypervisor Platform和虚拟机平台然后重启。这个问题和MySQL无关但Docker Desktop起不来后面所有步骤都是空谈。4.6 主从状态异常速查表干脆把高频问题整理成一张速查表做运维排错的时候直接对着看现象可能原因处理方式容器一直Restarting数据目录权限不足chown -R 999:999 数据目录Slave_IO_Running: No网络不通、账号权限不足、主库binlog丢失从库容器内手动连接主库测试确认账号和日志位置Slave_SQL_Running: No从库已有数据与主库冲突重建从库数据或跳过冲突事务后重新校准Got fatal error 1236从库需要的binlog已清理重建复制关系重新获取日志位置Access denied 1045复制账号密码或授权有误核对账号密码确认REPLICATION SLAVE授权server-uuid重复复制data目录导致删除从库auto.cnf后重启docker api权限报错用户不在docker组sudo usermod -aG docker $USER镜像下载慢镜像源问题配置registry-mirrors镜像加速补充一个重要提醒从库是“只读目标”但MySQL默认不会主动拦截写入。如果你在从库上执行了INSERT或UPDATE会导致从库数据和主库不一致甚至复制线程报主键冲突彻底中断。生产环境的从库建议在配置里加read_only1只允许复制线程写入其他连接全部只读。想从库还能接收业务写入的特殊架构比如双主另说不在本文主从模式范围内。我实际使用中的几个建议搭建主从这套流程我前前后后过手了很多遍有几个使用习惯是真的能减少麻烦的。第一个是生产环境一定要用GTID方式虽然position方式也能跑但一旦要从故障中恢复GTID能自动定位到正确日志位置省去大量手工对齐时间。第二个是容器命名、网段、固定IP这些细节刚开始可能觉得无所谓等到要排查问题时就知道有多重要一台机器上跑着七八个容器没有清晰命名和固定规划环境基本等于失控。第三个建议是数据目录挂载出来这件事千万别省。我遇到过容器重建后数据全丢的情况就是当初图方便没挂卷。只要把conf和data都挂到宿主机容器就是完全的“无状态执行环境”随便删随便重建数据一分不丢。这套环境搭完之后还可以继续扩展。想让从库分担读压力就把业务里的读请求连接从库的3307端口想做一个更完整的高可用方案可以在主从外面套一层HAProxy或者KeepAlived主库故障自动把流量切到从库。后续我打算专门写一篇用Docker Compose把这套主从配置固化成编排文件的做法到时候一键就能拉起整套环境比手动敲一条条docker run省事得多。
阅读完成 · 觉得有帮助?