《DDIA》阅读笔记(五):数据复制(多主节点)
本章结构如下:
- 数据复制
- 多主节点复制
- 使用场景
- 多数据中心
- 离线客户端
- 协作编辑
- 处理写冲突
- 同步/异步检测
- 避免冲突
- 收敛于一致状态
- 写时/读时冲突解决
- 自动冲突解决
- 无冲突数据类型
- 可合并的持久数据结构
- 操作转换
- 拓扑结构
- 环型
- 星型
- 全链接
- 存在问题
系统存在多个主节点,每个都可以接收写请求,客户端将写请求发送到其中的一个主节点上,由该主节点负责将数据更改事件同步到其他主节点和自己的从节点。
每个主节点还可以同时扮演其他主节点的从节点。
多主节点复制方案比单主节点复制方案更可靠,但代价是系统的复杂性和弱一致性保证。
使用场景
多数据中心
一些数据库已内嵌支持多主复制,也可以借助外部工具实现,比如 MySQL 的 Tungsten Replicator、PostgreSQL BDR 和 Oracle GoldenGate。
在多数据中心、多主节点复制环境下:
性能提高:每个写操作都可以在本地数据中心快速响应,然后采用异步复制方式将变化同步到其他数据中心。对上层屏蔽了数据中心之间的网络延迟。
容忍失效:每个数据中心独立于其他数据中心运行,发生故障的数据中心在恢复后更新到最新状态(无需经过跨数据中心的主节点选举过程)。
容忍网络问题:采用异步复制可更好地容忍网络性能和稳定性问题,例如临时网络闪断不会妨碍写请求最终成功。
存在问题:
不同的数据中心同时修改相同的数据时,必须解决潜在的写冲突。
在与其他数据库功能交互时有时会出现副作用:自增主键、触发器和完整性约束等,因此比较危险。
离线客户端
即应用在与网络断开后还继续保持工作:每个设备都有一个充当主节点的本地数据库,相当于网络连接极度不可靠的数据中心。
所有设备之间采用异步方式同步这些多主节点上的副本,同步滞后时间取决于设备何时恢复联网(如云笔记、CouchDB)。
协作编辑
当一名用户编辑文档时,所做的更改会立即应用到本地副本,然后异步复制到服务器以及编辑同一文档的其他用户。
要确保不会发生编辑冲突,在编辑前必须锁定。另一用户要编辑文档必须等待持有锁的用户提交修改并释放锁。
这种协作模式相当于主从复制模型下在主节点上执行事务操作。
为了加快协作编辑的效率,可编辑的粒度需要非常小。
处理写冲突
正常情况下主从复制只有主节点可以处理写请求,因此不会出现这种情况。但多主复制很可能发生写冲突:即异地请求都分别成功提交到本地主节点,在更改被异步复制到对方时发生冲突。
同步/异步检测
同步:写请求等待完成对所有副本的同步,再返回通知写入成功,会失去多主节点允许每个主节点独立接收写请求的主要优势。
异步:如果在稍后的时间点上才能异步检测到冲突,要求用户层来解决冲突则为时已晚。
避免冲突
如果应用层可以保证对特定记录的写请求总是通过同一个主节点就不会发生写冲突。
用户更新数据时要确保特定用户的更新请求总是路由到特定的数据中心,并在该数据中心的主节点上进行读写。不同的用户则可能对应不同的主数据中心(如基于地理位置)。从用户的角度来看基本等价于主从复制模型。
有时需要改变事先指定的主节点,例如:数据中心发生故障、将流量重新路由到其他数据中心,或者是因为用户已经漫游到另一个位置更靠近新数据中心。此时必须有措施来处理同时写入冲突的可能性。
状态收敛
主从复制模型数据更新符合顺序性原则:如果同一个字段有多个更新,则最后一个写操作将决定该字段的最终值。
多主节点复制模型不存在明确的写入顺序,最终值也会变得不确定。可以用以下方式解决收敛问题:
为每个写入分配唯一的 ID,最后写入者获胜,这种做法容易造成数据丢失。
在 1 的基础上制定规则,如序号高的优先。这种做法容易造成数据丢失。
以某种方式将这些值合并,例如按字母顺序排序然后拼接在一起。
利用预定义好的格式记录和保留冲突相关的所有信息,依靠应用层的逻辑事后解决冲突(可能会提示用户)。
写时/读时冲突解决
写时冲突:只要数据库系统在复制变更日志时检测到冲突,就调用应用层的冲突处理程序。例如 Bucardo 支持编写一段 Perl 代码。这个处理程序通常不能在线提示用户,而只能在后台运行,速度更快。
读时冲突:当检测到冲突时所有冲突写入值都会暂时保存下来。下一次读取数据时将数据的多个版本读返回给应用层。应用层可提示用户或自动解决冲突,将最后的结果返回到数据库(比如 CouchDB)。
自动冲突解决
使用无冲突数据类型:无冲突的复制数据类型 CRDT 是可以由多个用户同时编辑的数据结构,包括 map、ordered list、计数器等,并且以内置的合理方式自动地解决冲突。
使用可合并的持久数据结构:可合并的持久数据结构 Mergeable persitent data 跟踪变更历史,类似于 Git 版本控制系统,并提出三向合并功能。
操作转换:专为可同时编辑的有序列表而设计,如文本文档的字符列表。
拓扑结构
常见的拓扑结构有环型(每个节点接收来自前序节点的写入,并将这些写入以及自己的写入转发给后序节点),星型(一个指定的根节点将写入转发给所有其他节点),全链接(每个主节点将其写入同步到其他所有主节点)。
在环形和星形拓扑中写请求需要通过多个节点才能到达所有的副本,即中间节点需要转发从其他节点收到的数据变更。
为防止无限循环,每个节点需要赋予唯一标识符,在复制日志中每个写请求都标记了已通过的节点标识符。
如果节点收到了包含自身标识符的数据更改,表明该请求巳经被处理过,因此会忽略此变更请求避免重复转发。
每种拓扑结构都存在一定的问题:
环形和星形拓扑:如果某一个节点发生了故障,在修复之前会影响其他节点之间复制日志的转发。可采用重新配置拓扑结构的方法暂时排除掉故障节点。在大多数部署中这种重新配置必须手动完成。
全链接拓扑:存在某些网络链路比其他链路更快的情况,从而导致复制日志之间的覆盖(参考“前缀一致读”,而且引入时间戳仍不能解决,因为涉及时钟同步问题)。
为了使得日志消息正确有序,可以使用版本向量(参考“检测并发写入”)。