数据复制

Posted by KANG's BLOG on Monday, August 15, 2022

1 复制的意义

高可用性

当一个数据节点出现问题不能响应请求时,系统仍可以通过将请求打到其他数据节点上来保证系统的整体可用性。

低延迟

当系统为广泛地域提供服务时,可以将用户的请求分发到空间距离较近的数据中心以提供较低延迟的服务。

可扩展性

当请求并发量增长到单机无法承受时,多节点分摊请写压力成为必然的选择。

2 复制方式

2.1 预写日志复制

通过传输WAL预写日志来同步数据,缺点是预写日志包含数据的物理信息,数据结构非常低层,这就意味着传输的数据和存储引擎紧密耦合,数据库升级版本时向下兼容变得非常困难。

2.2 逻辑日志复制

逻辑日志中通常记录的是数据变更的逻辑,典型使用就是Mysql的Binlog(它也是Mysql同步和崩溃恢复的主要方式)。

逻辑日志不仅有良好的兼容性,也有更好的可读性,通常可以由外部系统解析后进行其他行为。

抛开这些优点,逻辑日志和预写日志相比,主要问题是数据复制时,前者写入需要解析、编译、优化等等步骤,后者则可以直接写入。

2.3 触发器复制

很多数据库支持配置触发器,当发生数据变更时,由触发器去调用应用层的用户自定义逻辑,比如将数据复制到另一个系统。

触发器复制机制开销更高,也更容易出错,但具备了更高的灵活性。

3 同步复制 or 异步复制?

采取同步的方式来复制数据通常是为了保证可靠性,但同步等待全部从机完成写入时,从机数量越多显然整体性能越差。

所以对于写入性能要求较高的场景一般采取“一同多异”的方式:有一台从库写入完成(至少成功返回日志写入成功的标识,如mysql的relay log),就提交事务,其他从库采取异步复制的方式。

4 复制方案

主从复制

通常主节点用来承担写入场景,数据同步到从机后,由从机承担数据读取任务。

主从复制场景下,数据流向单一,不存在多节点写入冲突的问题,但最直接的问题就是如果主节点出现问题,所有写入操作都将会收到影响。

多主节点复制

为了解决主从复制下单一主节点可用性较低的问题,最直接的解决方式当然是使用多个主节点。

多主节点复制相比主从复制的优势:

  • 性能

    单一主节点处理所有的写请求,必然对单节点性能有更多的考验,相比之下,多个主节点能够分摊写请求,再通过异步的方式将数据同步到其他主节点。

  • 分区容忍

    一主多从下,主节点宕机需要将其某一个从节点升级为主节点,在此期间,集群丧失写入能力。但多主的方案,则没有这个问题,某一个主节点宕机后,等待其恢复即可。

多主节点复制的主要问题是难以处理并发写入造成的数据冲突。

无主节点复制

核心思路是将写入请求发送给多个副本,读取时从多个节点并行读取,根据Quorum公式来确认写入成功和读取成功的最小副本数。

这个方案的复杂点是:

  1. 该方案需有集群有协调能力,可以设定独立的协调节点,也可以由收到请求的节点作为协调者,并将请求分发到其他节点。

  2. 更好的容错性,是通过至少3倍以上的硬件成本做支撑达到的。

  3. 与多主复制的方案具有相同的写入冲突问题

5 Quorum

Quorum [ˈkwɔːrəm]

n.(会议的)法定人数

[牛津词典] the smallest number of people who must be at a meeting before it can begin or decisions can be made

假设有N个副本,我们设定其成功写入W个服务即判定为写入成功(其他副本可能在异步写入过程中),那么读取数据时,至少要读取R个副本。R是多少呢?

如果要保证R中副本包含最新写入的数据,那么就需要W和R之间一定有重叠,即:W + R > N

通常N为奇数,且至少为3,W = R = (N + 1) / 2

6 一致性模型

写后读(read after write)

保证同一个用户更新数据后,再次读取数据应该是自己更新过的最新版本。

为保证写后读一致性,可以采用如下方式:在用户写入成功后的一分钟内,该用户的读取行为被指向跟写入相同的节点。

单调读

用户每次读取的数据版本不会比上一次读取的版本更早。

前缀一致读

当一组串行事件保存在不同节点中后,再将其从各个节点汇总起来,应当保持相同的串行顺序。

可以将同一组事件发送到相同节点,比如对事件关键字使用Hash来匹配节点。

7 并发场景下如何解决冲突

由于分布式系统中复杂的时钟同步问题,现实当中,我们很难严格确定它们是否同时发生。

为更好地定义并发性,我们并不依赖确切的发生时间,即不管物理的时机如何, 如果两个操作并不需要意识到对方,我们即可声称它们是并发操作 。

– 《DDIA》第五章:数据复制 Page.178

避免冲突

处理冲突最简单的方式就是避免冲突,一般做法是把相同用户的请求永远路由到相同的节点进行处理,比如在代理层做Hash类型的负载。

解决冲突

解决冲突可能有以下方案:

  1. 最后写入者获胜(Last Writer Win)

    为每个请求增加序列号,可以是有序的序列号,如时间戳,或者无序的序列号如UUID。时间戳包含时间概念,更容易确定是谁“后写入者”,但由于有“时钟问题”,所以也不能完全信任。

  2. 为节点分配唯一值

    合并冲突时,保留序列号较大的副本的请求。

  3. 连接冲突

    将冲突通过连接的方式保存在一起,但这种方式使字段缺少逻辑性。

  4. 保留冲突

    将冲突保留在一起,并最终反馈给用户,由用户人为决定如何解决该冲突。

  5. 自定义冲突解决逻辑

    某些系统提供钩子函数,可以由用户自定义该钩子函数的实现来提供解决冲突的判定逻辑。通常可以在写入时执行或者读取时执行。