ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MySQL主从复制原理与Docker部署实战:从binlog到GTID排错指南

MySQL主从复制原理与Docker部署实战:从binlog到GTID排错指南 很多人对“MySQL主从复制”的第一印象是这是DBA的活儿开发不用管。可真当自己负责一个系统、一台服务器甚至一个课程项目时数据库挂了没人帮你切读流量大了也没人帮你分担这时候才意识到主从集群的基础知识绕不过去。这篇我决定把MySQL主从复制的原理和一套基于Docker的实战部署放在一起讲先搞懂日志是怎么流转的再动手把容器环境搭起来最后给出我实际踩过的几个坑和排查思路。无论你是刚学MySQL没多久的技术新人还是正打算给团队搭一套测试环境的后端开发这篇文章都适合你照着操作一遍。先说结论主从复制没有想象中那么神秘核心就是一台主库把变更记录写进binlog从库把这段日志拉过来重新执行一遍。但真正动手部署时细节远比原理多——容器网络怎么规划、配置漂到哪里、位点怎么对齐、报错怎么排查每一步都可能卡住一两个小时。我会把整套过程拆成可复现的步骤并且把我遇到过的故障现场直接复盘给你。1. 从单库到主从先想清楚为什么要做这件事1.1 单库的三个天花板我见过不少团队一开始都觉得自己“用不上集群”理由是数据量没那么大、并发没那么高。但实际上单库模型会先后撞上三堵墙。第一堵墙是读写并发。业务一忙所有读和写都挤在一台实例上连接数先被占满接着慢查询和锁等待交替出现系统响应时间直线上升。第二堵墙是单点可用性。这台机器一旦宕机、磁盘满、内存耗尽或者被误操作清空整个依赖它的服务就全部瘫痪而且恢复时间完全看运气。第三堵墙是数据备份的低效。对单库做物理备份或逻辑备份要么独占资源要么备份过程本身就会影响线上查询。主从复制对这三堵墙给出了一套朴素但实用的解法主库负责写从库分担读主库意外挂掉后可以手动把从库提升为主库备份操作也能切到从库执行不干扰主业务。这也是为什么即便现在各种分布式数据库层出不穷MySQL主从仍然是最普遍、最经济的一种架构选择。1.2 主从不是万能药先搞清楚它的边界说句实在话不少人对主从复制寄予了过高的期望以为搭完之后数据库就自动“高可用”了。实际上MySQL原生主从复制并不会自动故障转移——主库挂了以后从库不会自己站出来接客。你需要借助MHA、Orchestrator这类额外工具或者自己写好脚本去做提升和切换。主从复制也解决不了“写的水平扩展”问题。所有的写入仍然要走主库从库只是把主库的日志拿来重新跑一遍本质上是增加读能力而不是改写能力。如果你期望的是分库分表、自动扩容那主从复制只是其中最底层的基座还得再叠加中间件层面的方案。另外还有一个经常被忽略的问题异步复制本身意味着从库数据“最终一致”极端情况下主库突然宕机有些日志还没来得及发给从库这部分数据就会丢失。生产环境想降低风险还得考虑半同步复制或无损复制方案。我把这些边界写在前头是不希望你们看完文章后把它直接搬到生产环境却发现和想象中不太一样。2. 主从复制的工作机制binlog日志的流转逻辑2.1 三个线程把日志串起来主从复制说白了是一个日志传递和回放的过程三条日志环节参与其中主库的binlog、从库的relay log中继日志、以及从库上执行的SQL线程。你可以把主从复制想象成一份报纸从编辑部送到读者手里的过程编辑部负责写稿主库写入binlog快递员把报纸从编辑部运到站点I/O线程拉取binlog到relay log读者再从站点取报阅读SQL线程回放relay log。三个环节只要有一个断了整个复制链路就会出问题。具体来说主库会运行一个专门的binlog dump线程每当有事务提交就把产生的日志记录写入binlog文件。从库上则有两个常驻线程I/O线程负责连接主库读取主库binlog中的新事件写到本地relay logSQL线程负责读取relay log并顺序执行这些事件使从库的数据变更和主库保持一致。这里有个很常见的误区从库并不是直接下载主库的binlog然后自己解析执行而是先完整落到relay log再回放。所以你在容器里查看从库数据目录时可能会看到类似relay-log.000002这样的文件它们是复制的中间产物没有它们复制链路照样转不起来。2.2 binlog格式怎么选binlog有三种格式STATEMENT、ROW、MIXED。STATEMENT格式记录的是SQL语句本身优点是日志量小缺点是某些语句在不同机器上执行结果不一定一致比如使用了UUID()、NOW()这类非确定性函数或者对数据依赖顺序的操作在主库和从库执行结果可能不一样。ROW格式记录的是实际变更前后的行数据日志量相对更大但复制结果最可靠任何一条数据改动都能精确地在从库重放也是官方推荐和8.0版本默认使用的格式。MIXED格式则让MySQL根据语句类型自行判断有时记录语句有时记录行。我的建议是没有特殊理由直接用ROW格式。虽然日志文件会变胖但换来了可预期的一致性遇到麻烦时定位问题也容易得多。排查复制报错时ROW格式的报错信息通常更直观能直接告诉你是哪一行数据出了问题。2.3 位点模式与GTID模式两种“物流单号”的区别传统的主从复制靠“文件名文件偏移量”来定位日志位置也就是常说的binlog文件名加POS。配置时通过MASTER_LOG_FILE和MASTER_LOG_POS指定从主库哪个位置开始拉取日志。这种方式最大的问题是位点信息是写在配置文件或命令里的一旦后续要重置复制关系、增加从库、切换主库很容易因为位点填写错误导致复制起点不对排查起来非常痛苦。GTIDGlobal Transaction Identifier就是为了解决这个问题而引入的全局事务标识。你可以把GTID理解成一个“快递单号”每个事务在全局都有唯一编号。从库不再需要记住某个文件的具体字节偏移只需要告诉主库“我已经执行过哪些事务编号”主库就知道该把剩下哪些事务发给它。配置上更简单逻辑上也更清晰跨库迁移或故障切换时会方便很多。我实操时的建议是如果是全新搭建的环境直接上GTID模式如果是已有老环境要改造需要先确认所有实例版本都支持GTID并按照官方步骤逐步切换。这篇文章里的部署会同时给出两种方式你可以自己选。3. Docker化部署前的三个关键决定3.1 镜像版本8.0还是5.7很多人还停留在MySQL 5.7的习惯里但这里我建议新项目直接使用8.0。8.0已经发布多年功能、性能和稳定性都成熟了参数默认值也更合理比如默认字符集是utf8mb4binlog默认就是ROW格式。8.0还在并行复制、安全认证等方面做了不少增强。如果你是因为老项目而不得不用5.7也要注意它已经停止官方维护尽量不要再用它搭新环境。镜像选官方仓库的mysql即可注意拉取时明确指定标签比如mysql:8.0不要随手敲个latest避免后面版本变化带来不必要的影响。还有个小提醒MySQL 8.0的默认认证插件是caching_sha2_password如果你使用的客户端工具版本较旧会出现连不上的情况需要在创建用户时显式指定mysql_native_password或者在客户端升级适配。这个坑在创建复制账号时也会遇到后面我会细说。3.2 网络模式容器主从必须互通Docker部署主从有一个容易想当然的环节网络。两个容器各起各的端口也都映射到宿主机了但容器内部的IP并不属于同一张网卡互相之间可能根本ping不通。最稳妥的做法是创建一个自定义bridge网络让主库和从库都连接到这个网络里并使用容器名作为互相访问的地址。这样做的好处是容器重建后IP变了也不影响通信因为MySQL连接时使用的是容器名对应的内部DNS解析。我见过有人图省事把主从连接地址写成宿主机IP加映射端口这在部分场景下也能跑通但当宿主机IP变化、防火墙调整、多套环境并存时问题会成倍增加。自定义网络并不麻烦强烈建议一开始就按规范来做。Windows和Mac上用Docker Desktop也一样先创建网络再启动容器这套流程是通用的。3.3 配置与数据目录第一次挂载最容易翻车Docker容器本身是“临时”的容器一删数据就没了。所以部署有状态服务时必须把配置目录、数据目录、日志目录通过-v参数挂载到宿主机上。主库和从库的配置需要分别写到各自的宿主机目录中例如/opt/mysql-master/my.cnf和/opt/mysql-slave/my.cnf再挂载到容器内MySQL会读取的目录。官方镜像通常会自动加载/etc/mysql/conf.d下的配置片段所以你只需要把自定义配置写到这个路径下就行。我遇到的翻车现场是有人把数据目录挂载到了宿主机上相同的位置导致主库从库互相覆盖数据还有人启动容器时忘了加-v初始化完才发现容器一重启配置全没了。所以先规划好目录结构再拿起始命令这一步别急。4. 从零部署MySQL主从环境核心命令与完整操作4.1 构建主库容器我先创建一条自定义网络名字起得清楚一点后面两个容器都用它docker network create mysql-cluster-net然后准备主库的配置文件存到宿主机/opt/mysql-master/my.cnf[mysqld] server-id 1 log-bin mysql-bin binlog_format row gtid-mode ON enforce-gtid-consistency ON log-slave-updates ON这里逐个解释一下。server-id在主从复制里必须唯一它是实例身份的标识log-bin开启二进制日志没有它主从复制无从谈起binlog_formatrow是前面说过推荐的复制格式gtid-mode和enforce-gtid-consistency用于启用GTID模式log-slave-updates让从库在回放主库日志时也记录自己的binlog这样从库以后还能继续做下一级主库这个参数建议直接开着避免以后要扩展时又要重启实例。启动主库容器docker run -d \ --name mysql-master \ --network mysql-cluster-net \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDRoot123456 \ -v /opt/mysql-master/my.cnf:/etc/mysql/conf.d/my.cnf \ -v /opt/mysql-master/data:/var/lib/mysql \ mysql:8.0端口映射这里有个细节我把主库的3306端口映射到宿主机3306从库到时候映射到3307。如果本机其他端口被占用你可以按自己的情况调整但后面连接时端口别搞混。容器启动后等待十几秒让数据库完成初始化再确认它正常运行docker ps | grep mysql-master docker exec -it mysql-master mysql -uroot -p输入密码进入MySQL控制台后跑一下SHOW VARIABLES LIKE server_id;确认读到的是配置里的值。如果读到默认的1也可能配置没生效如果读到自定义值说明配置文件加载正常。4.2 创建复制账号并锁定主库位点在主库上执行下面的SQL创建一个专门用于复制的账号注意账号的host是通配符%允许从库以任意来源IP连接CREATE USER repl% IDENTIFIED BY Repl123456; GRANT REPLICATION SLAVE ON *.* TO repl%; FLUSH PRIVILEGES;如果你用了MySQL 8.0这里要特别留意认证插件的问题。默认创建的账号使用caching_sha2_password一些版本的MySQL从库连接时会报错“Authentication plugin caching_sha2_password cannot be loaded”。最简单的兼容做法是创建时直接指定旧版认证插件CREATE USER repl% IDENTIFIED WITH mysql_native_password BY Repl123456; GRANT REPLICATION SLAVE ON *.* TO repl%; FLUSH PRIVILEGES;随后查看主库当前的日志文件和位点4.3 构建从库容器并建立复制通道从库的配置放在/opt/mysql-slave/my.cnf[mysqld] server-id 2 log-bin mysql-bin gtid-mode ON enforce-gtid-consistency ON log-slave-updates ON注意server-id不能和主库一样。从库其实也可以不开binlog但既然我们用的是GTID模式并且考虑到以后可能做级联复制或一主多从建议同样开启。启动从库容器docker run -d \ --name mysql-slave \ --network mysql-cluster-net \ -p 3307:3306 \ -e MYSQL_ROOT_PASSWORDRoot123456 \ -v /opt/mysql-slave/my.cnf:/etc/mysql/conf.d/my.cnf \ -v /opt/mysql-slave/data:/var/lib/mysql \ mysql:8.0这里端口映射成3307是因为同一台宿主机上3306已经被主库占用了。如果你在其它机器上部署端口可以保持一致。容器起来后进入从库执行CHANGE MASTER语句。这里分两种模式说明。使用GTID方式时从库侧执行CHANGE MASTER TO MASTER_HOST mysql-master, MASTER_PORT 3306, MASTER_USER repl, MASTER_PASSWORD Repl123456, MASTER_AUTO_POSITION 1, GET_MASTER_PUBLIC_KEY 1;MASTER_AUTO_POSITION1表示使用GTID自动定位GET_MASTER_PUBLIC_KEY1用于解决MySQL 8.0首次连接时获取公钥的问题。你可能会问为什么需要这个公钥参数因为8.0的账号默认使用caching_sha2_password首次握手建立加密连接时需要获取服务器的RSA公钥客户端默认不会自动向服务器请求所以提前加这个参数能少踩一个坑。如果你希望用传统位点方式那就不开GTID定位改用CHANGE MASTER TO MASTER_HOST mysql-master, MASTER_PORT 3306, MASTER_USER repl, MASTER_PASSWORD Repl123456, MASTER_LOG_FILE mysql-bin.000001, MASTER_LOG_POS 157, GET_MASTER_PUBLIC_KEY 1;这里的MASTER_LOG_FILE和MASTER_LOG_POS必须是从主库SHOW MASTER STATUS中读取到的最新值。新手最容易在这里填错比如填了0或者填了一个曾经看到过的旧位点结果复制的起始位置对不上后续必然报错。所以我更推荐GTID方式省去人工对准位点的麻烦。在文章后面的排错章节我会专门讲位点填错会引发什么现象。4.4 启动复制并检查关键状态执行完CHANGE MASTER后在从库启动复制线程START SLAVE;然后查看复制状态SHOW SLAVE STATUS\G重点关注三列Slave_IO_Running: 应该为Yes代表I/O线程连接主库正常并正在拉取日志。Slave_SQL_Running: 应该为Yes代表SQL线程正在回放日志。Seconds_Behind_Master: 从库落后主库的秒数新环境里通常是0或很小的值。如果看到的是Connecting或者No说明复制链路有问题。不用急我在下一节把最常见的情况都列出来。另外如果你用了GTID模式可以顺便检查GTID是否已在主从间同步SHOW MASTER STATUS;在主库和从库分别查看Executed_Gtid_Set正常情况下从库会逐渐追平主库的GTID集合这就表示复制正沿着正确轨道运行。5. 复制状态验证与四个高频故障排查链路5.1 一张表验证同步是否真正生效复制搭建完成后一定要做一个简单但关键的验证在从库避免写入的前提下主库建库建表插数据然后回从库查看结果。这能确认整个链路是通的。在主库执行CREATE DATABASE demo; USE demo; CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); INSERT INTO user (name) VALUES (张三), (李四);然后到从库执行SHOW DATABASES; USE demo; SELECT * FROM user;如果看到刚才插入的两行数据恭喜你主从链路已经跑通。对这个验证过程我有两个建议。第一从库上不要执行任何写操作哪怕手滑建了一张表都可能破坏复制的一致性。第二验证完之后再故意破坏一次用来观察故障现象——很多人不敢这么试但其实在测试环境里演练故障比生产出现问题再慌慌张张地查要好得多。5.2 故障一Slave_IO_Running一直Connecting这是IO线程连不上主库导致的。排查思路从网络、账号、参数三个层面依次推进。首先确认两个容器在同一条自定义网络里且能互通docker exec -it mysql-slave ping mysql-master如果ping不通检查网络名是否一致容器是否都运行在mysql-cluster-net上。Docker网络这块看起来简单但实际部署中我发现相当多的人栽在拼写错误上比如网络名多打了一个字符或者容器启动时忘了加--network参数。然后检查复制账号能否正常连接。可以在从库容器内手动尝试连接主库docker exec -it mysql-slave mysql -h mysql-master -P 3306 -u repl -p如果提示密码错误、账号不存在或host不匹配就去主库重新授权。记住你创建用户时写的是repl%如果写成repllocalhost从库容器自然连不进来。还有一种情况是主库的bind-address默认监听所有地址一般不会卡在这里但如果你在配置里手动限定了bind-address127.0.0.1那即使从库网络通畅也连不进来。遇到Slave_IO_RunningConnecting先别急着怀疑密码把网络连通性、账号权限、bind-address排查一遍大部分问题都能定位。5.3 故障二Slave_SQL_RunningNo与1062主键冲突SQL线程停止要比IO线程停止复杂一点因为它代表从库在回放日志时遇到了数据冲突。最常见的报错是一串数字比如1062对应“Duplicate entry”。原因通常是主库插入了某条记录从库对应表也已经存在相同主键的数据回放到这条时自然就报重复了。复现这个场景很简单主库插入一行id1的记录从库同步后有这条数据然后你在从库手动再插入一条id1的记录虽然我说过不要写从库但故障演练时可以这么做接着回主库修改或插入数据触发同步SQL线程就会卡在1062上。遇到这种报错先看具体错误信息SHOW SLAVE STATUS\G关注Last_SQL_Error字段它会明确告诉你出错时间、错误码、具体是哪条SQL执行失败。处理思路分两步先解决从库的数据冲突再跳过或修复出错的事务。如果只是测试环境数据也不重要可以手动从从库删除产生冲突的那条记录然后重启SQL线程STOP SLAVE; SET GLOBAL SQL_SLAVE_SKIP_COUNTER 1; START SLAVE;SQL_SLAVE_SKIP_COUNTER1的意思是让SQL线程跳过下一条出错事务适用于单条错误不严重、能够忽略的情况。但如果是生产环境我会严肃地提醒你跳过错误不是根本解决之道你应该对比主从两边的数据找出不一致的根源必要时用工具做一次数据校正。跳过机制只是给了你一个“先让业务跑起来”的窗口。5.4 故障三auto.cnf里藏着的UUID冲突这个坑多半出现在你使用了一个已经初始化过的数据目录去复用容器比如你把/opt/mysql-slave/data直接从备份目录或其他容器里拷过来。MySQL实例在数据目录下有一个auto.cnf文件里面保存了该实例的server UUID。如果主库和从库的auto.cnf内容恰好一致虽然主从复制配置正确但是复制启动时会被判定为同一台机器报错信息里会有Fatal error: The slave I/O thread stops because master and slave have equal MySQL server UUIDs。解决办法很简单先停止从库容器删掉从库数据目录里的auto.cnf再重新启动容器MySQL会自动生成一个新的UUID。如果主从已经互相建立了复制关系删除前记得先确认状态必要时STOP SLAVE重建UUID后重新START SLAVE即可。这类问题最坑的地方是它不按常理出牌配置看起来一切正常网络也通但复制就是起不来。我的经验是凡是复用了数据目录、或者克隆过容器的场景第一时间先检查两边的auto.cnf。5.5 故障四起始位点错误导致复制错位传统位点方式的部署中另一个高频故障是把MASTER_LOG_POS填错了。比如你执行CHANGE MASTER时填入了旧位点从库会尝试从那个位置开始拉取日志但主库的binlog并不会无限往后保存要么日志文件已经被清理要么起始位置的内容和从库当前状态对不上表现为复制通道建立失败或者复制出去了数据错乱。排查这个问题的思路是重新到主库执行SHOW MASTER STATUS把当前最新的日志文件和位点记录下来然后回到从库STOP SLAVE重新执行一次CHANGE MASTER填入最新位点后再START SLAVE。如果你确实需要从某个历史时间点开始复制那就要先确定该时间点对应的binlog和位点而不是想当然地填0或填1。这也是我一再强调GTID模式更省心的原因——它把定位的工作交给了MySQL自己。说到位点我再补充一个容易忽视的环节主库的binlog是有保留期和清理策略的。如果从库落后主库太久主库的binlog已经被轮转清理从库就无法追平只能重建整个从库实例。所以监控里Seconds_Behind_Master一变大要尽快处理别拖到日志被清掉。6. 主从架构的下一步从入门到可用的过渡6.1 一主多从、级联复制、双主跑通一主一从之后你自然会发现这套链路可以横向扩展。读压力继续增长时可以再拉两台从库把只读流量分散到多个实例上。扩展时不需要重新创建主库按照前面的步骤再添加一台从库让它从主库拉取日志即可。如果从库数量很多或者主从跨地域也可以考虑级联复制也就是让一台从库继续作为下一级从库的主库。这正好解释了为什么我在从库配置中把log-slave-updates打开——如果不开从库回放主库日志时不会写自己的binlog那它就没有资格再做下一级的主库。还有双主架构两个实例互为主从。这种方案能应对单点写入失败但写入冲突需要业务层处理复杂度会上一个台阶。我建议新手先把一主一从和一主多从搞熟练再去碰双主或MMM方案。6.2 安全加固与半同步如果你的主从链路是跨机房部署或者经过不完全可信的网络我建议至少开启MySQL SSL复制。8.0默认支持SSL配置上需要生成证书或使用自签名证书然后在CHANGE MASTER语句里追加MASTER_SSL1。另外前面提到异步复制存在主库宕机丢数据的风险生产环境通常会用半同步复制来缓解。MySQL 8.0原生支持半同步插件启用后主库在等待从库确认日志接收后才会提交事务能够在性能和可靠性之间取得一个折中点。如果对这块感兴趣可以单独拿一套环境去测重点观察rpl_semi_sync_master_status和rpl_semi_sync_slave_status的状态值。6.3 切换演练和我的实用建议最后说一个我亲身实践过的建议主从搭建完之后一定要做至少一次切换演练。把主库容器人为停掉在从库上执行STOP SLAVE; RESET SLAVE ALL;此时从库就变成了独立主库。让业务临时连到这个库上先跑着验证应用配置和新端口、新账号是否匹配。演练完之后再把原主库恢复重新建立复制关系。整个过程走一遍你才能真正理解主从切换不是一句“把从库改成主库”那么简单。我平时最常被人问的问题是主从复制已经搭好了怎么保证从库数据和主库完全一致实际操作中直接用percona-toolkit里的pt-table-checksum和pt-table-sync工具可以校对和修复一致性但它依赖从库临时开放写权限生产环境操作前一定要先评估。这个工具不在本文范围内但如果你遇到了两边账目对不上的情况可以往这个方向查一查。就我个人体会而言学主从复制的最佳方式就是亲手搭一遍然后故意把环境弄坏再亲手把它修好。光看不练你永远理解不了“位点”“relay log”“SQL线程停止”这些词到底意味着什么。等到你在测试环境里把Connecting、1062、UUID冲突都亲身处理过一遍再面对生产环境时心里就有底了。
返回列表