设计数据密集型应用(5):复制

2020-04-01 17:55:40 浏览数 (1)

数据复制是每一个存储系统的重要组成部分。

数据复制带来的好处:

  1. 可用性。当某个副本不可用时,可以将请求调度到其他副本。
  2. 读扩展。在某些场景,可以将读请求分散到多个副本,以分散读压力。
  3. 降低延迟。跨地域复制,异地用户可以实现就近接入。

数据复制带来的问题:

  1. 数据一致性问题。

常见的数据复制架构:

  • 单主复制(Single-Leader Replication)
  • 多主复制(Multi-Leader Replication)
  • 无主复制(Leaderless Replication)

单主复制

传统的单主复制有:异步复制、半同步复制

这里主要参考 MySQL 的 Primary-Secondary Replication。

首先,选择一台机器作为 leader,所有写请求都由 leader 执行,生成日志。

async-replication

异步复制:leader 直接提交(commit)请求,返回结果给客户端,通过后台线程将日志异步发送给 followers。

semisync-replication

半同步复制:leader 等待日志成功发送给 followers 后,提交(commit)请求,返回结果给客户端。

异步复制和半同步复制的优缺点

  • 异步复制
    • Leader 不用等日志成功发送给 followers,可用性、延迟不受 followers 的影响。
    • Followers 的数据可能落后 leader 非常多,数据一致性差。
  • 半同步复制
    • 正常情况下,leader 和 followers 之间的数据是一致的。
    • 但是每次写请求 leader 都要等待 followers 的返回,增加了延迟。
    • 同时,followers 故障也会影响 leader 的可用性。 为了避免 follwers 故障导致 leader 不可用,半同步复制一般会设置一个超时时间,如果超时了,就退化成异步复制。 或者当 followers 不止一个时,可以通过设置半同步等待 follower 返回个数来缓解这个问题。比如如果有两个 followers,当半同步复制收到其中一个 follower 的返回时,就可以提交(commit)请求。

故障转移(failover)

一般情况下,如果 followers 故障,启动后继续复制就好,不需要特殊处理。

Leader 故障的 failover 比较麻烦,总结起来有下面四件事要做:

  1. 故障检测。一般通过定时发送心跳包来实现故障检测。如果太久(比如 10s)没收到心跳包,则认为对方故障——在分布式系统中,其实很难确定一台机器是否真的故障,因为也有可能是网络问题。
  2. 提升 leader。检测到 leader 故障之后,需要选择一个 follower 将其提升为新的 leader。(这里可能还需要让旧的 leader 失效。)
  3. 将 client 的请求路由到新的 leader。
  4. 让其它 followers 切换到新的 leader。

细究起来,failover 这里有很多细节需要特别注意,一不留神就会留下 bug,比如:

  1. 如果使用异步复制,新 leader 的数据可能不是最新的,是否可以接受?旧 leader 重启后要如何处理不一致的数据?
  2. 脑裂(split brain):如何避免同时存在两个 leader?

Paxos/Raft 可以实现强一致的单主复制和故障转移,同时解决脑裂等问题。 关于 Paxos/Raft,可以参考链接的内容。

多主复制

多主复制,也称为 Multi-Master Replication

从数据库的角度看,多主复制可以实现写扩展和就近接入降低延迟,但是存在写冲突的问题 —— 如果同一条记录在不同的 master 被同时修改,就产生了冲突。

还有一些业务场景符合多主复制的逻辑,不过这里多主复制不一定是为了提升写性能。比如:

  1. App 的多端数据同步。每一个终端就是一个 master。CouchDB 是一个专门为解决这类问题设计的数据库。
  2. 多人协作编辑,比如腾讯文档、Google Docs。这里每一个人就是一个 master。

冲突处理

冲突处理最简单的方式就是避免冲突——让每个 master 修改一个独立的数据集合。

如果 masters 修改的数据集合会有相交,就有可能出现冲突。

在进行冲突处理之前要先检测出冲突的数据,一般可以通过 vector clock 来维护数据修改的时序依赖,以此来检测不同 master 修改是否有冲突。

冲突处理则要根据应用的容忍性进行选择,比如选择时间戳最大的、选择某个 master 的修改、业务定制化处理等等。

个人觉得,如果是为了实现写性能的扩展,可以通过分片(sharding)来实现。 实际业务中,建议避免使用这类会产生冲突的多主复制。 通过 Paxos 也可以实现强一致的多主复制,可以参考 MySQL Group Replication。

无主复制

TooManyPigeonsTooManyPigeons

无主复制,其实就是 NRW,复制逻辑是由客户端驱动的。 NRW 的思路来自鸽巢原理。

N - 复制的副本数。

R - 每次读操作需要读的副本数。

W - 每次写操作需要写的副本数。

一般情况下:

  1. 为了确保多数派故障数据不丢:W >= (N/2) 1。
  2. 为了能读取到最新数据:R W > N。
  3. 为了不受单机故障的影响:W < N && R < N。

比如,N 为 3。

比较稳妥的选择是:W=2,R=2。

为了最求写性能,W=1,R=3,这样单机故障有丢失数据的风险。

为了最求读性能,W=3,R=1,这样单机故障写操作就会失败。

NRW 看起来可以从理论上保证每次读取到最新的数据,但是实际上没那么简单:

  1. 由于复制是客户端驱动的——客户端直接向多个 servers 发起写请求,多个客户端的并发操作如何处理?如何保证多个结点数据的一致性?
  2. 脏读:读写并发,读到的数据可能还没完成复制。
  3. 脏读:部分写失败也可能被读到。

想深入了解 NRW 的话,可以看论文 Dynamo: amazon's highly available key-value store。

小结

  1. 个人认为,半同步复制已经可以抛弃了。因为:
    • 延迟上,半同步复制相比 Paxos/Raft 没有优势,都是一个 RTT。
    • 一致性上,Paxos/Raft 保证了数据的一致性,自动 failover 和处理脑裂问题的机制更加完整、完善。半同步复制的自动 failover 不完善,而且难以解决脑裂问题。
  2. 不建议使用需要业务或外部进行冲突处理的多主复制,这会让业务逻辑或系统维护变复杂。
  3. 一些特殊的简单场景可以使用 NRW,需要自己做好权衡。
  4. 异步复制在一些写入数据量大,对数据一致性要求不高的场景还有用武之地。
  5. 如果要求高可用 强一致,请选用 Paxos/Raft 作为数据复制的算法。

0 人点赞