“Better code, better life. ”
注意:本文部分内容/图片摘自《数据密集型应用系统设计》
所谓复制,是指在多台通过网络连接的机器上保存相同数据的拷贝。
那为什么需要复制数据呢?主要又以下几点原因:
- 允许系统在部分节点出现故障后继续工作(增加可用性)
- 地理上保持数据离用户更近(减少延迟)
- 扩展可以提供查询的机器数量(增加读吞吐)
复制算法:
- 单主复制:所有客户端都将写入操作发送到主节点上,该节点负责将数据更改事件发送到其它副本。每个副本都可以接受读请求,但内容可能是过期值。
- 多主复制:系统中存在多个主节点,每个都可以接受请求,客户端将写请求发送到其中一个主节点上,该节点负责将数据更改事件同步到其它主节点和自己的从节点。
- 无主复制:客户端将写请求发送多个节点上,读取时从多个节点上并行读取,以此检测和纠正某些过期数据。
三种不同的算法有各自不同的优势和劣势。
复制权衡:同步复制、异步复制,如何处理失效的副本。
主从复制
每个节点都保存数据库的一个副本,如何保证所有数据最终存在于所有副本中?所有的写入需要被每个副本处理。最常见的解决办法是:基于主节点的复制。
主从复制流程如下:
- 一个节点被指定为主节点,当客户端写入时,必须将请求发送到主节点
- 其它节点作为从节点,主节点将新数据写入本地存储时,还将数据变更信息发送给从节点(复制日志或者变更流)。从节点收到日志后更新本地数据,与主节点相同的顺序应用所有写入。
- 当客户端从数据库读时,可以从主节点读,或者从任意一个从节点读。
这种模式被很多系统所使用:
关系数据库:PG/MySQL/Oracle/SQL Server
非关系数据库:MongoDB/RethinkDB/Espresso
分布式消息队列:Kafka/RabbitMQ
还有一些网络文件系统和复制块设备入DRBD使用。
同步or异步
在主从复制中,客户端发送更新请求给主节点,主收到请求数据,在某个时刻主节点将数据变更发送给从节点,最终主节点通知客户端更新成功。
一主两从,一个同步从,一个异步从:
同步:从1返回应答后才给客户端返回成功,从1是同步的。
异步:主发送消息,但是不等从返回应答,从2是异步的。
延迟:在从2处理消息前,可能会有很大的延迟,大部分数据库在 1s 内应用数据变更。但是并无法保证延迟在什么范围内。有一些场景可能会落后主几分钟甚至更长时间,例如从故障恢复,系统达到最大容量时,或者节点间出现网络故障。
同步复制的优势:数据副本保持最新,并且与主保持一致。如果主出现故障,我们可以确保数据在从上可用。 同步复制的缺点:如果从节点不返回应答(从机故障,网络错误或其他原因),则无法写入。主节点必须阻塞所有写入等待同步副本再次可用。
半同步:如果一个同步从不可用或者很慢,则异步从中的一个变为同步。可以保证至少有两个节点的数据是更新的。
异步复制:基于主的复制通常被配置为异步的,如果主故障,则所有写入主但是没有复制到从的数据都会丢失。削弱持久性可能听起来像是一种糟糕的权衡,但异步复制仍然被广泛使用,特别是如果有很多从或者它们在地理上分布的时候。
其它复制方式:链式复制
新增从节点
增加副本数或者替换故障节点,都需要增加新的从节点。如果保证新的从节点与主数据有完全一样的拷贝数据?
将数据文件拷贝到另一个节点是否可以解决问题?由于客户端持续写入数据到数据库,所以文件拷贝不行。
将磁盘文件锁定(对所有写不可用),但是这样无法保证高可用。但幸运的是,增加新节点并不需要停机。
增加新节点流程:
- 在某个时间点获取数据库的快照。大多数数据库支持这个特性,例如用于备份。
- 将快照拷贝到从节点。
- 从节点连接主节点,请求快照后的所有变更数据。这需要将快照与主节点复制日志的准确位置相关联,例如:PG中称之为LSN,MySQL中称之为binlog coordinates。
- 当从节点处理了所有自快照以来的所有积压的数据后,则称之为追赶上了。从节点可以继续处理来自主节点的数据变更。
节点故障
如何保证在主复制模型下获得高可用?
从节点故障:追赶式恢复
如果从节点故障后重启,或者网络闪断,从节点恢复很容易:根据其日志,知道最后一次事务处理信息,从节点连接到主节点,然后请求故障以后的所有数据变更即可。当应用这些变更后,就追赶上了主节点,可以持续接受数据变更,跟原来一样。
主节点故障:故障转移
主节点发生故障后,需要推举出一个从节点为新的主节点,客户端需要重新配置,然后发送写入操作到新的主节点,其它从节点需要从新的主节点获取数据变更。
故障转移流程:
- 确定主节点故障:有许多事情可能会出错:崩溃,停电,网络问题等等。没有万无一失的方法来检测出错的地方,因此大多数系统只是使用超时:节点经常在彼此之间来回反弹消息,如果节点在一段时间内没有响应(比如30秒)它被认为发生故障。(如果故意将领导者下线,进行有计划维护,则不适用。)
- 选举新的主节点:这可以通过选举过程(其中领导者由大多数剩余的副本选择)来完成,或者可以由先前选出的控制器节点指定新的领导者。主节点的最佳候选者通常是具有来自旧领导者的最新数据变化的复制品(以最小化任何数据丢失)。让所有节点就新领导者达成一致是一个共识问题,在后面有详细讨论。
- 重新配置系统以使用新的主节点。客户端现在需要将他们的写请求发送给新的主节点(“请求路由”)。 如果旧主节点回来,它可能仍然认为它是主,没有意识到其他副本已迫使它下台。系统需要确保旧的领导者成为从节点并认可新的主节点。
故障转移有各种出错的情况:
异步复制:选主后会丢失旧主节点的更新,新旧主节点相互矛盾的写入。最普遍的解决方案是让旧主节点的未复制的写入被简单地丢弃,这可能会违反持久性。
丢失写入可能会非常危险:例如这个系统是其它系统的协调者。
脑裂:两个节点都认为自己是主节点,都接受写入,无法解决冲突。检测多个节点,如果出现脑裂,则杀死其中一个。但是如果设计不好,则会出现杀死两个的情况。
主节点的超时时间如何设置:太长会导致恢复时间过长,太短导致不必要的故障转移。高压力、网络出现严重拥堵,切换只会导致情况更糟糕。
手动还是自动故障转移。
节点失效、网络不可靠、副本一致性、持久性、可用性与延迟之间存在各种细微的协调,这些问题实际上也正是分布式系统核心的基本问题。
日志复制的实现
- 基于语句的复制:记录写相关的语句如 INSERT/UPDATE/DELETE,将其发送给备节点。但是时间、自增列、有副作用的语句(触发器、存储过程、UDF)会导致不同副本出现副作用。
- 基于WAL的复制:与存储系统耦合。不同版本无法共存。无法滚动升级,停机。(LSM-Tree日志是主要存储方式;对于覆盖写 B-tree 结构,每次修改页面会进行预写日志。)
- 基于行的逻辑复制:与物理表示解耦。更容易实现滚动升级,外部程序更容易解析,构建定制索引,缓存,change data capture。 (对于行插入,日志包含所有相关列的新值;对于行删除,日志里有足够信息来标识已删除的行,通常是主键,但是如果没有定义主键,就需要记录所有列的旧值;对于行更新,日志里有足够信息来标识更新的行,以及所有列的新值,或者至少包含所有列的新值)
- 基于触发器的复制:更高的灵活性,例如仅复制部分数据,到另一种数据库复制,冲突解决逻辑,这些场景可能需要将复制上升到应用层来解决。基于触发器的复制通常比其他复制方法具有更大的开销,并且比数据库的内置复制更容易出现错误和限制。 然而,由于其灵活性,它仍然是有用的。
多主复制
上面我们讨论了主从复制,主从复制存在一个明显的缺点:系统只有一个主节点,而所有写人都必须经由主节点,如果由于某种原因,比如与主节点之间的网络终端而导致主节点无法连接,主从复制方案就会影响所有的写入操作。
对主从复制模型进行自然的扩展,贝 lj 可以配置多个主节点,每个主节点都可以接受写操作,后面复制的流程类似 : 处理写的每个主节点都必须将该数据更改转发到所有其他节点 。这就是多主节点( 也称为主-主,或主动/主动)复制。 此时,每个主节点还同时扮演其他主节点的从节点。
适用场景
在一个数据中心内部使用多主节点基本没有太大意义,其复杂性已经超过所能带来的好处。但是,在以下场景这种配置则是合理的。
多数据中心
为了容忍整个数据中心级别故障或者更接近用户,可以把数据库的副本横跨多个数据中心。而如果使用常规的基于主从的复制模型,主节点势必只能放在其中的某一个数据中心,而所有写请求都必须经过该数据中心。
有了多主节点复制模型,则可以在每个数据中心都配置主节点,如图下图所示的基本架构。在每个数据中心内,采用常规的主从复制方案;而在数据中心之间,由各个数据中心的主节点来负责同其他数据中心的主节点进行数据的交换、更新。
可以对比一下在多数据中心环境下,部署单主节点的主从复制方案与多主复制方案之间的差异:
- 性能:对于主从复制,每个写请求都必须经由广域网传送至主节点所在的数据中心。这会大大增加写入延迟,井基本偏离 了采用多数据中心的初衷 (即就近访问)。而在多主节点模型中,每个写操作都可以在本地数据中心快速 II向应,然后采用异步复制方式将变化同步到其他数据中 心 。因此 ,对上层应用有效屏蔽了数据中心之间的网络延迟,使得终端用户所体验到的性能更好。
- 容忍数据中心失效:对于主从复制,如果主节点所在的数据中心发生故障,必须切换至另一个数据中心,将其中的一个从节点被提升为主节点。在多主节点模型中,每个数据中心则可以独立于其他数据中心继续运行,发生故障的数据中心在恢复之后更新到最新状态。
- 容忍网络问题:数据中心之间的通信通常经由广域网,它往往不如数据中心内的本地网络靠。对于主从复制模型,由于写请求是同步操作,对数据中心之间的网络性能和稳定性等更加依赖。多主节点模型则通常采用异步复制,可以更好地容忍此类问题,例如临时网络闪断不会妨碍写请求最终成功。
现在,有些数据库己内嵌支持了多主复制,但有些则借助外部工具来实现。
离线客户端操作
另一种多主复制比较适合的场景是,应用在与网络断开后还需要继续工作。
比如手机,笔记本电脑和其他设备上的日历应用程序。无论设备当前是否联网,用户需要能够随时查看当前的会议安(对应于读请求)或者添加新的会议(对应于写请求)。在离线状态下进行的任何更改,会在下次设备上线时伞,与服务器以及其他设备同步。
这种情况下,每个设备都有一个充当主节点的本地数据库(用来接受写请求),然后在所有设备之间采用异步方式同步这些多主节点上的副本,同步滞后可能是几小时或者数天,具体时间取决于设备何时可以再次联网。从架构层面来看,上述设置基本上等同于数据中心之间的多主复制,只不过是个极端情况,即一个设备就是数据中心,而且它们之间的网络连接非常不可靠。多个设备同步日历的例子表明,多主节点可以得到想要的结果,但中间过程依然有很多的未知数。
写冲突问题
尽管多主复制具有上述优势,但也存在一个很大的缺点:不同的数据中心可能会同 时修改相同的数据,因而必须解决潜在的写冲突。
例如,两个用户同时编辑Wiki页面,用户1将页面的标题从A更改为B ,与 此同时用户2却将标题从 A改为 C 。每个用户的更改者R顺利地提交到本地主节点。但是,当更改被异步复制到对方时,却发现存在冲突。
同步与异步冲突检测
如果是主从复制数据库,第二个写请求要么会被阻塞直到第一个写完成, 要么被中止用户必须重试) 。然而在多主节点的复制模型下,这两个写请求都是成功的,井且只能在稍后的时间点上才异步检测到冲突,那时再要求用户层来解决冲突为时已晚 。
理论上, 也可以做到同步冲突检测,即等待写请求完成对所有副本的同步,然后再通知用户写入成功 。 但是,这样做将会失去多主节点的主要优势 :允许每个主节点独立接受写请求。如果确实想要同步方式冲突检测 ,或许应该考虑采用单主节点的主从复制模型 。
无主复制
到目前为止本章所讨论的复制方怯,包括单主节点和多主节点复制,都是基于这样一种核心思路,即客户端先向某个节点(主节点)发送写请求,然后数据库系统负责将写请求复制到其他副本 。由主节点决定写操作的顺序, 从节点按照相同的顺序来应用主节点所发送的写日志。
一些数据存储系统则采用了不同的设计思路:选择放弃主节点,允许任何副本直接接受来自客户端的写请求。其实最早的数据复 制 系统就是无主节点的(或称为去中心复制,无中心复制),但后来到了关系数据库主导的时代 ,这个想怯被大家选择性遗忘了 。当亚马逊内部采用了Dynamo系统之后,无主复制又再次成为一种时髦的数据库架构。
对于某些无主节点系统实现,客户端直接将其写请求发送到多副本,而在其他一些实现中,由一个协调者节点代表客户端进行写人,但与主节点的数据库不同,协调者井不负责写入顺序的维护。我们很快就会看到,这种设计上的差异对数据库的使用方式有着深刻的影响 。
节点失效问题
假设一个三副本数据库,其中一个副本当前不可用(例如正在重启以安装系统更新)。在基于主节点复制模型下,如果要继续处理写操作,则需要执行切换操作。
对于无主节点配置,则不存在这样的切换操作。我们假设这么一种情况:用户1234将写请求并行发送到三个副本,有两个可用副本接受写请求,而不可用的副本无怯处理该写请求。如果假定三个副本中有两个成功确认写操作,用户1234收到两个确认的回复之后,即可认为写入成功。客户完全可以忽略其中一个副本无法写入的情况 。
现在设想一下,失效的节点之后重新上线,而客户端叉开始从中读取内容。由于节点失效期间发生的任何写入在该节点上都尚未同步,因此读取可能会得到过期的数据。
为了解决这个问题,当一个客户端从数据库中读取数据肘,它不是向一个副本发送请求,而是井行地发送到多个副本。客户端可能会得到不同节点的不同响应,包括某些节点的新值和某些节点的旧值。可以采用版本号技术确定哪个值更新。
复制滞后的问题
从异步的从节点读取数据,该副本落后于主节点,则会读取到过期的信息。这种不一致是一个暂时的状态,如果停止写数据库,经过一段时间后,从节点最终会赶上,并与主节点保持一致。这种效应也称之为:最终一致性。
读自己的写
用户在写入不久后查看自己的数据,如果可以在任意节点读取,则如果异步复制,则会出现从节点还没有最新数据的情况。对用户来说刚才写入的数据丢失了。此时需要写后读一致性,也就是读写一致性。
基于主从复制的系统该如何实现写后读一致性呢?有多种可行的方案:
- 如果用户访问可能被修改的内容,在主节点读取,否则在从节点读取。
- 跟踪最近更新的时间,监控从节点的复制滞后程度,避免从那些滞后的节点读取。
- 客户端跟踪更新时间,附加在读请求中,系统确保该用户读取的内容至少包含了该时间戳的更新。时间戳可以是逻辑时间戳或者实际的系统时钟。
- 如果副本分布在多数据中心,必须先把请求路由到主节点的数据中心。
多设备的写后读一致性。
- 记住用户上次更新时间戳实现起来比较困难。元数据必须做到全局共享。
- 副本分布在多个数据中心,无法保证来自所有设备到达同一个数据中心。例如台式机家庭宽带网络,移动设备使用蜂窝数据网络,如果要求方案要求必须从主节点读取,则首先要将不同设备路由到同一个数据中心。
单调读
单调读要求某个用户一次进行多次读取时,不会看到回滚的情况。单调读比强一致性弱,比最终一致性强。
单调读的一种实现方式是,确保每个用户总是从固定的同一个副本执行读取(不同的用户可以从不同的副本读取)。例如基于用户 ID 的哈希的方法而不是随机选择一个副本读取。但如果该副本发生失效,则用户的查询必须重新路由到另一个副本。
前缀一致读
第三个由于复制滞后导致因果反常的例子。
要防止这种类型的异常,需要引入:前缀一致读。对于一系列按照某种顺序发生的写请求,读取这些内容也按照当时写入的顺序。
这是分片数据库中出现的一个特殊的问题,许多分布式数据库中,不同的分区独立运行,因此没有全局顺序,这就会导致读取数据时,可能会看到某部分旧值和另一部分新值。一个解决方案是具有因果关系的写入都交给同一个分区完成,但实际该方案的失效效率会大打折扣,例如“Happened-before关系与并发”会继续讨论该问题。
复制滞后解决方案
使用最终一致性系统时,最好事先就考虑好这个问题:如果复制延迟增加到几分钟甚至几小时,那么应用层的行为会是什么样子?
应用层保证,但需要复杂的业务逻辑。
数据库如果能保证,则情况就会变得更简单。事务可以提供更强保证的一种方式。
单节点事务比较成熟。在分布式数据库中(支持复制和分区)的过程中,许多系统选择放弃事务,声称事务在性能和可用性方面代价过高,只能提供最终一致性,有一定的道理,但是情况远比所说的那么简单。