分布式数据系统的数据复制
date
Mar 13, 2024
slug
replication-of-distributed-data-system
status
Published
tags
编程开发
数据库
summary
数据复制在分布式数据库是老生常谈的事,几乎所有数据库都会支持数据复制,以实现数据系统的可靠性。我们在设计自己的分布式系统时,也应该关注这块的知识。最近在看《数据密集型应用系统设计》这本书,其中第二章详细介绍了这块的知识,因此想对这部份做下记录。
type
Post
数据复制在分布式数据库是老生常谈的事,几乎所有数据库都会支持数据复制,以实现数据系统的可靠性。我们在设计自己的分布式系统时,也应该关注这块的知识。最近在看《数据密集型应用系统设计》这本书,其中第二章详细介绍了这块的知识,因此想对这部份做下记录。
主从复制
说起数据复制,有个概念很重要,就是副本(replica)。很多人以为从节点才是副本,其实不是。任何保存有数据库完整数据集的节点都称为副本,也就是说主节点也是副本之一。
因此通常主副本也会被称为主节点,从副本被称为从节点,之后的介绍也会基于这个来进行。同时,我们的主节点可以接收读操作和写操作,而从节点只能接受读操作。
基于日志的复制
要实现副本之间的数据一致性,就得实行主从复制,通常来说是这样:
- 主节点在接受到客户端的写请求后,除了在本地执行写操作,还会将操作记录到日志中。
- 主节点将日志发送到各个从节点。
- 各个从节点接收到日志后在本地执行这些写操作,这样便能和主节点的数据达成一致性。

很容易想到,在进行数据复制的时候,数据库同时还在对外提供服务,此时依旧能接收到新的请求,如果主节点接收的是读请求倒还好,直接执行即可。而如果是写请求,则需要考虑这部份新的写请求如何在发生复制的时候也发送给从节点。
通常数据库都会采用快照的方式来实现,因此之前的复制流程也就演变成了如下:
- 主节点除了会对写操作记录日志之外,还会在某个时间点对数据生成一份数据快照。
- 从节点请求与主节点进行数据复制时,主节点将这份快照发送给从节点。
- 从节点先依据快照进行数据恢复,然后请求在这份快照之后的数据写操作日志记录,将这些记录也应用到本地,完成一个对主节点的追赶。
- 如果主节点还有新的数据,则继续采用上述1-3的过程。

数据同步模式
针对复制发生的时间点,则可以分为同步复制和异步复制。一般数据库都会提供是要采用同步还是异步的方式来进行数据复制。
对于同步复制来说,主数据库在接收到写请求后,除了要在本地执行完写操作,还必须同步将写操作发给从节点,并等待从节点执行完成,才可以认为这个写请求执行成功。
对于异步复制来说,主数据库在接收到写请求后,只需要在自己本地执行写操作,然后将操作写入日志即可返回客户端写操作执行成功。后续再在合适的时间点,将日志发给从节点完成数据同步。
显而易见,同步复制将会给写操作带来很大的阻塞,因为除了自己完成操作外还要等待所有节点的确认。好处就是主从节点的数据始终是一致的,也就是我们常说的数据是强一致性的。
异步复制则可以带来很好的写入效率,而异步的后果带来的就是如果复制没有及时进行,从节点的数据可能会与主节点存在一定时间的不一致,这种情况也叫主从延迟。只要主节点没有新的数据写入,从节点最终还是会追上主节点,这就是常说的最终一致性。不过在这种复制模式下,如果数据还未复制到从节点,主节点就发生故障了,还可能导致数据丢失。
除了同步复制和异步复制外,还有一种叫半同步复制,是结合了前面两种的一种复制方式。我们可以只和一个或部份从节点采用同步复制的方式,其余的从节点均采用异步复制。这样可以让我们的数据库保证至少有一个从节点的数据和主节点是强一致的。

