一台挂了数据不能升天: 复制的代价是副本不一致, 半同步是折中不是答案
复制就像抄课堂笔记: 老师讲课(主库写日志), 同桌照抄(副本重放), 你交作业时老师讲没讲到那页(ACK 时机)决定了你俩笔记差多少。机制本质是用日志搬运换取数据冗余——代价是副本永远慢半拍, 所以"一台挂了切过去"之前必须回答: 新副本上有多少客户端已经确认过的写?
同步、异步、半同步不是三种技术, 是同一个问题的三个回答位置: 在主库落盘时答应、在副本收到时答应、还是在全部落盘时答应。答得越晚越安全、越慢。而 WAL(先写日志再改数据)是这一切的地基: 崩溃恢复靠它重放, 复制靠它搬运, 持久性靠它 fsync。
-- MySQL 8.0.22+: 副本看自己追到哪了 (旧名 SHOW SLAVE STATUS) SHOW REPLICA STATUS\G -- 关键: Seconds_Behind_Source 是估算值, 靠它告警会漏报大延迟 -- → Replica_IO_Running / Replica_SQL_Running 双 Yes 才算活着
-- MySQL 半同步: 至少 1 副本收到 binlog 才向客户端返回 OK SET GLOBAL rpl_semi_sync_master_enabled = 1; SET GLOBAL rpl_semi_sync_master_timeout = 1000; -- ms, 超时降级 -- 关键: 降级是"静默"的 — 不配告警, 半同步就名存实亡
-- 双主防主键冲突: 一边奇数一边偶数, 步长 2 SET @@GLOBAL.auto_increment_offset = 1; -- A 库: 1,3,5... SET @@GLOBAL.auto_increment_increment = 2; -- B 库 offset=2 -- 关键: 错开自增只防主键撞, 防不了同行内容冲突 — 双主仍要业务避写
w+r>n 保证读写集合有交集; 反熵进程后台补齐差距。 n, w, r = 3, 2, 2 # Cassandra: QUORUM 级别读写 # 关键: w + r > n → 至少一个副本同时参与本次读写, 拿得到新值 # → 写 2/3 成功即返回; 读 2/3 版本冲突时按时间戳取最新
-- MySQL 默认 row: 记录行变更而非 SQL 文本 SELECT @@binlog_format; -- → ROW -- 关键: UPDATE ... SET t=NOW() 在语句格式下副本重放结果不同 -- → 一切"不确定函数"都是语句复制的天敌
# PostgreSQL: 提交是否等 WAL 刷盘 synchronous_commit = on # 默认: 本地刷盘才返回 # synchronous_commit = off # 只丢最近约 3×wal_writer_delay 的提交, 不损坏 # 关键: off 是"可能丢最近几笔"但绝不损坏 — 可接受的削延迟手段
# mysqldump: InnoDB 单事务一致性快照, 不锁全表 mysqldump --single-transaction --all-databases > full.sql # 关键: --single-transaction 靠 MVCC 快照, 期间其他会话照常写 # → 恢复点 = 备份开始时刻, 再补 binlog 才能到任意点
-- MySQL: redo 总容量决定检查点节奏 (8.0.30+) SHOW VARIABLES LIKE 'innodb_redo_log_capacity'; -- 关键: 检查点是"WAL 能删到哪"的进度条 — 崩溃恢复只重放检查点之后的日志
__consumer_offsets)。区别于按时间删除(delete)。 # Kafka 主题配置: key 只留最新值 cleanup.policy=compact min.cleanable.dirty.ratio=0.5 # 脏区过半才触发, 别太勤 # 关键: compact 不是 delete — 同 key 旧值被清, key 本身永在
# 每月把最近备份拉到隔离环境真恢复一次并跑校验 SQL mysql -h restore-test < full.sql && mysqlbinlog --verify-binlog-checksum binlog.000123 # 关键: 演练记录恢复耗时 — 这就是 RTO 的真实数字, 不是拍脑袋
innodb_flush_log_at_trx_commit=1 + sync_binlog=1)是全持久; 改 0/2 换性能, 断电丢最近事务。 -- 双 1: 每次 commit 都 fsync redo + binlog — 全持久, 最慢 innodb_flush_log_at_trx_commit = 1 sync_binlog = 1 -- 关键: 改 2/0 提速明显, 但 OS 崩溃丢最近 1 秒事务 — 先问 RPO 再动
# 一行写清 SLA, 贴在值班群 # RPO=0 (半同步) / RTO=3min (自动切主) / 延迟从库 1h (防误删兜底) # 关键: RPO 没定义就上异步复制 — "丢了 2 分钟数据"就是扯皮开始
-- ProxySQL: 副本落后超阈值自动摘出读池 UPDATE mysql_servers SET max_replication_lag=10 WHERE hostgroup_id=20; LOAD MYSQL SERVERS TO RUNTIME; -- 关键: 延迟超标的从库继续接读, 就是批量制造"改完没生效"工单
机房网卡抖动 → 半同步等 ACK 超时自动降级异步, 无告警; 两周后主库宕机切主, 客户端已确认的写蒸发。监控补课:
# my.cnf: 半同步开启 + 超时 1s plugin-load = "rpl_semi_sync_master=semisync_master.so" rpl_semi_sync_master_enabled = 1 rpl_semi_sync_master_timeout = 1000 # 监控必须盯的两个状态: 降级即告警 SHOW GLOBAL STATUS LIKE 'Rpl_semi_sync_master_status'; -- OFF = 已降级! SHOW GLOBAL STATUS LIKE 'Rpl_semi_sync_master_no_tx'; -- 降级期异步提交笔数 -- 关键: no_tx 增长就报警 — 这段时间 ACKed 的写切主时可能蒸发
加告警后当月抓到 3 次降级, 每次都在 2 分钟内恢复半同步, 再没出现过切主丢写。
主库与副本断链后仍在接受写, 切换回去就冲突。redis.conf 两条底线参数把"孤立主库"关进笼子:
# redis.conf: 主从 + 数据安全底线 replicaof 10.0.0.1 6379 min-replicas-to-write 1 # 健康副本 <1 时, 主库拒绝写 min-replicas-max-lag 10 # 副本延迟 >10s 不算健康 replica-read-only yes # 关键: 没有 min-replicas-to-write, 主库失联副本后还在"独写" # — 分区恢复后两份"主"数据对不齐, 只能人工比对
topic 建成单副本 + acks=1, broker 重启丢日志段。不丢消息的最低配三件套, 一次配齐:
# producer 侧: ISR 全部确认才算成功 acks=all enable.idempotence=true # 幂等生产者, 重试不重复 retries=2147483647 delivery.timeout.ms=120000 # broker/主题侧: 3 副本, 至少 2 个在 ISR 才可写 replication.factor=3 min.insync.replicas=2 # 关键: acks=all + rf=3 + min.insync.replicas=2 — 少一个都可能丢 # ISR 不足 2 时生产者报 NotEnoughReplicasException, 这是保护不是故障
把"崩溃恢复"变成年度演练: 在 staging 真停电一次, 量出真实 RTO 与日志重放行为:
# PostgreSQL 崩溃恢复演练: 模拟断电 (不走正常 shutdown) pg_ctl -D /data/pg stop -m immediate pg_ctl -D /data/pg start # 观察日志 — 这两行就是 WAL 在救你: # LOG: database system was interrupted; last known up at ... # LOG: database system was not properly shut down; automatic recovery in progress # 关键: 演练记录恢复耗时 = 真实 RTO; 双 1 配置与省 IO 配置分别测一遍
单机单介质备份在机房级事故面前等于零。每日全量 + 异地对象存储, 账号最小权限:
# 每日: xtrabackup 全量到本地 → rclone 到对象存储 (另一介质+异地) xtrabackup --backup --target-dir=/backup/$(date +%F) --parallel 4 rclone copy /backup/$(date +%F) oss:db-backup/$(date +%F) --transfers 8 # 保留策略: 本地 7 天 / 异地 35 天; 每月 staging 真恢复一次 # 备份账号只给最小权限 — 备份文件能拖库, 账号也能拖库 # 关键: 3 份、2 种介质、1 份异地 — 任何一条缺失都不叫 3-2-1
误删后 binlog 已 purge, 常规恢复只能回天到昨晚备份; 专用延迟从库把 RTO 从"彻夜奋战"变 10 分钟:
-- 专用延迟从库: 故意落后 3600s, 专治误删/误 UPDATE CHANGE REPLICATION SOURCE TO SOURCE_DELAY = 3600; -- 8.0.22+ (旧名 MASTER_DELAY) -- 事故时: 停复制保住现场 → 从延迟库回灌误删的表 → 恢复制 STOP REPLICA; -- 关键: 延迟从库不接任何读流量, 只当"时间机器" -- → 1h 内的误删, 比翻 binlog 逐事务前滚快一个数量级
全量快照停在昨晚 23:00, 误删发生在今天 09:47 — 中间全靠 binlog 前滚, 停在事故点之前:
# 1) 恢复昨晚全量 mysql -h restore < full_20260925.sql # 2) binlog 前滚, --stop-position 卡在 DROP 语句之前的 pos mysqlbinlog --start-datetime="2026-09-26 09:00:00" \ --stop-position=104857600 binlog.000451 | mysql -h restore # 关键: pos 从 SHOW BINLOG EVENTS 里人工核对 DROP 前的位置 # 前提: 全量之后的所有 binlog 都在 — 没归档 binlog 就没有 PITR
机房间 RTT 25ms, 半同步跨机房等 ACK 会拖死写性能; 三条底线把延迟和风险都锁住:
# 1) 半同步只在本机房内做: 跨机房等 ACK 会被 RTT 拖死写性能 rpl_semi_sync_master_wait_point = AFTER_SYNC # 5.7+: 先落日志再等副本 # 2) 机房间 binlog 压缩传输 (8.0.20+) binlog_transaction_compression = ON # 旧版本用 slave_compressed_protocol=1 (8.0.23 起已废弃) # 3) 跨机房 lag 单独告警: 本机房秒级, 机房间分钟级是常态, 阈值分开 # 关键: 机房间链路永远按"随时会断"设计 — 上海读服务要能独立降级
Seconds_Behind_Source 空闲时恒 0、大事务重放时不准, 告警形同虚设。用 pt-heartbeat 原理: 让数据自己报告延迟:
-- 主库每秒: 心跳表写入一个时间戳 (随 binlog 复制下去) UPDATE heartbeat SET ts = NOW(6) WHERE id = 1; -- 副本侧: 用"数据里的时间"与本地时钟做差 = 真实端到端延迟 SELECT TIMESTAMPDIFF(MICROSECOND, ts, NOW(6)) / 1000000.0 AS true_lag_sec; -- 关键: 副本自报的秒数不可信 — 空闲时恒 0, 不代表它追得上突发写
offset 提交日志只增不减, 磁盘月涨 800GB; compact 让同 key 只留最新值, 且新消费者能重放出完整状态:
# Kafka 内部主题 __consumer_offsets: 必须紧凑, 否则提交日志无限涨 cleanup.policy=compact segment.ms=86400000 # 段 1 天滚动, compact 以段为单位 min.cleanable.dirty.ratio=0.5 # 脏区过半才压, 默认值够用 delete.retention.ms=86400000 # tombstone 保留 1 天, 别设太短 # 关键: compact 让新消费者组重放全量 key 状态 — tombstone 被过早清掉, 状态就断代
-- 错: 默认异步 → 挂前 2s 的 ACKed 写不在副本上, 切主即丢 -- 对: rpl_semi_sync_master_enabled=1 + 切前 WAIT_FOR_EXECUTED_GTID_SET
timeout 到点自动降级, 不影响运行无人感知. 正解: 监控 Rpl_semi_sync_master_status + no_tx 告警。 -- 错: 只看 Replica_IO/SQL_Running 双 Yes → 降级看不出来 -- 对: Rpl_semi_sync_master_status=OFF 或 no_tx 增长 → 电话告警
# 错: 备份绿灯 300 天 → 出事当天解压报 corrupt block # 对: 每月 restore + checksum 校验 + 记录真实耗时作为 RTO
# 错: min.insync.replicas=1 → ISR 缩到 1 也放行, acks=all 形同虚设 # 对: replication.factor=3 + min.insync.replicas=2 + acks=all
# 错: pg_wal 和 base 同盘 → checkpoint 风暴, WAL 满 PANIC 拒写 # 对: pg_wal 单独 NVMe 卷 + 容量 ≥ 2×max_wal_size
# 错: 主 → 从1 → 从2 → 从3, 每级 2s 延迟叠加 # 对: 从库全部直连主库; IO 压力用 binlog 传输压缩缓解
server_id 唯一 + 启动校验。 -- 错: 两台从库 server_id=1 → 主库上互踢, 复制时断时续 -- 对: server_id 由 IP 后两段生成, CMDB 校验全局唯一
-- 错: DELETE FROM logs WHERE created_at < '2025-01-01'; -- 800 万行一锅端 -- 对: 循环 DELETE ... LIMIT 5000 直到影响行数为 0, 主从都从容
# 错: 每小时 xtrabackup 全量 → 磁盘 util 常年 90%+ # 对: 每日 1 全量 + binlog 实时归档, 恢复粒度照样到秒
-- 错: binlog_expire_logs_seconds=259200 (3天), 误删第 5 天才发现 -- 对: mysqlbinlog --raw --stop-never 实时流到远端; 本地 3 天远端 35 天
REPLICATION SLAVE, REPLICATION CLIENT。 -- 错: GRANT ALL PRIVILEGES ON *.* TO 'repl'@'%'; -- 对: GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'repl'@'10.%';
# 错: crontab 只有一行 xtrabackup, binlog 三天本地自清理 # 对: xtrabackup --backup + mysqlbinlog --raw --stop-never 流式归档远端
read_only 挡不住有 SUPER 权限的账号. 正解: super_read_only=1 + 副本账号最小化。 -- 错: SET GLOBAL read_only=1; -- DBA 账号有 SUPER, 照样写进去 -- 对: SET GLOBAL super_read_only=1; -- 连 SUPER 一起挡住
-- 错: 旧主网络恢复自动追上 → 双主互抄, 自增 ID 飞速内耗 -- 对: 切换脚本第一步: 旧主 STOP REPLICA + SET GLOBAL super_read_only=1
# 错: 架构评审没人提 RPO → 默认异步, 出事才知道丢了 90s 订单 # 对: SLA 声明 RPO=0 → 半同步起步; RPO=5min → 异步+延迟从库
# 错: backup.tar.zst 直接入库 → 出事时 Invalid checksum, 解不开 # 对: sha256sum backup.tar.zst > backup.sha 同传; 恢复前 sha256sum -c
replicate-do-db 让副本不是全量镜像. 正解: 热备副本全量复制, 过滤库单独建链路。 -- 错: replicate-do-db=report → 副本缺业务库, 提升即灾难 -- 对: 热备副本全量复制; 报表库单独链路 + 永不参与选主
-- 错: 延迟 1h 的从库混进读池 → 用户读到 1 小时前的订单 -- 对: 独立 hostgroup + 监控: 延迟从库出现业务 QPS 立即告警
--single-transaction, FLUSH TABLES WITH READ LOCK 锁全库. 正解: InnoDB + 单事务导出; 锁窗口放低峰。 # 错: mysqldump 裸跑 → FLUSH TABLES WITH READ LOCK 全库只读 # 对: mysqldump --single-transaction --set-gtid-purged=OFF (InnoDB)
# 错: 0 20 * * * xtrabackup → 晚高峰抢 IO # 对: 02:30 窗口 + ionice -c3 + xtrabackup --throttle=100