大数据杂谈(1):复制技术之主从节点

2,461 阅读12分钟

复制技术是一个古老的话题,网络的基本约束条件没有发生本质的变化,可以说自1070年所研究的基本复制原则到今天也是适用的。复制说到底是指通过网络在多台机器上保存相同数据的副本,而复制通常有以下目的:

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

在本文中,我们假定数据的规模不会达到用分区的程度,也就是说每一台机器都可以单独完整保存数据的副本,然后我们会讨论复制过程中可能出现的各类问题以及在现代工程项目中是如何处理应对的

复制过程中的一个核心难点在于数据的可变性,如果被复制的数据一成不变,那复制就非常容易,如今复制所面临的所有挑战就在于如何处理持续更改的数据。除此之外还有需要折中考虑的地方,如采用同步复制还是异步复制、如何处理失败的副本等,我们需要考虑不同选择可能导致的后果

复制主要有三种流行的方式:主从复制,多节点复制和无主节点复制

主从节点

每个保存完整数据的节点称之为副本,当存在多个副本时不可避免会引入一个新问题:如何确保所有副本之间的数据是一致的。对于每一个写数据操作,所有的副本都需要随之更新,否则数据将会出现不一致。最常见的解决方案是设定一个主节点,然后基于这个主节点进行复制(即主从复制)

image.png

如上图所示,主从复制的原理如下:

  1. 指定某一个副本为主节点,当用户写数据时,只能将数据写到主节点,主节点将数据存储于其本地
  2. 其它副本称之为是从节点,主节点将数据写入到本地后,会将数据更改发送给所有从节点,每个从节点获得更改日志后将其应用到本地,且严格保持与主节点相同的写入顺序
  3. 客户端从数据集中读取数据时,可以从主节点或从节点中执行查询,但只有主节点才能接收写操作,从客户的角度来说,从节点都是只读的,主节点是可读可写的

许多关系型数据库都内置了对主从复制的支持,如MySQLOracle Data Guard等,而MongoDBRethin kDB等非关系型数据库也支持主从复制。此外主从复制也不止应用于数据库,在分布式消息队列如KafkaRabbitMQ以及一些网络文件系统中也有应用

同步复制与异步复制

复制很重要的一个选项是同步复制还是异步复制,对于关系数据库,同步或异步通常是一个可配置项,而其它系统可能是硬性指定或二选一

我们假定这样一个场景,网站用户更新了自己的头像,此时流程如下图所示

image.png

从节点1的复制是同步的,也就是主节点需要等待从节点1确认写完成后才会向用户报告完成并且允许最新的写入信息对其他客户端可见。从节点2的复制是异步的,主节点不用等待从节点2完成确认。

通常情况下复制速度会很快,例如多数数据库系统可以在一秒内完成所有从节点的更新,但是系统不会保证一定会在多长时间内完成复制。某些情况下从节点可能落后主节点几分钟甚至更长时间,例如从节点刚从故障中恢复或系统已经接近最大设计上限或节点间网络出现故障

同步复制的优点是,一旦用户确认,从节点可以明确完成了更新,万一主节点发生故障,可以在从节点访问最新的数据;缺点也很明显,从节点一旦无法完成确认,写入就不能视为成功,主节点会阻塞后续的写操作直到从节点确认完成。由于同步复制的缺点很明显,所以把所有从节点都配置为同步复制不切实际,实践中如果数据库启动了同步复制,那么通常意味着其中一个从节点是同步的,其它节点还是异步模式,这样可以保证至少一主一从两个节点拥有最新的数据,这种模式也成为半同步

主从复制还可以配置为全异步模式,如果主节点失败且不可恢复会导致尚未复制到从节点的数据丢失。全异步的优势是系统的吞吐性更好。虽然听起来不太靠谱,但全异步还是被广泛应用,特别是节点数量巨大或地理环境广泛,后续我们会在 复制滞后 文章中接续探讨该话题

配置新从节点

当出现提高容错能力或替换失败的副本情况时,需要考虑增加新从节点,此时如何保证新的从节点与主节点一致呢?当用户仍然不断的写入数据时,简单的复制是不够的。锁定数据库使其不可写来保持文件的一致不可取,因为这会违反高可用的目标。如何做到在不停机,不中断服务的前提下完成从节点的设置,主要有以下几个步骤:

  1. 在某个时间点对主节点的数据副本产生一个快照,避免长时间锁定数据库(该功能大多数数据库都支持)
  2. 将此快照拷贝到新的从节点
  3. 从节点连接到主节点并请求快照之后发生的变更,这个过程成为追赶。接下来可以重复1~3步

节点失效

我们不能假定节点一直会正常工作,任何节点都可能因故障或日常维护而停机,如何在个别节点中断的情况下维持系统总体的稳定性是一个挑战

从节点失效

