《数据密集型应用系统设计》笔记五:第五章 数据复制

1. 数据复制的目的

复制主要指通过互联网络在多台机器上保存相同数据的副本。复制主要用于以下目的:

(1)高可用:即使某台机器出现故障,系统也能保持正常运行。
(2)低延迟:将数据放置在距离用户较近的地方,从而实现更快地交互。
(3)可扩展:采用多副本读取,大幅提高系统读操作的吞吐量。
(4)高容错:允许应用程序在出现网络中断时继续工作。

2. 数据复制方案及问题讨论

主要有三种复制方案:主从复制、多主节点复制和无主节点复制。

2.1 主从复制

所有的客户端写入操作都发送到主节点,由主节点负责将数据更改事件发送到其它副本(从节点)。每个副本都可以接收读请求,但内容可能是过期值。

主从复制原理

  1. 指定一个节点为主节点。当客户端写数据库时,必须将写请求发送给主节点,主节点将数据首先写入本地存储。
  2. 其它副本都是从节点。主节点将数据写入本地存储后,然后将数据更改以日志或更改流的方式发送给所有的从节点。每个从节点获得更改日志后将其应用到本地,且严格保持与主节点相同的写入顺序。
  3. 客户端读数据时,可以在主节点或从节点执行查询。只有主节点才可以接受写请求,从节点都是只读的!

主从复制问题

主从复制是最经典也使用最广泛的复制方案,但是仍然有许多问题需要考虑。

同步异步问题

同步复制:主节点需等待直到从节点确认完成写入,才会向客户端报告完成。
异步复制:主节点向从节点发送完消息之后立即返回,向客户端报告完成,不用等待从节点的完成确认。

同步异步的优缺点分析:

优缺点比较 同步复制 异步复制
优点 安全性高。一旦向用户确认,从节点可以明确保证完成了与主节点的更新同步,数据已经处于最新版本。万一主节点发生故障,总是可以在从节点继续访问最新的数据。 效率高,吞吐性能好。不管从节点上数据多么滞后,主节点不需要确认从节点写入,主节点总是可以响应写请求,系统的吞吐性能好。
缺点 效率低下。只要同步的从节点无法完成确认,写入就不能视为成功。主节点会阻塞其后所有的写操作,直到同步副本确认完成 安全性低下,可能导致数据丢失。如果主节点发生崩溃且不可恢复,则所有尚未复制到从节点的写请求都会丢失。

在实践中,将所有从节点都配置成同步复制是不必要且不切实际的。一般情况下,如果数据库启用了同步复制,通常意味着 其中某一个节点是同步的,而其他节点则是异步模式。这样可以保证至少有两个节点拥有最新的数据副本。这种配置也被成为“半同步”。

新的从节点如何复制数据?

主要逻辑是:主节点产生快照,将快照拷贝到新的从节点,从节点连接到主节点并请求快照点之后所发生的数据更改日志,获得日志后,从节点应用这些快照点之后的所有数据变更,这个过程称为“追赶”。

如何处理节点失效?

  1. 从节点失效:追赶式恢复
    从节点的本地磁盘上都保存了副本收到的数据变更日志。如果从节点发生崩溃,根据日志,从节点可以知道在发生故障之前所处理的最后一笔事务,然后连接到主节点,并请求自那笔事务之后的所有数据变更。在收到这些数据变更日志之后,将其应用到本地来追赶主节点。
  2. 主节点失效:节点切换
    如果主节点失效,问题则比较复杂,这里面涉及很多细节考量。总的来说,解决思路是固定的:选择某个从节点将其提升为主节点,客户端也需要更新,这样之后的写请求会发送给新的主节点,然后其他从节点要接收来自新的主节点的变更数据,这一过程称为切换。

    自动切换的步骤:
    (1)确认主节点失效
    (2)选举新的主节点
    (3)重新配置系统,使新的主节点生效。

复制日志的实现

