第五章备份-part3,designing Data-Intensive Applications 中文翻译摘要

多Leader备份(Multi-Leader Replication)

这章当目前位置我们都在讨论单Leader的备份架构,这个方案用的很普遍,但也有些问题。因为只有一个Leader,所有写请求都要通过通过这个Leader,一旦客户端和Leader连接断了,就不能写了。

一个最自然的解决方法是多个节点都可以接受写请求,备份方式是一样的,每个节点处理一个写请求后把更新发送给其他节点。我们管他叫多Leader配置。(multi-leader configuration)。每个节点都是Leader,有都是其他Leader的Follower。

多Leader备份的应用场景

如果你只有单个数据中心,那就不要用了,因为收益远比付出的代价小。但是在某些场景下这么搞是有意义的。

多数据中心

如果你的服务是多地部署的,那Leader只能在某一地的机房,这样所有的写请求都要发到这个机房去。跨机房跨地域的请求会很慢。在多Leader的配置下,你可以每个机房设置一个Leader,在机房内部,还是运用前面讲到的Leader和Follower的机制做备份。机房之间,2个Leader之间彼此把自己的更新发送给对方。方法如Figure 5-6


那么我们来对比下在多数据中心场景下,单Leader和多Leader的优劣

  • 性能
    在单Leader下,每个写请求都必须翻山越岭发到某一个数据中心的Leader去,这个会有很大的耗时,而且这违背了最初设立多个数据中心的目的。在多Leader下,每次写请求可以发送给本地的Leader,然后在异步的同步给其他的数据中心。从用户侧看来,性能更好。
  • 数据中心容错性
    单Leader配置下,一旦Leader所在的数据中心挂了,失败重启(failover)机制会在另一个数据中心再选一个Leader。在多Leader场景下,其他的数据中心可以继续工作,等到异常的数据中心恢复后,再让这个中心备份重新同步
  • 网络异常容错性
    跨数据中心的网络往往走公网,这就要比内部网络可靠性差很多了。单Leader 情况下,网络问题十分重要,因为一旦网络不好,这个系统就跪了。但是多Leader情况下对网络的容错性就好很多。因为客户端可以先发个本地的Leader然后异步的在Leader之间同步。

有些数据库默认支持多Leader,但是往往都是外部实现的。例如MySQL的Tungsten Replicator, PostgreSQL 的BDR,Oracle的GoldenGate。

虽说多Leader的配置很多数据库都作为一个额外特征,但是他和很多其他的数据库特征在一起可能会有很奇怪的问题。比如自增键, 触发器,完整性约束可能都是个问题。所以一般来说多Leader很多时候都是个危险的东西,能不用就不用。

离线操作

如果你的应用需要即使断网也能正常工作,那多Leader就很好用了。比如你有一个日历的app, 无论身处何地,网络如何,都要能够正常查看你的会议(读请求),加入新的会议(写请求)。这就要求当你做任何更新的时候,在设备下次连上网络的时候,需要把这些更新发送给服务器和其他的设备。

这种情况下,每个设备有一个自己的本地数据库作为Leader,同时还有一个异步的多Leader同步流程把你的数据正确备份到所有设备去。备份节点的落后可能有几个小时甚至几天。

从架构角度讲,这跟多个数据中心间进行多Leader备份没什么区别。每个设备是一个数据中心,网络连接非常不可靠。在日历同步的丰富历史中,多Leader备份机制是表现不错的一个。

协同编辑

实时协同编辑应用允许多个人同时编辑一个文档。比如Etherpad和Google Docs。我们一般不把这件事情当做一个数据库同步的问题,但是当出现离线编辑的时候,就跟上面的问题比较像了。当一个用户修改文档时,首先把更新发送给本地的备份,然后异步的把更新发送给server和其他用户。

如果你要保证永远没有编辑冲突,那应用就需要在一个用户编辑前加锁。当一个用户想要修改文档时,他必须等前一个人提交他们的修改并且释放锁。这种协同模型就跟单Leader备份加事务的方式很像了。但是为了更快,你可能希望每次更新更新的内容很小也不要锁。但这就会带来多Leader备份的各种问题,比如冲突解决。

应对写入冲突

多Leader架构最大的问题就是写入冲突,所以我们需要一个重提解决机制。举个例子,假设一个网页被两个人同时编辑,用户1把标题从A改成B,用户2把标题从A改成C,两个用户的成功把自己的更新提交到了本地的Leader。但是当更新异步的在节点中同步时,这就有冲突了。这个问题在单Leader的数据库中是不存在的。


同步方案 vs 异步冲突检测

在单Leader时,在第一个提成功提交之前,第二个请求会一直阻塞住,或者直接失败了。但是在多Leader时,两个写入都会成功,这种冲突会在未来的某个时间点异步备份时被发现。这个时候让用户来解决冲突已经太晚了。从道理上来说,你可以在线的检测冲突,也就是说只有在成功把更新数据发送到所有其他的节点后才告诉用户更新成功了。但是这就是去了多Leader的优势,你没有办法再允许每个备份节点独立的接受写请求了。如果要同步的冲突检测,还不如就搞一个单Leader备份框架呢。

避免冲突

最简单的处理冲突的方法就是避免冲突。如果应用能够保证对一条数据的所有写都发给同一个Leader,就不会有冲突了。因为大部分多Leader备份的实现对于冲突的处理能力都很弱,避免冲突往往是一个推荐的方法。

举个例子,如果一个用户修改他自己的数据,那你可以将特定用户的请求永远发给相同的数据中心,用这个数据中心的Leader进行读写。不同的用户有不同的数据中心(比如基于他们的地理位置选最近的数据中心),从单个用户的角度来看,这就变成了一个单Leader的架构。

