TiKV 使用了多副本的机制来保证一定程度的高可用(high availability)和数据安全(data safety)。值得注意的是,这里讲“一定程度”,意味着多副本也不能保证万无一失,极端情况下也是有一定不可用或丢失数据的风险的。
本文将从一些常见的误区着手,简要介绍 TiDB / TiKV 中多副本工作机制,探讨不同配置下、发生各种故障时数据所能得到的保障。
误解一:3 副本配置下,只要还有一个副本存活,就能正常工作。
引起这个误会的关键应该是把 3 副本想象成同样数据的 3 份复制了,实际上 TiKV 实现多副本所使用的 Raft 是一种基于多节点投票选举机制的共识算法,简单地说,当需要写入数据时,当且只要当超过半数的节点成功写入后,逻辑上来说数据就成功写入了。
注意“超过半数”这一点对于数据安全至关重要——它保证了只要有一半以上副本存活,则其中一定包含最新的数据,即不发生数据丢失。
了解了这些原理后,我们就能推断出假如 3 副本中只有一个副本存活,那么它一定不应该正常工作。原因其一,3 副本中可能只有 2 副本包含最新数据,只有 1 副本存活时无从得知其数据是否完整。其二,只有 1 副本存活的情况下无法达成超过半数节点写入。
当然了,不可用不代表不可恢复或者一定发生数据丢失。如果是主机断电之类的故障,只要重新启动后接入集群,凑够一半以上副本数就能恢复服务了。即便是极端情况下有多数副本同时发生不可恢复的磁盘故障,我们也可以通过强行重置副本数来恢复服务,虽然这种情况要承担一定数据不完整的风险,但是已经优于大多数基于备份或半同步复制的方案了。
误解二:3 副本配置下,尽量多部署一些 tikv 节点,只要还有一半以上节点存活,就能正常工作。
先了解一点背景知识。为了更好地做水平扩展和负载均衡,TiKV 会把数据拆分成很多段首尾相接的分片,每个分片称作 Region,集群运行过程中数据的移动、复制、平衡都是以 Region 为单位进行的,每个 Region 的多个副本构成 Raft group,并散落在多个不同的 TiKV 节点上。
为了数据的移动更高效,通常 Region 的尺寸会控制得比较小,目前默认配置是 96M。在一个典型的生产环境集群中,TiKV 节点可能有数十个或几十个,而 Region 数可能有数万个甚至数十万个,每个 TiKV 节点需维护约几千个 Region 的副本。
考虑 3 副本集群中某一个节点故障,直接后果就是数千个 Region 同时少了一个副本,而这数千个 Region 的其他副本通常是均匀地散落在集群的其他节点上的,如果不巧这时有另外一个节点也发生故障,一旦这两个节点包含了相同的 Region,这个 Region 就会因为丢失多数副本而不可用了。
那么结论就是,在配置为 3 副本的集群中,只要有 2 个节点同时掉线,很可能就会有 Region 不能工作;只要有 2 块磁盘同时损坏,很可能会造成数据丢失。根据概率知识我们知道,如果把不同节点故障看做独立事件,那么集群中的节点数越多,发生 2 个节点同时故障的概率也就越大,从而整个集群的可用性反而越低,这真是一个悲伤的事实!因此,3 副本配置只能提供比较基本的数据安全保障,对于核心业务的关键数据,建议使用 5 副本甚至 7 副本。
从另一个角度来看,现实中不同节点的故障不一定完全就是随机独立事件,比如两台服务器如果部署在同一机架,那么它们同时掉电的概率将大大增加。PD 现在支持通过简单地配置,按照拓扑结构将 TiKV 节点打上 label,PD 会自动调整同一 Region 的多份副本的存放位置,做到尽可能物理隔离,从而提高数据的安全性。
有一种可行的优化思路是 CopySet,它的基本思想是不让所有 Region 随机分布,Region 在调度时只能从一些预先规定的组合中找。这样做之后,当发生 2 个节点同时故障时,只要没有预定义组合同时包含这两个节点,就不会发生数据丢失。要注意的是这个方案其实也是有副作用的,那就是一旦预定义的组合被命中了,那么影响到的 Region 要多于随机分布的方案。将来 PD 上或许会支持 CopySet 选项,把选择权交给用户。
误解三:如果有节点宕机,必须恢复节点或添加新节点才能恢复数据副本数。
区别于基于 Hash 函数的 placement 算法(consistent hashing、CRUSH TiKV 中数据分片的存储位置是存储在中心节点 PD 上的,需要访问对应的数据之前,客户端会先通过 PD 接口查询存放对应数据的 tikv-server。这意味着有全局视角的 PD 可以在运行时根据需要随时调整数据副本,而不用担心副本的变化导致客户端无法定位到数据。
实际上 TiKV 集群在运行过程中,副本的变动是持续不断进行着的。变动的原因可能是需要平衡读写热点,或者写入不均匀导致需要均衡存储空间,或者为了优化数据隔离提高安全性,或者是为异常 Region 补充副本。
回到我们的问题,当节点发生宕机,PD 感知到故障后,会找出有副本分布在这个节点上的所有 Region,把这些 Region 标记为缺副本状态,随后就会开始为这些 Region 寻找可用节点添加新副本,当这一步完成后,我们的数据安全级别就又回到原先的水平了。并且令人庆幸的是,这整个恢复过程都是 PD 自动完成的,完全不需要人工的介入。
在实践中,很多情况下节点故障都是可以恢复后重新上线的。如果只要检测到节点断连就马上开始加副本的话,常常是不必要的,还会带来计算和存储资源的浪费。所以我们在节点故障一段时间之后(默认配置是一小时),才会开始补副本的过程。
运维时处理 TiKV 存储节点故障的最佳实践应该是:如果还能重新启动就尽早启动上线,可以避免不必要的补副本过程;如果确定无法恢复了,最好尽早把这个节点做下线处理,这样 PD 会立即开始补充副本,从而提前恢复副本数。当然了,如果是凌晨 3 点发生了故障,不妨就交给集群自行去恢复吧:)
误解四:由于有高可用和 Auto-balance,缩容时直接把节点停机就行了。
这么做一般从表现上来看没什么问题,据我了解有些集群一直都是这么干的。但这么做就相当于节点宕机,根据之前的讨论,这时如果再有一个节点故障,很可能就有数据不可用了。
标准的流程是在不停机的状态下通过 PD 接口把对应节点做下线处理,PD 随即开始把节点上的副本搬移到其它位置——此时标记为下线的节点还在正常提供服务,并且所有 Region 的副本数都是足量的。当搬移完成后,PD 会将此节点标记为 Tombstone 状态,之后就可以 100% 安全地停掉节点清空数据了。
误解五:3 机房 3 副本配置下,只要配置合理的 label,任意一个机房被隔离,不会中断服务。
看上去这个论断无懈可击,如果 label 配置合理,同一个 Region 的 3 个副本一定会被放置在不同的机房。这样当一个机房被隔离时,另外两个机房还有两副本,应该总是够半数的。
但是有一点别忘了,上面我们有提到,在集群运行过程中副本的搬移是持续不断进行着的。比如我们检测到某机房有个节点磁盘空间快不够了,我们会想把其中的某个副本搬到另外一个节点。
副本的搬动不是一瞬间就能完成的,而是一系列复杂的流程,这个过程不仅涉及到 Region 元数据的变更,还包括具体数据的拷贝和删除,还得保证变更的过程在多个节点间达成共识,好在 Raft 论文中给出了 ConfChange 的具体方案,我们可以借助多次 ConfChange 来完成迁移的过程。
在最初设计副本迁移功能时,我们面临着两种选择:1)先用 RemoveNode 移除旧副本,然后用 AddNode 添加新副本;2)先用 AddNode 添加新副本,再用 RemoveNode 移除旧副本。
令人沮丧的是,这两种方法都不是完美的,在 3 机房 3 副本的场景下都会在一定程度上影响数据的可用性。如果选择先移除旧副本,那么在添加新副本之前 Region 的总副本数是 2,此时如果其中一个副本被隔离,剩余的一副本就无法工作了;如果选择先添加新副本,那么添加完成时总副本数是 4,由于我们只有 3 个机房,根据鸽巢原理一定有个机房包含了至少两个副本,此时如果这个机房被隔离,剩余两个副本也无法达成“超过半数”这个条件了。
最后的方案是引入 Learner 角色,然后副本迁移过程是:AddLearner 添加新副本 + PromoteLearner + RemoveNode 删除旧副本。区别于 Leader 或 Follower,Learner 参与 raftlog 同步但是不参与选举,有了这个我们就能在不改变(参与投票的)总副本数的前提下创建(数据)副本。等到 Learner 的日志同步上了,PD 再连续发送 PromoteLearner 和 RemoveNode 两个 ConfChange 完成调度过程,由于这两个 ConfChange 都只涉及元数据的修改,所以瞬间就能完成,网络隔离恰好发生这个短暂的窗口期的概率极小,可以认为是仅存在理论上的可能。另外需要注意,在偶数节点 raft一文只也说明了,这种情况处于偶数副本的中间状态,只是不可用了,并不会发生数据丢失。
那么有没有可能完美解决呢?这就要依靠 Joint Consensus 这个功能了(也包括在 Raft 论文中)。简单的说,就是在 raft 内部支持把多个 ConfChange 变成原子的,这样就能把上面说的那个极短的窗口期给消除掉了。目前我们也在积极地推进这个功能,希望下个大版本能最终圆满处理好这个问题。