主从复制技术的实现依赖于复制的日志,这在实践中有多种不同的实现方法:

  1. 基于语句的复制

    这是最简单的情况,主节点记录所执行的每个写请求,并将该操作语句作为日志发送给从节点。这种复制日志最简单直观,但有一些不适用的场景:

    (1)任何调用了非确定性函数的语句,如NOW()获取当前时间,RAND()获取一个随机数等,可能会在不同的副本上产生不同的值。
    (2)如果语句中使用了自增列,则所有的副本必须按照完全相同的顺序执行,否则可能会产生不同的结果。在这种情况下,如果有多个同时并发执行的事务,会有很大的限制。
    (3)有副作用的语句(如:触发器,存储过程,用户定义的函数等),可能会在每个副本上产生不同的副作用。

    这种复制日志方式主要应用于MySQL5.1之前的版本。

  2. 基于预写日志传输

    所有对数据库写入的字节序列都被计入日志,因此可以使用完全相同的日志在另一个节点上构建副本,除了将日志写入磁盘外,主节点还可以通过网络将其发生给从节点。从节点收到日志进行处理,建立和主节点内容完全相同的数据副本。

    PostgreSQL、Oracle支持这种复制方式。
    主要缺点:日志描述的数据结果非常底层:一个WAL包含了哪些磁盘块的哪些字节发生改变,诸如此类的细节。这使得复制方案和存储引擎紧密耦合。如果数据库的存储格式从一个版本改为另一个版本,那么系统通常无法支持主从节点上运行不同版本的软件。

    对运营产生的影响:如果复制协议允许从节点的软件版本比主节点更新,则可以实现数据库软件的不停机升级。首先升级从节点,然后执行主节点切换,使升级后的从节点成为新的主节点。但是WAL传输,要求版本必须严格一致,那么势必以停机为代价。

  3. 基于行的逻辑日志复制
    由于WAL传输的问题,另一种方法是复制和存储引擎采用不同的日志格式,这样复制和存储逻辑剥离。这种复制日志称为逻辑日志,以区分物理存储引擎的数据表示。

    关系数据库的逻辑日志通常是指一系列记录来描述数据表行级别的写请求:

    (1)对于行插入:日志包含所有相关列的新值
    (2)对于行删除:日志里有足够的信息来唯一标识已删除的行。主要是靠主键,但如果表上没有定义主键,就需要记录所有列的旧值。
    (3)对于行更新,日志包含足够的信息来唯一标识更新的行,以及所有列的新值。

    基于行的逻辑日志的优势:

    (1)由于逻辑日志和存储引擎逻辑解耦,因此更容易地保持向后兼容,从而使主从节点能够运行不同版本的软件甚至是不同的存储引擎。
    (2)对于外部应用程序来说,逻辑日志程序也更容易解析。如果要将数据库的内容发送到外部系统(如离线分析的数据仓库),或者构建自定义索引和缓存等,基于逻辑日志的复制更有优势。

  4. 基于触发器的复制。

    前面的三种复制方法都是由数据库系统来实现的,不涉及任何应用程序代码。为了实现更高的灵活性,例如,只想复制数据的一部分,或者想从一种数据库复制到另一种数据库,或者需要定制、管理冲突解决逻辑,则需要将复制控制交给应用程序层

    通过触发器技术,可以将数据更改记录到一个单独的表中,然后外部处理逻辑访问该表,实施必要的自定义应用层逻辑。越灵活,越容易出错,基于触发器的复制通常问题多多。

复制滞后问题

对于异步复制,由于各种问题可能会出现复制滞后。如果一个应用正好从一个异步的从节点读取数据,而该副本落后于主节点,则应用可能会读到过期的信息。从而导致数据库中出现明显的不一致。复制滞后可能出现的三个关键问题如下:

  1. 读自己的写

    表现场景:用户在写入不久即查看数据,则新数据可能尚未到达从节点。在用户看来,似乎是刚刚提交的数据丢失了。

    解决方法:保证“写后读一致性”,也称为读写一致性。该机制保证如果用户重新加载页面,他们总能看到自己最近提交的更新。具体实现方法请参照P156.

  2. 单调读

    表现场景:用户看到了最新内容之后又读到了过期的内容,出现了用户数据向后回滚的奇怪情况,好像时间被回拨。

    解决方法:保证“单调读一致性”,这是一个比强一致性弱,但是比最终一致性强的保证。该机制保证如果用户依次进行多次读取,则他绝不会看到回滚现象,即在读取较新值之后又发生读旧值的情况。实现单调读一致性的最简单方式是:确保每个用户总是从固定的同一副本执行读取。

  3. 前缀一致读

    表现场景:这是分区(分片)数据库中出现的一个特殊问题。分区数据经多副本复制后出现了不同程度的滞后,导致观察者先看到果,后看到因。

    解决方法:保证“前缀一致读”。该机制保证,对于一系列按照某个顺序发生的写请求,那么读取这些内容时也会按照当时写入的顺序。实现前缀一致读的最简单方式是:确保任何具有因果顺序关系的写入都交给一个分区来完成。

