
复制
为了使得数据与用户在地理上接近统一(减少延迟)、部分系统故障不影响系统功能(高可用)等原因,需要数据通过网络在多个机器之间保留相同的副本,该种行为就叫做复制。
1、领导者和追随者
当一个系统中存在很多成为副本的节点时,如何确保每次写入的数据都在副本中进行同步是一个重要的问题,常见的解决方案是 主从复制 机制:
有一个副本被确认为主库,接收用户的写请求;
其他的副本称为从库,当主库接收到写请求并完成本地写入时通过复制日志或者变更流的方式,从库向主库拉取并更新自己的本地数据。
用户的查询请求可以到主从,但是只有主库接收写入操作。
1.1 同步&异步
同步复制:当主库接收到写请求时,该请求会一直等待直到副本之间数据同步完成,优点是可以很好的保证主从之间共享最新数据,但是一旦从库发生崩溃、网络分区的等其他问题时,会使等待时间过长,严重导致写入操作无法完成。
异步复制:从库数据同步有明显的延迟,在一些情况下可能会导致从库数据落后主库数据几分钟。如果在从库还未完成同步期间,主库宕机,会导致副本之间丢失部分数据。
还有一个比较折中的方式,半同步:至少一个副本使用同步复制,其他所有副本使用异步复制,这样能保证一个副本与主库保持相同的数据信息;如果在此期间同步副本的时间过长,则考虑提升一个异步副本为同步副本。
实际上使用最多的是异步复制能力。
1.2 节点变更
有时候需要对节点进行变更,增加新的从节点提高高可用、或者替换失败的节点保证高可用。
节点增加:首先获取到主库的一致性快照(全量数据),将快照复制到新节点进行数据恢复,并获取快照之后的所有变更(增量数据),完成之后从库设置成功,可以处理主库产生的数据变化了。
从库宕机:当从库发生故障或者因为网络中断而导致的数据差异问题时,从日志中可以拿到故障前的最后一个事务序列,通过该序列向主库请求之后的变更,并更新从库自身的数据。
主库宕机:主库故障之后需要进行故障切换,就是将其中一个与主库数据最接近的从库提升为主库,将客户端的连接全部指向该新主库。
可分为三个步骤执行,一是确定主库故障,由于现实环境造成问题的原因很多,大多数系统都是采用简单的超时时间来判断(例如一个心跳 30 秒没被响应则认为故障);二是选择一个从库来当新的主库,通过节点选举过程、或指定外部控制节点选举来选择一个从库,并在所有的副本节点中达成共识;三是流量需要写入到新的主库当中,并且系统要确保旧的主库重新起来之后放弃自己主库的身份成为新主库的从库。
在主库故障切换的过程中,有很多问题是需要进行考虑的:
如果使用的是异步复制,新主库和旧主库之间可能已经存在数据差异,而当旧主库重新起来之后,新主库可能已经接收到与旧主库未同步的写入冲突数据了(这是一个危险的动作);
因为网络分区而导致的脑裂问题,两个分区里面的从库都认为自己是主库,如果都接受写入操作,那不可避免的会造成数据冲突问题。
1.3 复制日志实现
1.3.1 基于语句复制
主库记录下执行的每个写入语句,并将该语句发送给从库进行同步,方法比较简单,但是存在很多的问题,一是非确定性函数每次执行的结果不一致,例如获取随机数、获取当前时间等;二是自增列的问题,这个需要保证多个事务之间的执行顺序相同。
可以使用执行不确定函数之后产生的固定值来替换函数等方式来解决上述问题,但是边缘情况还有很多,因此一般不直接采用该种复制方式。
1.3.2 传输预写式日志
将写操作追加到日志当中,日志包含所有数据库写入的追加字节序列,可以使用该日志文件在另一个节点上构建一模一样的副本。因为需要从日志文件恢复,但是日志属于底层的结构,需要主从之间存储格式保持一致,如果不一致则不能完成恢复。
1.3.3 逻辑日志复制
使用同存储引擎不同的日志格式,仅记录数据发生变更的重要数据,例如插入数据所有列的新值,删除数据能够标识当前行的唯一键(没有则需要全部旧数据),更新数据需要标识当前行以及所有列的新值。
2、复制延迟问题
在读可伸缩的系统当中,使用同步复制不太可能,但是异步复制又可能造成同样的查询在主从节点之间的执行结果不一致,因此出现了 最终一致性 的解释名词,表示写入主库的数据在一段时间之后能完成主从同步。
2.1 读己之写
当一个用户对信息表单进行提交之后,立即进行查看,如果这个时候路由到从库并且从库数据尚未同步更新,就会造成数据变更失败的假象,这样的情况是不允许存在的。
因此,在上面的场景下面需要 写后读一致性(读己之写一致性),如果用户重新加载界面,总能看到自己的最新提交,实现方式很多种:
对于用户可能修改的内容总是从主库读,例如自己的个人信息,其他的从从库读取;
记录更新的时间,在更新的一段时间内向主库读取,或者记录从库的延迟时间,超过某个时间限制的从库不提供读;
客户端记录最近一次写入时间戳,系统需要确保从库在处理用户读请求时该时间戳前的变更已经传播到该从库,否则从其他从库进行查询。
如果说用户在跨客户终端进行查看,例如在浏览器进行更新之后,从移动端进行查看,这样需要增加其他的考虑,至少客户端记录时间戳的方式不可靠,需要对这些元数据进行中心化的存储。
2.2 单调读
如果对于从库的读选择策略是随机的,可能第一次读到了一个正常同步的从库拿到正确信息,第二次读到一个延迟较久的从库没有拿到正确信息,就会产生时光倒流的感觉,因此需要使用在强一致性和最终一致性中间的一个单调读策略,来保证多次读请求不会拿到更旧的数据。
其中一种实现方式就是确保用户获取信息从同一个从库,而不是随机选择;当指定从库发生故障时才进行重新从库路由。
2.3 一致前缀读
在分区的场景下,如果说存在两人对话的这个场景,他们之间的写入和读取是有顺序的,假设对话被存储在不同分区中,这个时候其他人从分区副本中进行读取,就可能会造成两个人的对话颠倒。
为此,需要一致前缀读来保障:如果一系列写入按某个顺序发生,那么任何人读取这些写入时,也会看到他们以同样的顺序出现。解决方式则是确保任何因果相关的写入都写入相同的分区当中(显示跟踪因果关系的算法)。
3、多主复制
在分区的情况下,每个分区都应该有一个主库和各自的从库,复制在每个分区中以同样的方式发生。在多活的场景下面,多个节点接收写入操作,处理写入的每个节点都必须将该数据变更转发给其他所有节点。
3.1 处理写入冲突
多主写入的一大问题就是写入冲突,当多个用户编辑了数据,本地库得以更新,但是在进行异步复制的时候会出现冲突。
冲突检测同步:等待写入被复制到所有副本之后才认为写入成功(降低为单主写入);
避免冲突:由应用程序确保特定数据的写入都通过同一个主库;
收敛至一致状态:数据库以一种收敛的方式解决冲突,所有副本在完成变更复制时收敛至一个相同的最终值,可以通过定义唯一ID的方式,但是这样会导致数据丢失;或者以某种方式将冲突合并在一起进行记录。
3.2 自定义解决逻辑
解决冲突的最合适方法可能取决于应用程序自身:
写时执行:当数据库系统检测到复制更改日志汇总存在冲突时就解决冲突;
读时执行:将所有的冲突都存储,下一次读取数据时将多个版本的数据都返回给应用程序,提示用户或者自动解决冲突并完成数据落库。
还有很多用于自动冲突解决的算法,无冲突复制数据类型(可以由多用户同时编辑的集合等一系列数据结构)、可合并的持久数据结构(显示追踪历史记录)、操作转换(有序列表的并发编辑)。
3.3 多主复制拓扑
如果只有两个主库,那之间的传播拓扑是唯一的,彼此之间进行传播即可,但是当主库数量更多之后,拓扑结构会增加,但大致分为三类:
环形拓扑:按照一定的顺序,数据在每个节点之间进行流动,当前节点除了发送上一个节点的数据外,还会将自身的数据一起携带发送到下一个节点。
星型拓扑:以一个节点为中心,其他节点通过该中心同步自己的数据。
全拓扑:每两个节点之间都可以进行数据同步,是一个完全的排列关系。
在环形和星型拓扑当中,节点发送出去的数据可能会被再接收回来,为了保证不重复处理,这里需要赋予一个唯一的标识,当节点接收到的数据有自己的标识时则放弃处理该数据。
同时,如果拓扑中的一个节点挂掉之后会影响整个结果的数据复制,直到节点重新恢复。
4、无主复制
无主设计是客户端直接将写入发送到各个副本,或者由一个节点充当协调者代表客户端进行写入。
无主配置中,由于都是副本节点,因此不存在故障转移。但是当一个副本节点挂了之后,客户端的写入无法同步到该副本,甚至在恢复期间会丢失很多用户写入。一旦节点重启成功之后数据请求流量进行会出现没有数据问题,这个时候就需要客户端同时请求其他副本节点,用版本号确定哪个数据是最新的。
读修复和反熵:副本节点之间存在数据差异时有两种方式,一是读修复,客户端并行读取多个节点数据时,可以检测副本之间的新旧数据,并将新值写回缺少的副本;二是反熵,通过后台进程不断查找副本之间的差异,进行数据的同步。
读写法定人数:当选择 w + r > n 的读写副本数时,通常可以期望每次读取都是最新的值,但是某些情况下可以更宽松的界定(w + r <= n),这样可以在网络异常的时候尽可能的进行请求的处理。
- 感谢你赐予我前进的力量