当时有时候你可能会需要修改用户对应的数据中心,可能因为某一个数据中挂了,也可能是因为用户地理位置变了,你要把他移到当前离他最近的数据中心。这种情况下,又会有冲突出来,因为在某段时间内,同一个用户会写两个Leader。

一致性收敛

单Leader的架构下,写请求是有顺序的,多个更新同时发生时,最后一个写确定了这个字段的最终值。但是多Leader下,写入就没有一个确定顺序了。在Figure 5-7中,leader 1的标题先改成B,后又变成C,Leader 2却是先是C,后是B。这两个顺序没有谁比谁更对的问题。

如果每个备份节点只根据他收到请求的顺序更新自己的数据,那最终就会使一个不一致状态。leader 1的最终结果是C, leader 2的最终结果是B,这是不能接受的。我们要求所有备份最终的结果必须是一样的,这也就要求数据库必须用一种收敛的方法来处理冲突。这就有很多方法了

  • 给每个写一个唯一id(时间戳,随机数,UUID,key/value的hash值),选id最大的写请求作为赢家,把其他的都丢掉。如果用时间戳作为id,就加作最后写生效last write wins, LWW).LWW 我们这章最后还会讲。
  • 给每个备份节点一个唯一id,冲突时优先选用id大的节点的数据,但这会造成数据丢失。
  • 合并值, 比如把他们按照字母序合并,Figure 5-7的结果就会变成B/C
  • 把冲突记成一种特殊的数据格式然后交由应用后期处理。

自定义冲突解决逻辑

其实最合适解决冲突的方法依赖于应用,多Leader备份架构很多都允许你自定义冲突解决代码。这段代码可能是在写入或者读取时执行。

  • 写入执行
    当数据库系统在备份日志中检测到冲突,他会调用冲突处理器。这种处理器一般无法提示用户,因为他是在后台进程中执行的,所以必须速度很快。Bucardo就是这么搞的。
  • 读取执行
    当发现冲突时,所有冲突的现场都被保留下来。当这条数据下次被读取的时候,这条数据的多个版本都会返回给应用。应用把这个冲突数据提示给用户,然后由用户手动解决冲突,再把最终结果写到数据库中。CouchDB 就是这么搞的。

有一点注意,冲突解决往往是针对一条数据,而不是一个事务。所以虽然你的一个事务可能一次性包含多个写请求,但是他们还是在冲突解决中分开处理的,也就说可能会不一致。

什么是冲突?

有时候冲突其实很显然,就好像Figure 5-7一样,两个人同时修改同一条数据的同一个字段,毫无疑问这是冲突。

但是其他的冲突就很难检测了。比如想象你有一个会议室,这个会议室在任何时刻都只能有一个人预订。在这种场景下,当一个会议室在某个时间点有超过1个人预订时,这就是一个冲突了。(但是他和Figure 5-7的区别是他不再是同一个字段的问题,而是一个时间段的问题,就好像2个人一个人预约7-8,一个人预约9-10就不冲突,但是一个人预约7-10,一个人预约9-11就冲突了。)即使应用层在预订之前检查会议室在这个时间段是否可用,也不一定能避免冲突,因为请求可能发给多个Leader导致冲突。

多Leader备份拓扑图

备份拓扑(replication topology)是指更新在节点间传播的路径,如果你像Figure 5-7一样只有2个节点,那就很简单了。但是如果你有多个节点,就可能有多钟拓扑关系,例如Figure 5-8

最常见的拓扑关系是全相连,如(c)。但是其他的拓扑结构也有人用,比如MySQL默认是有环装拓扑,如(a), 每个节点从上个节点接收更新请求,把收到的更新加上自己的更新发给下一个节点。(b)中的星型结构也很流行,一个节点负责总控分发,其他节点只跟总控通信。

在环型和星型拓扑中。一个写请求要传递多个节点才能实现所有节点都备份完成。因此一个节点要把他收到的内容进行转发。为了防止无限循环,每个节点都会有一个唯一的编号,当一个节点处理完一条备份日志后,会给这个日志当上这个节点的id。当再次收到这条日志发现已经打上了自己的id后,节点直接把这条日志丢掉。

环型和星型的问题是一旦有一个节点挂了,那整个系统的消息备份都会受影响。这就要求系统必须在一个节点挂掉后重新配置通信的节点,去掉失效节点,但是这个工作往往依赖于人工介入。这点就是全连接的拓扑结构的优势所在,他有着更好的容错性,因为一个节点挂了,其他节点还是能从别的通路中获取到所有的更新消息。

不要以为全连接就没有问题,在网络环境中,有些链路会比其他的更快,这可能会导致有些更新被莫名其妙的覆盖了。就像Figure 5-9一样。客户端A 通过Leader1插入了一条数据, 客户端B通过Leader3更新这条数据,但是Leader 2收到的日志顺序就跟实际不一样了,这就是前面讲到的逻辑前后顺序的问题。这种情况下,简单的用时间戳都不能解决问题,因为时间戳也不一定能保证时序的严格一直,这个第8章讲。


为了解决这个问题,用到了一个叫版本列表(version vectors)的技术,这个也后面再讲了。但是残酷的事实是冲突解决在很多多Leader备份的系统中实现的都很差。如果你要用一个而类似这样的系统,一定是小心这些问题,仔细读文档。充分测试,确保他的实际工作结果跟你预想的一样。

你可能感兴趣的:(第五章备份-part3,designing Data-Intensive Applications 中文翻译摘要)