同步操作将从 turnon/blog 强制同步,此操作会覆盖自 Fork 仓库以来所做的任何修改,且无法恢复!!!
确定后同步将在后台操作,完成时将刷新页面,请耐心等待。
title: 分布式复制
date: 2022-06-11 10:40:10
categories:
- 分布式
- 分布式协同
- 分布式协同综合
tags:
- 分布式
- 协同
- 复制
permalink: /pages/47f7bd/
分布式复制是指:在多个节点上保存相同数据的副本,每个副本具体的存储位置可能不尽相同。复制方住可以提供冗余:如果某些节点发生不可用,则可以通过其他节点继续提供数据访问服务。复制也可以帮助提高系统性能。
数据复制的作用
分布式系统的复制方式有以下几种:
分布式系统的复制需要考虑以下问题:
每个保存数据库完整数据集的节点称之为副本。有了多副本,必然会面临一个问题:如何确保所有副本之间的数据是一致的?
主从复制的工作原理如下:
典型应用:
对于关系数据库系统,同步或异步通常是一个可配置的选项:而其他系统则可能是硬性指定或者只能二选一。同步复制与异步复制基本流程是,客户将更新请求发送给主节点,主节点接收到请求,接下来将数据更新转发给从节点。最后,由主节点来通知客户更新完成。
通常情况下, 复制速度会非常快,例如多数数据库系统可以在一秒之内完成所有从节点的更新。但是,系统其实并没有保证一定会在多段时间内完成复制。有些情况下,从节点可能落后主节点几分钟甚至更长时间,例如,由于从节点刚从故障中恢复,或者系统已经接近最大设计上限,或者节点之间的网络出现问题。
因此,把所有从节点都配置为同步复制有些不切实际。因为这样的话,任何一个同步节点的中断都会导致整个系统更新停滞不前。实际应用中,很多数据库推荐的模式是:只要有一个从节点或半数以上的从节点同步成功,就视为同步,直接返回结果;剩下的节点都通过异步方式同步。万一同步的从节点变得不可用或性能下降, 则将另一个异步的从节点提升为同步模式。这样可以保证至少有两个节点(即主节点和一个同步从节点)拥有最新的数据副本。这种配置有时也称为半同步 。
主从复制还经常会被配置为全异步模式。此时如果主节点发生失败且不可恢复,则所有尚未复制到从节点的写请求都会丢失。这意味着即使向客户端确认了写操作, 却无法保证数据的持久化。但全异步配置的优点则是,不管从节点上数据多么滞后, 主节点总是可以继续响应写请求,系统的吞吐性能更好。
当如果出现以下情况时,如需要增加副本数以提高容错能力,或者替换失败的副本,就需要考虑增加新的从节点。但如何确保新的从节点和主节点保持数据一致呢?
简单地将数据文件从一个节点复制到另一个节点通常是不够的。主要是因为客户端仍在不断向数据库写入新数据,数据始终处于不断变化之中,因此常规的文件拷贝方式将会导致不同节点上呈现出不同时间点的数据。
另一种思路是:考虑锁定数据库(使其不可写)来使磁盘上的文件保持一致,但这会违反高可用的设计目标。
在不停机、数据服务不中断的前提下,也有一种可行性复制方案,其主要操作步骤如下:
建立新的从副本具体操作步骤可能因数据库系统而异。
系统中的任何节点都可能因故障或者计划内的维护(例如重启节点以安装内核安全补丁)而导致中断甚至停机。如果能够在不停机的情况下重启某个节点,这会对运维带来巨大的便利。我们的目标是,尽管个别节点会出现中断,但要保持系统总体的持续运行,并尽可能减小节点中断带来的影响。
如何通过主从复制技术来实现系统高可用呢?
从节点的本地磁盘上都保存了副本收到的数据变更日志。如果从节点发生崩溃,然后顺利重启,或者主从节点之间的网络发生暂时中断(闪断),则恢复比较容易,根据副本的复制日志,从节点可以知道在发生故障之前所处理的最后一笔事务,然后连接到主节点,并请求自那笔事务之后中断期间内所有的数据变更。在收到这些数据变更日志之后,将其应用到本地来追赶主节点。之后就和正常情况一样持续接收来自主节点数据流的变化。
选择某个从节点将其提升为主节点;客户端也需要更新,这样之后的写请求会发送给新的主节点,然后其他从节点要接受来自新的主节点上的变更数据,这一过程称之为切换。
故障切换可以手动进行,例如通知管理员主节点发生失效,采取必要的步骤来创建新的主节点;或者以自动方式进行。自动切换的步骤通常如下:
上述切换过程依然充满了很多变数:
最简单的情况,主节点记录所执行的每个写请求(操作语句)井将该操作语句作为日志发送给从节点。对于关系数据库,这意味着每个 INSERT 、UPDATE 或 DELETE 语句都会转发给从节点,并且每个从节点都会分析井执行这些 SQU 吾句,如同它们是来自客户端那样。
听起来很合理也不复杂,但这种复制方式有一些不适用的场景:
NOW()
获取当前时间,或 RAND()
获取一个随机数等,可能会在不同的副本上产生不同的值。UPDATE ... WHERE <某些条件>
),则所有副本必须按照完全相同的顺序执行,否则可能会带来不同的结果。进而,如果有多个同时并发执行的事务时, 会有很大的限制。有可能采取一些特殊措施来解决这些问题,例如,主节点可以在记录操作语句时将非确定性函数替换为执行之后的确定的结果,这样所有节点直接使用相同的结果值。但是,这里面存在太多边界条件需要考虑,因此目前通常首选的是其他复制实现方案。
MySQL 5.1 版本之前采用基于操作语句的复制。现在由于逻辑紧凑,依然在用,但是默认情况下,如果语句中存在一些不确定性操作,则 MySQL 会切换到基于行的复制(稍后讨论)。VoltDB 使用基于语句的复制,它通过事务级别的确定性来保证复制的安全。
通常每个写操作都是以追加写的方式写入到日志中:
不管哪种情况,所有对数据库写入的字节序列都被记入日志。因此可以使用完全相同的日志在另一个节点上构建副本:除了将日志写入磁盘之外, 主节点还可以通过网络将其发送给从节点。
PostgreSQL 、Oracle 以及其他系统等支持这种复制方式。其主要缺点是日志描述的数据结果非常底层: 一个 WAL 包含了哪些磁盘块的哪些字节发生改变,诸如此类的细节。这使得复制方案和存储引擎紧密耦合。如果数据库的存储格式从一个版本改为另一个版本,那么系统通常无能支持主从节点上运行不同版本的软件。
看起来这似乎只是个有关实现方面的小细节,但可能对运营产生巨大的影响。如果复制协议允许从节点的软件版本比主节点更新,则可以实现数据库软件的不停机升级:首先升级从节点,然后执行主节点切换,使升级后的从节点成为新的主节点。相反,复制协议如果要求版本必须严格一致(例如 WALf 专输),那么就势必以停机为代价。
另一种方能是复制和存储引擎采用不同的日志格式,这样复制与存储逻辑剥离。这种复制日志称为逻辑日志,以区分物理存储引擎的数据表示。
关系数据库的逻辑日志通常是指一系列记录来描述数据表行级别的写请求:
如果一条事务涉及多行的修改,则会产生多个这样的日志记录,并在后面跟着一条记录,指出该事务已经提交。MySQL 的二进制日志 binlog (当配置为基于行的复制时)使用该方式。
由于逻辑日志与存储引擎逻辑解耦,因此可以更容易地保持向后兼容,从而使主从节点能够运行不同版本的软件甚至是不同的存储引擎。
对于外部应用程序来说,逻辑日志格式也更容易解析。
在某些情况下,我们可能需要更高的灵活性。例如,只想复制数据的一部分,或者想从一种数据库复制到另一种数据库,或者需要订制、管理冲突解决逻辑( 参阅本章后面的“处理写冲突”),则需要将复制控制交给应用程序层。
有一些工具,可以通过读取数据库日志让应用程序获取数据变更。另一种方法则是借助许多关系数据库都支持的功能:触发器和存储过程。
触发器支持注册自己的应用层代码,使得当数据库系统发生数据更改(写事务)时自动执行上述自定义代码。通过触发器技术,可以将数据更改记录到一个单独的表中,然后外部处理逻辑访问该表,实施必要的自定义应用层逻辑,例如将数据更改复制到另一个系统。Oracle 的 Databus 和 Postgres 的 Bucardo 就是这种技术的典型代表。基于触发器的复制通常比其他复制方式开销更高, 也比数据库内置复制更容易出错,或者暴露一些限制。然而,其高度灵活性仍有用武之地。
主从复制要求所有写请求都经由主节点,而任何副本只能接受只读查询。对于读操作密集的负载(如 Web ),这是一个不错的选择:创建多个从副本,将读请求分发给这些从副本,从而减轻主节点负载井允许读取请求就近满足。
在这种扩展体系下,只需添加更多的从副本,就可以提高读请求的服务吞吐量。但是,这种方法实际上只能用于异步复制,如果试图同步复制所有的从副本,则单个节点故障或网络中断将使整个系统无法写入。而且节点越多,发生故障的概率越高,所以完全同步的配置现实中反而非常不可靠。
不幸的是,如果一个应用正好从一个异步的从节点读取数据,而该副本落后于主节点,则应用可能会读到过期的信息。这会导致数据库中出现明显的不一致:由于并非所有的写入都反映在从副本上,如果同时对主节点和从节点发起相同的查询,可能会得到不同的结果。经过一段时间之后,从节点最终会赶上并与主节点数据保持一致。这种效应也被称为最终一致性。
许多应用让用户提交一些数据,接下来查看他们自己所提交的内容。例如客户数据库中的记录,亦或者是讨论主题的评论等。提交新数据须发送到主节点,但是当用户读取数据时,数据可能来自从节点。这对于读密集和偶尔写入的负载是个非常合适的方案。
然而对于异步复制存在这样一个问题,如图所示,用户在写入不久即查看数据,则新数据可能尚未到达从节点。对用户来讲, 看起来似乎是刚刚提交的数据丢失了,显然用户不会高兴。
对于这种情况,我们需要读写一致性。该机制保证如果用户重新加载页面, 他们总能看到自己最近提交的更新。但对其他用户则没有任何保证,这些用户的更新可能会在稍后才能刷新看到。如何实现呢?有以下几种可行性方案:
如果同一用户可能会从多个设备访问数据,情况会更加复杂。
用户看到了最新内容之后又读到了过期的内容,好像时间被回拨, 此时需要单调读一致性。
单调读一致性可以确保不会发生这种异常。这是一个比强一致性弱,但比最终一致性强的保证。当读取数据时,单调读保证,如果某个用户依次进行多次读取,则他绝不会看到回攘现象,即在读取较新值之后又发生读旧值的情况。
实现单调读的一种方式是,确保每个用户总是从固定的同一副本执行读取(而不同的用户可以从不同的副本读取)。例如,基于用户 ID 的哈希的方怯而不是随机选择副本。但如果该副本发生失效,则用户的查询必须重新路由到另一个副本。
前缀一致读:对于一系列按照某个顺序发生的写请求,那么读取这些内容时也会按照当时写入的顺序。
如果数据库总是以相同的顺序写入,则读取总是看到一致的序列,不会发生这种反常。然而,在许多分布式数据库中,不同的分区独立运行,因此不存在全局写入顺序。这就导致当用户从数据库中读数据时,可能会看到数据库的某部分旧值和另一部分新值。
一个解决方案是确保任何具有因果顺序关系的写入都交给一个分区来完成,但该方案真实实现效率会大打折扣。现在有一些新的算法来显式地追踪事件因果关系。
使用最终一致性系统时,最好事先就思考这样的问题:如果复制延迟增加到几分钟甚至几小时,那么应用层的行为会是什么样子?如果这种情况不可接受,那么在设计系统肘,就要考虑提供一个更强的一致性保证,比如写后读; 如果系统设计时假定是同步复制,但最终它事实上成为了异步复制,就可能会导致灾难性后果。
在应用层可以提供比底层数据库更强有力的保证。例如只在主节点上进行特定类型的读取,而代价则是,应用层代码中处理这些问题通常会非常复杂,且容易出错。
如果应用程序开发人员不必担心这么多底层的复制问题,而是假定数据库在“做正确的事情”,情况就变得很简单。而这也是事务存在的原因,事务是数据库提供更强保证的一种方式。
单节点上支持事务已经非常成熟。然而,在转向分布式数据库(即支持复制和分区)的过程中,有许多系统却选择放弃支持事务,并声称事务在性能与可用性方面代价过高,所以选择了最终一致性。
主从复制方法较为常见,但存在一个明显的缺点:系统只有一个主节点,而所有写入都必须经由主节点。如果由于某种原因,例如与主节点之间的网络中断而导致主节点无法连接,主从复制方案就会影响所有的写入操作。
对主从复制模型进行自然的扩展,则可以配置多个主节点,每个主节点都可以接受写 s 操作,后面复制的流程类似: 处理写的每个主节点都必须将该数据更改转发到所有其他节点。这就是多主节点( 也称为主-主,或主动/主动)复制。此时,每个主节点还同时扮演其他主节点的从节点。
在一个数据中心内部使用多主节点基本没有太大意义,其复杂性已经超过所能带来的好处。
但是,以下场景这种配置则是合理的:
为了容忍整个数据中心级别故障或者更接近用户,可以把数据库的副本横跨多个数据中心。而如果使用常规的基于主从的复制模型,主节点势必只能放在其中的某一个数据中心,而所有写请求都必须经过该数据中心。
有了多主节点复制模型,则可以在每个数据中心都配置主节点。在每个数据中心内,采用常规的主从复制方案;而在数据中心之间,由各个数据中心的主节点来负责同其他数据中心的主节点进行数据的交换、更新。
部署单主节点的主从复制方案与多主复制方案之间的差异
另一种多主复制比较适合的场景是,应用在与网络断开后还需要继续工作。
这种情况下,每个设备都有一个充当主节点的本地数据库(用来接受写请求),然后在所有设备之间采用异步方式同步这些多主节点上的副本,同步滞后可能是几小时或者数天,具体时间取决于设备何时可以再次联网。
从架构层面来看,上述设置基本上等同于数据中心之间的多主复制,只不过是个极端情况,即一个设备就是数据中心,而且它们之间的网络连接非常不可靠。多个设备同步日历的例子表明,多主节点可以得到想要的结果,但中间过程依然有很多的未知数。
有一些工具可以使多主配置更为容易,如 CouchDB 就是为这种操作模式而设计的。
实时协作编辑应用程序允许多个用户同时编辑文档。
我们通常不会将协作编辑完全等价于数据库复制问题,但二者确实有很多相似之处。当一个用户编辑文档时· ,所做的更改会立即应用到本地副本( Web 浏览器或客户端应用程序),然后异步复制到服务器以及编辑同一文档的其他用户。如果要确保不会发生编辑冲突,则应用程序必须先将文档锁定,然后才能对其进行编辑。如果另一个用户想要编辑同一个文档, 首先必须等到第一个用户提交修改并释放锁。这种协作模式相当于主从复制模型下在主节点上执行事务操作。
为了加快协作编辑的效率, 可编辑的粒度需要非常小。例如,单个按键甚至是全程无锁。然而另一方面, 也会面临所有多主复制都存在的挑战, 即如何解决冲突。
多主复制的最大问题是可能发生写冲突。
如果是主从复制数据库,第二个写请求要么会被阻塞直到第一个写完成, 要么被中止(用户必须重试) 。然而在多主节点的复制模型下,这两个写请求都是成功的,井且只能在稍后的时间点上才能异步检测到冲突,那时再要求用户层来解决冲突为时已晚。
理论上, 也可以做到同步冲突检测,即等待写请求完成对所有副本的同步,然后再通知用户写入成功。但是,这样做将会失去多主节点的主要优势:允许每个主节点独立接受写请求。如果确实想要同步方式冲突检测,或许应该考虑采用单主节点的主从复制模型。
处理冲突最理想的策略是避免发生冲突,即如果应用层可以保证对特定记录的写请求总是通过同一个主节点,这样就不会发生写冲突。现实中,由于不少多主节点复制模型所实现的冲突解决方案存在瑕疵,因此,避免冲突反而成为大家普遍推荐的首选方案。
但是,有时可能需要改变事先指定的主节点,例如由于该数据中心发生故障,不得不将流量重新路由到其他数据中心,或者是因为用户已经漫游到另一个位置,因而更靠近新数据中心。此时,冲突避免方式不再有效,必须有措施来处理同时写入冲突的可能性。
对于主从复制模型,数据更新符合顺序性原则,即如果同一个字段有多个更新,则最后一个写操作将决定该字段的最终值。
对于多主节点复制模型,由于不存在这样的写入顺序,所以最终值也会变得不确定。
实现收敛的冲突解决有以下可能的方式:
解决冲突最合适的方式可能还是依靠应用层,所以大多数多主节点复制模型都有工具来让用户编写应用代码来解决冲突。可以在写入时或在读取时执行这些代码逻辑:
注意, 冲突解决通常用于单个行或文档, 而不是整个事务。因此,如果有一个原子事务包含多个不同写请求,每个写请求仍然是分开考虑来解决冲突。
单主节点和多主节点复制,都是基于这样一种核心思路,即客户端先向某个节点(主节点)发送写请求,然后数据库系统负责将写请求复制到其他副本。由主节点决定写操作的顺序, 从节点按照相同的顺序来应用主节点所发送的写日志。
一些数据存储系统则采用了不同的设计思路:选择放弃主节点,允许任何副本直接接受来自客户端的写请求。对于某些无主节点系统实现,客户端直接将其写请求发送到多副本,而在其他一些实现中,由一个协调者节点代表客户端进行写人,但与主节点的数据库不同,协调者井不负责写入顺序的维护。
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。