MongoDB 对Raft协议的修改

412 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第30天,点击查看活动详情

MongoDB 对Raft协议的修改

MongoDB早先是主备复制的架构,显然这会有诸多问题,例如丢失更新,切换一致性,可用时间等。后来,MongoDB对此做了大量重构,实现了pv0协议,这虽不是一个完美的协议,根据 MongoDB 口述历史,最初的集群协议——pv0——是由一位经验相对有限的工程师编写的,据说MongoDB 工程师将 pv0 描述为“很棒”和“几乎正确”。不幸的是,在分布式系统工作中,“几乎正确”是不够的。

尽管 pv0 在简单的情况下工作,但它存在一些基本问题。在已知情况下,明显提交的数据可能会在集群故障转移期间丢失。

即使在非失败模式下,仍然可能在这些早期版本的 MongoDB 中看到不一致的结果。当网络错误使 MongoDB 不确定数据是否已到达远程节点时,可能会发生这种情况。在其他情况下,MongoDB 将允许读取未提交的数据。

在 3.6 版本中,MongoDB 实现了 Raft 协议,解决了这些问题。 注意MongoDB对raft协议做了诸多的修改,以适配他的特殊需求,当我在调优基于Raft协议复制副本的存储系统时,遇到了诸多不满的和困惑的地方,当时我并不知道方向在哪里,终点在哪里,是否因为Raft协议本身就过多缺陷不适合我们的场景呢?直到有一天我看到MongoDB的Raft协议修改,显然MongoDB团队对raft协议是不满意的地方其实还有其他原因,我会逐一慢慢道来:

基于Pulling的Raft协议

我们知道传统raft协议是基于push的,下图左边中,leader主动将日志push到follower中,而MongoDB的修改版raft如右图所示:节点主动pulling日志。这是对raft协议的巨大修改,最困难的地方在于,leader不知道follower是否收到了日志,如何去确认quorum?投票箱balloBox什么时候可以仲裁完成?如果leader基于pulling轮询的机制去检查日志是否完成仲裁,这显然拖慢了进度。

image-20221229215140008

MongoDB的做法是:下图中,拉取客户端follower需要主动去确认日志4已经被安全地保存。这无疑增加了通信成本,当然,消息应该可以复用,例如将ack确认消息放在pulling请求中(捎带),这其实会增加延迟(commit延迟)。

image-20221229215928302

居于pulling的机制,MongoDB的选择居于一下点:

  1. 拓扑结构更松散灵活,
  2. 减低leader负载。
  3. 跨区WAN场景有节省带宽的优势。(考虑有两个副本follower在额外的区域)

MongoDB可以支持非leader去同步数据,这称之为同步源节点,如下图:

image-20221229220447058

它的好处如下:

  • 同步服务器从其同步源请求新的日志条目。
  • 同步源回复一系列日志条目。
  • 如果项目遵循 相同的历史 记录,同步服务器会附加日志项目 。
  • 如果源日志和服务器日志不同,同步服务器可能有未提交的数据,必须清除不同的后缀。日志分歧的检测是基于匹配同步服务器的最后一个日志条目与源发送的日志段的第一个条目。如果它们匹配,则源已发送相同历史的延续。
  • 同步服务器通知其同步源它的最新程度,包括同步服务器的期限。

该论文讨论了一些其他优化。我将只提到一个似乎最有趣的——投机执行。通过推测执行,节点不会等待提交通知来应用操作,而是推测性地立即将其应用于存储。由于底层存储引擎是多版本的,因此系统仍然可以通过跟踪最新提交的版本来返回强一致性读取。多版本存储还允许在某些操作无法提交的情况下进行回滚。

你能想象吗?raft已经提交给状态机的数据,MongoDB是可以对其进行回滚的!这是非常复杂的设计了,其设计来源于MongoDB的可调一致性需求,业务可以选择不同的一致性级别,在副本上,MongoDB可以快速的提交日志,但是会根据客户端writeConcern设置,如是否为majority。

请注意,MongoDB收购了wiredTiger,对其提出了回滚数据的请求得以被实现,正常的如rocksdb等存储引擎是没有这个能力的。

image-20221229220628283

​​