复制
复制意味着在通过网络连接的多台机器上保留相同数据的副本。复制的困难之处在于处理复制数据的变更。我们将
讨论三种流行的变更复制算法:单领导者,多领导者和无领导者。几乎所有分布式数据库都使用这三种方法之一。
主从结构
存储数据库副本的每个节点称为副本。当存在多个副本时,会不可避免的出现一个问题:如何确保所有数据都落在了所有的副本上?
每一次向数据库的写入操作都需要传播到所有副本上,否则副本就会包含不一样的数据。最常见的解决方案被称为基于领导者的复制也称主动/被动 或 主/从复制,它的工作原理如下:
- 副本之一被指定为领导者(leader),也称为 主库(master) 。当客户端要向数据库写入时,它必须将请求发送给领导者,领导者会将新数据写入其本地存储。
- 其他副本被称为追随者(followers),亦称为只读副本(read replicas),从库(slaves),热备 。每当领导者将新数据写入本地存储时,它也会将数据变更发送给所有的追随者,称之为复制日志记录或变更流。每个跟随者从领导者拉取日志,并相应更新其本地数据库副本,方法是按照领导者处理的相同顺序应用所有写入。
- 当客户想要从数据库中读取数据时,它可以向领导者或追随者查询。 但只有领导者才能接受写操作(从客户端的角度来看从库都是只读的)。
这种复制模式是许多关系数据库的内置功能,如PostgreSQL,MySQL,Oracle Data Guard 和SQL Server的AlwaysOn可用性组。 它也被用于一些非关系数据库,包括MongoDB,RethinkDB和Espresso。 最后,基于领导者的复制并不仅限于数据库:像Kafka和RabbitMQ高可用队列这样的分布式消息代理也使用它。 某些网络文件系统,例如DRBD这样的块复制设备也与之类似。
同步复制与异步复制
同步复制的优点是,从库保证有与主库一致的最新数据副本。如果主库突然失效,我们可以确信这些数据仍然能在从库上上找到。缺点是,如果同步从库没有响应(比如它已经崩溃,或者出现网络故障,或其它任何原因),主库就无法处理写入操作。主库必须阻止所有写入,并等待同步副本再次可用。
因此,将所有从库都设置为同步的是不切实际的:任何一个节点的中断都会导致整个系统停滞不前。实际上,如果在数据库上启用同步复制,通常意味着其中一个跟随者是同步的,而其他的则是异步的。如果同步从库变得不可用或缓慢,则使一个异步从库同步。这保证你至少在两个节点上拥有最新的数据副本:主库和同步从库。 这种配置有时也被称为半同步。
通常情况下,基于领导者的复制都配置为完全异步。 在这种情况下,如果主库失效且不可恢复,则任何尚未复制给从库的写入都会丢失。 这意味着即使已经向客户端确认成功,写入也不能保证持久。 然而,一个完全异步的配置也有优点:即使所有的从库都落后了,主库也可以继续处理写入。
设置新从库
客户端不断向数据库写入数据,数据总是在不断变化,标准的数据副本会在不同的时间点总是不一样。复制的结果可能没有任何意义。可以通过锁定数据库(使其不可用于写入)来使磁盘上的文件保持一致,但是这会违背高可用的目标。拉起新的从库通常并不需要停机。从概念上讲,过程如下所示:
- 在某个时刻获取主库的一致性快照(如果可能),而不必锁定整个数据库。大多数数据库都具有这个功能,因为它是备份必需的。对于某些场景,可能需要第三方工具,例如MySQL的innobackupex 【12】。
- 将快照复制到新的从库节点。
- 从库连接到主库,并拉取快照之后发生的所有数据变更。这要求快照与主库复制日志中的位置精确关联。该位置有不同的名称:例如,PostgreSQL将其称为日志序列号(logsequence number, LSN),MySQL将其称为二进制日志坐标(binlog coordinates)。
- 当从库处理完快照之后积压的数据变更,我们说它赶上(caught up)了主库。现在它可以继续处理主库产生的数据变化了。
主库失效——故障切换
其中一个从库需要被提升为新的主库,需要重新配置客户端,以将它们的写操作发送给新的主库,其他从库需要开始拉取来自新主库的数据变更。步骤如下:
- 确认主库失效。有很多事情可能会出错:崩溃,停电,网络问题等等。没有万无一失的方法来检测出现了什么问题,所以大多数系统只是简单使用超时:节点频繁地相互来回传递消息,并且如果一个节点在一段时间内(例如30秒)没有响应,就认为它挂了。
- 选择一个新的主库。这可以通过选举过程(主库由剩余副本以多数选举产生)来完成,或者可以由之前选定的控制器节点来指定新的主库。主库的最佳人选通常是拥有旧主库最新数据副本的从库(最小化数据损失)。
- 重新配置系统以启用新的主库。客户端现在需要将它们的写请求发送给新主库。如果老领导回来,可能仍然认为自己是主库,没有意识到其他副本已经让它下台了。系统需要确保老领导认可新领导,成为一个从库。
复制的实现
基于语句的复制
在最简单的情况下,主库记录下它执行的每个写入请求并将该语句日志发送给其从库。对于关系数据库来说,这意味着每个 INSERT , UPDATE 或 DELETE 语句都被转发给每个从库,每个从库解析并执行该SQL语句,就像从客户端收到一样。但有很多问题会搞砸这种复制方式:
- 任何调用非确定性函数(nondeterministic)的语句,可能会在每个副本上生成不同的值。例如,使用 NOW() 获取当前日期时间,或使用 RAND() 获取一个随机数。
- 如果语句使用了自增列(auto increment),或者依赖于数据库中的现有数据(例如, UPDATE ... WHERE <某些条件> ),则必须在每个副本上按照完全相同的顺序执行它们,否则可能会产生不同的效果。当有多个并发执行的事务时,这可能成为一个限制。
- 有副作用的语句(例如,触发器,存储过程,用户定义的函数)可能会在每个副本上产生不同的副作用,除非副作用是绝对确定的。
传输预写式日志(WAL)
WAL包含哪些磁盘块中的哪些字节发生了更改。这使复制与存储引擎紧密耦合。如果数据库将其存储格式从一个版本更改为另一个版本,通常不可能在主库和从库上运行不同版本的数据库软件。
逻辑日志复制(基于行)
另一种方法是,复制和存储引擎使用不同的日志格式,这样可以使复制日志从存储引擎内部分离出来。这种复制日志被称为逻辑日志,以将其与存储引擎的(物理)数据表示区分开来。
关系数据库的逻辑日志通常是以行的粒度描述对数据库表的写入的记录序列:
- 对于插入的行,日志包含所有列的新值。
- 对于删除的行,日志包含足够的信息来唯一标识已删除的行。通常是主键,但是如果表上没有主键,则需要记录所有列的旧值。
- 对于更新的行,日志包含足够的信息来唯一标识更新的行,以及所有列的新值(或至少所有已更改的列的新值)。
修改多行的事务会生成多个这样的日志记录,后面跟着一条记录,指出事务已经提交。MySQL的二进制日志(当配置为使用基于行的复制时)使用这种方法
基于触发器的复制
触发器是注册在数据库系统中,发生数据更改(写入事务)时自动执行的自定义应用程序代码。触发器有机会将更改记录到一个单独的表中,使用外部程序读取这个表,再加上任何业务逻辑处理,会后将数据变更复制到另一个系统去。
复制延迟问题
基于主库的复制要求所有写入都由单个节点处理,但只读查询可以由任何副本处理。所以对于读多写少的场景,一个选择是创建很多从库。在这种扩展体系结构中,只需添加更多的追随者,就可以提高只读请求的服务容量。但是,这种方法实际上只适用于异步复制。不幸的是,当应用程序从异步从库读取时,如果从库落后,它可能会看到过时的信息。下面是一些可能的解决方案
读己之写
如果用户重新加载页面,他们总会看到他们自己提交的任何更新。它不会对其他用户的写入做出承诺:其他用户的更新可能稍等才会看到。它保证用户自己的输入已被正确保存。
- 读用户可能已经修改过的内容时,都从主库读;这就要求有一些方法,不用实际查询就可以知道用户是否修改了某些东西。例如,从主库读取用户自己的档案,在从库读取其他用户的档案。
- 如果应用中的大部分内容都可能被用户编辑,那这种方法就没用了,因为大部分内容都必须从主库读取。在这种情况下可以使用其他标准来决定是否从主库读取。例如可以跟踪上次更新的时间,在上次更新后的一分钟内,从主库读。
- 客户端可以记住最近一次写入的时间戳,系统需要确保从库为该用户提供任何查询时,该时间戳前的变更都已经传播到了本从库中。
单调读
如果用户从不同从库进行多次读取,就可能发生这种情况。首先查询了一个延迟很小的从库,然后是一个延迟较大的从库。
单调读取仅意味着如果一个用户顺序地进行多次读取,则他们不会看到时间后退,即,如果先前读取到较新的数据,后续读取不会得到更旧的数据。
一致前缀读
如果一系列写入按某个顺序发生,那么任何人读取这些写入时,也会看见它们以同样的顺序出现。
这是分区(分片)数据库中的一个特殊问题:不同的分区独立运行,因此不存在全局写入顺序,当用户从数据库中读取数据时,可能会看到数据库的某些部分处于较旧的状态,而某些处于较新的状态。
多主结构
在数据中心之间,每个数据中心的主库都会将其更改复制到其他数据中心的主库中。
多领导者复制的最大问题是可能发生写冲突,这意味着需要解决冲突。实现冲突合并解决有多种途径:
- 给每个写入一个唯一的ID(例如,一个时间戳,一个长的随机数,一个UUID或者一个键和值的哈希),挑选最高ID的写入作为胜利者,并丢弃其他写入。
- 为每个副本分配一个唯一的ID,ID编号更高的写入具有更高的优先级。
- 在保留所有信息的显式数据结构中记录冲突,并编写解决冲突的应用程序代码(也许通过提示用户的方式)。
无主结构
在一些无领导者的实现中,客户端直接将写入发送到到几个副本中,而另一些情况下,一个协调者节点代表客户端进行写入。但与主库数据库不同,协调者不执行特定的写入顺序。在无领导配置中,故障切换不存在。
假设三个副本中的两个承认写入是足够的:在用户已经收到两个确定的响应之后,我们认为写入成功。客户简单地忽略了其中一个副本错过了写入的事实。
- 读修复:当客户端并行读取多个节点时,它可以检测到任何陈旧的响应。
- 反熵过程:一些数据存储具有后台进程,该进程不断查找副本之间的数据差异,并将任何缺少的数据从一个副本复制到另一个副本。
如果有n个副本,每个写入必须由w节点确认才能被认为是成功的,并且我们必须至少为每个读取查询r个节点,(在我们的例子中,n = 3,w = 2,r = 2)。只要w + r>n,我们期望在读取时获得最新的值,因为r个读取中至少有一个节点是最新的。遵循这些r值,w值的读写称为法定人数(quorum) 的读和写。