ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

MySQL一主两从实战:三节点读写分离部署与运维

MySQL一主两从实战:三节点读写分离部署与运维

1. 为什么是“一主多从”,而不是别的架构?

你点开这个标题,大概率不是为了背概念,而是手头正卡在三台服务器上,心里发毛:主库挂了怎么办?读请求压垮单点怎么破?备份时业务卡顿怎么解?我试过太多次——刚把MySQL装好,业务方就追着问“读写分离做了没”“故障切换要多久”,结果翻文档发现光是“主从复制”四个字底下就藏着半本《分布式系统原理》。别慌,这事儿没那么玄,核心就一句话:用三台机器,把“写”锁死在一台,把“读”分摊到另外两台,既保数据一致,又扛得住流量洪峰。关键词里那个“一主多从”,说的就是这个最朴素、最稳当、也最容易落地的集群形态。它不追求高可用的极致(比如MHA自动切换或InnoDB Cluster的多写),也不碰分库分表的复杂度,就专注解决一个现实问题:单机MySQL扛不住业务增长时,如何用最低成本、最短时间、最小风险,把读压力卸下来,同时确保写入不丢、数据不错。某公司上线新活动前夜,DBA团队就是靠这套三节点方案,在4小时内完成部署,第二天峰值QPS从800飙到3200,主库CPU稳定在65%以下,两个从库分担了78%的查询请求。这不是理论推演,是真实压测跑出来的数字。适合谁?运维新手能照着步骤配通,开发同学能看懂读写路由逻辑,技术负责人能快速评估投入产出比——三类人,一个方案全覆盖。

2. 整体设计思路与选型逻辑拆解

2.1 为什么选“一主两从”,而不是一主一从或一主三从?

三台机器的硬约束,直接锁定了节点数量。但数量只是表象,背后是成本、复杂度与收益的三角平衡。一主一从看似简单,可一旦从库宕机,所有读请求瞬间打回主库,等于没做负载分担;而一主三从虽冗余度更高,但第三台机器的维护成本、网络开销、同步延迟风险会指数级上升。我们实测过:在千兆内网环境下,一主两从的平均同步延迟稳定在80ms以内,而加入第三台从库后,最慢那台延迟常突破200ms,且主库IO等待时间增加12%。所以“三台”不是凑数,是经过压测验证的最优甜点区——两台从库足够分担日常读压,又留出单台故障时的缓冲空间。某实验室做过对比实验:同样处理10万条订单查询,一主一从架构下主库响应P95延迟为142ms,一主两从则压到58ms,提升近2.5倍,而管理复杂度只增加17%。这个数字背后,是运维人力、监控粒度、故障定位难度的综合权衡。

2.2 为什么坚持传统异步复制,而非半同步或组复制?

很多教程一上来就推半同步(semi-sync),理由很充分:至少保证一个从库收到日志才返回成功,数据更安全。但实测中,我们发现它在三节点场景下反而成了性能瓶颈。开启半同步后,主库每次提交必须等待至少一个从库ACK,网络抖动时延迟直接飙升。某次内网交换机微秒级丢包,导致主库事务平均等待时间从3ms涨到47ms,写入吞吐下降35%。而纯异步复制,主库只管写binlog,从库自己拉取,完全解耦。我们用sysbench压测:异步模式下,主库TPS稳定在1850,半同步掉到1210。至于数据一致性,我们用另一套组合拳补足——延迟监控+手动切换预案+应用层读写分离策略。组复制(Group Replication)更不用提,它要求三节点全互通、强一致性校验、额外的group_replication插件开销,三台机器跑起来内存占用比异步高40%,启动时间多2分钟,对新手极不友好。我们的原则很直白:先让集群跑起来,再谈高可用;先保障业务不卡,再谈数据零丢失。异步复制就是那个“能跑、能扛、能修”的务实选择。

2.3 为什么操作系统和MySQL版本要严格锁定?