复制滞后的解决方案:是否支持事务?
显而易见,事务是数据库提供更强保证的一种方式,使用事务可以很方便的实现强一致性。然而,在分布式数据库系统中,有很多缺放弃了支持事务,并声称事务在性能和可用性方面代价过高(这是事实),因此断言在可扩展的分布式系统中最终的一致性是无法避免的终极选择(优点夸张了,现在Hive就支持了事务)

2.2 多主节点复制

系统存在多个主节点,每个都可以接收写请求,客户端将写请求发送到其中的一个主节点上,由该主节点负责将数据更改事件同步到其它主节点和自己的从节点。

适用场景

在一个数据中心内部使用多节点基本没有太多意义,多主节点主要的应用场景如下:

  1. 多数据中心

    在每个数据中心都配置主节点;
    在每个数据中心内,仍然采用常规的主从复制方案;
    在数据中心之间,由各个数据中心的主节点来负责同其他数据中心的主节点进行数据的交换、更新。

    多数据中心的优势:

    (1)性能更好。传统的一个数据中心的主从复制,所有写请求都要由广域网传送至主节点所在的数据中心,这大大增加了写入延迟。多数据中心的设计,就近原则。使得每个写操作都可以在本地数据中心快速响应,然后采用异步复制方式将变化同步到其它数据中心。这相当于屏蔽了数据中心之间的网络延迟,极大地提高了性能。
    (2)容错性更好。只有一个数据中心,如果主节点所在的数据中心发生故障,必须进行主从切换,这中间涉及全部从节点的认主问题。多数据中心则不必那么麻烦,每个数据中心都是独立于其他数据中心的,只需要将发生故障的数据中心进行切换即可,影响范围极大地缩小了。
    (3)可靠性更好。数据中心之间由于地域的限制,通常使用广域网,它往往不如数据中心内的本地网络可靠。多数据中心之间通常采用异步复制,可以更好地容忍此类问题。

    多数据中心的缺点;

    不同数据中心可能会同时修改相同的数据,因此必须解决潜在的写冲突。

  2. 离线客户端操作
    另一种多主复制比较适合的场景是:应用在与网络断开后还需要继续工作。

    这种情况下,每个设备都有一个充当主节点的本地数据库,然后在所有设备之间采用异步方式同步这些多主节点上的副本,同步时间可能是数个小时或者几天,具体时间取决于设备何时可以再次联网。

    从架构层面看,上述设置基本等同于数据中心之间的多主复制,只不过是极端情况,即一个设备就是一个数据中心,而且他们之间的网络连接非常不可靠。

  3. 协作编辑
    实时协作编辑应用程序允许多个用户同时编辑文档,它不完全等价于数据库复制问题,但两者有很多相似之处。当一个用户编辑文档时,所做的更改会立即应用到本地副本,然后异步复制到服务器以及编辑同一文档的其它用户。

    为了确保不会发生编辑冲突,应用程序必须先将文档锁定,然后才能对其进行编辑。如果另一个用户想要编辑同一个文档,首先必须等到第一个用户提交修改并释放锁,这相当于在主节点上执行事务操作。

最大问题

多主复制的最大问题是可能产生写冲突。

如果是主从复制数据库,第二个写请求会被阻塞直到第一个写完成,要么被中止。但是在多主复制模式,这两个写请求都是成功的,并且只能在稍后的时间点才能异步检测到冲突。

