Redis PSYNC2 缺陷事后复盘
原文由 Salvatore Sanfilippo 于 发布,订阅该博客
四天前,有用户在 Redis 的 GitHub 仓库提交了一个严重问题。该问题与 Redis 4.0 全新的 PSYNC2 复制协议有关,性质非常严重。PSYNC2 为 Redis 复制带来了诸多改进,包括在故障转移之后,乃至在从节点受控重启之后,仅通过交换差异数据而非全量数据集来实现重新同步。而这次的问题就出在后一项功能上:在 PSYNC2 中,RDB 文件会附带上复制相关的信息。从节点重启后,会重新加载这些复制元数据,并尝试向主节点发起 PSYNC,握手并接收自上次断开以来的增量数据。
从运维的角度看,这些都是好消息,然而尽管 PSYNC2 自 Redis 4.0.0 正式版发布以来整体相当稳定,涉及从节点重启的这项功能的可靠性却明显不足。这一功能存在两个问题:第一,它是临近发布前才临时加入 PSYNC2 的,并不在最初的设计文档之内。它更像是基于 PSYNC2 已有工作顺带做的一个扩展,却没有像规范的其他部分那样经过同等严格的审查来排查潜在的缺陷和问题。第二,这项功能实际比最初看起来要复杂得多,要在重启后完整恢复从节点上与复制相关的所有状态并非易事。而且,未能恢复某些状态在大多数情况下并不会引发明显的错误,因此很难通过集成测试发现。只有在特定条件触发时,状态的缺失才会导致问题。例如,若未能在从节点的复制状态中正确重建当前选中的数据库,只有在对不同 Redis 数据库都有写入操作时才会出问题,而这个未能正确保存当前选中数据库的缺陷,也只有在特殊条件下才会暴露出来。
在众多 Redis 贡献者的帮助下,尤其是 GitHub 用户 @soloestoy(阿里巴巴的一位 Redis 开发者)的大力协助下,我们近期一直在着手改进 PSYNC2 的若干潜在问题。所有这些工作原本计划在对现有补丁进行充分测试后,于几天内随 Redis 4.0.3 一并发布。然而在收到 issue #4483 后,我意识到必须加快发布 4.0.3,因为这个问题远比我们此前发现的其他 Redis 4 PSYNC2 问题严重得多。
issue #4483 中描述的缺陷本身相当简单,却极具危害性。从节点重启并从 RDB 重新加载复制状态后,主节点的复制积压缓冲区中可能包含以 EVALSHA 命令形式存在的 Lua 脚本执行记录。然而从节点在重启后,其 Lua 脚本引擎中的所有脚本都会被清空,因此无法处理这类命令。其结果是,从节点将无法处理由 Lua 脚本产生的写操作,除非这些脚本使用的是“命令复制”模式——而这并非默认行为,默认是直接复制脚本本身。
我当时有点慌了……为此写了几个备选补丁提交到该 issue 下。最终我们选择了那个无需与旧版本产生 RDB 不兼容的方案,于是我便尽快发布了 Redis 4.0.3。然而我犯了一个错误……这几周我改成了去办公室办公,而不是在家办公。平常我晚上是不工作的,但为了 4.0.3,回到家后我还是打开家里的笔记本,把补丁合并到了 4.0 分支,并做了一些测试。第二天回到办公室后,我换了另一台电脑继续工作,却没有意识到有一个在家合并的提交还没有推送到仓库,因此被遗漏了。
结果我发布的 4.0.3 实际上包含了除那个最关键的复制缺陷修复之外的所有 PSYNC2 修复。我只好尽快又发布了一个补丁版本 Redis 4.0.4,补上了这个复制问题的修复。这本身就已经很糟糕了:升级 Redis 是一项需要计划安排的工作,没有人愿意因为我在准备发布时的失误而连续升级两次……但更糟的还在后面。我在 4.0.4 中加入的那个修复——即针对执行 PSYNC2 的重启从节点上的脚本复制问题——存在一个错误,它完全躲过了所有复制集成测试:该修复的做法是将从节点内存中的 Lua 脚本直接存入 RDB,以便之后重新加载。但没有考虑到,加载脚本的函数在脚本已存在于内存时会触发断言,因此当从节点从主节点接收全量同步并开始加载 RDB 文件时,会因内存中已存在重复脚本而立即崩溃。有用户很快通过 Twitter 报告了这个问题,我在不到 45 分钟内就完成了修复并发布了 4.0.5,但这并不能弥补这一连串修复发布失败所可能造成的各种潜在影响。
上述问题由多方面原因造成:
- 考虑到 PSYNC2 的复杂度和代码改动量,Redis 4.0 被过早地标记为稳定版。下一次,即使以推迟发布为代价,我也会让 Redis 的新大版本在最后一个候选发布版阶段停留更长时间,以便在发布正式版之前发现此类缺陷。
- 我过于急于为 issue #4483 提供修复。即使该缺陷十分严重,也本该多花些时间来搞清楚具体情况,并评估修复本身可能带来的潜在问题。而且正因为过于匆忙,我没有足够仔细地核对构成 4.0.3 的提交集合,导致遗漏了一个修复,不得不再次发布新版本。
- 尽管 Redis 4.0 已有非常严格的 PSYNC2 单元测试和集成测试,包括模拟连续故障转移并在每一步检查数据一致性的测试,但经由此次缺陷我才发现,测试从未尝试将 PSYNC2 与 Lua 脚本复制结合起来。这一点必须改进。此外,基于从节点 RDB 重启后的 PSYNC2 复制测试也需要加强。
我会先从第“3”点的改进做起,今后在每次发布新版本前都会牢记第“1”和第“2”点的教训。对于这次因我的失误而给大家带来的困扰,我致以诚挚的歉意,同时也衷心感谢 @soloestoy 给予的大量帮助与支持。
随机一篇博客
评论
登录后参与讨论