Design Data-Intensive Applications 读书笔记十四 第五章:多主节点备份
多主节点备份
目前我们只讨论了只有一个主节点的情况,所有的写入都要发送至主节点,如果客户端与主节点的通信发生问题,那么就无法写入数据至数据库。这是主从备份的一个主要缺点。
主从备份的一个扩展就是让写入发送至不止一个节点。备份时,所有需要处理写入的节点都要将数据更改发送至其他节点。我们称之为 multi-leader,多主节点。这种情况下,每个节点都类似于其他节点的从节点。
多主节点备份的应用场景
单一数据中心的场景使用多主节点没有多大的意义,因为带来的收益比不过带来的复杂度。但是,有一些场景使用多主节点有些作用。
多数据中心
现在设想你有多个数据中心,为了容灾,抑或是为了离用户更近。使用一般的主从备份,主节点必须在一个数据中心中,所有的写入需要通过那个数据中心。
多主节点的配置下,你每个数据中心需要有个主节点,图5-6显示了结构,数据中心中,使用主从备份,数据中心之间,每个数据中心的主节点将修改发送至其他数据中心。
现在比较在多数据中心的情况下,单主节点和多主节点的收益:
性能:单主节点的情况下,每个写入需要通过网络发送至每个数据中心,这会造成很大的延迟,违背了多数据中心的初衷。多主节点的情况,写入会发送至本地的数据中心,然后异步发送至其他主节点;因此,对于用户来说,网络延迟被掩盖了,这意味着看上去性能更好。
应对宕机:单主节点情况,主节点宕机后,故障转移会将从节点升级为主节点。多主节点情况,每个数据中心相对独立,一个主节点故障然后恢复后,其备份可以赶上其他主节点。
应对网络问题:数据中心间的通信一般是通过公网,这比起数据中心内的局域网,可能并不可靠。单主节点情况对于数据中心间的连接很敏感,因为写入都是通过网络同步进行的。多主节点,异步备份更能应对网络问题:暂时的网络问题不会停止写入过程。
但是多主节点有一个巨大的缺点:相同的数据可能同时被两个数据中心修改,写入冲突必须要解决。我们以后会讨论到。
多主节点备份像是多数据库的改装过的特性,很多潜在的配置陷阱和多数据库间的意外的交互。例如:自增主键,触发器和完整性约束都会成为问题。因此,多主节点一般被看成是危险的,应该避免的。
离线设备
另外一个适用于多主节点备份的场景就是有一个应用需要长时间离线工作的情况。
例如,你的手机,平板上的日程APP。你需要查看会议安排(读请求),写入会议计划(写入请求),不管是否接入网络。当下次接入网络时,离线状态下的做的修改都需要同步至服务器和其他设备。
这种情况下,每个设备都有本地的数据库来充当主节点,以及在所有有日程APP的设备间。有一个异步多主节点备份过程。备份延迟可能是数小时或者几天,这取决于你什么时候接入网络。
从架构角度看,这种结构类似于数据中心间的多主节点备份,每个设备类似于一个“数据中心”,它们之间的网络连接非常不可靠。随着日程需要同步的历史越来越多,多主节点难以正确备份。
合作编辑
实时合作编辑应用,能让多个用户同时编辑一个文件。比如 Etherpad 和Google Docs允许多个用户同时编辑文档或者表格。我们通常不会将合作编辑当做数据库备份问题,但是它与之前提到的用户离线编辑的情况很像。当一个用户修改了文档,修改会马上应用至本地副本,然后异步发送至服务器和其他用户正在编辑的相同的文档。
如果你想确保没有编辑冲突,那么应用需要在用户编辑前给文档加锁。如果另一个用户想要编辑相同的文档,需要等前一个用户编辑完,提交修改,文档解锁后才行。合作模型等价于单主节点备份模型中与主节点的交互。
但是为了加快协作,你可能想要缩小更改的单元,以及避免加锁。这个方案允许多个用户同时编辑,但是它也带来了类似多主节点备份的问题,包括需要解决冲突。
处理写入冲突
多主节点备份最大的问题就是写入冲突,这意味着需要冲突解决方案。
例如:两个用户同时修改一个维基页面,如图5-7,用户1将页面标题从A改至B,用户2将页面标题从A改至C。每个用户都成功地将修改发送至本地主节点,但是当异步备份修改时,会发生冲突。这个问题不会发生在单主节点的情况。
异步冲突检测对比同步冲突检测
在单主节点数据库中,第二个写入要么被阻塞,等待第一个写入完成;要么废弃掉,通知用户重新写入。但是在多主节点的情况下,两个写入都成功,一段时间后异步检测到冲突,这种情况下,通知用户重写写入可能太晚了。
原则上可以将冲突检测设为同步进行,例如等待写入备份到所有的主节点,然后通知用户写入成功;但是这样就丢失了多主节点最大的优势:允许每个备份独立处理写入。如果想同步检测冲突,可以使用但主节点配置。
避免冲突
处理冲突的最简单的方法就是:避免冲突;如果应用能保证所有特定的写入能经过相同的主节点,那么冲突就不会发生了。因为多主节点备份很难处理冲突,一般会建议避免冲突。
例如,一个应用允许用户编辑自己的用户数据,你可以确保一个用户的请求都经过相同的数据中心,使用数据中心的主节点进行读写。不同用户可能有不同的本地主节点(可能是地理上最近),但是从任意一个用户的视角来看,都是单主节点配置。
但是,有时候你可能需要改变一个记录指定的主节点,因为一个数据中心可能故障了,你需要重新导向至其他数据中心。或者是用户搬家,离另外一个数据中心更近了。这种情况下,冲突避免就会失效,你不得不处理不同主节点上可能的并发写入。
合并保持状态一致
但主节点数据库顺序写入:如果相同字段更新多次,最后的写入值决定了字段的值。
多主节点情况下,没有特定的写入顺序,所以字段最后的值是什么,并不明确。如果每个备份只是按照接收到的写入顺序来写入,那么数据库最终会不一致:主节点1最终值可能是C,主节点2上可能是B。这是不可接受的,因此数据库必须要用最终收敛的方法解决冲突,这意味着,应用所有的修改后,所有备份必须有相同的值。有几种方法来实现收敛性解决冲突。
1、给每个写入一个ID(时间戳,Long类型随机数,UUID,或者哈希值),选取最高的ID作为胜者(winner),然后丢弃掉其他的写入。如果使用时间戳,这就被称为最新写入胜出,lase write wins,LWW。尽管这个方法很流行,但是这容易产生数据丢失。
2、给每个备份一个ID,然后优先处理数据上更高的备份产生的写入;这个方法也可能产生数据丢失。
3、某种方法合并数据,例如按字母顺序排序然后拼接它们(图5-7中,标题可能是B/C)。
4、使用一个明确的数据结构来记录冲突信息,然后使用应用后续处理冲突。
自定义冲突解决方案
最合适的冲突解决方案可能依赖于应用,绝大多数的多主节点备份工具允许你使用应用代码来写入冲突解决逻辑。读取和写入时都可能执行代码:
写入:只要数据库检测到备份日志中有冲突,就会调用冲突处理器。处理器一般不会通知用户,它在后台运行,而且很快。例如, Bucardo允许写入Perl脚本。
读取:当检测到冲突时,所有的冲突都存储起来。下次读取的时候,多版本的数据但会给应用。应用可能提示用户或者是自动解决冲突,然后将结果写入到数据库。CouDB就是使用这种方法。
注意冲突解决方案的处理单位是行或者是文档,而不是整个事务。因此如果你在一个事务里做出了多个不同的修改,每个写入都会被冲突解决方案单独看待。
什么是冲突?
一些冲突很明显。图5-7中,两个写入同时修改了相同记录的相同字段为不同的值。毫无疑问是冲突。
另一种冲突更难被发现。比如:会议室预订系统:它跟踪哪个会议室在什么时间被哪个组织预定。这个应用需要确保每个会议室在一个时间只被一个组织预定。这种情况下,如果相同时间相同房间创建了两个相同订单,冲突就发生了。即便系统可以在预定时检测,但是如果是两个不同的主节点建立订单,冲突还是可能发生。
这种情况没有快速解决方法,后续会讨论这种问题。
多主节点备份拓扑学
备份拓扑结构描述的就是写入从一个节点传递至另一个节点的路径。如果你有两个节点,那么只有一种结构就是从一个节点传递至另一个节点。如果节点数大于2,那么就有多种结构,如图5-8
最常见的结构是的是全对全结构,也有使用更简单的结构的,比如MySQL使用环装结构,每个节点将写入转发到下一个节点(附加上自己的写入);另外一个使用的结构是星型结构,有一个根节点负责转发写入至其他所有节点,星型结构可以生成树结构。
在环和星结构中,数据需要经过多个节点才能传递至所有的节点。因此节点需要向前转发收到的所有数据变更。为了防止形成无限循环,需要给每个节点一个id,每个备份日志都会用经过的节点的id做标记,如果一个节点收到带有自己id的数据变更,就忽略。
环和星结构的另一个问题是如果一个节点出问题,那么备份节点间的通信会中断。拓扑结构可以配置为跳过出故障的节点,但是绝大多数情况是手工操作。拓扑连接程度越强,对故障的容忍度就越强,因为这允许信息进过不同的路径至其他节点,避免单一节点的故障的影响。
另一方面,全对全结构也有它的问题;一些情况下,某些节点的网络连接情况较好,那么它们的备份信息可能“超过”了其他节点。如图5-9:
用户A在主节点1插入一条记录,用户B在主节点3更新了那条记录,但是主节点2可能是按不同的顺序收到消息:可能是先收到更新消息,再收到插入消息。这是逻辑问题,我们可以在每条记录上打上时间戳,但是这也不可靠,因为时钟是不可靠的。为了修正这个问题,可以使用version vector版本向量。
如果你使用多主节点备份,那么一定要意识到这些问题。