我们既不想丧失多主节点的优势,又想解决了冲突问题。该怎么做?

  1. 避免冲突

    (1)最理想的解决冲突的策略是避免发生冲突,即如果应用层可以保证对特定记录的写请求总是通过同一个主节点,这样就不会发生写冲突。
    (2)实践中实现,不同的用户总是对应不同的数据中心,并只在该数据中心的主节点上进行读写。

  2. 收敛于一致状态

    多主节点复制模型的最终不一致性:

    (1)对于主从复制,如果同一个字段有多个更新,则最后一个写操作将决定该字段的最终值。无论如何,都能达到最终一致性。
    (2)对于多主节点复制,因为有多个主节点,没有绝对一致的写入顺序,每个副本都只是按照它看到的写入顺序执行,那么数据库将处于最终不一致状态。

    最终一致性是复制模型的最基本要求,因此多主节点复制模型必须以一种收敛趋同的方式来解决冲突。实现收敛的冲突解决方案有如下几种:

    (1)基于时间戳:最后写入者获胜。
    (2)基于ID:最高(级别)写入者获胜。为每个副本分配一个唯一的ID ,规则为:序号高的副本写入始终优先于序号低的副本。
    (3)利用预定义好的格式来记录和保留冲突相关的所有信息,然后依靠应用层的逻辑,事后解决冲突

  3. 自定义冲突解决逻辑

    解决冲突的最合适方式还是依靠应用层,绝大多数多主节点复制模式都有工具来让用户编写应用代码来解决冲突。

    (1)在写入时执行:只要数据库系统在复制变更日志时检测到冲突,就会调用应用层的冲突处理程序。
    (2)在读取时执行:当检测到冲突时,所有冲突写入值都会暂时保存下来。下一次读取数据时,会将数据的多个版本读返回给应用层。应用层提示用户解决或者自动解决冲突并将最终结果返回到数据库。

  4. 自动冲突解决
    有一些有意思的研究尝试自动解决并发修改所产生的冲突(这是好的。),如下:

    (1)无冲突的复制数据类型
    (2)可合并的持久数据结构
    (3)操作转换

多主节点模型的拓扑结构

常见的有三种拓扑结构:环形拓扑、星形拓扑和全部-至-全部型拓扑。

(1)目前基本都使用全部-至-全部型拓扑:每个主节点将其写入同步到其它所有主节点。
(2)MySQL比较特别,默认只支持环形拓扑:每个主节点接收前序主节点的写入,并将这些写入转发给后序主节点。
(3)基本没有使用星形拓扑:一个指定的根节点将写入转发给所有其它节点。(这种方式不就是主从复制吗?和多主节点没啥关系了)

环形和星形拓扑的问题是:如果某一个节点发生了故障,在修复前,会影响其它节点之间复制日志的转发。
全部-至-全部型拓扑的问题是:存在某些网络链路比其他链路更快的情况,从而导致复制日志之间的覆盖。

2.3 无主节点复制

没有主节点,客户端将写请求发送到多个节点上,读取时从多个节点上并行读取,以此检测和纠正某些过期数据。

其实最早的数据复制系统就是无主节点的(去中心复制),但是后来被关系型数据库主导时代了。当亚马逊采用了Dynamo系统后,无主复制再次流行了起来。Riak,Cassandra,Voldemort都是受Dynamo的启发而设计的无主节点,开源数据库系统,这类数据库也被称为Dynamo风格数据库。

无主节点复制的读取和写入操作总是并行发送到所有的副本,并根据节点的响应情况作出判断,这里面涉及如下重要问题:

节点失效

  1. 读写quorum——判断读写是否有效
    仲裁条件: ω + r > n \omega+r>n 。表示如果有 n n 个副本,写入需要 w w 个节点确认,读取至少需要查询 r r 个节点,则达到仲裁条件可以认为写入成功,读取的节点节点中一定会包含最新值。满足上述的 w w r r 也被称为法定票数写和法定票数读,也可以认为 w w r r 是用来判定写、读是否有效的最低票数(必须同时满足)。如果可用节点数小于所需的 w w r r ,则写入或读取就会返回错误。

    (1)在Dynamo风格的数据库中,参数 w w r r 通常是可配置的。
    (2)最常见的配置是设置 n n 为奇数,然后 w = r = ( n + 1 ) 2 w=r=\frac{(n+1)}{2} (向上取整)
    (3)可以根据需求和应用场景,灵活配置。例如,对于读多写少的负载,设置 w = n w=n r = 1 r=1 比较适合,这样读取速度更快,缺点是一个失效的节点就会使得数据库所有写入因无法满足quorum而失败。(当 w = n w=n r = 1 r=1 时,得到的是WARO机制:在更新时写所有副本,只有所有副本中更新都成功才算成功,保证了所有副本的一致性。此时,读取时读取任意一个副本即可。)

  2. 读修复和反熵——修复失效节点
    任何复制模型的最基本要求是:确保所有的数据最终复制到所有的副本,在所有副本上达到最终一致性。当一个失效的节点重新上线之后,它如何赶上中间错过的写请求呢?

    (1)读修复:当客户端并行读取多个副本时,可以检测到过期的返回值。这种方法主要适合那些被频繁读取的场景。读修复的问题是:只有在发生读取时才可能执行修复,那些很少访问的数据有可能在某些副本中已经丢失而无法检测到。
    (2)反熵:一些数据存储有后台进程不断查找副本之间数据的差异,将任何缺少的数据从一个副本复制到另一个副本。反熵过程的缺点是:并不保证以特定的顺序复制写入,并且会引入明显的同步滞后。

