gitbook/分布式数据库30讲/docs/277028.md
2022-09-03 22:05:03 +08:00

15 KiB
Raw Permalink Blame History

07 | 数据复制为什么有时候Paxos不是最佳选择

你好我是王磊你也可以叫我Ivan。今天我们要学习的是数据复制。

数据复制是一个老生常谈的话题了典型的算法就是Paxos和Raft。只要你接触过分布式就不会对它们感到陌生。经过从业者这些年的探索和科普网上关于Paxos和Raft算法的高质量文章也是一搜一大把了。

所以,今天这一讲我不打算全面展开数据复制的方方面面,而是会聚焦在与分布式数据库相关的,比较重要也比较有意思的两个知识点上,这就是分片元数据的存储和数据复制的效率。

分片元数据的存储

我们知道,在任何一个分布式存储系统中,收到客户端请求后,承担路由功能的节点首先要访问分片元数据(简称元数据),确定分片对应的节点,然后才能访问真正的数据。这里说的元数据,一般会包括分片的数据范围、数据量、读写流量和分片副本处于哪些物理节点,以及副本状态等信息。

从存储的角度看元数据也是数据但特别之处在于每一个请求都要访问它所以元数据的存储很容易成为整个系统的性能瓶颈和高可靠性的短板。如果系统支持动态分片那么分片要自动地分拆、合并还会在节点间来回移动。这样元数据就处在不断变化中又带来了多副本一致性Consensus的问题。

下面,让我们看看,不同的产品具体是如何存储元数据的。

静态分片

最简单的情况是静态分片。我们可以忽略元数据变动的问题只要把元数据复制多份放在对应的工作节点上就可以了这样同时兼顾了性能和高可靠。TBase大致就是这个思路直接将元数据存储在协调节点上。即使协调节点是工作节点随着集群规模扩展会导致元数据副本过多但由于哈希分片基本上就是静态分片也就不用考虑多副本一致性的问题。

但如果要更新分片信息,这种方式显然不适合,因为副本数量过多,数据同步的代价太大了。所以对于动态分片,通常是不会在有工作负载的节点上存放元数据的。

那要怎么设计呢有一个凭直觉就能想到的答案那就是专门给元数据搞一个小规模的集群用Paxos协议复制数据。这样保证了高可靠数据同步的成本也比较低。

TiDB大致就是这个思路但具体的实现方式会更巧妙一些。

TiDB无服务状态

在TiDB架构中TiKV节点是实际存储分片数据的节点而元数据则由Placement Driver节点管理。Placement Driver这个名称来自Spanner中对应节点角色简称为PD。

在PD与TiKV的通讯过程中PD完全是被动的一方。TiKV节点定期主动向PD报送心跳分片的元数据信息也就随着心跳一起报送而PD会将分片调度指令放在心跳的返回信息中。等到TiKV下次报送心跳时PD就能了解到调度的执行情况。

由于每次TiKV的心跳中包含了全量的分片元数据PD甚至可以不落盘任何分片元数据完全做成一个无状态服务。这样的好处是PD宕机后选举出的新主根本不用处理与旧主的状态衔接在一个心跳周期后就可以工作了。当然在具体实现上PD仍然会做部分信息的持久化这可以认为是一种缓存。

我将这个通讯过程画了下来,希望帮助你理解。

三个TiKV节点每次上报心跳时由主副本Leader提供该分片的元数据这样PD可以获得全量且没有冗余的信息。

虽然无状态服务有很大的优势但PD仍然是一个单点也就是说这个方案还是一个中心化的设计思路可能存在性能方面的问题。

有没有完全“去中心化”的设计呢当然是有的。接下来我们就看看P2P架构的CockroachDB是怎么解决这个问题的。

CockroachDB去中心化

CockroachDB的解决方案是使用Gossip协议。你是不是想问为什么不用Paxos协议呢

这是因为Paxos协议本质上是一种广播机制也就是由一个中心节点向其他节点发送消息。当节点数量较多时通讯成本就很高。

CockroachDB采用了P2P架构每个节点都要保存完整的元数据这样节点规模就非常大当然也就不适用广播机制。而Gossip协议的原理是谣言传播机制每一次谣言都在几个人的小范围内传播但最终会成为众人皆知的谣言。这种方式达成的数据一致性是 “最终一致性”,即执行数据更新操作后,经过一定的时间,集群内各个节点所存储的数据最终会达成一致。

看到这,你可能有点晕。我们在第2讲就说过分布式数据库是强一致性的,现在搞了个最终一致性的元数据,能行吗?

