刚换的头像,刷新一下又变回去了
读写分离上线第一天,用户反馈就来了:刚换了头像,刷新一下又变回旧的,再刷新又好了。开发同学一头雾水,我倒觉得眼熟——这正是第 7 篇讲延迟双删时埋的那个伏笔:主从延迟。想讲清楚延迟从哪来,得先把「数据是怎么从主库流向从库的」这件事看透。
先说为什么要有主从。三件事:一是读写分离,读 QPS 涨了就加从库分流;二是高可用,主库挂了从库顶上(第 6 篇治本篇提过);三是数据冗余,给热内存留一份冷备份。主从复制是哨兵和集群共同的地基——地基没搞懂,上面两层的所有故障都看不明白。
第一次握手:全量同步三步走
从库第一次连上主库,彼此还不认识,只能从零开始对账,这就是全量同步:
- 第一步:从库发送
psync ? -1——第一次复制,不知道主库的复制 ID 和进度,只能带问号上场。 - 第二步:主库回复
FULLRESYNC,把自己的 replid 和 offset 交给从库,然后 bgsave 生成 RDB 快照发过去——对,就是第 10 篇那套 fork 加写时复制的流程。 - 第三步:从库清空旧数据、加载 RDB;这期间主库新产生的写命令,先存在复制缓冲区里,等从库加载完一并补发。补发完成,两边对齐。
全量同步是重武器:主库要 fork、要生成快照、要全量传输,从库要清库重载。几十 G 的实例来一次,主库网络和 CPU 都得颤三颤——所以工程上最关心的问题是:什么情况下会退化成全量?怎么避免?答案藏在下一节的环形缓冲区里。
断线重连:增量同步的门票
主库身边有一块复制积压缓冲区(repl_backlog),本质是个环形缓冲区,默认只有 1MB,持续记录最近的写命令流。从库断线重连时会带上自己的 replid 和 offset:如果 offset 对应的数据还在缓冲区里,主库回个 CONTINUE,只把差量命令补发过去,这就是增量同步;如果断得太久、offset 早就被环形覆盖,对不起,重新全量。
所以调大 backlog 就等于多买保险——它的大小按这个账算:平均写入速率 × 能容忍的最大断线时长。写入 5MB/s、希望扛住一分钟网络抖动,就给 300MB。配置长这样:
# 从库配置:连主库、开只读、调大积压缓冲区
replicaof 10.0.0.1 6379
replica-read-only yes
repl-backlog-size 64mb两种同步方式放一张表里:
| 维度 | 全量同步 | 增量同步 |
|---|---|---|
| 触发时机 | 首次复制,或 offset 被环形缓冲区覆盖 | 断线重连且 offset 仍在缓冲区内 |
| 传输内容 | RDB 快照 + 期间的写命令 | 缓冲区里的差量命令 |
| 代价 | fork 生成快照 + 全量网络传输 | 极小,补发差量即可 |
命令传播是异步的:延迟从这来
对齐之后进入日常状态——命令传播:主库执行完写命令,异步发给从库。注意这个词,异步。主库不等从库确认就返回成功了,网络抖一下、从库忙一下,延迟窗口就出现了。开头那个头像 bug 的完整链条:写请求落到主库成功,读请求马上落到还没同步的从库,旧头像闪现。第 7 篇的延迟双删,兜的就是这个窗口。
延迟本身不可怕,可怕的是看不见。主库上跑个 info replication,从库 lag 一目了然:
# 主库上查看复制进度与从库滞后
info replication
# role:master
# connected_slaves:2
# master_repl_offset:183746250
# slave0:ip=10.0.0.2,offset=183746001,lag=1真被延迟困扰时的三板斧:一是写后立刻要读的请求强制走主库,前端路由按业务标记分流;二是用 WAIT 命令等指定数量的从库确认同步后再返回,代价是这次写变慢;三是 min-replicas-to-write 强制主库在有足够健康从库时才接受写入——但慎用,从库全抖动时主库会拒绝写,这是拿可用性换一致性,值不值要看业务。
工程实践清单
应用侧的读写分离,Lettuce 一行配置就支持:
// Lettuce 开启读写分离:读请求优先走从库,全部不可用时回主库
LettuceClientConfiguration config = LettuceClientConfiguration.builder()
.readFrom(ReadFrom.REPLICA_PREFERRED)
.build();再补几条运维经验:
- 一主多从别贪多:每新增一个从库,全量同步就是一次 fork 加一份 RDB 传输,主库网卡容易先成为瓶颈。从库多就上级联复制——从库再挂从库,让主库只复制给少数几个。
- 磁盘慢的机器开无盘复制:
repl-diskless-sync yes让快照直接走网络,省掉落盘环节。 - 从库保持只读:
replica-read-only yes是默认值,别为了图省事关掉它,主从双写等于自造数据分叉。
复盘一下:主从复制靠 replid 加 offset 对账,首次和断线太久走全量,断线不久走增量,日常靠异步命令传播——延迟由此而生,监控和三板斧由此而设。但还有个悬而未决的问题:主库真挂了,谁来决定让哪个从库顶上?下一篇聊聊让 Redis 自己站起来的哨兵:客观下线怎么判定?领导者选举怎么进行?脑裂又是什么?下篇见。
咖啡凉了,记得趁热喝。
评论 (0)