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

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

相关推荐
NiNg_1_23435 分钟前
SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
java·spring boot·后端
Chrikk2 小时前
Go-性能调优实战案例
开发语言·后端·golang
幼儿园老大*2 小时前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
canyuemanyue2 小时前
go语言连续监控事件并回调处理
开发语言·后端·golang
杜杜的man2 小时前
【go从零单排】go语言中的指针
开发语言·后端·golang
customer084 小时前
【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源
Yaml45 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
小码编匠6 小时前
一款 C# 编写的神经网络计算图框架
后端·神经网络·c#