这里我先告诉你结论,CockroachDB真的是基于“最终一致性”的元数据实现了强一致性的分布式数据库。我画了一张图,我们一起走下这个过程。

  1. 节点A接到客户端的SQL请求要查询数据表T1的记录根据主键范围确定记录可能在分片R1上而本地元数据显示R1存储在节点B上。
  2. 节点A向节点B发送请求。很不幸节点A的元数据已经过时R1已经重新分配到节点C。
  3. 此时节点B会回复给节点A一个非常重要的信息R1存储在节点C。
  4. 节点A得到该信息后向节点C再次发起查询请求这次运气很好R1确实在节点C。
  5. 节点A收到节点C返回的R1。
  6. 节点A向客户端返回R1上的记录同时会更新本地元数据。

可以看到CockroachDB在寻址过程中会不断地更新分片元数据促成各节点元数据达成一致。

看完TiDB和CockroachDB的设计我们可以做个小结了。复制协议的选择和数据副本数量有很大关系如果副本少参与节点少可以采用广播方式也就是Paxos、Raft等协议如果副本多节点多那就更适合采用Gossip协议。

复制效率

说完了元数据的存储我们再看看今天的第二个知识点也就是数据复制效率的问题具体来说就是Raft与Paxos在效率上的差异以及Raft的一些优化手段。在分布式数据库中采用Paxos协议的比较少知名产品就只有OceanBase所以下面的差异分析我们会基于Raft展开。

Raft的性能缺陷

我们可以在网上看到很多比较Paxos和Raft的文章它们都会提到在复制效率上Raft会差一些主要原因就是Raft必须“顺序投票”不允许日志中出现空洞。在我看来顺序投票确实是影响Raft算法复制效率的一个关键因素。

接下来,我们就分析一下为什么“顺序投票”对性能会有这么大的影响。

我们先看一个完整的Raft日志复制过程

  1. Leader 收到客户端的请求。
  2. Leader 将请求内容即Log Entry追加Append到本地的Log。
  3. Leader 将Log Entry 发送给其他的 Follower。
  4. Leader 等待 Follower 的结果,如果大多数节点提交了这个 Log那么这个Log Entry就是Committed EntryLeader就可以将它应用Apply到本地的状态机。
  5. Leader 返回客户端提交成功。
  6. Leader 继续处理下一次请求。

以上是单个事务的运行情况。那么,当多事务并行操作时,又是什么样子的呢?我画了张图来演示这个过程。

我们设定这个Raft组由5个节点组成T1到T5是先后发生的5个事务操作被发送到这个Raft组。

事务T1的操作是将X置为15个节点都Append成功Leader节点Apply到本地状态机并返回客户端提交成功。事务T2执行时虽然有一个Follower没有响应但仍然得到了大多数节点的成功响应所以也返回客户端提交成功。

现在轮到T3事务执行没有得到超过半数的响应这时Leader必须等待一个明确的失败信号比如通讯超时才能结束这次操作。因为有顺序投票的规则T3会阻塞后续事务的进行。T4事务被阻塞是合理的因为它和T3操作的是同一个数据项但是T5要操作的数据项与T3无关也被阻塞显然这不是最优的并发控制策略。

同样的情况也会发生在Follower节点上第一个Follower节点可能由于网络原因没有收到T2事务的日志即使它先收到T3的日志也不会执行Append操作因为这样会使日志出现空洞。

Raft的顺序投票是一种设计上的权衡虽然性能有些影响但是节点间日志比对会非常简单。在两个节点上只要找到一条日志是一致的那么在这条日志之前的所有日志就都是一致的。这使得选举出的Leader与Follower同步数据非常便捷开放Follower读操作也更加容易。要知道我说的可是保证一致性的Follower读操作它可以有效分流读操作的访问压力。这一点我们在24讲再详细介绍。

Raft的性能优化方法TiDB

当然在真正的工程实现中Raft主副本也不是傻傻地挨个处理请求还是有一些优化手段的。TiDB的官方文档对Raft优化说得比较完整我们这里引用过来着重介绍下它的四个优化点。

  1. **批操作Batch。**Leader 缓存多个客户端请求,然后将这一批日志批量发送给 Follower。Batch的好处是减少的通讯成本。
  2. **流水线Pipeline。**Leader本地增加一个变量称为NextIndex每次发送一个Batch后更新NextIndex记录下一个Batch的位置然后不等待Follower返回马上发送下一个Batch。如果网络出现问题Leader重新调整NextIndex再次发送Batch。当然这个优化策略的前提是网络基本稳定。
  3. **并行追加日志Append Log Parallelly。**Leader将Batch发送给Follower的同时并发执行本地的Append操作。因为Append是磁盘操作开销相对较大而标准流程中Follower与Leader的Append是先后执行的当然耗时更长。改为并行就可以减少部分开销。当然这时Committed Entry的判断规则也要调整。在并行操作下即使Leader没有Append成功只要有半数以上的Follower节点Append成功那就依然可以视为一个Committed EntryEntry可以被Apply。
  4. **异步应用日志Asynchronous Apply。**Apply并不是提交成功的必要条件任何处于Committed状态的Log Entry都确保是不会丢失的。Apply仅仅是为了保证状态能够在下次被正确地读取到但多数情况下提交的数据不会马上就被读取。因此Apply是可以转为异步执行的同时读操作配合改造。

