DDIA-分布式冗余
生活08-02-2024
简单总结
2024年2月8日,终于读完了这一章,其实很久之前就读了,但是都零零散散,没有做好专注的读书规划,也没有进行深入的思考,这段时间一直在内耗,因为我发现好像外界认为的后端技术是业务方向的,而我自己其实更喜欢偏向技术点的东西吧,加上一整年的质疑,很压抑。这段时间在公司做安全研发,一直纠结是否转行,因为发现这个行业的业务还挺有意思的,以及思考下学期的宿舍门禁和密码学怎么办,毕竟找不到学弟愿意代课和可能会被辅导员找夜不归宿。思索许多,最终决定还是向学校妥协了,年底就回来辞职了,回学校当好学生吧🙃
这一章其实就是讲分布式中,多集群相关的一些问题。
先说多集群,就会有复制同步的问题,也会有宕机的问题。面向用户的读和写,我们要保证一致性,这种一致性包括数据层面,逻辑层面,时许层面,用专业术语来说,就是读己之写、单调读、一致性前缀读。可能事务是一个比较好的解决方案,过去因为难以落地,许多中间件并不支持分布式事务,现在开始好了很多,更大厂商也开始拥抱,这是一个必然的趋势。而对于宕机,就会有恢复的情况,恢复之后,面向主模型和副本模型,也会有不到的同步策略。从业务保证、再到技术实现,复制方式,同步恢复,都有很多技术考究点。
针对多集群部署,业界有两种情况,一种是多主模型,另外一种是无主模型。对于多主模型,其目的是为了解决单主写入瓶颈的压力,而引用多主,就会导致每个节点的写入存在冲突的问题。处理冲突,我们要界定好冲突的定义,以及处理冲突的方式。整体来说是比较好理解的。
第二种是无主模型,无主模型个人感觉有点像raft算法的处理思想,只需要保证法定人数内返回即可,有时候我们会松散法定人数的界定,在保证单点故障之后,有重新的稳拖的替代来使得写入成功而写入成功之后,也应该保证数据进行反熵。同时对于数据的并发,我们要界定要有没有因果关系,以及对于冲突同步,是否要进行覆盖或者合并。
这大体是读这章的有关,核心思路是围绕复制数据层面,逻辑层面,时许层面,来探讨多集群部署模型各种可能存在的问题。真正在落地实现的时候,应该结合业务需要,去做一些调整。
为什么要做数据冗余?
- (低延迟)降低延迟:可以在地理上同时接近不同地区的用户。
- (可用性)提高可用性:当系统部分故障时仍然能够正常提供服务。
- (可伸缩性)提高读吞吐:平滑扩展可用于查询的机器。
Leader与Flower
主从模型架构,写数据的时候只写入领导。
同步与异步
同步复制:
- 优点:从库保证和主库一直的最新数据副本
- 缺点:如果从库没有响应(如已崩溃、网络故障),主库就无法处理写入操作。主库必须阻止所有的写入,等待副本再次可用。
半同步:通常使用一个从库与主库是同步的,而其他从库是异步的。这保证了至少两个节点拥有最新的数据副本。
通常情况下,基于领导者的复制都配置为完全异步,交给网络去做保证了。
- 注意,主库故障可能导致丢失数据。
新增从库(副本)
本地做一致性快照复制到副本节点+序列号实现做增量同步。
这个过程一般是自动化的,通过Raft等操作,也可以手动化。
宕机处理
从副本宕机:追赶恢复
看缺失的量级:
- 全量+增量
- 只做增量
主副本宕机:故障转移
步骤:确认主副本故障、选择新的主副本、让系统感知新主副本
选择标准:最新来避免数据损失、避免脑裂出现
问题:新老数据冲突、相关外部系统冲突、新老主副本角色冲突、超时阀值选取
故在实际过程中,一般上线初期更原因通过手动进行切换,后续再逐步优化为自动化。
日志复制
增量来做多副本同步。
主要是下面几种实现方式:
- 基于语句:自增序列依赖于现有数据、存在非确定性函数、有副作用
解决方法:
- 识别所有产生非确定性结果的语句。
- 对于这些语句同步值而非语句。
- 传输预写WAL:追加写,可重放
天然适合备份同步。本质是因为磁盘的读写特点和网络类似:磁盘是顺序写比较高效,网络是只支持流式写。
问题:后向兼容下,先升从库再升主库,否则只能停机升级了。
- 逻辑日志(基于行)
类似于MySQL的binlog,行是一个合适的粒度。
- 方便新旧版本的代码兼容,更好的进行滚动升级。
- 允许不同副本使用不同的存储引擎。
- 允许导出变动做各种变换。如导出到数据仓库进行离线分析、建立索引、增加缓存等等。
- 触发器
用户决策,使用触发器和存储过程。可以将用户代码 hook 到数据库中去执行。
但是灵活性带来的问题便是更容易出错。
复制延迟问题
我们所实现的一致性是最终一致性。
- 主从异步同步会有延迟:导致同时对主库和从库的查询,结果可能不同。
- 因为从库会赶上主库,所以上述效应被称为「最终一致性」。
- 复制延迟可能超过几秒或者几分钟,下文是 3 个例子。
读己之写
解决问题:保证写后读顺序
一种情况:明明写了,但是读不到。
为了解决上面这种读写延迟导致的不一致,我们使用读写一致性保证。
解决方法:
- 按内容分类,修改的去主库读
- 按时间,维护时间阈值,一般根据延迟经验值确定
- 利用时间戳,校验从库和客户端是否已经同步
一些增加复杂的情况:
- 多端产品客户端时间戳不一致且难以同步
- 数据分布在多个数据中心,一般会汇集到一个数据中心再读
单调读
解决问题:保证多次读之间的顺序。
避免时光倒流问题,比如一个从库有,另外一个从库无。
解决方法:
- 只从一个副本读数据
- 时间戳机制
一致前缀读
解决问题:保证因果逻辑正确。 数据分布在多个分区,可能会因为延迟,导致一方获取的数据,因果发生了倒置。
解决方法:
- 不分区
- 让因果关系的事件路由到一个分区
难点:如何追踪因果?
事务或许是终极方案
单机事务存在很久,数据库走向分布式时代,很多NoSQL抛弃事务,因为
- 更容易实现、更好性能、更好可用性
固然复杂度转移到了应用层。
但随着经验的积累,事务必然引回,现在很多数据库都开始支持事务。
多主模型
很多时候复杂度远大于收益。
其想法是想解决单个主库写入时的压力。
一些场景可能实话
- 协同编辑
- 横跨多个数据中心
- 离线工作的客户端
场景举例
个人觉得,多主模型的问题是如何处理冲突。
协同编辑,同步时要如何处理协同冲突,乐观or悲观?
多个离线客户端,在回复网络后,应该如何处理不同端的数据冲突和同步?
处理冲突
冲突的发生:修改后同步或者异步复制的过程。
冲突检测
单主模型,因为只有一个,写入端很容易可以做检测。但是多主模型,貌似没有什么方法能保存多主模型特点下解决问题。如果让写入的时候便去保证多主同步,但这样便失去多主的特性,退化为单主模型。
冲突避免
解决冲突的方法便是在设计的时候避免冲突。
可以根据数据集进行分区,比如不同区域的用户路由到不同区域的服务。
这样就能分摊写入压力,但是在数据迁移的时候,可能会存在麻烦。以及该区域服务损坏,其他端无法获得对应的数据。
冲突收敛
单主的写入总是后覆盖前。
但是多主,事件顺序无法定义,主副本来看,每个事件都是不一致的。
可以通过一些规则:
- 事件序号
- 副本序号
- 合并冲突
- 冲突策略
即区分多主优先级,以及提供自定义操作将冲突处理交给第三方而不是系统本身。
自定义策略
- 写时执行
- 读时执行
界定冲突
什么是冲突,这很难定义,需要做好界定。
拓扑图
- 环形拓扑。通信跳数少,但是在转发时需要带上拓扑中前驱节点信息。如果一个节点故障,则可能中断复制链路。
- 星型拓扑。中心节点负责接受并转发数据。如果中心节点故障,则会使得整个拓扑瘫痪。
- 全连接拓扑。每个主库都要把数据发给剩余主库。通信链路冗余度较高,能较好的容错。
一些可能存在的问题:每个消息应该有自己的唯一标识,如果是自己,则过滤不处理。此外,在全连接模型下,由于延迟,可能会导致数据违反因果关系。因为不同的leader同步数据的时间不一样,而后来的事情插入可能会在该同步语句之前,如下:
我们需要对每个写入事件进行一个全局排序,依赖于本机的物理时钟不行,会存在回退和不同步的问题。一般是借用版本向量的策略。
无主模型
多主模型,副本有故障需要作切换,但是无主模型不需要,忽略即可。
- 多数派写入,多数派读取,以及读时修复。
读时修复和反熵
- 读时修复(read repair),本质上是一种捎带修复,在读取时发现旧的就顺手修了。
- 反熵过程(Anti-entropy process),本质上是一种兜底修复,读时修复不可能覆盖所有过期数据,因此需要一些后台进程,持续进行扫描,寻找陈旧数据,然后更新。
Quorum读写
- 鸽巢原理:W+R > N
- n 越大冗余度就越高,也就越可靠。
- r 和 w 都常都选择超过半数,如 (n+1)/2
- w = n 时,可以让 r = 1。此时是牺牲写入性能换来读取性能。
Quornum一致性的局限
w + r > n,总会至少有一个节点能保存最新的数据,因此总是期望能读到最新的。但是也有一些局限情况:
- 使用宽松的 Quorum 时(n 台机器范围可以发生变化),w 和 r 可能并没有交集。
- 对于写入并发,如果处理冲突不当时。比如使用 last-win 策略,根据本地时间戳挑选时,可能由于时钟偏差造成数据丢失。
- 对于读写并发,写操作仅在部分节点成功就被读取,此时不能确定应当返回新值还是旧值。
- 如果写入节点数 < w 导致写入失败,但并没有对数据进行回滚时,客户端读取时,仍然会读到旧的数据。
- 虽然写入时,成功节点数 > w,但中间有故障造成了一些副本宕机,导致成功副本数 < w,则在读取时可能会出现问题。
- 即使都正常工作,也有可能出现一些关于时序(timing)的边角情况。
w + r <= n,可能会读取到过期的数据。
一致性监控
- 多副本模型:因为基于领导者,所以复制顺序一致,副本可以方便给出每个落后的Tag。
- 无主模型,没有固定写入顺序,难以监控,难以界定,最终一致性是一个很模糊的保证。
放松的Quorum和提示转交
总节点大于n,对于失败的写入,转交给其他正常的节点。
- 保证w和r个返回,问题得到解决后,由后台反熵做数据转移。
多数据中心
基于无主模型的一些数据中心策略:
- 其中 Cassandra 和 Voldemort 将 n 配置到所有数据中心,但写入时只等待本数据中心副本完成就可以返回。
- Riak 将 n 限制在一个数据中心内,因此所有客户端到存储节点的通信可以限制到单个数据中心内,而数据复制在后台异步进行。
并发写入检测
由于允许多个客户端同时写入,就会存在不同副本,收到的内容不一致。
后者胜LWW
后覆盖前,加上一个时间戳。
但是问题可能是会有读写不一致问题,保证安全的方法是:key是一次可写,后变为只读。key可以使用UUID,每个写操作都只会有一个唯一的键。
发生于之前和并发关系
两个事件的操作:因果or无因果
A 和 B 并发 < === > A 不 happens-before B && B 不 happens-before A
如果可以定序,走LWW,否则要进行冲突解决。
确定关系
如果有因果关系,那么可以通过版本号的策略,来做到覆盖,每次更新,更新覆盖V <= Vx的版本。
合并并发值
并发值合并策略会涉及到数据是否会被丢失的问题。
如果是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
每个副本在遇到写入时,会增加对应键的版本号,同时跟踪从其他副本中看到的版本号,通过比较版本号大小,来决定哪些值要覆盖哪些值要保留。