第5章 数据复制

概述

复制主要指通过互联网络在多台机器上保存相同数据的副本

通过数据复制方案,人们通常希望达到以下目的:

  • 使数据在地理位置上更接近用户,从而降低访问延迟
    • 杭州:访问靠近杭州服务器,北京:访问靠近北京的服务器
  • 当部分组件出现故障,系统依然可以继续工作,从而提高可用性
  • 扩展至多台机器以同时提供数据访问服务,从而提高系统读吞吐量

技术难点:在于处理那些持续变化的数据。

流行的复制变化数据的方法**:主从复制、多主节点复制和无主节点复制**。

主节点和从节点

主从复制的工作原理如下:

  1. 指定某一个副本为主副本(或称为主节点)。当客户写数据库时,必须将写请求首先发送给主副本,主副本首先将新尹写人本地存储。
  2. 其他副本则全部称为从副本(或称为从节点)。主副本把新数据写人本地存储后,然后将数据更改作为复制的日志或更改流发送给所有从副本。每个从副本获得更改日志之后将其应用到本地,且严格保持与主副本相同的写人顺序。
    • MySQL通过binlog日志实现主从复制 3.客户端从数据库中读数据时,可以在主副本或者从副本上执行查询。只有主副本才可以接受写请求;从客户端的角度来看,从副本都是只读的。

同步复制与异步复制

复制的一个核心要素就是选择 同步复制 还是 异步复制

主从复制中,包含同步和异步的流程:

  • 从节点1的复制是同步的:主节点需等待直到从节点1确认完成了写人,然后才会向用户报告完成,并且将最新的写人对其他客户端可见。
  • 从节点2的复制是异步的:主节点发送完消息之后立即返回,不用等待从节点2的完成确认。

同步复制的优缺点:

  • 优点:一旦向用户确认,从节点可以明确保证完成了与主节点的更新同步,数据已经处于最新版本。万一主节点发生故障,总是可以在从节点继续访问最新数据。

  • 缺点:如果同步的从节点无法完成确认(例如,由于从节点发生崩溃,或网络故障,或任何其他原因),写人就不能视为成功。主节点会阻塞其后所有的写操作,直到同步副本确认完成。

因此,把所有从节点都配置为同步复制有些不切实际。因为这样的话,任何一个同步节点的中断都会导致整个系统更新停滞不前

方案一:半同步

如果数据库启用了同步复制,通常意味着其中某一个从节点是同步的,而其他节点则是异步模式。万一同步的从节点变得不可用或性能下降,则将另一个异步的从节点提升为同步模式。这样可以保证至少有两个节点(即主节点和一个同步从节点)拥有最新的数据副本。

方案二:全异步

如果主节点发生失败且不可恢复,则所有尚未复制到从节点的写请求都会丢失。这意味着即使向客户端确认了写操作,却无法保证数据的持久化。优点是不管从节点上数据多么滞后,主节点总是可以继续响应写请求,系统的吞吐性能更好。

虽然异步模式弱化了持久性,但是实际应用中还是广泛使用,特别是节点数量巨大或者分布于广域地理环境。

配置新的从节点

在不停机、数据服务不中断的前提下完成从节点的设置。逻辑上的主要操作步骤如下:

  1. 在某个时间点对主节点的数据副本产生一个一致性快照,这样避免长时间锁定整个数据库
  2. 将快照拷贝到新的从节点。
  3. 从节点连接到主节点并请求快照点之后所发生的数据更改日志
  4. 获得日志之后,从节点来应用这些快照点之后所有数据变更,这个过程称之为追赶。接下来,它可以继续处理主节点上新的数据变化。并重复步骤1、步骤4。

处理节点失效

从节点失效:追赶式恢复

根据日志里index偏移量,得知从节点失效前处理的最后一笔事务,然后连接到主节点,请求自从那笔事务之后中断期间内所有的数据变更。

主节点失效:节点切换

处理主节点故障的情况则比较棘手**:选择某个从节点将其提升为主节点**;客户端也需要更新,这样之后的写请求会发送给新的主节点,然后其他从节点要接受来自新的主节点的变更数据,这一过程称之为切换。

故障切换可以手动进行,例如通知管理员主节点发生失效,采取必要的步骤来创建新的主节点;或者以自动方式进行。自动切换的步骤通常如下:

  1. 确认主节点失效。基于超时的机制:节点间频繁地互相发生发送心跳存活消息
  2. 选举新的主节点。候选节点最好与原主节点的数据差异最小,这样可以最小化数据丢失的风险。
  3. 重新配置系统使新主节点生效。客户端现在需要将写请求发送给新的主节点。

切换过程中存在的问题

  1. 异步复制时,原主节点未完成复制的写请求就此丢弃。
  2. 如果在数据库之外有其他系统依赖于数据库的内容并在一起协同使用,丢弃数据的方案就特别危险。如MySQL和Redis缓存的数据同步。
  3. 可能会发生两个节点同时都自认为是主节点。这种情况被称为脑裂,它非常危险:两个主节点都可能接受写请求,并且没有很好解决冲突的办法,最后数据可能会丢失或者破坏。
  4. 如何设置合适的超时来检测主节点失效呢?主节点失效后,超时时间设置得越长也意味着总体恢复时间就越长。但如果超时设置太短,可能会导致很多不必要的切换。

