DDIA-分布式冗余

生活08-02-2024

简单总结

2024年2月8日,终于读完了这一章,其实很久之前就读了,但是都零零散散,没有做好专注的读书规划,也没有进行深入的思考,这段时间一直在内耗,因为我发现好像外界认为的后端技术是业务方向的,而我自己其实更喜欢偏向技术点的东西吧,加上一整年的质疑,很压抑。这段时间在公司做安全研发,一直纠结是否转行,因为发现这个行业的业务还挺有意思的,以及思考下学期的宿舍门禁和密码学怎么办,毕竟找不到学弟愿意代课和可能会被辅导员找夜不归宿。思索许多,最终决定还是向学校妥协了,年底就回来辞职了,回学校当好学生吧🙃
这一章其实就是讲分布式中,多集群相关的一些问题。
先说多集群,就会有复制同步的问题,也会有宕机的问题。面向用户的读和写,我们要保证一致性,这种一致性包括数据层面,逻辑层面,时许层面,用专业术语来说,就是读己之写、单调读、一致性前缀读。可能事务是一个比较好的解决方案,过去因为难以落地,许多中间件并不支持分布式事务,现在开始好了很多,更大厂商也开始拥抱,这是一个必然的趋势。而对于宕机,就会有恢复的情况,恢复之后,面向主模型和副本模型,也会有不到的同步策略。从业务保证、再到技术实现,复制方式,同步恢复,都有很多技术考究点。
针对多集群部署,业界有两种情况,一种是多主模型,另外一种是无主模型。对于多主模型,其目的是为了解决单主写入瓶颈的压力,而引用多主,就会导致每个节点的写入存在冲突的问题。处理冲突,我们要界定好冲突的定义,以及处理冲突的方式。整体来说是比较好理解的。
第二种是无主模型,无主模型个人感觉有点像raft算法的处理思想,只需要保证法定人数内返回即可,有时候我们会松散法定人数的界定,在保证单点故障之后,有重新的稳拖的替代来使得写入成功而写入成功之后,也应该保证数据进行反熵。同时对于数据的并发,我们要界定要有没有因果关系,以及对于冲突同步,是否要进行覆盖或者合并。
这大体是读这章的有关,核心思路是围绕复制数据层面,逻辑层面,时许层面,来探讨多集群部署模型各种可能存在的问题。真正在落地实现的时候,应该结合业务需要,去做一些调整。


为什么要做数据冗余?

Leader与Flower

主从模型架构,写数据的时候只写入领导。

同步与异步


同步复制:

半同步:通常使用一个从库与主库是同步的,而其他从库是异步的。这保证了至少两个节点拥有最新的数据副本。
通常情况下,基于领导者的复制都配置为完全异步,交给网络去做保证了。

新增从库(副本)

本地做一致性快照复制到副本节点+序列号实现做增量同步。
这个过程一般是自动化的,通过Raft等操作,也可以手动化。

宕机处理

从副本宕机:追赶恢复

看缺失的量级:

主副本宕机:故障转移

步骤:确认主副本故障、选择新的主副本、让系统感知新主副本
选择标准:最新来避免数据损失、避免脑裂出现
问题:新老数据冲突、相关外部系统冲突、新老主副本角色冲突、超时阀值选取

故在实际过程中,一般上线初期更原因通过手动进行切换,后续再逐步优化为自动化。

日志复制

增量来做多副本同步。

主要是下面几种实现方式:

解决方法:

  1. 识别所有产生非确定性结果的语句。
  2. 对于这些语句同步值而非语句。

天然适合备份同步。本质是因为磁盘的读写特点和网络类似:磁盘是顺序写比较高效,网络是只支持流式写。
问题:后向兼容下,先升从库再升主库,否则只能停机升级了。

类似于MySQL的binlog,行是一个合适的粒度。

  1. 方便新旧版本的代码兼容,更好的进行滚动升级。
  2. 允许不同副本使用不同的存储引擎。
  3. 允许导出变动做各种变换。如导出到数据仓库进行离线分析、建立索引、增加缓存等等。

用户决策,使用触发器和存储过程。可以将用户代码 hook 到数据库中去执行。
但是灵活性带来的问题便是更容易出错。

复制延迟问题

我们所实现的一致性是最终一致性。

读己之写

解决问题:保证写后读顺序

一种情况:明明写了,但是读不到。

为了解决上面这种读写延迟导致的不一致,我们使用读写一致性保证。
解决方法:

一些增加复杂的情况:

单调读

解决问题:保证多次读之间的顺序。

避免时光倒流问题,比如一个从库有,另外一个从库无。
image.png
解决方法:

一致前缀读

解决问题:保证因果逻辑正确。 数据分布在多个分区,可能会因为延迟,导致一方获取的数据,因果发生了倒置。

image.png
解决方法:

难点:如何追踪因果?

事务或许是终极方案

单机事务存在很久,数据库走向分布式时代,很多NoSQL抛弃事务,因为

固然复杂度转移到了应用层。
但随着经验的积累,事务必然引回,现在很多数据库都开始支持事务。

多主模型

很多时候复杂度远大于收益。
其想法是想解决单个主库写入时的压力。
一些场景可能实话

场景举例

个人觉得,多主模型的问题是如何处理冲突。
协同编辑,同步时要如何处理协同冲突,乐观or悲观?
多个离线客户端,在回复网络后,应该如何处理不同端的数据冲突和同步?

处理冲突

冲突的发生:修改后同步或者异步复制的过程。

冲突检测

