DDIA-分布式冗余

简单总结

这一章其实就是讲分布式中,多集群相关的一些问题。

先说多集群,就会有复制同步的问题,也会有宕机的问题。面向用户的读和写,我们要保证一致性,这种一致性包括数据层面,逻辑层面,时许层面,用专业术语来说,就是读己之写、单调读、一致性前缀读。可能事务是一个比较好的解决方案,过去因为难以落地,许多中间件并不支持分布式事务,现在开始好了很多,更大厂商也开始拥抱,这是一个必然的趋势。而对于宕机,就会有恢复的情况,恢复之后,面向主模型和副本模型,也会有不到的同步策略。从业务保证、再到技术实现,复制方式,同步恢复,都有很多技术考究点。

针对多集群部署,业界有两种情况,一种是多主模型,另外一种是无主模型。对于多主模型,其目的是为了解决单主写入瓶颈的压力,而引用多主,就会导致每个节点的写入存在冲突的问题。处理冲突,我们要界定好冲突的定义,以及处理冲突的方式。整体来说是比较好理解的。

第二种是无主模型,无主模型个人感觉有点像raft算法的处理思想,只需要保证法定人数内返回即可,有时候我们会松散法定人数的界定,在保证单点故障之后,有重新的稳拖的替代来使得写入成功而写入成功之后,也应该保证数据进行反熵。同时对于数据的并发,我们要界定要有没有因果关系,以及对于冲突同步,是否要进行覆盖或者合并。

这大体是读这章的有关,核心思路是围绕复制数据层面,逻辑层面,时许层面,来探讨多集群部署模型各种可能存在的问题。真正在落地实现的时候,应该结合业务需要,去做一些调整。


为什么要做数据冗余?

  • (低延迟)降低延迟:可以在地理上同时接近不同地区的用户。
  • (可用性)提高可用性:当系统部分故障时仍然能够正常提供服务。
  • (可伸缩性)提高读吞吐:平滑扩展可用于查询的机器。

Leader与Flower

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

同步与异步

同步复制:

  • 优点:从库保证和主库一直的最新数据副本
  • 缺点:如果从库没有响应(如已崩溃、网络故障),主库就无法处理写入操作。主库必须阻止所有的写入,等待副本再次可用。

半同步:通常使用一个从库与主库是同步的,而其他从库是异步的。这保证了至少两个节点拥有最新的数据副本。

通常情况下,基于领导者的复制都配置为完全异步,交给网络去做保证了。

  • 注意,主库故障可能导致丢失数据。

新增从库(副本)

本地做一致性快照复制到副本节点+序列号实现做增量同步。

这个过程一般是自动化的,通过Raft等操作,也可以手动化。

宕机处理

从副本宕机:追赶恢复

看缺失的量级:

  • 全量+增量
  • 只做增量

主副本宕机:故障转移

步骤:确认主副本故障、选择新的主副本、让系统感知新主副本

选择标准:最新来避免数据损失、避免脑裂出现

问题:新老数据冲突、相关外部系统冲突、新老主副本角色冲突、超时阀值选取

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

日志复制

增量来做多副本同步。

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

  • 基于语句:自增序列依赖于现有数据、存在非确定性函数、有副作用

解决方法:

  1. 识别所有产生非确定性结果的语句。
  2. 对于这些语句同步值而非语句。
  • 传输预写WAL:追加写,可重放

天然适合备份同步。本质是因为磁盘的读写特点和网络类似:磁盘是顺序写比较高效,网络是只支持流式写。

问题:后向兼容下,先升从库再升主库,否则只能停机升级了。

  • 逻辑日志(基于行)

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

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

用户决策,使用触发器和存储过程。可以将用户代码 hook 到数据库中去执行。

但是灵活性带来的问题便是更容易出错。

复制延迟问题

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

  • 主从异步同步会有延迟:导致同时对主库和从库的查询,结果可能不同。
  • 因为从库会赶上主库,所以上述效应被称为「最终一致性」。
  • 复制延迟可能超过几秒或者几分钟,下文是 3 个例子。

读己之写

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

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

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

解决方法:

  • 按内容分类,修改的去主库读
  • 按时间,维护时间阈值,一般根据延迟经验值确定
  • 利用时间戳,校验从库和客户端是否已经同步

一些增加复杂的情况:

  • 多端产品客户端时间戳不一致且难以同步
  • 数据分布在多个数据中心,一般会汇集到一个数据中心再读