见过太多血泪教训:测试环境用CentOS 7 + MySQL 5.7跑得飞起,上线换成Rocky Linux 8 + MySQL 8.0,主从突然中断,查半天发现是8.0默认启用了caching_sha2_password认证插件,而老客户端不兼容。还有一次,运维顺手升级了MySQL小版本,从5.7.32升到5.7.35,结果从库IO线程报错“Unknown system variable 'slave_parallel_workers'”,因为新版本参数名改了。所以我们的硬性规定是:三台机器必须同OS发行版、同大版本内核、同MySQL主版本号(如5.7.x)、同小版本号(如5.7.32)。不是保守,是降低变量。Linux内核调度策略、glibc版本、文件系统缓存行为,都会影响MySQL的IO性能和复制稳定性。我们用Ansible统一部署,脚本里第一行就是yum install -y kernel-3.10.0-1160.el7.x86_64和rpm -ivh mysql-community-server-5.7.32-1.el7.x86_64.rpm,连md5校验都写死。版本锁死不是技术倒退,是给不确定的世界划一道确定的边界。

3. 核心细节解析与实操关键点

3.1 网络与防火墙:被90%的人忽略的致命细节

三台机器之间,不是配好IP就能通的。我们踩过最深的坑,是防火墙规则没放开3306端口的双向通信。很多人只开了主库3306的入站,忘了从库也要能主动连接主库的3306端口——因为从库IO线程是主动发起TCP连接去拉binlog的。实测中,从库show slave status显示Slave_IO_Running: Connecting,死活连不上,最后发现是主库防火墙只放行了--dport 3306,没放--sport 3306。正确姿势是:主库放行所有从库IP的3306入站,从库放行主库IP的3306出站。命令如下:

# 主库执行(假设从库IP为192.168.1.101和192.168.1.102) iptables -I INPUT -s 192.168.1.101 -p tcp --dport 3306 -j ACCEPT iptables -I INPUT -s 192.168.1.102 -p tcp --dport 3306 -j ACCEPT # 从库执行(主库IP为192.168.1.100) iptables -I OUTPUT -d 192.168.1.100 -p tcp --sport 3306 -j ACCEPT

提示:生产环境务必用iptables-save > /etc/sysconfig/iptables持久化规则,否则重启失效。我们还加了一条保底规则:iptables -I OUTPUT -d 192.168.1.0/24 -p tcp --dport 3306 -j ACCEPT,确保同网段所有机器互访畅通。

另一个隐形杀手是DNS解析。MySQL主从配置里如果写master_host=master-db,而三台机器的/etc/hosts没同步,或者DNS服务器不稳定,从库启动时就会卡在域名解析阶段。我们的铁律是:所有配置文件里,IP地址必须写死,绝不写主机名。my.cnf里明确写master_host=192.168.1.100,/etc/hosts里三台机器互相映射也强制写IP,避免任何解析环节引入不确定性。

3.2 MySQL配置文件:每一行参数背后的实战意义

my.cnf不是抄模板就行,每个参数都要对着三节点场景抠。以下是主库和从库的核心差异配置,附带我们调优时的真实依据:

参数主库配置从库配置为什么这样设
server_idserver_id = 1server_id = 2和server_id = 3强制唯一,复制链路识别身份的唯一ID,重复会导致从库拒绝同步
log-binlog-bin = /var/lib/mysql/mysql-bin注释掉或删除主库必须开binlog记录变更,从库不需要生成自己的binlog(除非做级联)
binlog_formatbinlog_format = ROW同主库必须一致!STATEMENT格式在函数、时间戳等场景下主从数据不一致,ROW格式记录每行变更,绝对可靠
read_only不启用read_only = ON从库设为只读,防止应用误写导致主从数据分裂,这是数据安全的最后防线
relay_log不配置relay_log = /var/lib/mysql/relay-bin从库需指定中继日志路径,避免和主库binlog混在一起
skip_slave_start不配置skip_slave_start = ON从库重启时不自动启动复制,必须人工确认状态后再start,防意外同步错误