如果从节点失效,然后重启,就会按照上面提到的追赶式恢复

主节点失效

主节点失效比较棘手,需要选择某个从节点作为新的主节点,用户写请求发送给新的主节点,其它从节点要接受新主节点的数据变更,这一过程成为切换。切换可以手动进行也可以自动进行,自动进行的流程如下:

  1. 确认主节点失效。因为没法确切知道问题出在哪,大多数系统采用的是超时机制。节点间发送心跳存活消息,如果超出一定的时间没有响应则判定为失效
  2. 选举新的主节点。通过选举的方式选出新的主节点,或是由一个控制节点来指定新主节点。候选主节点最好与原著节点差异最小,最小化数据丢失的损失
  3. 重新配置系统使新主节点生效。新的写操作将发送到新的主节点

上述过程充满变数:

  1. 如果使用的是异步复制,且主节点失效之前新主节点未完成原主节点的数据同步,选举之后原主节点很快又重新上线并入集群,原主节点如果没有变更身份,还会尝试同步其它节点,那么接下来的写操作会出现冲突。解决方法一般是丢弃原主节点上未同步的数据,但这会和数据持久化的承诺相违背
  2. 在数据库之外有其它系统依赖于数据集群一块协同的时候,数据集群数据丢失就会更加棘手,影响范围更大
  3. 某些情况下,坑出现两个节点同时认为自己是主节点,这种情况称为是脑裂,这种情况非常危险。一般解决方案是强制关闭其中一个节点,然而在设计的时候可能考虑步骤,出现两个节点都被关闭的情况
  4. 如何设置合适的超时来检测主节点失效?太长太短都不合适。如果设置的短了。系统负载高,响应慢的情况下会误认为是失效,此时切换只能导致系统压力更大

上述问题没有简单的解决方案,很多时候运维团队会更喜欢用手动方式来控制整个切换过程

上述问题涉及到的节点失效、网络可靠性、副本一致性、数据持久性、可用性以及延迟之间的权衡,是分布式系统的核心问题

主从复制实现原理

主从复制具体是如何工作的呢?实际中有多种不同的实现方式,下面逐一介绍

基于语句的复制

最简单的情况,主节点记录所执行的每个写请求操作语句,并将其作为日志发送给从节点。对于关系型数据库来说,这意味着每个INSERT、UPDATE或DELETE语句都会转发给从节点,并且每个从节点都会分析并执行这些SQL语句,就好像这些操作是来自客户端那样。这样实现起来相对简单,但存在一些无法胜任的场景:

  • 一些非确定性语句,如NOW()(获取时间)或RAND()(获取随机数)等,每次执行的结果不尽相同,会导致产生不同的副本数据
  • 如果语句中使用了自增列,或者依赖于数据库的现有数据,则所有副本必须按照完全相同的顺序执行,否则可能带来不同的结果,进而在多个并发执行的事物时,会有较大的限制
  • 有副作用的语句(如触发器、存储过程、用户定义的函数等)可能会在每个服本上产生不同的副作用

因上述原因,所以目前的首选是其它复制实现方案

基于预写日志(WAL)传输

通常每个写操作都是以追加写的方式写入到日志中,所有对数据库写入的字节序列都被记入日志,因此可以使用完全相同的日志在另一个节点上构建副本。其主要缺点是日志描述的数据结果非常底层,一个WAL包含了哪些磁盘块的哪些字节发生改变,诸如此类的细节使得复制方案和存储引擎紧密耦合。如果数据库的存储格式从一个版本改为另一个版本,那么系统通常无法支持从节点上运行不同版本的软件

如果复制协议允许从节点的软件版本比主节点更新,那么可以实现不停机升级,反之就势必以停机为代价

基于行的逻辑日志复制

复制和存储引擎采用不同的日志格式,复制与存储逻辑剥离,这种复制日志称之为逻辑日志,以区分物理存储引擎的数据表示。关系型数据库的逻辑日志通常是指一系列记录描述表行级别的写请求:

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

逻辑日志与存储引擎逻辑解耦,可以更容易的保持向后兼容,从而使朱从节点能够运行不同版本的软件甚至是不同的存储引擎。对于外部应用程序来说,逻辑日志格式也更容易解析,如果需要将数据库的内容发送到外部系统,或构建自定义所以和缓存等,基于逻辑日志的复制更有优势

基于触发器的复制

目前为止所描述的复制方法都是由数据库来实现的,但在某些情况下可能需要更好的灵活性,比如我们只想复制数据的一部分,或者从一种数据库复制到另一种数据库,那么就需要将复制交由应用程序员。许多关系型数据库都支持触发器和存储过程。触发器支持注册自己得应用层代码,当数据发生变更时自动执行上述自定义代码

基于触发器的复制通常比其它复制方式开销更高,也比内置复制更容易出错,但因其高度灵活性所以仍有广泛应用