分布式系统入门_复制
数据密集型应用设计读书笔记第五章
有时候需要数据分布到不同机器上。
至少有以下三点理由,需要这么做:
1.可伸缩性:一台机器的存储量和处理能力有限
2.容错/高可用性:冗余提供存储和服务
3.延迟,在各地部署服务器,可以就近提供更快速的服务
如果只是想提高负载,有种办法是纵向伸缩,即共享架构,共享cpu,内存,硬盘等等。但是都有些缺陷,主要是成本上升太快,数据竞争锁定的限制等因素。
而横向伸缩,即无共享架构的方案就比较成熟。所以是本书主要讨论的东西。
复制(Replication)
在几个不同的节点上保存数据的相同副本,可能放在不同的位置。 复制提供了冗余:如果一些节点不可用,剩余的节点仍然可以提供数据服务。 复制也有助于改善性能。
分区 (Partitioning)
将一个大型数据库拆分成较小的子集(称为分区(partitions)),从而不同的分区可以指派给不同的节点(node)(亦称分片(shard))。
理解了这些概念,就可以开始讨论在分布式系统中需要做出的困难抉择。
存储数据库副本的每个节点称为 副本(replica) 。当存在多个副本时,会不可避免的出现一个问题:如何确保所有数据都落在了所有的副本上?
每一次向数据库的写入操作都需要传播到所有副本上,否则副本就会包含不一样的数据。最常见的解决方案被称为 基于领导者的复制(leader-based replication) (也称 主动/被动(active/passive) 或 主/从(master/slave) 复制)
领导者要包办所有的写事务,并同步或异步地将这些更改传播到追随者。同步意味着一致性更强,异步意味着更快的响应。半同步指至少一个追随者是同步的,其他追随者则异步(如果在数据库上启动同步复制,很多时候实际是半同步)。
通常情况下,基于领导者的复制都配置为完全异步。 在这种情况下,如果主库失效且不可恢复,则任何尚未复制给从库的写入都会丢失。 这意味着即使已经向客户端确认成功,写入也不能保证 持久(Durable) 。 然而,一个完全异步的配置也有优点:即使所有的从库都落后了,主库也可以继续处理写入。 弱化的持久性可能听起来像是一个坏的折衷,然而异步复制已经被广泛使用了,特别当有很多追随者,或追随者异地分布时。
一般如何初始化一个新的从库?先从主库获取最近的快照备份,然后再从这个快照点开始拉取复制日志,直到跟上最新状态。
节点宕机如何处理?
从库宕机,追赶恢复:从库开机后检查日志,从主库拉取断开连接后未记录的新的数据变更,直到追上。
主库宕机,故障切换:提拔某个从库成为新主库。检测是否故障一般系统会根据超时时间来判断。选取新的主库,则可以通过选举,或者控制器节点指定。主库的最佳人选通常是拥有旧主库最新数据副本的从库(最小化数据损失)。让所有的节点同意一个新的领导者,是一个共识问题。选举后的新领导者要让客户端请求路由到这个新领导节点,并且旧领导恢复后要意识到新领导的存在。
所以,主库的故障切换问题按顺序,一般分为检测,选举,上任这几个方面。
故障切换带来的麻烦:
-
异步复制带来的冲突。一般是舍弃旧领导节点的未同步写入。损害用户对数据持久性的期望。
-
如果数据库和其他外部组件互相有沟通,则会导致外部组件里的不一致。(所以这时候往往需要谨慎设计)
-
发生某些故障时可能会导致同时存在两个节点认为自己是主节点的状况存在。这就是脑裂。解决的方法就是检测到这种情况就杀死其中一个,但如果过于粗糙,可能把两个都杀掉了。
-
超时时间的设置,因为有时候可能瞬时负载过高,或者网络发生片刻的拥堵。频繁切换是不值当的。超时时间需要设计的比较恰当。
复制日志的几种实现方式
-
基于语句的复制。对于从库,效果就像是从客户端接受请求一样。但是语句的执行结果必须是确定性的。
-
传输预写式日志(WAL)。就是把主库的WAL传输到从库。如果这种日志和数据库的版本是强绑定的,则在升级版本时带来困难。(如果复制协议允许从库使用比主库更新的软件版本,则可以先升级从库,然后执行故障切换,使升级后的节点之一成为新的主库,从而执行数据库软件的零停机升级。如果复制协议不允许版本不匹配(传输WAL经常出现这种情况),则此类升级需要停机。)
-
逻辑日志复制(基于行)。使用的复制日志与存储引擎的日志格式不同。这种复制日志称为逻辑日志。表示对数据库操作的逻辑行为。如果是SQL数据库,日志粒度通常是行。这样的好处很显而易见,主库和从库的数据库版本不用匹配,甚至可以使用不同的存储引擎。外部应用程序更容易解析。例如复制到数据仓库进行离线分析,或建立自定义索引和缓存。 这种技术被称为 数据变更捕获(change data capture)
-
基于触发器的复制,数据库的触发器在数据变更时讲某些信息记录写到其他表中,再由上层应用程序去读取表。限制大,性能慢,灵活。
复制延迟问题
讲的就是最终一致性。书中介绍了三个例子和解决方法。
-
读己之写。读不到自己的写。解决方法
-
读用户可能已经修改过的内容时,都从主库读。
-
根据上次请求修改的时间,如果在短时间内要读,就从主库读。
-
客户端记录时间戳,然后查询时,从库要作出保证,在这个时间戳发布的数据已经被更新到该从库。如果无法保证,就换个从库查询。(难点在于时间戳的一致性如何保证)
-
将读请求发送到离主库更近一点的从库(例如在同一个数据中心)
如果设备异构,那么跨设备的读到自己写,是个更复杂的问题。(记录时间戳的元数据需要用统一的中心存储;把同用户的请求路由到同一个主库或其所在的数据中心)
-
-
单调读。比最终一致性稍强。单调读取仅意味着如果一个用户顺序地进行多次读取,则他们不会看到时间后退,即,如果先前读取到较新的数据,后续读取不会得到更旧的数据。解决方法
-
根据用户ID哈希值,总是映射到某个从库读。
-
-
一致前缀读。这个保证说:如果一系列写入按某个顺序发生,那么任何人读取这些写入时,也会看见它们以同样的顺序出现。一般分区后,如果各个分区独立运行,不存在全局写入顺序,会出现这种问题。例如,因为某个客户端分别从分区A和分区B的从库读分区A的和分区B的主库写入,不同分区主从同步速度不一致导致客户端读到的顺序不符合实际写入的顺序。解决方法
-
确保任何因果相关的写入都写入相同的分区,某些显式跟踪因果依赖关系的算法
-
多主复制
一般用到多主复制,都是在有多个数据中心的情况下。每一个数据中心有一个主节点,不同数据中心的主节点之间进行异步的复制。客户端可以选择就近的主节点提供写入服务。尽管多主复制有不少优势,但也有一个很大的缺点:两个不同的数据中心可能会同时修改相同的数据,写冲突是必须解决的。
有类似多主复制的思想的应用有多设备同步和协同编辑等。
解决写入冲突有几个思路
-
避免冲突,每个用户的写入请求路由到确定的主库。
-
收敛至一致的状态。也就说,同时有多个写入,那么只要有种机制确保最终只接受某个值就行了。
-
给每个写入唯一的ID(时间戳,随机数等),确定优先级
-
给每个副本唯一的ID,确定优先级
-
全部写入值合并为一个写入值
-
冲突时保留所有信息,编写解决冲突的应用程序代码(也可以将选择权交给用户)
-
写时执行,异步复制时发现冲突,不提示用户,应用自己解决冲突
-
读时执行,应用读数据时才告诉应用程序发生冲突了
已经有一些有趣的研究来自动解决由于数据修改引起的冲突。有几行研究值得一提:
无冲突复制数据类型(Conflict-free replicated datatypes)(CRDT)【32,38】是可以由多个用户同时编辑的集合,映射,有序列表,计数器等的一系列数据结构,它们以合理的方式自动解决冲突。一些CRDT已经在Riak 2.0中实现【39,40】。
可合并的持久数据结构(Mergeable persistent data structures)【41】显式跟踪历史记录,类似于Git版本控制系统,并使用三向合并功能(而CRDT使用双向合并)。
可执行的转换(operational transformation)[42]是Etherpad 【30】和Google Docs 【31】等合作编辑应用背后的冲突解决算法。它是专为同时编辑项目的有序列表而设计的,例如构成文本文档的字符列表。
-
-
除了写入冲突以外,多主复制也存在其他更微妙的问题,例如幻写(在主库A提交了插入,在主库B提交更新这个插入,但是主库B还没有复制这个插入)。要正确排序事件,可以使用一种称为 版本向量(version vectors) 的技术。
多主复制还存在复制拓扑(如何选择复制的传播路径,环形,星形,全对全形等)该如何选择的问题。
无主复制
在这种架构下,写入有两种实现
-
客户端向多个副本写入
-
有一个协调者搜集写请求,去代表客户端进行写入
在无主复制中,就不存在故障切换了。那么一个节点在故障恢复后(以及正常的状态落后时),该如何跟上最新状态呢?
-
读修复,在客户端读到多个节点副本的数据后,进行对比,告诉落后的节点新的值。
-
反熵算法,一些数据存储具有后台进程,该进程不断查找副本之间的数据差异,并将任何缺少的数据从一个副本复制到另一个副本。
读写的人数:有多少个副本写入算成功写w,多少个算成功读w。
法定人数条件w+r>n允许系统容忍不可用的节点,如下所示:
-
如果w<n,如果节点不可用,我们仍然可以处理写入。
-
如果r<n,如果节点不可用,我们仍然可以处理读取。
-
对于n=3,w=2,r=2,我们可以容忍一个不可用的节点。
-
对于n=5,w=3,r=3,我们可以容忍两个不可用的节点。
-
通常,读取和写入操作始终并行发送到所有n个副本。 参数w和r决定我们等待多少个节点,即在我们认为读或写成功之前,有多少个节点需要报告成功。
-
设置策略:如果读频繁,可以把r设置小一点,反之则大一点。另外,r和w都设置为(1+n)/2向上取整,可以尽可能容忍多的节点故障。
但是法定人数对一致性的保证在具体实现中,仍旧可能有许多问题,需要注意。在使用这种机制的时候,监控过时测量值(读取到旧的数据的比例),以便察觉问题并作出调整是比较好的。更强有力的保证通常需要事务或共识
比如,如果因为网络问题,一个客户端连接不上部分节点怎么办,此时n明显不够了。
宽松的法定人数(sloppy quorum),写和读仍然需要w和r成功的响应,但是那些可能包括不在指定的n个“主”节点中的值。也就是向不在指定的n个节点里先借一节点,凑够n个。一旦网络中断得到解决,代表另一个节点临时接受的一个节点的任何写入都被发送到适当的“本地”节点。这就是所谓的提示移交(hinted handoff)。
宽松的法定人数意义在于有更高的容错,避免客户端凑不够w个。保证了有w个节点存了写入数据,但无法保证r个节点的读取能保证读到。这样的无主复制也很适合多数据中心。
无主复制中,检测并解决并发写入冲突仍旧是最重要的问题。
有以下这些方法:
-
最后写入胜利,丢弃并发写入。允许 “更旧” 的值被覆盖和抛弃。然后,只要我们有一种明确的方式(比如时间戳)来确定哪个写是“最近的”,并且每个写入最终都被复制到每个副本,那么复制最终会收敛到相同的值。保证了最终一致性,但舍弃了持久性保证。
-
合并同时写入的值
-
版本号,用于判断事件是并发的还是有前后因果关系的一种办法。另外,除了对键使用版本号以外,副本也要有版本号(很像超级账本fabric,键的版本号由区块高度和交易号两部分组成。副本号就类似区块高度)。对所有副本的版本号集合称为版本向量(version vector)。在读的时候返回当前版本向量,并在写入时需要客户端将其返回给副本节点。这有助于判断写入是覆盖写入还是并发写入。
版本向量和向量时钟
版本向量有时也被称为矢量时钟,即使它们不完全相同。 差别很微妙——请参阅参考资料的细节【57,60,61】。 简而言之,在比较副本的状态时,版本向量是正确的数据结构。
单主复制是非常流行的,因为它很容易理解,不需要担心冲突解决。在出现故障节点,网络中断和延迟峰值的情况下,多领导者和无领导者复制可以更加稳健,但以更难以推理并仅提供非常弱的一致性保证为代价。
白话就是,拍板的人越多,事情越容易冲突。但是拍板的人少,这个人出事了就会事情崩溃。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战