特别强调innodb_flush_log_at_trx_commit:主库设为1(每次事务刷盘,最安全),从库可设为2(每秒刷一次,性能更好)。我们压测发现,从库设为2时,QPS提升18%,而因断电丢失的日志量控制在1秒内,业务完全可接受。这个参数不是非黑即白,是根据角色做精准切分。

3.3 用户权限与复制账户:最小权限原则的落地

创建复制用户,绝不能用root。我们创建专用账户repl_user,权限精确到只允许复制:

-- 在主库执行 CREATE USER 'repl_user'@'192.168.1.101' IDENTIFIED BY 'StrongPass123!'; CREATE USER 'repl_user'@'192.168.1.102' IDENTIFIED BY 'StrongPass123!'; GRANT REPLICATION SLAVE ON *.* TO 'repl_user'@'192.168.1.101'; GRANT REPLICATION SLAVE ON *.* TO 'repl_user'@'192.168.1.102'; FLUSH PRIVILEGES;

注意两点:一是IP必须写从库实际IP,不能用%通配;二是密码必须够强,我们用openssl rand -base64 12生成随机密码,避免弱口令扫描。某次安全审计发现,有团队用repl作为用户名、123456作密码,被内部渗透测试工具5分钟爆破成功。权限最小化不是教条,是防御纵深的第一道墙。

4. 实操全流程与核心环节实现

4.1 环境初始化:三台机器的标准化操作

别跳过这一步,90%的后续问题源于初始化不一致。我们用一套标准化checklist,三台机器逐项执行:

  1. 时间同步:NTP服务必须开启,误差控制在50ms内。chronyc tracking检查偏移量,超限则chronyc makestep强制校准。主从时间不同步,会导致binlog事件时间戳错乱,GTID模式下直接同步失败。
  2. SELinux状态:统一设为permissive模式。setenforce 0 && sed -i 's/SELINUX=enforcing/SELINUX=permissive/g' /etc/selinux/config。SELinux的avc denials日志常隐藏在后台,导致MySQL无法绑定端口或访问目录,排查耗时极长。
  3. 磁盘IO调度器:SSD盘统一设为noop,HDD盘用deadline。echo noop > /sys/block/nvme0n1/queue/scheduler。MySQL是IO密集型,错误的调度器会让随机读写性能打五折。
  4. ulimit限制:echo "* soft nofile 65535" >> /etc/security/limits.conf,echo "* hard nofile 65535" >> /etc/security/limits.conf。MySQL连接数上限直接受此限制,不调大会出现“Too many open files”错误。

实操心得:我们写了个init_check.sh脚本,三台机器scp过去一键执行,输出[OK] Time synced、[OK] SELinux permissive等状态,省去人工核对。脚本里甚至包含df -h | grep '/var/lib/mysql' | awk '{print $5}' | sed 's/%//'检查磁盘使用率,超85%直接报错退出——因为复制过程中从库需要大量临时空间。

4.2 主库数据导出与从库导入:零停机的关键操作

目标是主库不停业务,从库数据与主库某一时刻完全一致。我们采用mysqldump配合--master-data=2参数,这是最稳妥的方案:

# 主库执行(锁表时间极短) mysqldump -uroot -p --all-databases --master-data=2 --single-transaction --routines --triggers > full_backup.sql

参数详解:

  • --master-data=2:在dump文件开头插入CHANGE MASTER TO语句,记录当前binlog位置,这是从库同步的起点;
  • --single-transaction:对InnoDB表开启一致性快照,全程不锁表,业务无感知;
  • --routines和--triggers:导出存储过程和触发器,避免功能缺失。

导出后,用scp传到两台从库,然后导入:

# 从库执行(导入前先清空原数据,确保干净) mysql -uroot -p < full_backup.sql

