TiKV Region Split 全流程分析
<p>分裂可以说是 Region 生命周期中最为重要的一步,如同细胞一般,分裂是 Region 被创造并持续增多的唯一方式。</p>
<p>本文将介绍以下内容:</p>
<ul>
<li>Region Split 是由谁触发的。</li>
<li>Region Split 是如何计算 Split Key 的。</li>
<li>Region Split 最终是如何执行的。</li>
</ul>
<p>我们先来看一个 Region Split 过程的大致流程:</p>
<ol>
<li>TiKV/PD/TiDB 触发 Region Split 事件。</li>
<li>Raftstore 处理 Region Split 事件,计算 Split Key。</li>
<li>Raftstore 执行 Split。</li>
</ol>
<h1>Region Split 的触发方式</h1>
<p>我们可以将 Region 的分裂从动机上分为两类:</p>
<ul>
<li>内部机制导致的 Region 被动分裂(例如 Region 的大小超过阈值,Load Base Split 被触发等)</li>
<li>人工手段对 Region 进行主动分裂(建表或手动 Split Region)</li>
</ul>
<h2>TiKV 触发分裂</h2>
<p>因为 Region 是 TiKV 的逻辑存储单元,Region 最基本的分裂方式也是来源于 TiKV 的控制。</p>
<h3>定期检查</h3>
<p>TiKV 默认会 10s 进行一次 Region 的分裂检查,此举由 Raft 状态机驱动,定期 Tick 进行触发。函数名称为 <a href="https://github.com/tikv/tikv/blob/1fb8980ccab9ff40c1adc206df52952dab8e8ad8/components/raftstore/src/store/fsm/peer.rs#L5005"><code>PeerFsmDelegate::on_split_region_check_tick</code></a>。</p>
<p>因为 Region Split 的行为后续会作为一条 Raft log 在副本间进行同步,所以该函数会首先检查当前 Region peer 是否为 leader,以避免进行无用的检查。</p>
<pre style="background-color:#2d2d2d;"><code class="language-Rust"><span style="color:#cc99cc;">if </span><span style="color:#d3d0c8;">!</span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.fsm.peer.</span><span style="color:#66cccc;">is_leader</span><span style="color:#d3d0c8;">() {
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">return</span><span style="color:#d3d0c8;">;
</span><span style="color:#d3d0c8;">}
</span><span style="color:#d3d0c8;">
</span><span style="color:#cc99cc;">if </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.fsm.peer.may_skip_split_check
</span><span style="color:#d3d0c8;"> && </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.fsm.peer.compaction_declined_bytes < </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.ctx.cfg.</span><span style="color:#66cccc;">region_split_check_diff</span><span style="color:#d3d0c8;">().</span><span style="color:#f99157;">0
</span><span style="color:#d3d0c8;"> && </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.fsm.peer.size_diff_hint < </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.ctx.cfg.</span><span style="color:#66cccc;">region_split_check_diff</span><span style="color:#d3d0c8;">().</span><span style="color:#f99157;">0
</span><span style="color:#d3d0c8;">{
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">return</span><span style="color:#d3d0c8;">;
</span><span style="color:#d3d0c8;">}
</span></code></pre>
<p>紧接着 Leader check 之后,就是对 Split 必要性的检查,为了避免过多的 Split check,我们设置了以下 3 个条件来进行过滤:</p>
<ul>
<li>Region peer 的 <code>may_skip_split_check flag</code> 是否为 True</li>
<li>Region peer 的 <code>compaction_declined_bytes</code> 是否小于 <code>region-split-check-diff</code> 阈值</li>
<li>Region peer 的 <code>size_diff_hint</code> 是否小于 <code>region-split-check-diff</code> 阈值</li>
</ul>
<p><code>may_skip_split_check</code> 的 flag 会在必要时被设置为 False 来确保 Split 检查会尽可能地被执行(例如 TiKV 刚刚启动时)。<code>compaction_declined_bytes</code> 和 <code>size_diff_hint</code> 均是对 Region 大小变化的增量统计(分别统计自 Compaction 数据和 Apply 数据的过程),它们在此隐含了这样一个条件:只有 Region 的大小变化超过 <code>region-split-check-diff</code> 后才需要进行分裂检查(这个配置的默认值是 <code>region-split-size</code> 的 1/16,即 96 / 16 = 6 MB)。</p>
<p>而后就是一些特殊逻辑的检查,在此不进一步展开,他们包括:</p>
<ul>
<li>当前是否有堆积未完成的 Split 任务</li>
<li>当前是否处于 Lightning/BR 的导入过程中</li>
<li>当前是否正在生成 Snapshot</li>
</ul>
<p>需要注意此阶段的检查仅仅是触发了 Region Split 的事件,具体能否分裂以及如何分裂还取决于后续的 Split 触发过程。</p>
<h3>Load Base Split</h3>
<p>TiKV 还有一个会触发 Region Split 的功能来自于 <a href="https://docs.pingcap.com/zh/tidb/dev/configure-load-base-split">Load Base Split</a>。其核心代码位于 <a href="https://github.com/tikv/tikv/blob/b0f67e6128e4596367dba7b0400065b2496c65a3/components/raftstore/src/store/worker/split_controller.rs#L560"><code>AutoSplitController::flush</code></a>。StatsMonitor 会收集读请求的统计信息,包括请求的数目,请求读取的流量以及读取的 Key Range 等。对于 QPS 或 Byte 满足 <code>qps_threshold</code> 和 <code>byte_threshold</code> 的 Region,则会在之前收集的 Key Range 基础上对 Key 进行采样,选择一个切分后左右 Region 上的请求数量最为均衡的 Key 作为切分点进行切分。</p>
<h2>PD 触发分裂</h2>
<p>PD 也可以进行分裂的触发。此举可以通过以下方式进行:</p>
<ul>
<li>调用 /regions/split 的 HTTP API 触发</li>
<li>通过 pd-ctl 创建 Operator 触发</li>
<li>通过调用 gRPC 接口 <code>SplitRegions</code>/<code>SplitAndScatterRegions</code> 来触发</li>
</ul>
<p>其中,pd-ctl 作为主要面向用户的操作,方式如下:</p>
<pre style="background-color:#2d2d2d;"><code class="language-Shell"><span style="color:#d3d0c8;">>> operator add split-region 1 --policy=approximate // 将 Region 1 对半拆分成两个 Region,基于粗略估计值
</span><span style="color:#d3d0c8;">>> operator add split-region 1 --policy=scan // 将 Region 1 对半拆分成两个 Region,基于精确扫描值
</span></code></pre>
<p>上述操作的本质都是创建一个 Split 的 Operator 并下发给对应 Region。具体的 PD 侧代码可以通过 <a href="https://github.com/tikv/pd/blob/3792f0751dd9d5c76a03f7cca6243010e0d05e5f/server/schedule/region_splitter.go#L68"><code>RegionSplitter::SplitRegions</code></a> 函数进行自上而下的研究,在此不多做表述。</p>
<p>Operator 通过 Region 心跳下发给 TiKV 后,TiKV 会根据下发的 Split 任务类型去创建对应的事件,具体代码<a href="https://github.com/tikv/tikv/blob/2ffb7cf01ae75afde9261fb9154304ec1676ad77/components/raftstore/src/store/worker/pd.rs#L1434">在此</a>。</p>
<pre style="background-color:#2d2d2d;"><code class="language-Rust"><span style="color:#cc99cc;">if</span><span style="color:#d3d0c8;"> resp.</span><span style="color:#66cccc;">has_split_region</span><span style="color:#d3d0c8;">() {
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">let mut</span><span style="color:#d3d0c8;"> split_region = resp.</span><span style="color:#66cccc;">take_split_region</span><span style="color:#d3d0c8;">();
</span><span style="color:#d3d0c8;"> info!("</span><span style="color:#99cc99;">try to split</span><span style="color:#d3d0c8;">"; "</span><span style="color:#99cc99;">region_id</span><span style="color:#d3d0c8;">" => region_id, "</span><span style="color:#99cc99;">region_epoch</span><span style="color:#d3d0c8;">" => ?epoch);
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> msg = </span><span style="color:#cc99cc;">if</span><span style="color:#d3d0c8;"> split_region.</span><span style="color:#66cccc;">get_policy</span><span style="color:#d3d0c8;">() == pdpb::CheckPolicy::Usekey {
</span><span style="color:#d3d0c8;"> CasualMessage::SplitRegion {
</span><span style="color:#d3d0c8;"> region_epoch: epoch,
</span><span style="color:#d3d0c8;"> split_keys: split_region.</span><span style="color:#66cccc;">take_keys</span><span style="color:#d3d0c8;">().</span><span style="color:#66cccc;">into</span><span style="color:#d3d0c8;">(),
</span><span style="color:#d3d0c8;"> callback: Callback::None,
</span><span style="color:#d3d0c8;"> source: "</span><span style="color:#99cc99;">pd</span><span style="color:#d3d0c8;">".</span><span style="color:#66cccc;">into</span><span style="color:#d3d0c8;">(),
</span><span style="color:#d3d0c8;"> }
</span><span style="color:#d3d0c8;"> } </span><span style="color:#cc99cc;">else </span><span style="color:#d3d0c8;">{
</span><span style="color:#d3d0c8;"> CasualMessage::HalfSplitRegion {
</span><span style="color:#d3d0c8;"> region_epoch: epoch,
</span><span style="color:#d3d0c8;"> policy: split_region.</span><span style="color:#66cccc;">get_policy</span><span style="color:#d3d0c8;">(),
</span><span style="color:#d3d0c8;"> source: "</span><span style="color:#99cc99;">pd</span><span style="color:#d3d0c8;">",
</span><span style="color:#d3d0c8;"> cb: Callback::None,
</span><span style="color:#d3d0c8;"> }
</span><span style="color:#d3d0c8;"> };
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">if let </span><span style="color:#d3d0c8;">Err(e) = router.</span><span style="color:#66cccc;">send</span><span style="color:#d3d0c8;">(region_id, PeerMsg::CasualMessage(msg)) {
</span><span style="color:#d3d0c8;"> error!("</span><span style="color:#99cc99;">send halfsplit request failed</span><span style="color:#d3d0c8;">"; "</span><span style="color:#99cc99;">region_id</span><span style="color:#d3d0c8;">" => region_id, "</span><span style="color:#99cc99;">err</span><span style="color:#d3d0c8;">" => ?e);
</span><span style="color:#d3d0c8;"> }
</span><span style="color:#d3d0c8;">}
</span></code></pre>
<p>可以看到根据不同的 Split 方式,所创建的事件也不同——若是给定了分裂点 Key 则会直接下发 <code>CasualMessage::SplitRegion</code> 事件,否则根据不同的分裂策略创建一个 <code>CasualMessage::HalfSplitRegion</code> 事件,期以对 Region 进行对半分。这里的策略主要分为 Scan 和 Approximate 两类,具体的区别会在后文中进行介绍。</p>
<h2>TiDB 触发分裂</h2>
<h3>DDL</h3>
<p>在建表或添加分区时,TiDB 会在 DDL 阶段对表的 Region 进行预切分,为每个表或分区创建单独的 Region,用于避免发生大量建表和写入造成的热点问题。此举也是通过调用 PD 的 Split 接口达成的(早期版本是 TiDB 直接下发给 TiKV,现已废弃)。具体的代码入口在 <a href="https://github.com/pingcap/tidb/blob/15b8ddf7b8e29e7aab7671c4f58358a60e8a217a/ddl/ddl_api.go#L2516"><code>ddl::preSplitAndScatter</code></a> 接口,你可以通过该方法的调用情况来看不同的 Split Table 发生在何时何处。</p>
<h3>SQL</h3>
<p>除了建表时自动为每个表切分出的一个 Region,如果在单表内部存在写入热点,我们也可以通过 SQL 来手动 Split Region。这个原理其实和上述的 DDL 过程相同,均是调用统一的 <a href="https://github.com/pingcap/tidb/blob/d282940b4202a8fe82b7e3298ce62bcdb7d355d1/kv/kv.go#L493"><code>SplitRegions</code></a> 接口来进行 Split 任务的下发。</p>
<p>具体的 SQL 语法可以参考官方文档:<a href="https://docs.pingcap.com/zh/tidb/dev/sql-statement-split-region#split-region-%E4%BD%BF%E7%94%A8%E6%96%87%E6%A1%A3">Split Region 使用文档</a>。</p>
<h2>其他</h2>
<p>上面只阐述了 3 大组件的常见 Region Split 触发流程,事实上还有很多其他机制会触发 Region Split,例如 Lightning/BR 这样的工具导入数据前也会对 Region 进行预切分和打散,以求导入后数据的均衡。tikv-ctl 也可以触发 Region 的 Split。</p>
<h1>Region Split Key 的计算方式</h1>
<p>以上述方式触发 Region Split 事件后,具体的 Split 的 Key 可以以多种方式和维度被计算出来。例如通过精确的 Scan 扫描来确定 Region 大小上的中点进行分裂,或通过指定的 Key 直接进行分裂等,不同的方式往往用于不同的场景,具体原理如下。</p>
<h2>Coprocessor</h2>
<p>此 Coprocessor 非 TiKV 中用于下推 SQL 执行的 Coprocessor,而是 raftstore 代码中的一个概念。其主要作用相当于外挂在 TiKV 的 Raft 层上的一个协处理工具集合,用于观测和处理与 Raft 相关的周边事件。<a href="https://github.com/tikv/tikv/blob/2ffb7cf01ae75afde9261fb9154304ec1676ad77/components/raftstore/src/coprocessor/mod.rs#L120"><code>SplitChecker</code></a> 就是其中之一,用于接受,处理和下发与 Region Split 有关的事件。</p>
<pre style="background-color:#2d2d2d;"><code class="language-Rust"><span style="color:#747369;">/// SplitChecker is invoked during a split check scan, and decides to use
</span><span style="color:#747369;">/// which keys to split a region.
</span><span style="color:#cc99cc;">pub trait </span><span style="color:#d3d0c8;">SplitChecker<E> {
</span><span style="color:#d3d0c8;"> </span><span style="color:#747369;">/// Hook to call for every kv scanned during split.
</span><span style="color:#d3d0c8;"> </span><span style="color:#747369;">///
</span><span style="color:#d3d0c8;"> </span><span style="color:#747369;">/// Return true to abort scan early.
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">fn </span><span style="color:#6699cc;">on_kv</span><span style="color:#d3d0c8;">(&</span><span style="color:#cc99cc;">mut </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">, _: &</span><span style="color:#cc99cc;">mut </span><span style="color:#d3d0c8;">ObserverContext<'_>, _: &KeyEntry) -> </span><span style="color:#cc99cc;">bool </span><span style="color:#d3d0c8;">{
</span><span style="color:#d3d0c8;"> </span><span style="color:#f99157;">false
</span><span style="color:#d3d0c8;"> }
</span><span style="color:#d3d0c8;">
</span><span style="color:#d3d0c8;"> </span><span style="color:#747369;">/// Get the desired split keys.
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">fn </span><span style="color:#6699cc;">split_keys</span><span style="color:#d3d0c8;">(&</span><span style="color:#cc99cc;">mut </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">) -> Vec<Vec<</span><span style="color:#cc99cc;">u8</span><span style="color:#d3d0c8;">>>;
</span><span style="color:#d3d0c8;">
</span><span style="color:#d3d0c8;"> </span><span style="color:#747369;">/// Get approximate split keys without scan.
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">fn </span><span style="color:#6699cc;">approximate_split_keys</span><span style="color:#d3d0c8;">(&</span><span style="color:#cc99cc;">mut </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">, _: &Region, _: &E) -> Result<Vec<Vec<</span><span style="color:#cc99cc;">u8</span><span style="color:#d3d0c8;">>>> {
</span><span style="color:#d3d0c8;"> Ok(vec![])
</span><span style="color:#d3d0c8;"> }
</span><span style="color:#d3d0c8;">
</span><span style="color:#d3d0c8;"> </span><span style="color:#747369;">/// Get split policy.
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">fn </span><span style="color:#6699cc;">policy</span><span style="color:#d3d0c8;">(&</span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">) -> CheckPolicy;
</span><span style="color:#d3d0c8;">}
</span></code></pre>
<p>一个 <code>SplitChecker</code> 包含 4 个方法,分别是:</p>
<ul>
<li><code>on_kv</code>,在使用 Scan 方式时,用于在 Iterator 扫描 Key 的过程中接受 Key,并在内部维护对应的状态来实现不同的分裂方式。</li>
<li><code>split_keys</code>,在完成扫描后通过此方法来拿到最终的 Split Key 结果。</li>
<li><code>approximate_split_keys</code>,在使用 Approximate 方式时,不进行 Scan 而直接拿到 Split Key 结果</li>
<li><code>policy</code>,返回当前的 Split 检查策略,有 Scan/Approximate 两种方式。</li>
</ul>
<p>对这 4 个方法不同的实现也就决定了不同的分裂方式,下面我们分别介绍 TiKV 内部支持的所有不同的分裂方式。</p>
<h2>Half</h2>
<p><a href="https://github.com/tikv/tikv/blob/2ffb7cf01ae75afde9261fb9154304ec1676ad77/components/raftstore/src/coprocessor/split_check/half.rs"><code>HalfCheckObserver</code></a> 实现了对 Region 的 Sizie 对半切策略,在 Scan 模式下,为了找到一个 Region 内 Size 维度上的中点,把所有的 Key 都记录下来显然是不合理的,这样可能会占用大量的内存。取而代之的方式是根据配置计算出一个最小的 Size 单位 n MB,计算函数名为 <a href="https://github.com/tikv/tikv/blob/2ffb7cf01ae75afde9261fb9154304ec1676ad77/components/raftstore/src/coprocessor/split_check/half.rs#L99"><code>half_split_bucket_size</code></a> 通过将 <code>region_max_size</code> 除以 <code>BUCKET_NUMBER_LIMIT</code>(常量,值为 1024),计算出一个 Bucket 大小,最小为 1 MB,最大为 512 MB。</p>
<pre style="background-color:#2d2d2d;"><code class="language-Rust"><span style="color:#cc99cc;">fn </span><span style="color:#6699cc;">half_split_bucket_size</span><span style="color:#d3d0c8;">(</span><span style="color:#f2777a;">region_max_size</span><span style="color:#d3d0c8;">: </span><span style="color:#cc99cc;">u64</span><span style="color:#d3d0c8;">) -> </span><span style="color:#cc99cc;">u64 </span><span style="color:#d3d0c8;">{
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">let mut</span><span style="color:#d3d0c8;"> half_split_bucket_size = region_max_size / </span><span style="color:#f99157;">BUCKET_NUMBER_LIMIT </span><span style="color:#d3d0c8;">as </span><span style="color:#cc99cc;">u64</span><span style="color:#d3d0c8;">;
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> bucket_size_limit = ReadableSize::mb(</span><span style="color:#f99157;">BUCKET_SIZE_LIMIT_MB</span><span style="color:#d3d0c8;">).</span><span style="color:#f99157;">0</span><span style="color:#d3d0c8;">;
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">if</span><span style="color:#d3d0c8;"> half_split_bucket_size == </span><span style="color:#f99157;">0 </span><span style="color:#d3d0c8;">{
</span><span style="color:#d3d0c8;"> half_split_bucket_size = </span><span style="color:#f99157;">1</span><span style="color:#d3d0c8;">;
</span><span style="color:#d3d0c8;"> } </span><span style="color:#cc99cc;">else if</span><span style="color:#d3d0c8;"> half_split_bucket_size > bucket_size_limit {
</span><span style="color:#d3d0c8;"> half_split_bucket_size = bucket_size_limit;
</span><span style="color:#d3d0c8;"> }
</span><span style="color:#d3d0c8;"> half_split_bucket_size
</span><span style="color:#d3d0c8;">}
</span></code></pre>
<p>在后续的扫描过程中,仅在每扫描过 n MB 大小后才记录下当前的 Key,这样可以通过牺牲一定的精度换来了较少的内存占用。</p>
<pre style="background-color:#2d2d2d;"><code class="language-Rust"><span style="color:#cc99cc;">fn </span><span style="color:#6699cc;">on_kv</span><span style="color:#d3d0c8;">(&</span><span style="color:#cc99cc;">mut </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">, _: &</span><span style="color:#cc99cc;">mut </span><span style="color:#d3d0c8;">ObserverContext<'_>, </span><span style="color:#f2777a;">entry</span><span style="color:#d3d0c8;">: &KeyEntry) -> </span><span style="color:#cc99cc;">bool </span><span style="color:#d3d0c8;">{
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">if </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.buckets.</span><span style="color:#66cccc;">is_empty</span><span style="color:#d3d0c8;">() || </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.cur_bucket_size >= </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.each_bucket_size {
</span><span style="color:#d3d0c8;"> </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.buckets.</span><span style="color:#66cccc;">push</span><span style="color:#d3d0c8;">(entry.</span><span style="color:#66cccc;">key</span><span style="color:#d3d0c8;">().</span><span style="color:#66cccc;">to_vec</span><span style="color:#d3d0c8;">());
</span><span style="color:#d3d0c8;"> </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.cur_bucket_size = </span><span style="color:#f99157;">0</span><span style="color:#d3d0c8;">;
</span><span style="color:#d3d0c8;"> }
</span><span style="color:#d3d0c8;"> </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.cur_bucket_size += entry.</span><span style="color:#66cccc;">entry_size</span><span style="color:#d3d0c8;">() as </span><span style="color:#cc99cc;">u64</span><span style="color:#d3d0c8;">;
</span><span style="color:#d3d0c8;"> </span><span style="color:#f99157;">false
</span><span style="color:#d3d0c8;">}
</span><span style="color:#d3d0c8;">
</span><span style="color:#cc99cc;">fn </span><span style="color:#6699cc;">split_keys</span><span style="color:#d3d0c8;">(&</span><span style="color:#cc99cc;">mut </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">) -> Vec<Vec<</span><span style="color:#cc99cc;">u8</span><span style="color:#d3d0c8;">>> {
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> mid = </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.buckets.</span><span style="color:#66cccc;">len</span><span style="color:#d3d0c8;">() / </span><span style="color:#f99157;">2</span><span style="color:#d3d0c8;">;
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">if</span><span style="color:#d3d0c8;"> mid == </span><span style="color:#f99157;">0 </span><span style="color:#d3d0c8;">{
</span><span style="color:#d3d0c8;"> vec![]
</span><span style="color:#d3d0c8;"> } </span><span style="color:#cc99cc;">else </span><span style="color:#d3d0c8;">{
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> data_key = </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.buckets.</span><span style="color:#66cccc;">swap_remove</span><span style="color:#d3d0c8;">(mid);
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> key = keys::origin_key(&data_key).</span><span style="color:#66cccc;">to_vec</span><span style="color:#d3d0c8;">();
</span><span style="color:#d3d0c8;"> vec![key]
</span><span style="color:#d3d0c8;"> }
</span><span style="color:#d3d0c8;">}
</span></code></pre>
<p>在后续计算中点 Key 的过程中,也只需要取我们收集到的 Key 的中间元素,即可获得近似的 Region Size 中点,用于后续的切分。</p>
<p>对于具体 <code>approximate_split_keys</code> 的实现取决于不同的 KV Engine,以默认的 RocksDB 为例,为了避免对整个区间上全 Key-Value 的扫描,我们使用了 RocksDB 的 <a href="https://tikv.github.io/deep-dive-tikv/key-value-engine/rocksdb.html#tableproperties">TableProperties</a> 特性,来在 RocksDB 构建每个 SST 文件的时候就提前收集一些 Key 相关的信息,从而可以在此时避免进行 I/O 操作即可获得近似的 Key Range 上的 Key 信息,再辅之以采样等手段,相较于 Scan 策略会更不精准,但省去了不少资源。对应的代码在 <a href="https://github.com/tikv/tikv/blob/aaf47d0cbb62518a760f3a3deb43acc9e319595e/components/engine_rocks/src/range_properties.rs#L167"><code>RocksEngine::get_range_approximate_split_keys_cf</code></a> 方法中。</p>
<h2>Size</h2>
<p><a href="https://github.com/tikv/tikv/blob/3f698eb1b7e0ed94547fa8ecd1ef68e061ba5d39/components/raftstore/src/coprocessor/split_check/size.rs"><code>SizeCheckObserver</code></a> 实现了根据 Region Size 切分 Region 的策略。其逻辑相对简单,在默认配置下,会对 Region 的 KV 进行 Scan 遍历,每扫描过 96 MB 的数据便会记录下当前的 Key,一次最多记录 10 个。</p>
<pre style="background-color:#2d2d2d;"><code class="language-Rust"><span style="color:#cc99cc;">fn </span><span style="color:#6699cc;">on_kv</span><span style="color:#d3d0c8;">(&</span><span style="color:#cc99cc;">mut </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">, _: &</span><span style="color:#cc99cc;">mut </span><span style="color:#d3d0c8;">ObserverContext<'_>, </span><span style="color:#f2777a;">entry</span><span style="color:#d3d0c8;">: &KeyEntry) -> </span><span style="color:#cc99cc;">bool </span><span style="color:#d3d0c8;">{
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> size = entry.</span><span style="color:#66cccc;">entry_size</span><span style="color:#d3d0c8;">() as </span><span style="color:#cc99cc;">u64</span><span style="color:#d3d0c8;">;
</span><span style="color:#d3d0c8;"> </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.current_size += size;
</span><span style="color:#d3d0c8;">
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">let mut</span><span style="color:#d3d0c8;"> over_limit = </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.split_keys.</span><span style="color:#66cccc;">len</span><span style="color:#d3d0c8;">() as </span><span style="color:#cc99cc;">u64 </span><span style="color:#d3d0c8;">>= </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.batch_split_limit;
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">if </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.current_size > </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.split_size && !over_limit {
</span><span style="color:#d3d0c8;"> </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.split_keys.</span><span style="color:#66cccc;">push</span><span style="color:#d3d0c8;">(keys::origin_key(entry.</span><span style="color:#66cccc;">key</span><span style="color:#d3d0c8;">()).</span><span style="color:#66cccc;">to_vec</span><span style="color:#d3d0c8;">());
</span><span style="color:#d3d0c8;"> </span><span style="color:#747369;">// if for previous on_kv() self.current_size == self.split_size,
</span><span style="color:#d3d0c8;"> </span><span style="color:#747369;">// the split key would be pushed this time, but the entry size for this time should not be ignored.
</span><span style="color:#d3d0c8;"> </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.current_size = </span><span style="color:#cc99cc;">if </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.current_size - size == </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.split_size {
</span><span style="color:#d3d0c8;"> size
</span><span style="color:#d3d0c8;"> } </span><span style="color:#cc99cc;">else </span><span style="color:#d3d0c8;">{
</span><span style="color:#d3d0c8;"> </span><span style="color:#f99157;">0
</span><span style="color:#d3d0c8;"> };
</span><span style="color:#d3d0c8;"> over_limit = </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.split_keys.</span><span style="color:#66cccc;">len</span><span style="color:#d3d0c8;">() as </span><span style="color:#cc99cc;">u64 </span><span style="color:#d3d0c8;">>= </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.batch_split_limit;
</span><span style="color:#d3d0c8;"> }
</span><span style="color:#d3d0c8;">
</span><span style="color:#d3d0c8;"> </span><span style="color:#747369;">// For a large region, scan over the range maybe cost too much time,
</span><span style="color:#d3d0c8;"> </span><span style="color:#747369;">// so limit the number of produced split_key for one batch.
</span><span style="color:#d3d0c8;"> </span><span style="color:#747369;">// Also need to scan over self.max_size for last part.
</span><span style="color:#d3d0c8;"> over_limit && </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.current_size + </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.split_size >= </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.max_size
</span><span style="color:#d3d0c8;">}
</span><span style="color:#d3d0c8;">
</span><span style="color:#cc99cc;">fn </span><span style="color:#6699cc;">split_keys</span><span style="color:#d3d0c8;">(&</span><span style="color:#cc99cc;">mut </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">) -> Vec<Vec<</span><span style="color:#cc99cc;">u8</span><span style="color:#d3d0c8;">>> {
</span><span style="color:#d3d0c8;"> </span><span style="color:#747369;">// make sure not to split when less than max_size for last part
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">if </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.current_size + </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.split_size < </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.max_size {
</span><span style="color:#d3d0c8;"> </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.split_keys.</span><span style="color:#66cccc;">pop</span><span style="color:#d3d0c8;">();
</span><span style="color:#d3d0c8;"> }
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">if </span><span style="color:#d3d0c8;">!</span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.split_keys.</span><span style="color:#66cccc;">is_empty</span><span style="color:#d3d0c8;">() {
</span><span style="color:#d3d0c8;"> std::mem::take(&</span><span style="color:#cc99cc;">mut </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.split_keys)
</span><span style="color:#d3d0c8;"> } </span><span style="color:#cc99cc;">else </span><span style="color:#d3d0c8;">{
</span><span style="color:#d3d0c8;"> vec![]
</span><span style="color:#d3d0c8;"> }
</span><span style="color:#d3d0c8;">}
</span></code></pre>
<p><code>approximate_split_keys</code> 的实现和 Half 类似,在此不表,依然是基于 RocksDB 的 TableProperties 功能。</p>
<h2>Keys</h2>
<p><a href="https://github.com/tikv/tikv/blob/3f698eb1b7e0ed94547fa8ecd1ef68e061ba5d39/components/raftstore/src/coprocessor/split_check/keys.rs"><code>KeysCheckObserver</code></a> 实现了根据 Region Key 数量切分 Region 的策略,其原理和 <code>SizeCheckObserver</code> 相同,只不过把计算方式改成了 Key 数量的统计,在此不过多展开,</p>
<h2>Tabel</h2>
<p><a href="https://github.com/tikv/tikv/blob/3f698eb1b7e0ed94547fa8ecd1ef68e061ba5d39/components/raftstore/src/coprocessor/split_check/table.rs"><code>TableCheckObserver</code></a> 实现了根据 Region 范围内 Key 所属的 Table 进行切分的策略。这个 Checker 的实现比较特殊,它在 TiKV 内部引入了 SQL 层的概念。原理也比较简单,在 Scan 时去 Decode 每个 Key,检查其所属的表 ID 和之前 Key 是否相同,若不同则加入 Split Key 进行分裂。</p>
<pre style="background-color:#2d2d2d;"><code class="language-Rust"><span style="color:#747369;">/// Feed keys in order to find the split key.
</span><span style="color:#747369;">/// If `current_data_key` does not belong to `status.first_encoded_table_prefix`.
</span><span style="color:#747369;">/// it returns the encoded table prefix of `current_data_key`.
</span><span style="color:#cc99cc;">fn </span><span style="color:#6699cc;">on_kv</span><span style="color:#d3d0c8;">(&</span><span style="color:#cc99cc;">mut </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">, _: &</span><span style="color:#cc99cc;">mut </span><span style="color:#d3d0c8;">ObserverContext<'_>, </span><span style="color:#f2777a;">entry</span><span style="color:#d3d0c8;">: &KeyEntry) -> </span><span style="color:#cc99cc;">bool </span><span style="color:#d3d0c8;">{
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">if </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.split_key.</span><span style="color:#66cccc;">is_some</span><span style="color:#d3d0c8;">() {
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">return </span><span style="color:#f99157;">true</span><span style="color:#d3d0c8;">;
</span><span style="color:#d3d0c8;"> }
</span><span style="color:#d3d0c8;">
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> current_encoded_key = keys::origin_key(entry.</span><span style="color:#66cccc;">key</span><span style="color:#d3d0c8;">());
</span><span style="color:#d3d0c8;">
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> split_key = </span><span style="color:#cc99cc;">if </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.first_encoded_table_prefix.</span><span style="color:#66cccc;">is_some</span><span style="color:#d3d0c8;">() {
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">if </span><span style="color:#d3d0c8;">!</span><span style="color:#66cccc;">is_same_table</span><span style="color:#d3d0c8;">(
</span><span style="color:#d3d0c8;"> </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.first_encoded_table_prefix.</span><span style="color:#66cccc;">as_ref</span><span style="color:#d3d0c8;">().</span><span style="color:#66cccc;">unwrap</span><span style="color:#d3d0c8;">(),
</span><span style="color:#d3d0c8;"> current_encoded_key,
</span><span style="color:#d3d0c8;"> ) {
</span><span style="color:#d3d0c8;"> </span><span style="color:#747369;">// Different tables.
</span><span style="color:#d3d0c8;"> Some(current_encoded_key)
</span><span style="color:#d3d0c8;"> } </span><span style="color:#cc99cc;">else </span><span style="color:#d3d0c8;">{
</span><span style="color:#d3d0c8;"> None
</span><span style="color:#d3d0c8;"> }
</span><span style="color:#d3d0c8;"> } </span><span style="color:#cc99cc;">else if </span><span style="color:#66cccc;">is_table_key</span><span style="color:#d3d0c8;">(current_encoded_key) {
</span><span style="color:#d3d0c8;"> </span><span style="color:#747369;">// Now we meet the very first table key of this region.
</span><span style="color:#d3d0c8;"> Some(current_encoded_key)
</span><span style="color:#d3d0c8;"> } </span><span style="color:#cc99cc;">else </span><span style="color:#d3d0c8;">{
</span><span style="color:#d3d0c8;"> None
</span><span style="color:#d3d0c8;"> };
</span><span style="color:#d3d0c8;"> </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.split_key = split_key.</span><span style="color:#66cccc;">and_then</span><span style="color:#d3d0c8;">(to_encoded_table_prefix);
</span><span style="color:#d3d0c8;"> </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.split_key.</span><span style="color:#66cccc;">is_some</span><span style="color:#d3d0c8;">()
</span><span style="color:#d3d0c8;">}
</span></code></pre>
<p>由于工作原理决定了它只能基于 Scan 策略进行工作,所以没有提供 <code>approximate_split_keys</code> 方法的实现。</p>
<h2>优先级</h2>
<p>上面一共介绍了 TiKV 支持的 4 种 Split 方式,那么具体工作过程中,实际到底哪一个方式会被触发呢?答案是都有可能。</p>
<p>每个 SplitChecker 都会被加入到一个 SplitCheckerHost 中,并被赋予不同的优先级,每次 Split 都会依次“询问”每个 SplitChecker 的“意见”,如果高优先级的 Checker 不能给出 Split Key 那么就依次向更低优先级的 Checker 轮训,直到得到一个 Split Key 或确认无法 Split。优先级在将 SplitChecker 注册到 Coprocessor 时就被定义好了,代码位于 <a href="https://github.com/tikv/tikv/blob/deb614531835a3820e55d85aad7ced22b0d9b70e/components/raftstore/src/coprocessor/dispatcher.rs#L324"><code>CoprocessorHost::new</code></a>。</p>
<pre style="background-color:#2d2d2d;"><code class="language-Rust"><span style="color:#cc99cc;">pub fn </span><span style="color:#6699cc;">new</span><span style="color:#d3d0c8;"><C: CasualRouter<E> + Clone + Send + </span><span style="color:#cc99cc;">'static</span><span style="color:#d3d0c8;">>(
</span><span style="color:#d3d0c8;"> </span><span style="color:#f2777a;">ch</span><span style="color:#d3d0c8;">: C,
</span><span style="color:#d3d0c8;"> </span><span style="color:#f2777a;">cfg</span><span style="color:#d3d0c8;">: Config,
</span><span style="color:#d3d0c8;">) -> CoprocessorHost<E> {
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">let mut</span><span style="color:#d3d0c8;"> registry = Registry::default();
</span><span style="color:#d3d0c8;"> registry.</span><span style="color:#66cccc;">register_split_check_observer</span><span style="color:#d3d0c8;">(
</span><span style="color:#d3d0c8;"> </span><span style="color:#f99157;">200</span><span style="color:#d3d0c8;">,
</span><span style="color:#d3d0c8;"> BoxSplitCheckObserver::new(SizeCheckObserver::new(ch.</span><span style="color:#66cccc;">clone</span><span style="color:#d3d0c8;">())),
</span><span style="color:#d3d0c8;"> );
</span><span style="color:#d3d0c8;"> registry.</span><span style="color:#66cccc;">register_split_check_observer</span><span style="color:#d3d0c8;">(
</span><span style="color:#d3d0c8;"> </span><span style="color:#f99157;">200</span><span style="color:#d3d0c8;">,
</span><span style="color:#d3d0c8;"> BoxSplitCheckObserver::new(KeysCheckObserver::new(ch)),
</span><span style="color:#d3d0c8;"> );
</span><span style="color:#d3d0c8;"> registry.</span><span style="color:#66cccc;">register_split_check_observer</span><span style="color:#d3d0c8;">(</span><span style="color:#f99157;">100</span><span style="color:#d3d0c8;">, BoxSplitCheckObserver::new(HalfCheckObserver));
</span><span style="color:#d3d0c8;"> registry.</span><span style="color:#66cccc;">register_split_check_observer</span><span style="color:#d3d0c8;">(
</span><span style="color:#d3d0c8;"> </span><span style="color:#f99157;">400</span><span style="color:#d3d0c8;">,
</span><span style="color:#d3d0c8;"> BoxSplitCheckObserver::new(TableCheckObserver::default()),
</span><span style="color:#d3d0c8;"> );
</span><span style="color:#d3d0c8;"> CoprocessorHost { registry, cfg }
</span><span style="color:#d3d0c8;">}
</span></code></pre>
<p>可以看到 <code>HalfCheckObserver</code> 有最高优先级,其次是 <code>SizeCheckObserver</code> 和 <code>KeysCheckObserver</code>,<code>TableCheckObserver</code> 最低。但是我们所见到的大多数 Region 分裂都是基于 Size 的,Half 分裂尽管有最高优先级,为什么不会被频繁触发呢?答案是我们每次基于注册在 Coprocessor 的 Split Checker 创建 <code>SplitCheckerHost</code> 时(代码入口在 <a href="https://github.com/tikv/tikv/blob/deb614531835a3820e55d85aad7ced22b0d9b70e/components/raftstore/src/coprocessor/dispatcher.rs#L434"><code>CoprocessorHost::new_split_checker_host</code></a>),并不会将所有的 Checker 都导入,而是根据不同的配置以及场景进行有选择的添加。例如只有 <code>auto_split</code> 选项设置为关闭时,<code>HalfCheckObserver</code> 才会被添加到 Host 中,这个选项在 TiKV 定时检查触发 Split 时会开启,所以在对应场景下 <code>HalfCheckObserver</code> 不会起作用。</p>
<pre style="background-color:#2d2d2d;"><code class="language-Rust"><span style="color:#d3d0c8;">#[</span><span style="color:#f2777a;">derive</span><span style="color:#d3d0c8;">(Clone)]
</span><span style="color:#cc99cc;">pub struct </span><span style="color:#d3d0c8;">HalfCheckObserver;
</span><span style="color:#d3d0c8;">
</span><span style="color:#cc99cc;">impl </span><span style="color:#d3d0c8;">Coprocessor </span><span style="color:#cc99cc;">for </span><span style="color:#d3d0c8;">HalfCheckObserver {}
</span><span style="color:#d3d0c8;">
</span><span style="color:#cc99cc;">impl</span><span style="color:#d3d0c8;"><E> SplitCheckObserver<E> </span><span style="color:#cc99cc;">for </span><span style="color:#d3d0c8;">HalfCheckObserver
</span><span style="color:#cc99cc;">where
</span><span style="color:#d3d0c8;"> E: KvEngine,
</span><span style="color:#d3d0c8;">{
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">fn </span><span style="color:#6699cc;">add_checker</span><span style="color:#d3d0c8;">(
</span><span style="color:#d3d0c8;"> &</span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">,
</span><span style="color:#d3d0c8;"> _: &</span><span style="color:#cc99cc;">mut </span><span style="color:#d3d0c8;">ObserverContext<'_>,
</span><span style="color:#d3d0c8;"> </span><span style="color:#f2777a;">host</span><span style="color:#d3d0c8;">: &</span><span style="color:#cc99cc;">mut </span><span style="color:#d3d0c8;">Host<'_, E>,
</span><span style="color:#d3d0c8;"> _: &E,
</span><span style="color:#d3d0c8;"> </span><span style="color:#f2777a;">policy</span><span style="color:#d3d0c8;">: CheckPolicy,
</span><span style="color:#d3d0c8;"> ) {
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">if</span><span style="color:#d3d0c8;"> host.</span><span style="color:#66cccc;">auto_split</span><span style="color:#d3d0c8;">() {
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">return</span><span style="color:#d3d0c8;">;
</span><span style="color:#d3d0c8;"> }
</span><span style="color:#d3d0c8;"> host.</span><span style="color:#66cccc;">add_checker</span><span style="color:#d3d0c8;">(Box::new(Checker::new(
</span><span style="color:#d3d0c8;"> </span><span style="color:#66cccc;">half_split_bucket_size</span><span style="color:#d3d0c8;">(host.cfg.</span><span style="color:#66cccc;">region_max_size</span><span style="color:#d3d0c8;">().</span><span style="color:#f99157;">0</span><span style="color:#d3d0c8;">),
</span><span style="color:#d3d0c8;"> policy,
</span><span style="color:#d3d0c8;"> )))
</span><span style="color:#d3d0c8;"> }
</span><span style="color:#d3d0c8;">}
</span></code></pre>
<p>再例如只有当 <code>split_region_on_table</code> 配置开启时,<code>TableCheckObserver</code> 才会被添加到 Host 中,该配置默认关闭。</p>
<pre style="background-color:#2d2d2d;"><code class="language-Rust"><span style="color:#d3d0c8;">#[</span><span style="color:#f2777a;">derive</span><span style="color:#d3d0c8;">(Default, Clone)]
</span><span style="color:#cc99cc;">pub struct </span><span style="color:#d3d0c8;">TableCheckObserver;
</span><span style="color:#d3d0c8;">
</span><span style="color:#cc99cc;">impl </span><span style="color:#d3d0c8;">Coprocessor </span><span style="color:#cc99cc;">for </span><span style="color:#d3d0c8;">TableCheckObserver {}
</span><span style="color:#d3d0c8;">
</span><span style="color:#cc99cc;">impl</span><span style="color:#d3d0c8;"><E> SplitCheckObserver<E> </span><span style="color:#cc99cc;">for </span><span style="color:#d3d0c8;">TableCheckObserver
</span><span style="color:#cc99cc;">where
</span><span style="color:#d3d0c8;"> E: KvEngine,
</span><span style="color:#d3d0c8;">{
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">fn </span><span style="color:#6699cc;">add_checker</span><span style="color:#d3d0c8;">(
</span><span style="color:#d3d0c8;"> &</span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">,
</span><span style="color:#d3d0c8;"> </span><span style="color:#f2777a;">ctx</span><span style="color:#d3d0c8;">: &</span><span style="color:#cc99cc;">mut </span><span style="color:#d3d0c8;">ObserverContext<'_>,
</span><span style="color:#d3d0c8;"> </span><span style="color:#f2777a;">host</span><span style="color:#d3d0c8;">: &</span><span style="color:#cc99cc;">mut </span><span style="color:#d3d0c8;">Host<'_, E>,
</span><span style="color:#d3d0c8;"> </span><span style="color:#f2777a;">engine</span><span style="color:#d3d0c8;">: &E,
</span><span style="color:#d3d0c8;"> </span><span style="color:#f2777a;">policy</span><span style="color:#d3d0c8;">: CheckPolicy,
</span><span style="color:#d3d0c8;"> ) {
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">if </span><span style="color:#d3d0c8;">!host.cfg.split_region_on_table {
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">return</span><span style="color:#d3d0c8;">;
</span><span style="color:#d3d0c8;"> }
</span><span style="color:#d3d0c8;"> ...
</span><span style="color:#d3d0c8;">}
</span></code></pre>
<p>所以说在大多数情况下,只有 <code>KeysCheckObserver</code> 和 <code>SizeCheckObserver</code> 主导 Region 的分裂方式。</p>
<h1>Region Split 的执行过程</h1>
<p>通过 Raftstore 的 Coprocessor 确定好 Region 的 Split Key 后,最后就来到了 Split 的执行阶段。Region 的 Split 任务会被下发到具体的 Region,继而触发 <a href="https://github.com/tikv/tikv/blob/1fb8980ccab9ff40c1adc206df52952dab8e8ad8/components/raftstore/src/store/fsm/peer.rs#L5075"><code>PeerFsmDelegate::on_prepare_split_region</code></a> 函数,正式开启 Region 的 Split 执行。</p>
<h2>Pre-check</h2>
<p>首先 TiKV 会再次确认当前 Region 为 leader,并检查 Epoch 等属性是否发生了变化,Epoch 内的 Version 属性只有在完成 Split 或 Merge 的情况下才会增加,因为 Version 一定是严格单调递增的,所以 PD 使用了这个规则去判断范围重叠的不同 Region 的新旧。在检查通过后,便向 PD 发送 <a href="https://github.com/tikv/tikv/blob/2ffb7cf01ae75afde9261fb9154304ec1676ad77/components/raftstore/src/store/worker/pd.rs#L970">AskBatchSplit</a> 请求为即将分裂出来的新 Region 获取 Region ID,并触发 Raft 开始进行 Split log 的 Proposal。</p>
<pre style="background-color:#2d2d2d;"><code class="language-Rust"><span style="color:#d3d0c8;">info!(
</span><span style="color:#d3d0c8;"> "</span><span style="color:#99cc99;">try to batch split region</span><span style="color:#d3d0c8;">";
</span><span style="color:#d3d0c8;"> "</span><span style="color:#99cc99;">region_id</span><span style="color:#d3d0c8;">" => region.</span><span style="color:#66cccc;">get_id</span><span style="color:#d3d0c8;">(),
</span><span style="color:#d3d0c8;"> "</span><span style="color:#99cc99;">new_region_ids</span><span style="color:#d3d0c8;">" => ?resp.</span><span style="color:#66cccc;">get_ids</span><span style="color:#d3d0c8;">(),
</span><span style="color:#d3d0c8;"> "</span><span style="color:#99cc99;">region</span><span style="color:#d3d0c8;">" => ?region,
</span><span style="color:#d3d0c8;"> "</span><span style="color:#99cc99;">task</span><span style="color:#d3d0c8;">" => task,
</span><span style="color:#d3d0c8;">);
</span><span style="color:#d3d0c8;">
</span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> req = </span><span style="color:#66cccc;">new_batch_split_region_request</span><span style="color:#d3d0c8;">(
</span><span style="color:#d3d0c8;"> split_keys,
</span><span style="color:#d3d0c8;"> resp.</span><span style="color:#66cccc;">take_ids</span><span style="color:#d3d0c8;">().</span><span style="color:#66cccc;">into</span><span style="color:#d3d0c8;">(),
</span><span style="color:#d3d0c8;"> right_derive,
</span><span style="color:#d3d0c8;">);
</span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> region_id = region.</span><span style="color:#66cccc;">get_id</span><span style="color:#d3d0c8;">();
</span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> epoch = region.</span><span style="color:#66cccc;">take_region_epoch</span><span style="color:#d3d0c8;">();
</span><span style="color:#66cccc;">send_admin_request</span><span style="color:#d3d0c8;">(
</span><span style="color:#d3d0c8;"> &router,
</span><span style="color:#d3d0c8;"> region_id,
</span><span style="color:#d3d0c8;"> epoch,
</span><span style="color:#d3d0c8;"> peer,
</span><span style="color:#d3d0c8;"> req,
</span><span style="color:#d3d0c8;"> callback,
</span><span style="color:#d3d0c8;"> Default::default(),
</span><span style="color:#d3d0c8;">);
</span></code></pre>
<h2>Raft Proposal & Apply</h2>
<p>通过 Raft log 将 Split 同步到各个 Peer 之上完成 Commit 之后,<a href="https://github.com/tikv/tikv/blob/b0f67e6128e4596367dba7b0400065b2496c65a3/components/raftstore/src/store/fsm/apply.rs#L2191"><code>ApplyDelegate::exec_batch_split</code></a> 便开始执行 Region 的分裂。创建新 Region,更改 Region 边界,并将 Region 的新信息写入落盘。</p>
<pre style="background-color:#2d2d2d;"><code class="language-Rust"><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">for</span><span style="color:#d3d0c8;"> new_region in &regions {
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">if</span><span style="color:#d3d0c8;"> new_region.</span><span style="color:#66cccc;">get_id</span><span style="color:#d3d0c8;">() == derived.</span><span style="color:#66cccc;">get_id</span><span style="color:#d3d0c8;">() {
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">continue</span><span style="color:#d3d0c8;">;
</span><span style="color:#d3d0c8;"> }
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> new_split_peer = new_split_regions.</span><span style="color:#66cccc;">get</span><span style="color:#d3d0c8;">(&new_region.</span><span style="color:#66cccc;">get_id</span><span style="color:#d3d0c8;">()).</span><span style="color:#66cccc;">unwrap</span><span style="color:#d3d0c8;">();
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">if let </span><span style="color:#d3d0c8;">Some(</span><span style="color:#cc99cc;">ref</span><span style="color:#d3d0c8;"> r) = new_split_peer.result {
</span><span style="color:#d3d0c8;"> warn!(
</span><span style="color:#d3d0c8;"> "</span><span style="color:#99cc99;">new region from splitting already exists</span><span style="color:#d3d0c8;">";
</span><span style="color:#d3d0c8;"> "</span><span style="color:#99cc99;">new_region_id</span><span style="color:#d3d0c8;">" => new_region.</span><span style="color:#66cccc;">get_id</span><span style="color:#d3d0c8;">(),
</span><span style="color:#d3d0c8;"> "</span><span style="color:#99cc99;">new_peer_id</span><span style="color:#d3d0c8;">" => new_split_peer.peer_id,
</span><span style="color:#d3d0c8;"> "</span><span style="color:#99cc99;">reason</span><span style="color:#d3d0c8;">" => r,
</span><span style="color:#d3d0c8;"> "</span><span style="color:#99cc99;">region_id</span><span style="color:#d3d0c8;">" => </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.</span><span style="color:#66cccc;">region_id</span><span style="color:#d3d0c8;">(),
</span><span style="color:#d3d0c8;"> "</span><span style="color:#99cc99;">peer_id</span><span style="color:#d3d0c8;">" => </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.</span><span style="color:#66cccc;">id</span><span style="color:#d3d0c8;">(),
</span><span style="color:#d3d0c8;"> );
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">continue</span><span style="color:#d3d0c8;">;
</span><span style="color:#d3d0c8;"> }
</span><span style="color:#d3d0c8;"> </span><span style="color:#66cccc;">write_peer_state</span><span style="color:#d3d0c8;">(kv_wb_mut, new_region, PeerState::Normal, None)
</span><span style="color:#d3d0c8;"> .</span><span style="color:#66cccc;">and_then</span><span style="color:#d3d0c8;">(|_| </span><span style="color:#66cccc;">write_initial_apply_state</span><span style="color:#d3d0c8;">(kv_wb_mut, new_region.</span><span style="color:#66cccc;">get_id</span><span style="color:#d3d0c8;">()))
</span><span style="color:#d3d0c8;"> .</span><span style="color:#66cccc;">unwrap_or_else</span><span style="color:#d3d0c8;">(|</span><span style="color:#f2777a;">e</span><span style="color:#d3d0c8;">| {
</span><span style="color:#d3d0c8;"> panic!(
</span><span style="color:#d3d0c8;"> "</span><span style="color:#99cc99;">{} fails to save split region {:?}: {:?}</span><span style="color:#d3d0c8;">",
</span><span style="color:#d3d0c8;"> </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.tag, new_region, e
</span><span style="color:#d3d0c8;"> )
</span><span style="color:#d3d0c8;"> });
</span><span style="color:#d3d0c8;">}
</span><span style="color:#66cccc;">write_peer_state</span><span style="color:#d3d0c8;">(kv_wb_mut, &derived, PeerState::Normal, None).</span><span style="color:#66cccc;">unwrap_or_else</span><span style="color:#d3d0c8;">(|</span><span style="color:#f2777a;">e</span><span style="color:#d3d0c8;">| {
</span><span style="color:#d3d0c8;"> panic!("</span><span style="color:#99cc99;">{} fails to update region {:?}: {:?}</span><span style="color:#d3d0c8;">", </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.tag, derived, e)
</span><span style="color:#d3d0c8;">});
</span></code></pre>
<p>在默认的分裂方式下,原 Region 要分裂到右侧,举例而言,假设分裂前的 Region 数量一共有 2 个,ID 分别为 1 和 2。2 是即将要分裂的 Region,且 Split Key 为 "b"。</p>
<p>Region 1 ["", "a"), Region 2 ["a", "")</p>
<p>分裂后的新 Region 被分配了 ID 3,那么分裂后的 Region 会形如:</p>
<p>Region 1 ["", "a"), Region 3 ["a", "b"), Region 2 ["b", "")</p>
<p>在 TiKV 完成 Split log 的 Apply 后,会通过 ApplyResult::Res 事件触发 <a href="https://github.com/tikv/tikv/blob/1fb8980ccab9ff40c1adc206df52952dab8e8ad8/components/raftstore/src/store/fsm/peer.rs#L3519"><code>PeerFsmDelegate::on_ready_split_region</code></a> 来完成 Split 的预后工作。如果当前 Region 是 leader,则会给 PD 发送一个 Report(Batch)Split 的 RPC 请求,仅供 PD 打个日志记录,方便我们在查问题时通过 PD 的日志看到各个 Region 的 Split 记录。由于 Region 的 ID 分配也是严格保证单调递增,所以我们可以说 Region ID 越大的 Region 则越新。</p>
<pre style="background-color:#2d2d2d;"><code class="language-Rust"><span style="color:#cc99cc;">if</span><span style="color:#d3d0c8;"> is_leader {
</span><span style="color:#d3d0c8;"> </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.fsm.peer.approximate_size = estimated_size;
</span><span style="color:#d3d0c8;"> </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.fsm.peer.approximate_keys = estimated_keys;
</span><span style="color:#d3d0c8;"> </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.fsm.peer.</span><span style="color:#66cccc;">heartbeat_pd</span><span style="color:#d3d0c8;">(</span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.ctx);
</span><span style="color:#d3d0c8;"> </span><span style="color:#747369;">// Notify pd immediately to let it update the region meta.
</span><span style="color:#d3d0c8;"> info!(
</span><span style="color:#d3d0c8;"> "</span><span style="color:#99cc99;">notify pd with split</span><span style="color:#d3d0c8;">";
</span><span style="color:#d3d0c8;"> "</span><span style="color:#99cc99;">region_id</span><span style="color:#d3d0c8;">" => </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.fsm.</span><span style="color:#66cccc;">region_id</span><span style="color:#d3d0c8;">(),
</span><span style="color:#d3d0c8;"> "</span><span style="color:#99cc99;">peer_id</span><span style="color:#d3d0c8;">" => </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.fsm.</span><span style="color:#66cccc;">peer_id</span><span style="color:#d3d0c8;">(),
</span><span style="color:#d3d0c8;"> "</span><span style="color:#99cc99;">split_count</span><span style="color:#d3d0c8;">" => regions.</span><span style="color:#66cccc;">len</span><span style="color:#d3d0c8;">(),
</span><span style="color:#d3d0c8;"> );
</span><span style="color:#d3d0c8;"> </span><span style="color:#747369;">// Now pd only uses ReportBatchSplit for history operation show,
</span><span style="color:#d3d0c8;"> </span><span style="color:#747369;">// so we send it independently here.
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> task = PdTask::ReportBatchSplit {
</span><span style="color:#d3d0c8;"> regions: regions.</span><span style="color:#66cccc;">to_vec</span><span style="color:#d3d0c8;">(),
</span><span style="color:#d3d0c8;"> };
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">if let </span><span style="color:#d3d0c8;">Err(e) = </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.ctx.pd_scheduler.</span><span style="color:#66cccc;">schedule</span><span style="color:#d3d0c8;">(task) {
</span><span style="color:#d3d0c8;"> error!(
</span><span style="color:#d3d0c8;"> "</span><span style="color:#99cc99;">failed to notify pd</span><span style="color:#d3d0c8;">";
</span><span style="color:#d3d0c8;"> "</span><span style="color:#99cc99;">region_id</span><span style="color:#d3d0c8;">" => </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.fsm.</span><span style="color:#66cccc;">region_id</span><span style="color:#d3d0c8;">(),
</span><span style="color:#d3d0c8;"> "</span><span style="color:#99cc99;">peer_id</span><span style="color:#d3d0c8;">" => </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.fsm.</span><span style="color:#66cccc;">peer_id</span><span style="color:#d3d0c8;">(),
</span><span style="color:#d3d0c8;"> "</span><span style="color:#99cc99;">err</span><span style="color:#d3d0c8;">" => %e,
</span><span style="color:#d3d0c8;"> );
</span><span style="color:#d3d0c8;"> }
</span><span style="color:#d3d0c8;">}
</span></code></pre>
<p>其余则是一些向 PD 上报心跳,统计信息的初始化工作,更新分裂后的 Region epoch 并在 Raft group 中注册 Region 的路由。这些工作完成后,当前 TiKV 上的 Region 可以说是已经完成分裂了。</p>
<h2>Raft Election</h2>
<p>对于分裂前的原 Region 是 Leader 的 Peer 来说,分裂后的 Region 是可以立马发起选举的,而对于原 Region 非 Leader 的 Peer 来说,它分裂创建出的新 Region 是不能立马发起选举的,而是需要等待一个 Raft 的选举超时时间。这样实现的原因是存在下列的 Case:</p>
<ol>
<li>假设有一个 3 副本的 Region</li>
<li>Split 的 Log 已经复制到了所有的 Follower 上</li>
<li>所有的 Follower 完成了 Region Split Log 的 Apply,完成了分裂</li>
<li>Region 的 Leader 还没有开始或完成分裂</li>
</ol>
<p>如果允许原 Peer 非 Leader 的新 Region 分裂出来后立马开始选举,则会出现同一个数据范围内存在两个 Region leader 对外提供服务,一个是分裂后的新的更小的 Region leader,一个是尚未分裂的原 Region leader(Lease 尚未过期),这样一来就存在破坏线性一致性的可能。由于一次 Raft 的选举超时时间要大于 Leader 的 Lease 时间,所以只要我们保证以下两点:</p>
<ol>
<li>完成分裂的 Region 等待一个 Raft 的选举超时时间再开始选举</li>
<li>需要 Split 的 Region 不再续约 Lease</li>
</ol>
<p>所以当新分裂的 Region 开始选举时,旧的 Region leader 早些时候一定会因为发现自身的 Epoch 与其余两个 Follower 不同而选举失败完成退选。</p>
<h1>踩坑经验</h1>
<h2>Split Key 的格式为 Encoded Key without TS</h2>
<p>在 TiDB 和 TiKV 的语境下,当我们说到 Key 编码时,它可能指的是以下几种情况:</p>
<ul>
<li>Raw Key</li>
<li>Encoded Key without TS</li>
<li>Encoded Key with TS</li>
</ul>
<p>TiDB 在发送请求时使用的是 Raw Key,也即不带任何与 MVCC 相关的信息,也没有 Padding,只包括诸如 TableID,RowID 等基本信息。</p>
<p>TiKV 的 Raftstore 以及 PD 在处理诸如 Region 边界,Split 等 Key 时使用的是 Encoded Key without TS,它在 Raw Key 的基础上进行了 Encode,添加了用于保持字典序的 Padding,但由于此层尚未涉及到具体的事务,所以并没有 TS 参与其中。</p>
<p>TiKV 在实际读写底层 RocksDB 数据时,会将请求的 TS 一并 Encode 到 Key 里来区分 MVCC 信息,所以这一层使用的是 Encoded Key with TS。</p>
<p>Region Split 发生在 Raftstore 这一层,所以其格式均为 Encoded Key without TS,在开发相关功能时,要注意对 Key 进行 Encode,并且剔除 TS 信息,以免出现一些预期外的行为。</p>
<h1>参考</h1>
<ul>
<li><a href="https://pingcap.com/zh/blog/tikv-source-code-reading-20">TiKV 源码解析系列文章(二十)Region Split 源码解析</a></li>
<li><a href="https://pingcap.com/zh/blog/tikv-source-code-reading-19">TiKV 源码解析系列文章(十九) read index 和 local read 情景分析 | PingCAP</a></li>
<li><a href="https://pingcap.com/zh/blog/tikv-source-code-reading-18">TiKV 源码解析系列文章(十八) Raft Propose 的 Commit 和 Apply 情景分析 | PingCAP</a></li>
<li><a href="https://tikv.github.io/deep-dive-tikv/overview/introduction.html">Deep Dive TiKV</a></li>
<li><a href="https://pingcap.feishu.cn/wiki/wikcnkZoy41zHBJy4HTecCf7jig">Region 的一生</a></li>
<li><a href="https://longfangsong.github.io/tipedia/zh/what/Memory%20Comparable%20Encoding.html">Tipedia • Memory Comparable Encoding</a></li>
</ul>