TiKV源码解析系列文章(二十)Region Split源码解析

Easter79
• 阅读 746

在学习了之前的几篇 raft-rs,raftstore 相关文章之后(如 Raft Propose 的 Commit 和 Apply 情景分析Raftstore 概览等),raft-rs 以及 raftstore 的流程大家应该基本了解了。其中 raft-rs 解决的是单个 Raft group(即单个 Region) 的问题,raftstore 解决的是多个 Raft group (即多个 Region)的问题。Split 和 Merge 则是 raftstore 多个 Raft group 所独有的操作。TiKV 中的 Split 能把一个 Region 分裂成多个 Region,Merge 则能把 Range 相邻的 2 个 Region 合成一个 Region。本文接下来要介绍的是 Split 的源码。

Region epoch

message RegionEpoch {    // Conf change version, auto increment when add or remove peer    uint64 conf_ver = 1;    // Region version, auto increment when split or merge    uint64 version = 2;}

我们先从  region epoch  讲起,上面是它的 protobuf 定义,在之前的  源码分享文章 中提到过,它的本质就是两个版本号,更新规则如下:

1. 配置变更的时候,  conf_ver  + 1。

2. Split 的时候,原 region 与新 region 的  version  均等于原 region 的  version  + 新 region 个数。

3. Merge 的时候,两个 region 的  version  均等于这两个 region 的  version  最大值 + 1。

2 和 3 两个规则可以推出一个有趣的结论:如果两个 Region 拥有的范围有重叠,只需比较两者的  version  即可确认两者之间的历史先后顺序, version  大的意味着更新,不存在相等的情况。

证明比较简单,由于范围只在 Split 和 Merge 的时候才会改变,而每一次的 Split 和 Merge 都会更新影响到的范围里 Region 的  version ,并且更新到比原范围中的  version  更大,对于一段范围来说,不管它属于哪个 Region,它所在 Region 的  version  一定是严格单调递增的。

PD 使用了这个规则去判断范围重叠的不同 Region 的新旧。

每条 Proposal 都会在提出的时候带上 PeerFsm 的 Region epoch,在应用的时候检查该 Region epoch 的合法性,如果不合法就跳过。

TiKV源码解析系列文章(二十)Region Split源码解析

上图所示,新 Proposal 的 Region epoch 是应用了 Applied Index 那条 Proposal 之后得到的,如果在 Applied Index + 1 到 Last Index 之间的 Proposal 有修改 Region Epoch 的操作,新 Proposal 就有可能会在应用的时候被跳过。

列举两个被跳过的情况,其他的可参照码 store::util::check_region_epoch

1. 非 Admin Request, Proposal 中的 version 与当前的不相等。

2. Split,Merge 的 Request,Proposal 中的 Region epoch 与当前的不相等。

Split 触发

Split 触发的条件大体分两种:

1. PD 触发

2. TiKV 每个 Region 自行定时检查触发

PD 触发主要是指定哪些 key 去 Split, Split Region 使用文档  中的功能就是利用 PD 触发实现的。

每个 Region 每隔  split-region-check-tick-interval (默认 10s)就会触发一次 Split 检查,代码见  PeerFsmDelegate::on_split_region_check_tick ,以下几个情况不触发检查:

  • 有检查任务正在进行;

  • 数据增量小于阈值;

  • 当前正在生成 snapshot 中并且触发次数小于定值。如果频繁 Split,会导致生成的 snapshot 可能因为  version  与当前不一致被丢弃,但是也不能一直不 Split,故设置了触发上限。

触发检查后,会发送任务至  split_checker_worker ,任务运行时调用  split_checker.rs  中函数  Runner::check_split

1. 调用  coprocessor::new_split_checker_host  获取  SplitCheckerHost ,获取时会对每一个注册过的  split_check_observers  调用  add_checker ,若满足触发阈值则会把它的  split_check 加入  SplitCheckerHost::checkers  中,如果  checkers  为空则结束检查。(值得一提的是,这里的  coprocessor  并不是指的是计算下推的那个  coprocessor ,而是观测 raftstore 事件,给外部提供事件触发的  coprocessor ,它的存在可以很好的减少外部观测事件对 raftstore 代码的侵入)。

2. 获取  policy ,这里的  policy  只有两种,分别是  SCAN  和  APPROXIMATE ,即扫描和取近似,遍历  split_checker  调用它们的  policy ,只要有一个给出的  policy  是取近似,那么最终的结果就是取近似,反之则是扫描。

3. 获取 Split key。

  1. 若  policy  为扫描,调用  scan_split_keys ,扫描读出该 Region 范围大 Column Family 的所有数据,对于每一对 KV,调用每个  split_checker  的  on_kv  计算 Split key,扫描完成后遍历  split_checker  的  split_keys  返回第一个不为空的结果。由于需要扫描存储的数据,这个策略会引入额外的 I/O。

  2. 若为取近似,调用  approximate_split_keys ,遍历  split_checker  的  approximate_split_keys ,返回第一个不为空的结果。这是通过 RocksDB 的 property 来实现的,几乎没有额外的 I/O 被引入,因而性能上是更优的策略。

4. 发送  CasualMessage::SplitRegion  给这个 Region。

SplitCheckerHost  只是聚合了 split_check  的结果,具体实现还是在这些  split_check  中,它们均实现了  SplitChecker  trait,由上文的流程叙述也都提到了这些函数。

pub trait SplitChecker<E> {    /// Hook to call for every kv scanned during split.    ///    /// Return true to abort scan early.    fn on_kv(&mut self, _: &mut ObserverContext<'_>, _: &KeyEntry) -> bool {        false    }    /// Get the desired split keys.    fn split_keys(&mut self) -> Vec<Vec<u8>>;    /// Get approximate split keys without scan.    fn approximate_split_keys(&mut self, _: &Region, _: &E) -> Result<Vec<Vec<u8>>> {        Ok(vec![])    }    /// Get split policy.    fn policy(&self) -> CheckPolicy;}

split_check 有以下几种:

1. 检查 Region 的总或者近似 Size,代码位于 size.rs

2. 检查 Region 的总或者近似 Key 数量是否超过阈值,代码位于 key.rs

3. 根据 Key 范围二分 Split,代码位于 half.rs,除了上文讲的 PD 指定 key 来 Split,这种方式也是由 PD 触发的,目前只有通过 pd-ctltikv-ctl 的命令来手动触发。

4. 根据  Key 所属 Table 前缀 Split,代码位于 table.rs,配置默认关闭。

由于篇幅所限,具体的实现细节可参阅代码。

Split 实现

Split 的实现相对简单,总的来说,Split 这个操作被当做一条 Proposal 通过 Raft 达成共识,然后各自的 Peer 分别执行 Split。

讲一下具体的流程。

在触发 Split 之后,触发方会发送一条 CasualMessage::SplitRegion 给这个 Region,处理代码见 PeerFsmDelegate::on_prepare_split_region,除了需要检查是否是 leader,还需检查 version 是否有变化,如有变化就拒绝触发 Split。

检查成功后,发送一条 RPC 向 PD 请求分配一些新的 ID,包含所有新 Region 的 ID 以及它所有的 Peer ID,等到 PD 回复后,构造一个类型为 AdminCmdType::BatchSplit 的 Proposal 提给该 Peer。代码在 pd_worker 下的 handle_ask_batch_split

之后的流程就如 Raft Propose 的 Commit 和 Apply 情景分析 所描述的那样,如上文所述,在应用前会判断 Region epoch 的合法性,如果不合法就需要跳过。假设它没有被跳过,接下来看这条 Proposal 应用的地方 ApplyDelegate::exec_batch_split

1. 更新原 Region 的 version,新 Region 的 epoch 继承原 Region 的 epoch。

  1. right_derive 为 true 的,原 Region 要分裂到右侧,为 false 则反之,依次设置每个 Region 的 start key 与 end key。

3. 对每个 Split 出来的新 Region 调用 write_peer_statewrite_initial_apply_state 创建元数据。

在应用完成之后,ApplyFsm 会发送 PeerMsg::ApplyRes 给 PeerFsm, PeerFsm 处理的代码在 PeerFsmDelegate::on_ready_split_region

1. 如果是 leader,上报 PD 自己以及新 Region 的 meta 信息(包含范围,Region epoch 等等一系列信息)。

2. 依次创建新 Region 的 PeerFsm 和 ApplyFsm,做一些注册的工作。

3. 更新 PeerFsm 的 Region epoch。

需要注意的是,如果在应用完成落盘后宕机,这部分的工作能在重启后恢复。其实所有日志应用的设计都需要满足这个原则。

到这里 Split 的工作就完成了,等到原 Region 大多数的 Peer 都完成了 Split 的工作后,新 Region 就可以成功选出 leader 并且提供服务了。

Split 过程中的一致性

在各机器时钟偏移不超过一定范围的前提下,某个 Region 的 Leader 持有 Raft 租约能保证这段时间不会产生其他 term 更大的 Leader,基于这个保证,使用租约可以提供 线性一致性 的本地读取功能,具体实现可以参考上一篇 源码阅读文章

但是在 Split 过程中,原 Region 持有的租约并不能保证这一点。

假设 3 个副本,考虑如下情况:Split Proposal 在 2 个 Follower 上已经应用完成,同时 Leader 上还没有应用(由于 apply 是异步的,Follower 上的应用进度可能超过 Leader)。

Split 之后原 Region 的范围缩小,其余的范围属于新 Region,而新 Region 存活的 Peer 个数已经超过了 Raft 所要求的大多数副本,故可以合理的发起选举并产生 Leader,并且正常服务读写请求。此时原 Region Leader 仍然还未应用 Split Proposal,如果因为持有租约继续服务原范围的读请求,就会破坏 线性一致性。

TiKV 处理方式是在 Split 期间不续约租约。方法是记录最后一条 Split Proposal 的 index last_committed_split_idx, 记录位置见 Peer::handle_raft_ready_append。只需判断 last_committed_split_idx 是否大于 applied_index 即可得知是否在 Split 期间(Peer::is_splitting)。

阅读过 Peer::handle_raft_ready_append 中记录 last_committed_split_idx 的小伙伴应该能注意这里并没有让租约立马失效,仅仅设置 index 阻止下次续约。换句话说,在 Split 期间的那次租约时间内是可以让原 Region 的 Leader 提供本地读取功能的。根据前面的分析,这样做貌似是不合理的。

原因十分有趣,对于原 Region 非 Leader 的 Peer 来说,它创建新 Region 的 Peer 是不能立马发起选举的,得等待一个 Raft 的选举超时时间,而对于原 Region 是 Leader 的 Peer 来说,新 Region 的 Peer 可以立马发起选举。Raft 的超时选举时间是要比租约时间长的,这是保证租约正确性的前提。所以在 Split 期间的那次租约时间内,在其他 TiKV 上的新 Region Peer 就算创建出来了,也不会发起选举,因此保证了不会有新数据写入,故在原 Region 上读取不会破坏  线性一致性 。

总结

Region Split 的基础流程比较简单,简单来说就是依赖原 Region 的 Raft 提供的可靠复制功能实现的,而与此相对的 Region Merge 由于两个 Region 属于不同的 Raft group,与 Region Split,Raft Snapshot 的相互作用,再加上网络隔离带来的影响,无疑有更大的复杂度。在之后的源码阅读文章中我们会继续讲解 Region Merge,敬请期待!

💡 文中划线部分均有跳转,请点击【阅读原文】查看原版

TiKV 源码解析系列文章

(一)序

(二)raft-rs proposal 示例情景分析

(三)Prometheus(上)

(四)Prometheus(下)

(五)fail-rs 介绍

(六)raft-rs 日志复制过程分析

(七)gRPC Server 的初始化和启动流程

(八)grpc-rs 的封装与实现

(九)Service 层处理流程解析

(十)Snapshot 的发送和接收

(十一)Storage - 事务控制层

(十二)分布式事务

(十三)MVCC 数据读取

(十四)Coprocessor 概览

(十五)表达式计算框架

(十六)TiKV Coprocessor Executor 源码解析

(十七)raftstore 概览

(十八)Raft Propose 的 Commit 和 Apply 情景分析

(十九)read index 和 local read 情景分析

TiKV 是一个开源的分布式事务 Key-Value 数据库,支持跨行 ACID 事务,同时实现了自动水平伸缩、数据强一致性、跨数据中心高可用和云原生等重要特性。作为一个基础组件,TiKV 可作为构建其它系统的基石。目前,TiKV 已用于支持分布式 HTAP 数据库—— TiDB 中,负责存储数据,并已被多个行业的领先企业应用在实际生产环境。2019 年 5 月,CNCF 的 TOC(技术监督委员会)投票决定接受 TiKV 晋级为孵化项目。

· 源码地址https://github.com/tikv/tikv

· 更多信息https://tikv.org

TiKV源码解析系列文章(二十)Region Split源码解析

TiKV源码解析系列文章(二十)Region Split源码解析

文章转载自PingCAP。点击这里阅读原文了解更多

TiKV源码解析系列文章(二十)Region Split源码解析

扫描二维码联系我们!


CNCF (Cloud Native Computing Foundation)成立于2015年12月,隶属于Linux  Foundation,是非营利性组织。

_CNCF云原生计算基金会)致力于培育和维护一个厂商中立的开源生态系统,来推广云原生技术。我们通过将最前沿的模式民主化,让这些创新为大众所用。请长按以下二维码进行关注。_

TiKV源码解析系列文章(二十)Region Split源码解析

本文分享自微信公众号 - CNCF(lf_cncf)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
4个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Easter79 Easter79
3年前
TiKV 源码解析系列文章(二十)Region Split 源码解析
在学习了之前的几篇raftrs,raftstore相关文章之后(如RaftPropose的Commit和Apply情景分析(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fpingcap.com%2Fblogcn%2Ftikvsourcecod
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
10个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这
Easter79
Easter79
Lv1
今生可爱与温柔,每一样都不能少。
文章
2.8k
粉丝
5
获赞
1.2k