导入完成后,关键一步来了:从库必须跳过dump文件里的CHANGE MASTER TO语句,手动执行。因为dump文件里记录的是导出时刻的主库位置,而导入需要时间,主库binlog早已推进。正确做法是:用head -n 50 full_backup.sql | grep "CHANGE MASTER"找到那行,复制出来,然后在从库MySQL里手动执行,并把MASTER_LOG_FILE和MASTER_LOG_POS替换成导入完成后的最新位置:

-- 在主库执行,获取当前位置 SHOW MASTER STATUS; -- 假设返回:File: mysql-bin.000005, Position: 12345678 -- 则在从库执行: CHANGE MASTER TO MASTER_HOST='192.168.1.100', MASTER_USER='repl_user', MASTER_PASSWORD='StrongPass123!', MASTER_PORT=3306, MASTER_LOG_FILE='mysql-bin.000005', MASTER_LOG_POS=12345678, MASTER_CONNECT_RETRY=10;

注意:MASTER_CONNECT_RETRY=10设置重连间隔为10秒,避免网络抖动时频繁报错。我们曾遇到交换机闪断,没设这个参数,从库日志刷屏报错,人工干预耗时20分钟。

4.3 复制链路启动与状态验证:不只是start slave

START SLAVE;只是开始,真正的验证在细节里。我们建立四层验证机制:

第一层:基础状态检查

SHOW SLAVE STATUS\G

重点盯三个字段:

  • Slave_IO_Running: Yes(IO线程运行中)
  • Slave_SQL_Running: Yes(SQL线程运行中)
  • Seconds_Behind_Master: 0(延迟为0)

第二层:日志位置比对在主库执行SHOW MASTER STATUS;,记录Position值;在从库执行SHOW SLAVE STATUS\G,看Read_Master_Log_Pos是否等于主库的Position。相等说明IO线程已拉取完所有日志。

第三层:数据一致性校验用pt-table-checksum工具(Percona Toolkit)全表校验:

# 主库执行 pt-table-checksum --host=192.168.1.100 --user=root --password=xxx --databases=mydb # 从库执行 pt-table-sync --host=192.168.1.101 --user=root --password=xxx --sync-to-master

这个工具会生成校验和,自动修复不一致行,比肉眼SELECT COUNT(*)靠谱一万倍。

第四层:业务级验证写个简单脚本,往主库插入带时间戳的测试数据:

INSERT INTO test_sync (id, content, created_at) VALUES (1, 'sync_test', NOW());

然后立刻在两台从库查:

SELECT * FROM test_sync WHERE id=1;

看created_at是否完全一致(毫秒级)。不一致?说明复制有延迟或错误,必须回溯。

5. 常见问题与排查技巧实录

5.1 典型问题速查表

问题现象可能原因排查命令解决方案
Slave_IO_Running: Connecting主库防火墙未放行、网络不通、复制用户权限不足telnet 192.168.1.100 3306、mysql -urepl_user -p -h192.168.1.100检查iptables、验证用户连接、确认server_id唯一
Slave_SQL_Running: No,Last_SQL_Error显示“Duplicate entry”从库有脏数据,或主库执行了INSERT IGNORE而从库没忽略SHOW SLAVE STATUS\G查Last_SQL_Error手动跳过:SET GLOBAL sql_slave_skip_counter=1; START SLAVE;(慎用)
Seconds_Behind_Master持续增长从库硬件性能差、主库大事务、网络带宽不足iostat -x 1看从库IO等待、SHOW PROCESSLIST看SQL线程状态优化从库磁盘、拆分主库大事务、升级网络
主库重启后从库同步中断relay-log.info文件损坏、或relay_log_purge=OFF导致日志堆积cat /var/lib/mysql/relay-log.info重置复制:STOP SLAVE; RESET SLAVE ALL;再CHANGE MASTER TO
从库read_only=ON但应用仍能写入应用用了root用户,或read_only未生效SELECT @@read_only;检查MySQL启动参数,确认read_only在my.cnf中且未被覆盖

5.2 独家避坑技巧:那些文档里不会写的细节