Quorum一致的局限性

仲裁条件失效

即使在 ω + r > n \omega+r>n 的情况下,也可能存在返回旧值的边界条件。可能的情况包括:

  1. 如果采用sloppy quorum,写操作的 w w 个节点和读取的 r r 个节点可能完全不同(因为这里涉及节点集合n之外的节点),因此,无法保证读写请求一定存在重叠的节点。
  2. 如果具有新值的节点后来发生了失效,但恢复数据来自某个旧值,则总的新值副本数实际会低于 w w ,这就打破了之前的判定条件。
  3. 即使一切工作正常,也会出现一些边界情况,例如“可线性化与quorum”

监控旧值

人的容忍度是有限的,而最终一致性的实现时间不确定的,那么旧值的落后就没有一个上限。即使应用程序可以容忍读取旧值,也需要仔细监控了解复制的当前运行状态。如果出现了明显的滞后,它就是一个重要的信号提醒我们需要采取必要措施来排查原因。

主从复制已经建立了完善的统一监控模块,可以导出复制滞后的相关指标。而对于无主节点复制系统,并没有固定的写入顺序,因而监控就变的更加困难。如果数据库只支持读时修复(不支持反熵),旧值的落后就没有一个上限。

sloppy quorum与数据回传

在一个大规模集群中,客户可能在网络中断期间还能连接到某些数据库节点,但这些节点又不是能够满足数据仲裁的那些节点。此时,数据库设计者就面临着一个选择:

(1)如果无法达到 w w r r 所要求的quorum,将错误明确的返回给客户端?
(2)或者,我们应该接受该写请求,只是将它们暂时写入一些可访问的节点中?(需要注意的是:这些节点并不在n个节点集合中)

后一种方案就是放松的仲裁(sloppy quorum):写入和读取仍然需要 w w r r 个成功的响应,但包含了那些并不在先前指定的 n n 个节点。一旦网络问题解决,临时节点需要把接收到的写入全部发送到原始主节点上,这就是数据回传

sloppy quorum的优点:提高写入可用性
带来的问题是:即使满足 w + r > n w+r>n ,也不能保证在读取时,一定可以读到最新值,因为新值可能被临时写入 n n 之外的某些节点且尚未传回来。

检测并发写

Dynamo数据库允许多个客户端对相同的主键同时发起写操作,即使采用严格的quorum机制也可能会发生写冲突。

如何保证最终一致性?

核心的问题是:由于网络延迟不稳定或局部失效,请求在不同的节点上可能会呈现不同的顺序。我们希望副本可以收敛于相同的内容,这样才能达到最终一致,但是以谁为准?

  1. 最后写入者获胜
    为每个写请求附加一个时间戳,然后选择最新的时间戳,丢弃较早时间戳的写入。
  2. 避免并发
    要想完全避免LWW完全无副作用的唯一方法是:只写入一次然后写入值视为不可变,这样就避免了对同一个主键的并发写。例如,Cassandra的一个推荐使用方法就是采用UUID作为主键,这样每个写操作都针对不同的、系统唯一的主键。

并发的问题

  1. 并发处理
    服务器判断操作是否并发的依据主要依靠对比版本号(或时间戳),而不需要解释新旧值本身。算法的工作流程如下:

    (1)服务器为每个主键维护一个版本号,每当主键新值写入时递增版本号,并将新版本号与写入的值一起保存。
    (2)当客户端读取主键时,服务器将返回所有(未被覆盖)的当前值以及最新的版本号。
    (3)客户端写主键,写请求必须包含之前读到的版本号,读到的值和新值合并后的集合。
    (4)当服务器收到特定版本号的写入时,覆盖该版本号或更低版本的所有值(因为这些值已经被合并到新传入的值集合中),但必须保存更高版本号的所有值(因为这些值与当前的写操作属于并发)

  2. 并发删除
    并发合并在删除时不能简单地从数据库删除,系统必须保留一个对应的版本号以恰当的标记该项目需要在合并时被剔除,这种删除标记被称为“墓碑”。

思考:无主节点复制的去中心化,在区块链技术中是否有很多用处?

发布了5 篇原创文章 · 获赞 1 · 访问量 94

猜你喜欢

转载自blog.csdn.net/weixin_43902592/article/details/103918630