单调读

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

避免时光倒流问题,比如一个从库有,另外一个从库无。

解决方法:

  • 只从一个副本读数据
  • 时间戳机制

一致前缀读

解决问题:保证因果逻辑正确。

数据分布在多个分区,可能会因为延迟,导致一方获取的数据,因果发生了倒置。

解决方法:

  • 不分区
  • 让因果关系的事件路由到一个分区

难点:如何追踪因果?

事务或许是终极方案

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

  • 更容易实现、更好性能、更好可用性

固然复杂度转移到了应用层。

但随着经验的积累,事务必然引回,现在很多数据库都开始支持事务。

多主模型

很多时候复杂度远大于收益。

其想法是想解决单个主库写入时的压力。

一些场景可能实话

  • 协同编辑
  • 横跨多个数据中心
  • 离线工作的客户端

场景举例

个人觉得,多主模型的问题是如何处理冲突。

协同编辑,同步时要如何处理协同冲突,乐观or悲观?

多个离线客户端,在回复网络后,应该如何处理不同端的数据冲突和同步?

处理冲突

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

冲突检测

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

冲突避免

解决冲突的方法便是在设计的时候避免冲突。

可以根据数据集进行分区,比如不同区域的用户路由到不同区域的服务。

这样就能分摊写入压力,但是在数据迁移的时候,可能会存在麻烦。以及该区域服务损坏,其他端无法获得对应的数据。

冲突收敛

单主的写入总是后覆盖前。

但是多主,事件顺序无法定义,主副本来看,每个事件都是不一致的。

可以通过一些规则:

  • 事件序号
  • 副本序号
  • 合并冲突
  • 冲突策略

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

自定义策略

  • 写时执行
  • 读时执行

界定冲突

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

拓扑图

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

一些可能存在的问题:每个消息应该有自己的唯一标识,如果是自己,则过滤不处理。此外,在全连接模型下,由于延迟,可能会导致数据违反因果关系。因为不同的leader同步数据的时间不一样,而后来的事情插入可能会在该同步语句之前,如下:

我们需要对每个写入事件进行一个全局排序,依赖于本机的物理时钟不行,会存在回退和不同步的问题。一般是借用版本向量的策略。

无主模型

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

  • 多数派写入,多数派读取,以及读时修复。

读时修复和反熵

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

Quorum读写

  • 鸽巢原理:W+R > N
  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,可能会读取到过期的数据。

一致性监控

  • 多副本模型:因为基于领导者,所以复制顺序一致,副本可以方便给出每个落后的Tag。
  • 无主模型,没有固定写入顺序,难以监控,难以界定,最终一致性是一个很模糊的保证。

放松的Quorum和提示转交

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

  • 保证w和r个返回,问题得到解决后,由后台反熵做数据转移。

多数据中心

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

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

并发写入检测

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

后者胜LWW

后覆盖前,加上一个时间戳。

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

发生于之前和并发关系

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

css 复制代码
A 和 B 并发 < === > A 不 happens-before B  && B 不 happens-before A

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

确定关系

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

合并并发值

并发值合并策略会涉及到数据是否会被丢失的问题。

如果是LWW,那么则会覆盖,但是像一些场景,如购物车,我们需要的是,取并集来做合并。

版本向量

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

less 复制代码
    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

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

相关推荐
王码码20354 小时前
Go语言的测试:从单元测试到集成测试
后端·golang·go·接口
王码码20354 小时前
Go语言中的测试:从单元测试到集成测试
后端·golang·go·接口
嵌入式×边缘AI:打怪升级日志5 小时前
使用JsonRPC实现前后台
前端·后端
小码哥_常5 小时前
从0到1:Spring Boot 中WebSocket实战揭秘,开启实时通信新时代
后端
lolo大魔王6 小时前
Go语言的异常处理
开发语言·后端·golang
IT_陈寒8 小时前
Python多进程共享变量那个坑,我差点没爬出来
前端·人工智能·后端
码事漫谈8 小时前
2026软考高级·系统架构设计师备考指南
后端
AI茶水间管理员9 小时前
如何让LLM稳定输出 JSON 格式结果?
前端·人工智能·后端
其实是白羊9 小时前
我用 Vibe Coding 搓了一个 IDEA 插件,复制URI 再也不用手动拼了
后端·intellij idea
用户8356290780519 小时前
Python 操作 Word 文档节与页面设置
后端·python