坦白讲(摊牌了),对于这些问题没有简单的解决方案。因此,即使系统可能支持自动故障切换,有些运维团队仍然更愿意以手动方式来控制整个切换过程。

上述这些问题,包括节点失效、网络不可靠、副本一致性、持久性、可用性与延迟之间各种细微的权衡,实际上正是分布式系统核心的基本问题。

复制日志的实现

基于语句的复制

主节点记录所执行的每个写请求(操作语句)并将该操作语句作为日志发送给从节点。对于关系数据库,这意味着每个INSERT、UPDATE或DELETE语句都会转发给从节点,并且每个从节点都会分析并执行这些SQL语句,如同它们是来自客户端那样。

这种复制方式有一些不适用的场景:

  • 任何调用作确定性函数的语句,如NOW()获取当前时间,或RAND()获取一个随机数等,可能会在不同的副本上产生不同的值。
  • 如果语句中使用了自增列,或者依赖于数据库的现有数据(例如,UPDATE WHERE<某些条件>),则所有副本必须按照完全相同的顺序执行,否则可能会带来不同的结果。进而,如果有多个同时并发执行的事务时,会有很大的限制。
  • 有副作用的语句(例如,触发器、存储过程、用户定义的函数等),可能会在每个副本上产生不同的副作用。

可以采取一些特殊措施来解决这些问题。

  • 主节点可以在记录操作语句时将非确定性函数替换为执行之后的确定的结果,这样所有节点直接使用相同的结果值。
    • MySQL 5.1版本之前采用基于操作语句的复制。
    • 默认情况下,如果语句中存在一些不确定性操作,则MySQL会切换到基于行的复制。

基于预写日志(WAL)传输

存储引擎的磁盘数据结构,通常每个写操作都是以追加写的方式写入到日志中:

  • 对于日志结构存储引擎(SSTables和LSM-trees),日志是主要的存储方式。日志段在后台压缩并支持垃圾回收。
  • 对于采用覆盖写磁盘的B-tree结构,每次修改会预先写入日志,如系统发生崩溃,通过索引更新的方式迅速恢复到此前一致状态。

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

基于行的逻辑日志复制

复制和存储引擎采用不同的日志格式,这样复制与存储逻辑剥离。这种复制日志称为逻辑日志,以区分物理存储引擎的数据表示。

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

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

如果一条事务涉及多行的修改,则会产生多个这样的日志记录,并在后面跟着一条记录,指出该事务已经提交。MySQL的二进制日志binlog(当配置为基于行的复制时)使用该方式。

基于触发器的复制

到目前为止所描述的复制方法都是由数据库系统来实现的,不涉及任何应用程序代码。通常这是大家所渴望的,不过,在某些情况下,我们可能需要更高的灵活性。

有一些工具,例如Oracle GoldenGate,可以通过读取数据库日志让应用程序获取数据变更。另一种方法则是借助许多关系数据库都支持的功能**:触发器和存储过程**。

触发器支持注册自己的应用层代码,使得当数据库系统发生数据更改(写事务)时自动执行上述自定义代码。通过触发器技术,可以将数据更改记录到一个单独的表中,然后外部处理逻辑访问该表,实施必要的自定义应用层逻辑

基于触发器的复制通常比其他复制方式开销更高,也比数据库内置复制更容易出错,或者暴露一些限制。然而,其高度灵活性仍有用武之地。

复制滞后问题

主从复制要求所有写请求都经由主节点,而任何副本只能接受只读查询。这种方式利于将读请求分发给从副本,从而减轻主节点负载并允许读取请求就近满足。

且主从复制在多从节点的场景下只能用于异步复制,如果试图同步复制所有的从副本,则单个节点故障或网络中断将使整个系统无法写人。而且节点越多,发生故障的概率越高,所以完全同步的配置现实中反而非常不可靠。

如果同时对主节点和从节点发起相同的查询,可能会得到不同的结果。这种不一致只是一个暂时的状态,如果停止写数据库,经过一段时间之后,从节点最终会赶上并与主节点保持一致。这种效应也被称为最终一致性

读自己的写

写后读一致性,也称读写一致性,该机制保证如果用户重新刷新页面,总能看到自己最近的提交记录。

单调读

单调读一致性可以确保不会发生这种异常(看到最新内容之后又读到了过期的内容,好像时间被回拨)。这是一个比强一致性弱,但比最终一致性强的保证。当读取数据时,单调读保证,如果某个用户依次进行多次读取,则他绝不会看到回滚现象,即在读取较新值之后又发生读旧值的情况。

实现方式:确保每个用户总是从固定的同一副本执行读取。例如,基于用户ID的哈希的方法而不是随机选择副本。但如果该副本发生失效,则用户的查询必须重新路由到另一个副本。

前缀一致读