节点失效与恢复
由于从节点只接收读请求,因此如果从节点发生奔溃宕机,其重启后的数据恢复会比较容易。只需要将这段时间落后于主节点的数据同步过来进行恢复即可。
但如果是主节点发生宕机,则会比较麻烦了。因为主节点还负责写请求的接收,如果集群中此时没有主节点,将会导致所有写请求都无法被接受。因此我们在主节点恢复前,需要选举一个从节点升级为主节点,同时通知客户端主节点发生了变更。而等到原来的主节点恢复了,还需要将其降级为从节点,并将其与新的主节点进行连接。这个过程也就是常说的故障转移(Failover)。它可以是手动进行的,也可以是自动进行。
接下来说说,主节点切换的基本流程。
确认主节点是否失效下线。
我们需要通过一定的机制去检测主节点是否还在线,一般会让节点之间相互发送心跳,如果在一段时间里没收到某个节点发来的心跳,则可以认为其已经下线,这个下线称为主观下线。通常来说不会让单个节点认为目标节点已经下线就认为其真的下线了,而是需要集群中多数节点都认为其下线了,集群才会将该节点判断为下线,这个时候的下线就被称为客观下线。
选择新的主节点。
当主节点被判断为客观下线,则需要通过选举的方式来从剩余的从节点中选出一个担任主节点,被选出的从节点必须是和主节点差距最小的。这个选举的过程也被称为“共识”。
更新节点配置信息。
当选出了新的主节点后,则需要将这个新的主节点发送给所有从节点,同时也需要告知客户端需要更换主节点了。而原来的主节点上线后还需要将其降级为从节点,并连接到新的主节点。
然而重新选主是可能存在风险的:
- 数据的异步复制,可能会导致新的从节点数据是落后于原来的主节点的,导致了这部份数据丢失了。
- 如果数据库和其他系统存在关联,可能会导致风险。例如有一张表的数据在新的主节点是落后的,但是在原来的主节点里这张表的id已经被关联到了其他系统,那就会导致这些id被重复关联了。
- 脑裂的发生,也就是集群中出现了两个主节点。例如原来的主节点并没有发生宕机下线,但是集群内部的网络出现了问题,导致将其判断为下线并选出了新的主节点。
- 心跳超时时间的设置。这个时间既不能设置太短,不然容易频繁发生选举。也不能设置的太长,不然选举的周期将会被拉长,使集群可用性受到影响。
多主节点复制
前面提到的都是只有一个主节点的情况,带来的问题就是如果客户端与主节点的网络出现问题,将直接影响写入操作。这时候我们可以设立多个主节点,这样写操作可以被多个主节点接受,主节点之间也需要同时扮演其他主节点的从节点。
引入多个主节点,势必也会带来更多的复杂性,因此通常只会在多个数据中心各自设置一个主节点,然后不同数据中心的主节点相互复制。
多个数据中心,可以容忍整个数据中心的故障,同时也可以根据用户的地理位置挑选更接近用户的数据中心。

多主节点存在的最大问题就是冲突问题。不同客户端同时发起对不同主节点的同一条记录的写操作,这时如果还是参照之前的主从复制,这些主节点将无法知道哪个数据才是对的(实际上它们都是对的),这时就需要一些方式来解决冲突。
- 如果用户只是更新自己的数据,那我们可以让特定用户的请求都路由到同一个数据中心,不同用户则对应不同的数据中心。
- 通过一些机制对冲突进行收敛,例如挑选时间戳最大的。
- 在应用层解决冲突是最合理的,将出现冲突的记录都合并,然后交由应用层处理,通常也就是让用户来判断如何处理。
无主节点复制
当所有的节点都能够接受读写操作时,也就没有了主节点和从节点的分别。而无主节点的数据库集群存在两种数据写入的模式:
- 客户端将写请求发送给多个节点。
- 由一个协调者来接受客户端的写请求,再分发给数据库的所有节点。
不管怎样这些写请求都是需要通过某一个机制发送给所有节点。

当其中一个节点失效时,我们可以通过定义一个配置值,例如只需要多少个节点返回写操作成功则可以认为写请求成功。而等到这个失效的节点恢复之后,可能存在短暂的数据不一致,此时如果发起读请求,可以通过读取多个节点的方式,并判断数据记录的版本号来获取最新的数据。
失效的节点恢复之后,则需要采取一些方式来追上落后的数据。通常使用如下两种机制:
- 读修复。客户端并行从多个节点获取数据,检测出存在落后记录的节点,将最新的记录写到该节点上,适用于频繁读取的场景。如果这些数据没有被读取到,则这个节点可能会一直落后于其他节点。
- 反熵过程。通过一些后台的进程不断的去检测节点之间的数据差异并进行修复。这种方式无法保证以特定的顺序进行复制写入,并且会有数据滞后的风险。
前面提到,当有节点失效下线时,只需要部份节点正常既可以返回写入正常。如果有n个节点,要求写入需要w个节点确认,读取至少需要r个节点,那么只需要满足 w + r > n,那么读取的节点一定会包含最新的值。常见的设置是n为奇数(如3或5),w = r = (n + 1) / 2 (向上取整)。