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

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

数据复制带来的好处:

  1. 可用性。当某个副本不可用时,可以将请求调度到其他副本。

  2. 读扩展。在某些场景,可以将读请求分散到多个副本,以分散读压力。

  3. 降低延迟。跨地域复制,异地用户可以实现就近接入。

数据复制带来的问题:

  1. 数据一致性问题。

常见的数据复制架构:

  • 单主复制(Single-Leader Replication)

  • 多主复制(Multi-Leader Replication)

  • 无主复制(Leaderless Replication)

单主复制

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

这里主要参考 MySQL 的 Primary-Secondary Replication [1]

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

异步复制

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

半同步复制

半同步复制: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) [2] :如何避免同时存在两个 leader?

Paxos [3] / Raft [4] 可以实现强一致的单主复制和故障转移,同时解决脑裂等问题。

关于 Paxos/Raft,可以参考链接的内容。

多主复制

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

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

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

  1. App 的多端数据同步。每一个终端就是一个 master。 CouchDB [6] 是一个专门为解决这类问题设计的数据库。

  2. 多人协作编辑,比如腾讯文档、Google Docs。这里每一个人就是一个 master。

冲突处理

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

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

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

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

个人觉得,如果是为了实现写性能的扩展,可以通过分片(sharding)来实现。

实际业务中,建议避免使用这类会产生冲突的多主复制。

通过 Paxos 也可以实现强一致的多主复制,可以参考 MySQL Group Replication [7]

无主复制

图片来自 Wikipedia

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

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 [9]

小结

  1. 个人认为,半同步复制已经可以抛弃了。因为:

    • 延迟上,半同步复制相比 Paxos/Raft 没有优势,都是一个 RTT。

    • 一致性上,Paxos/Raft 保证了数据的一致性,自动 failover 和处理脑裂问题的机制更加完整、完善。半同步复制的自动 failover 不完善,而且难以解决脑裂问题。

  2. 不建议使用需要业务或外部进行冲突处理的多主复制,这会让业务逻辑或系统维护变复杂。

  3. 一些特殊的简单场景可以使用 NRW,需要自己做好权衡。

  4. 异步复制在一些写入数据量大,对数据一致性要求不高的场景还有用武之地。

  5. 如果要求高可用+强一致,请选用 Paxos/Raft 作为数据复制的算法。

参考资料

[1]

MySQL 的 Primary-Secondary Replication: https://dev.mysql.com/doc/refman/8.0/en/group-replication-primary-secondary-replication.html

[2]

脑裂(split brain): https://en.wikipedia.org/wiki/Split-brain

[3]

Paxos: https://lamport.azurewebsites.net/pubs/paxos-simple.pdf

[4]

Raft: https://raft.github.io/

[5]

Multi-Master Replication: https://en.wikipedia.org/wiki/Multi-master_replication

[6]

CouchDB: https://couchdb.apache.org/

[7]

MySQL Group Replication: https://dev.mysql.com/doc/refman/8.0/en/group-replication-multi-primary-mode.html

[8]

鸽巢原理: https://zh.wikipedia.org/wiki/%E9%B4%BF%E5%B7%A2%E5%8E%9F%E7%90%86

[9]

Dynamo: amazon's highly available key-value store: https://www.allthingsdistributed.com/files/amazon-dynamo-sosp2007.pdf

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章