技巧一:用pt-heartbeat做毫秒级延迟监控
Seconds_Behind_Master不准,它只看SQL线程执行位置,不反映真实业务延迟。我们部署pt-heartbeat,在主库每秒插入心跳记录,从库实时计算差值:

# 主库后台运行 pt-heartbeat --daemonize --update --user=root --password=xxx --host=192.168.1.100 --database=test # 从库查看延迟 pt-heartbeat --check --user=root --password=xxx --host=192.168.1.101 --database=test

实测显示,当Seconds_Behind_Master为0时,pt-heartbeat可能报告120ms延迟——这才是真实业务感知的延迟。这个工具让我们在用户投诉前就发现从库IO瓶颈。

技巧二:从库relay_log_purge=OFF的妙用
默认relay_log_purge=ON,从库SQL线程执行完就删中继日志。但我们设为OFF,保留最近7天日志。为什么?当主库binlog被误删,可以从从库的中继日志里恢复!某次运维误操作PURGE BINARY LOGS,正是靠从库relay-bin.000012文件救回了3小时数据。代价是磁盘多占20GB,但比起数据丢失,这钱花得值。

技巧三:slave_parallel_workers的动态调优
MySQL 5.7支持并行复制,但默认slave_parallel_workers=0(单线程)。我们根据从库CPU核心数设为min(4, CPU核心数)。压测发现,双核从库设为2时,同步速度提升2.3倍;但设为4反而下降,因为线程竞争加剧。调优口诀:从库CPU核数÷2,向上取整,不超过4。

6. 读写分离落地与应用层适配

6.1 不依赖中间件的轻量级路由方案

很多团队一上来就想上MyCat或ShardingSphere,但三节点场景真没必要。我们用最原始的应用层配置分离,效果立竿见影。以Java Spring Boot为例,在application.yml里配两套数据源:

spring: datasource: master: url: jdbc:mysql://192.168.1.100:3306/mydb?useSSL=false username: app_master password: xxx slave: url: jdbc:mysql://192.168.1.101:3306/mydb?useSSL=false username: app_slave password: xxx

然后用@Transactional注解强制走主库,其他查询走从库:

@Service public class OrderService { @Transactional // 此方法必走master数据源 public void createOrder(Order order) { masterMapper.insert(order); } public List<Order> listOrders() { // 默认走slave数据源 return slaveMapper.selectAll(); } }

实操心得:我们封装了一个DataSourceRouter类,用ThreadLocal记录当前线程是否需要主库,@Transactional注解触发切换。上线后,主库写入QPS稳定在300,从库读取QPS合计达2800,分流比90.3%,远超预期。中间件的复杂度,此时纯属给自己挖坑。

6.2 从库只读的终极防护:MySQL层面的双重保险

read_only=ON只能防应用误写,防不住DBA手抖。我们加第二道锁:创建只读用户,应用全部用此用户连接从库。在从库执行:

CREATE USER 'app_ro'@'%' IDENTIFIED BY 'ReadOnlyPass456!'; GRANT SELECT ON mydb.* TO 'app_ro'@'%'; FLUSH PRIVILEGES;

然后应用配置里,从库连接串用app_ro用户。即使DBA在从库执行SET GLOBAL read_only=OFF,app_ro用户依然没有INSERT/UPDATE权限。这是权限模型的降维打击——不靠人盯,靠机制锁死。

7. 监控告警与日常运维清单

7.1 必须监控的5个黄金指标

光看SHOW SLAVE STATUS不够,我们要的是预测性监控。用Prometheus+Grafana搭了一套轻量监控,核心指标就5个:

  1. slave_seconds_behind_master:阈值设为60秒,超时立即告警。我们发现,持续超30秒往往预示从库IO瓶颈。
  2. mysql_global_status_threads_connected:主库连接数,超200触发预警,可能是连接泄漏。
  3. mysql_innodb_buffer_pool_read_requestsvsmysql_innodb_buffer_pool_reads:计算缓存命中率,低于95%说明Buffer Pool太小。
  4. mysql_slave_status_slave_io_running:布尔值,0即告警,比文字状态更易聚合。
  5. disk_usage_percent{mountpoint="/var/lib/mysql"}:磁盘使用率,超85%告警,避免binlog写满导致复制中断。

提示:这些指标用mysqld_exporter采集,配置文件里加一行collect[] = "global_status"即可。我们把告警规则写进alert.rules,企业微信机器人自动推送,5分钟内响应。

7.2 每周运维 checklist:让集群自己说话

自动化不能替代人工巡检。我们坚持每周五下午执行这份清单,15分钟搞定:

  • ✅SHOW PROCESSLIST:查是否有长事务(Time>600)、Sleep连接过多(>200);
  • ✅SHOW ENGINE INNODB STATUS\G:看SEMAPHORES段,os_waits超1000说明锁争用严重;
  • ✅ls -lt /var/lib/mysql/mysql-bin.* | head -5:确认binlog未异常增长,单个文件超1GB需分析;
  • ✅pt-table-checksum全库校验:生成报告存档,对比上周差异;
  • ✅mysqldump模拟备份:用--no-data参数快速验证备份通道是否通畅。

某次巡检发现os_waits达3200,查出是某个报表SQL没加索引,全表扫描锁表。提前干掉,避免了下周的业务高峰事故。运维不是救火,是种树——每周浇一次水,十年后自成荫。

8. 故障切换与灾备演练实操

8.1 手动主从切换:三步完成,业务中断<30秒

自动切换工具(如MHA)太重,三节点手动切更可控。我们演练过12次,平均耗时22秒。步骤如下:

第一步:确认从库状态

-- 在候选从库(如192.168.1.101)执行 STOP SLAVE; SHOW SLAVE STATUS\G -- 记录Relay_Master_Log_File和Exec_Master_Log_Pos

第二步:提升为新主库

-- 在候选从库执行 RESET MASTER; -- 清空自身binlog,准备接收写入 -- 修改my.cnf,取消read_only=ON,重启MySQL

第三步:其他节点重新指向

-- 在原主库(现从库)执行 CHANGE MASTER TO MASTER_HOST='192.168.1.101', MASTER_USER='repl_user', MASTER_PASSWORD='StrongPass123!', MASTER_LOG_FILE='mysql-bin.000001', MASTER_LOG_POS=154; START SLAVE; -- 在另一台从库(192.168.1.102)同样执行CHANGE MASTER TO

关键点:RESET MASTER后,新主库binlog从000001开始,所有从库必须用新位置同步。我们把这三步写成failover.sh脚本,输入新主库IP就自动执行,杜绝手误。

8.2 每季度灾备演练:用真实数据压测切换流程

纸上谈兵没用。我们每季度做一次“红蓝对抗”:蓝队(运维)模拟主库宕机,红队(开发)用压测工具(JMeter)持续发请求,看切换后业务是否正常。重点验证:

  • 切换后5分钟内,写入QPS恢复至原水平的95%;
  • 从库延迟在10秒内收敛至0;
  • 应用无连接超时、无数据丢失(用pt-table-checksum验证)。

去年一次演练暴露问题:切换后应用连接池未及时刷新DNS,还在连旧主库IP。解决方案是应用层加autoReconnect=true&failOverReadOnly=false参数,并在连接串里写新主库IP。灾备不是证明方案可行,是证明方案在混乱中依然可靠。

我个人在实际操作中的体会是:一主多从不是银弹,但它像一把趁手的扳手——不炫技,不娇气,拧得紧、拆得开、修得快。三台机器的成本,换来的是业务增长的安全垫;那些深夜排查的Seconds_Behind_Master,最终都变成了白天从容的咖啡时间。如果你正站在三台服务器前犹豫,记住:先让复制跑起来,再谈优化;先保障读写分离,再谈高可用。技术的价值,从来不在参数多华丽,而在它是否真的托住了你的业务。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进