前缀一致读:对于一系列按照某个顺序发生的写请求,那么读取这些内容时也会接照当时写入的顺序。

这是分区数据库中出现的一个特殊问题。如果数据库总是以相同的顺序写入,则读取总是看到一致的序列,不会发生这种反常然而,在许多分布式数据库中,不同的分区独立运行,因此不存在全局写入顺序。这就导致当用户从数据库中读数据时,可能会看到数据库的某部分旧值和另一部分新值。

复制滞后的解决方案

应用层可以提供比底层数据库更强的保证。但是,到了分布式系统中最终一致性是无法避免的终极选择

多主节点复制

主从复制存在的缺点:只有一个主节点负责写入,存在单点故障。

多主节点复制适用于多数据中心场景下,每个数据中心可以配置自己的主节点。各个数据中心的主节点来负责同其他数据中心的主节点进行数据的交换、更新。

主从复制和多主复制方案的差异

  • 性能
    • 主从复制:每个写请求都必须经由广域网传送至主节点所在的数据中心。增加写入延迟,并基本偏离了采用多数据中心的初衷(即就近访问)。
    • 多主复制:每个写操作都可以在本地数据中心快速响应,然后采用异步复制方式将变化同步到其他数据中心。对上层应用有效屏蔽了数据中心之间的网络延迟,使得终端用户所体验到的性能更好。
  • 容忍数据中心失效
    • 主从复制:如果主节点所在的数据中心发生故障,必须切换至另一个数据中心,将其中的一个从节点被提升为主节点。
    • 多主复制:每个数据中心则可以独立于其他数据中心继续运行,发生故障的数据中心在恢复之后更新到最新状态。
  • 容忍网络
    • 主从复制:由于写请求是同步操作,对数据中心之间的网络性能和隐定性等更加依赖。
    • 多主复制:通常采用异步复制,可以更好地容忍此类问题。

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

离线客户端操作

多主复制适用于,应用与网络断开连接后还需要继续工作的场景。例如,利用本地数据库写入文本内容,等网络恢复后同步到云端。

协作编辑

实时协作编辑应用程序允许多个用户同时编辑文档。

处理写冲突

两个用户同时编辑Wiki页面

两个写操作同时修改同一个记录中的同一个字段,并将其设置为不同的值,即一个冲突。

例如一个会议室预订系统,它主要记录哪个房间由哪个人在哪个时间段所预订。这个应用程序需要确保每个房间只能有一组人同时预定。如果为同一个房间创建两个不同的预订,可能会发生冲突。尽管应用在预订时会检查房间是否可用,但如果两个预订是在两个不同的主节点上进行,则还是存在冲突的可能。

解决方法

  • 同步与异步冲突检测
  • 避免冲突
  • 收敛于一致状态
  • 自定义冲突解决逻辑:依赖应用层

拓扑结构

复制的拓扑结构描述了写请求从一个节点的传播到其他节点的通信路径。

环形和星形拓扑的问题是,如果某一个节点发生了故障,在修复之前,会影响其他节点之间复制日志的转发。可以采用重新配置拓扑结构的方法暂时排除掉故障节点。在大多数部署中,这种重新配置必须手动完成。

对于链接更密集的拓扑(如全部到全部),消息可以沿着不同的路径传播,避免了单点故障,因而有更好的容错性。

但另一方面,全链接拓扑也存在一些自身的问题。主要是存在某些网络链路比其他链路更快的情况(例如由于不同网络拥塞),从而导致复制日志之间的覆盖。

类似前面提及的前缀一致读,更新操作一定是依赖于前面完成的插入操作。

为了使得日志消息正确有序,可以使用一种称为版本向量的技术

无主节点复制

到目前为止本章所讨论的复制方法,包括单主节点和多主节点复制,都是基于这样一种核心思路:即客户端先向某个节点(主节点)发送写请求,然后数据库系统负责将写请求复制到其他副本。由主节点决定写操作的顺序,从节点按照相同的顺序来应用主节点所发送的写日志。

一些数据存储系统则采用了不同的设计思路:选择放弃主节点,允许任何副本直接接受来自客户端的写请求。其实最早的数据复制系统就是无主节点的(或称为去中心复制,无中心复制)。

节点失效时写入数据库

三个副本数据库,其中一个失效,其他两个数据库能成功确认写操作,则表现为写入成功。

读修复与反熵

读修复: * 当客户端并行读取多个副本时,可以检测到过期的返回值。将新值写入过期副本数据库。 反熵过程 * 一些数据存储有后台进程不断查找副本之间数据的差异,将任何缺少的数据从一个副本复制到另一个副本。与基于主节点复制的复制日志不同,此反熵过程并不保证以特定的顺序复制写入,并且会引入明显的同步滞后。

读写quorum

理论:

如果有n个副本,写入需要w个节点确认,读取必须至少查询r个节点,则只要w+r>n,读取的节点中一定会包含最新值。例如在前面的例子中,n=3,w=2,r=2。满足上述这些r、w值的读/写操作称之为法定票数读(或仲裁读)或法定票数写(或仲裁写)。