单主模型,因为只有一个,写入端很容易可以做检测。但是多主模型,貌似没有什么方法能保存多主模型特点下解决问题。如果让写入的时候便去保证多主同步,但这样便失去多主的特性,退化为单主模型。

冲突避免

解决冲突的方法便是在设计的时候避免冲突。
可以根据数据集进行分区,比如不同区域的用户路由到不同区域的服务。
这样就能分摊写入压力,但是在数据迁移的时候,可能会存在麻烦。以及该区域服务损坏,其他端无法获得对应的数据。

冲突收敛

单主的写入总是后覆盖前。
但是多主,事件顺序无法定义,主副本来看,每个事件都是不一致的。
可以通过一些规则:

即区分多主优先级,以及提供自定义操作将冲突处理交给第三方而不是系统本身。

自定义策略

界定冲突

什么是冲突,这很难定义,需要做好界定。

拓扑图

image.png

  1. 环形拓扑。通信跳数少,但是在转发时需要带上拓扑中前驱节点信息。如果一个节点故障,则可能中断复制链路。
  2. 星型拓扑。中心节点负责接受并转发数据。如果中心节点故障,则会使得整个拓扑瘫痪。
  3. 全连接拓扑。每个主库都要把数据发给剩余主库。通信链路冗余度较高,能较好的容错。

一些可能存在的问题:每个消息应该有自己的唯一标识,如果是自己,则过滤不处理。此外,在全连接模型下,由于延迟,可能会导致数据违反因果关系。因为不同的leader同步数据的时间不一样,而后来的事情插入可能会在该同步语句之前,如下:
image.png
我们需要对每个写入事件进行一个全局排序,依赖于本机的物理时钟不行,会存在回退和不同步的问题。一般是借用版本向量的策略。

无主模型

多主模型,副本有故障需要作切换,但是无主模型不需要,忽略即可。

image.png

读时修复和反熵

  1. 读时修复(read repair),本质上是一种捎带修复,在读取时发现旧的就顺手修了。
  2. 反熵过程(Anti-entropy process),本质上是一种兜底修复,读时修复不可能覆盖所有过期数据,因此需要一些后台进程,持续进行扫描,寻找陈旧数据,然后更新。

Quorum读写

image.png

  1. n 越大冗余度就越高,也就越可靠。
  2. r 和 w 都常都选择超过半数,如 (n+1)/2
  3. w = n 时,可以让 r = 1。此时是牺牲写入性能换来读取性能。

Quornum一致性的局限

w + r > n,总会至少有一个节点能保存最新的数据,因此总是期望能读到最新的。但是也有一些局限情况:

  1. 使用宽松的 Quorum 时(n 台机器范围可以发生变化),w 和 r 可能并没有交集。
  2. 对于写入并发,如果处理冲突不当时。比如使用 last-win 策略,根据本地时间戳挑选时,可能由于时钟偏差造成数据丢失。
  3. 对于读写并发,写操作仅在部分节点成功就被读取,此时不能确定应当返回新值还是旧值。
  4. 如果写入节点数 < w 导致写入失败,但并没有对数据进行回滚时,客户端读取时,仍然会读到旧的数据。
  5. 虽然写入时,成功节点数 > w,但中间有故障造成了一些副本宕机,导致成功副本数 < w,则在读取时可能会出现问题。
  6. 即使都正常工作,也有可能出现一些关于时序(timing)的边角情况。

w + r <= n,可能会读取到过期的数据。

一致性监控

放松的Quorum和提示转交

总节点大于n,对于失败的写入,转交给其他正常的节点。

image.png

多数据中心

基于无主模型的一些数据中心策略:

  1. 其中 Cassandra 和 Voldemort 将 n 配置到所有数据中心,但写入时只等待本数据中心副本完成就可以返回。
  2. Riak 将 n 限制在一个数据中心内,因此所有客户端到存储节点的通信可以限制到单个数据中心内,而数据复制在后台异步进行。

并发写入检测

由于允许多个客户端同时写入,就会存在不同副本,收到的内容不一致。
image.png

后者胜LWW

后覆盖前,加上一个时间戳。
但是问题可能是会有读写不一致问题,保证安全的方法是:key是一次可写,后变为只读。key可以使用UUID,每个写操作都只会有一个唯一的键。

发生于之前和并发关系

两个事件的操作:因果or无因果

A 和 B 并发 < === > A 不 happens-before B  && B 不 happens-before A

如果可以定序,走LWW,否则要进行冲突解决。

确定关系

如果有因果关系,那么可以通过版本号的策略,来做到覆盖,每次更新,更新覆盖V <= Vx的版本。
image.png

合并并发值

并发值合并策略会涉及到数据是否会被丢失的问题。
如果是LWW,那么则会覆盖,但是像一些场景,如购物车,我们需要的是,取并集来做合并。

版本向量

基于一个版本的信息来做数据处理,显然只适合单个副本的情况。对于多副本,我们应该该每个副本都引入版本号,对于同一个键,不同副本的版本会构成版本向量。

    key1
A   Va
B   Vb
C   Vc

key1: [Va, Vb, Vc]

[Va-x, Vb-y, Vc-z] <= [Va-x1, Vb-y1, Vc-z1]  <==>
x <= x1 && y <= y1 && z <= z1

每个副本在遇到写入时,会增加对应键的版本号,同时跟踪从其他副本中看到的版本号,通过比较版本号大小,来决定哪些值要覆盖哪些值要保留。

Author's photo

HuanXin-Chen

A tech enthusiast and avid sharer, this dream chaser firmly believes that great things will happen!

See other articles: