1. 问题场景还原:当从节点罢工时
那天凌晨3点,监控系统突然告警——电商平台的商品详情页加载超时率飙升到47%。我们紧急登录服务器排查,发现所有查询请求都堆积在主库上,而本该承担读流量的3个从库实例全部显示"Replica stopped"。更棘手的是,这套基于MySQL主从架构的读写分离系统已经平稳运行了两年多,突如其来的崩溃让整个团队措手不及。
这种情况在采用主从架构的中大型系统中并不罕见。根据Percona的年度数据库报告,约68%的MySQL生产环境至少经历过一次从库异常中断。当从节点不可用时,原本设计精巧的读写分离架构会立即退化成单点模式,所有流量压向主库,轻则查询延迟飙升,重则引发主库雪崩。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 主从架构的脆弱性解剖
2.1 复制线程的死亡三角
MySQL的主从复制依赖于三个关键线程:主库的binlog dump线程和从库的I/O线程、SQL线程。在我们的案例中,通过SHOW SLAVE STATUS命令发现SQL线程报错"Could not execute Write_rows event on table product_sku; Duplicate entry '1837' for key 'PRIMARY'"——这是典型的主从数据不一致导致复制中断。
这种不一致往往源于:
- 人为直接在从库执行写操作(开发同学有时会图方便在从库临时改数据)
- 主库异常重启导致binlog位置跳变
- 网络闪断导致部分事务丢失
2.2 连接池的雪球效应
现代应用通常通过连接池(如HikariCP、Druid)访问数据库。当从库宕机时,连接池的行为会放大故障:
- 健康检查失败标记从库连接为不可用
- 连接池将读请求全部路由到主库
- 主库连接数暴增触发
max_connections限制 - 新连接等待超时又引发应用层重试
我们当时的监控显示,主库连接数在8分钟内从120激增到850(上限1000),系统开始丢连接。
3. 应急恢复三板斧
3.1 快速止血:临时读写混合
立即在应用配置中注释掉读写分离策略,让所有流量走主库。对于Spring Boot项目,调整spring.datasource.hikari.read-only属性为false。这虽然违背了架构设计初衷,但能最快恢复服务。
重要提示:此操作会导致主库负载激增,需同步实施SQL限流。我们通过设置
max_execution_time=2000强制终止执行超过2秒的查询。
3.2 数据一致性修复
使用Percona Toolkit的pt-table-checksum和pt-table-sync工具:
bash复制# 校验主从差异
pt-table-checksum --replicate=test.checksums h=主库IP,u=admin,p=密码
# 自动修复差异
pt-table-sync --replicate=test.checksums h=主库IP,u=admin,p=密码 --sync-to-master
修复后执行:
sql复制STOP SLAVE;
START SLAVE;
3.3 连接池熔断配置
在Druid连接池中添加以下配置,避免从库故障拖垮主库:
xml复制<bean id="readDataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="validationQuery" value="SELECT 1"/>
<property name="testWhileIdle" value="true"/>
<property name="timeBetweenEvictionRunsMillis" value="30000"/>
<property name="connectionErrorRetryAttempts" value="3"/>
<property name="breakAfterAcquireFailure" value="true"/> <!-- 关键参数 -->
</bean>
4. 高可用改造方案
4.1 从库健康检查增强
传统方案只检查从库进程是否存在,我们升级为三层检测:
- 端口检测:基础TCP连接
- 复制状态检测:
SHOW SLAVE STATUS中的Seconds_Behind_Master值 - 业务SQL探测:定期执行
SELECT * FROM heartbeat LIMIT 1
使用Keepalived实现自动摘除故障节点:
config复制vrrp_script chk_mysql_slave {
script "/usr/local/bin/check_slave.sh"
interval 3
weight -20
}
vrrp_instance VI_1 {
track_script {
chk_mysql_slave
}
}
4.2 读写分离中间件选型
对比三种主流方案:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 应用层路由 | 零额外组件,延迟最低 | 故障转移慢,代码侵入性强 | 小型稳定系统 |
| ProxySQL | 动态配置,支持查询重写 | 增加5-8ms延迟 | 中大型动态环境 |
| MyCat | 分片能力强 | 运维复杂,存在单点问题 | 超大规模分库分表 |
我们最终选择ProxySQL,其核心配置如下:
sql复制INSERT INTO mysql_servers(hostgroup_id,hostname,port) VALUES
(10,'master',3306),
(20,'slave1',3306),
(20,'slave2',3306);
INSERT INTO mysql_query_rules (rule_id,active,match_pattern,destination_hostgroup,apply) VALUES
(1,1,'^SELECT.*FOR UPDATE',10,1),
(2,1,'^SELECT',20,1);
4.3 延迟补偿机制
对于无法忍受旧数据的场景(如支付系统),实现"主库优先"策略:
java复制@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ReadPreference {
ReadType value() default ReadType.ANY;
}
enum ReadType {
MASTER, // 强制走主库
PREFER_MASTER, // 优先主库,失败降级
ANY // 任意从库
}
5. 预防性监控体系
5.1 关键指标监控项
部署以下Prometheus监控规则:
yaml复制- alert: MySQL_Slave_Lag
expr: mysql_slave_status_seconds_behind_master > 30
for: 5m
labels:
severity: warning
annotations:
summary: "从库延迟超过30秒 (instance {{ $labels.instance }})"
- alert: MySQL_Slave_Not_Running
expr: mysql_slave_status_slave_io_running == 0 or mysql_slave_status_slave_sql_running == 0
for: 1m
labels:
severity: critical
5.2 定期校验脚本
设置每周自动运行的校验任务:
python复制def check_replica_consistency():
for slave in slaves:
diff = run_pt_table_checksum(master, slave)
if diff > 0:
alert(f"主从不一致表数量: {diff}")
if auto_repair:
run_pt_table_sync(master, slave)
6. 容器化环境特别注意事项
在K8s环境中部署MySQL主从时,需要特别注意:
- 持久化卷声明必须使用
ReadWriteMany模式 - 配置反亲和性避免主从在同节点
yaml复制affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values: [mysql]
topologyKey: kubernetes.io/hostname
- 使用Sidecar容器监控复制状态:
dockerfile复制FROM alpine
COPY check_slave.sh /
CMD ["sh", "/check_slave.sh"]
那次事故后,我们花了三周时间完成了这套高可用改造。现在当任何一个从库崩溃时,系统能在15秒内自动隔离故障节点,并通过动态权重调整将流量合理分配到健康实例。最关键的领悟是:主从架构不是配置完复制参数就高枕无忧了,需要建立从连接池到中间件的完整容错体系。