其实Raft算法的这四项优化并不是TiDB独有的CockroachDB和一些Raft库也做了类似的优化。比如SOFA-JRaft也实现了Batch和Pipeline优化。

不知道你有没有听说过etcd它是最早的、生产级的Raft协议开源实现TiDB和CockroachDB都借鉴了它的设计。甚至可以说它们选择Raft就是因为etcd提供了可靠的工程实现而Paxos则没有同样可靠的工程实现。既然是开源为啥不直接用呢因为etcd是单Raft组写入性能受限。所以TiDB和CockroachDB都改造成多个Raft组这个设计被称为Multi Raft所有采用Raft协议的分布式数据库都是Multi Raft。这种设计可以让多组并行一定程度上规避了Raft的性能缺陷。

同时Raft组的大小也就是分片的大小也很重要越小的分片事务阻塞的概率就越低。TiDB的默认分片大小是96MCockroachDB的分片不超过512M。那么TiDB的分片更小就是更好的设计吗也未必因为分片过小又会增加扫描操作的成本这又是另一个权衡点了。

小结

好了,今天的内容就到这里。我们一起回顾下这节课的重点。

  1. 分片元数据的存储是分布式数据库的关键设计,要满足性能和高可靠两方面的要求。静态分片相对简单,可以直接通过多副本分散部署的方式实现。
  2. 动态分片满足高可靠的同时还要考虑元数据的多副本一致性必须选择合适的复制协议。如果搭建独立的、小规模元数据集群则可以使用Paxos或Raft等协议传播特点是广播。如果元数据存在工作节点上数量较多则可以考虑Gossip协议传播特点是谣言传播。虽然Gossip是最终一致性但通过一些寻址过程中的巧妙设计也可以满足分布式数据的强一致性要求。
  3. Paxos和Raft是广泛使用的复制协议也称为共识算法都是通过投票方式动态选主可以保证高可靠和多副本的一致性。Raft算法有“顺序投票”的约束可能出现不必要的阻塞带来额外的损耗性能略差于Paxos。但是etcd提供了优秀的工程实现促进了Raft更广泛的使用而etcd的出现又有Raft算法易于理解的内因。
  4. 分布式数据库产品都对Raft做了一定的优化另外采用Multi Raft设计实现多组并行再通过控制分片大小降低事务阻塞概率提升整体性能。

讲了这么多回到我们最开始的问题为什么有时候Paxos不是最佳选择呢一是架构设计方面的原因看参与复制的节点规模规模太大就不适合采用Paxos同样也不适用其他的共识算法。二是工程实现方面的原因在适用共识算法的场景下选择Raft还是Paxos呢因为Paxos没有一个高质量的开源实现而Raft则有etcd这个不错的工程实现所以Raft得到了更广泛的使用。这里的深层原因还是Paxos算法本身过于复杂直到现在实现Raft协议的开源项目也要比Paoxs更多、更稳定。

有关分片元数据的存储在我看来TiDB和CockroachDB的处理方式都很优雅但是TiDB的方案仍然建立在PD这个中心点上对集群的整体扩展性对于主副本跨机房、跨地域部署有一定的局限性。

关于Raft的优化方法大的思路就是并行和异步化其实这也是整个分布式系统中常常采用的方法在第10讲原子协议的优化中我们还会看到类似的案例。

思考题

最后是今天的思考题时间。我们在第1讲就提到过分布式数据库具备海量存储能力,那么你猜,这个海量有上限吗?或者说,你觉得分布式数据库的存储容量会受到哪些因素的制约呢?欢迎你在评论区留言和我一起讨论,我会在答疑篇回复这个问题。

你是不是也经常听到身边的朋友讨论数据复制的相关问题呢,而且得出的结论有可能是错的?如果有的话,希望你能把今天这一讲分享给他/她,我们一起来正确地理解分布式数据库的数据复制是怎么一回事。