# [SOP] TiKV 因掉电、非规范等操作导致 region is unavaliable 或数据丢失的 unsafe recover 原理及操作流程

## 一、unsafe recover 发生背景

1. Raft 一致性原理简述

    - TiKV 默认配置为 3 副本，因此在 TiKV 实例数大于等于 3 的情况下，每一个 Region 都会在集群中保存 3 份称为 raft group，raft group 通过 Raft 协议来选举 Leader 并同步数据。

    - Raft 协议可以保证在数量小于副本数（注意，不是节点数）一半的节点挂掉或者隔离的情况下，仍然能够提供服务，并且不丢失任何数据。即：对于 3 副本集群，挂掉一个节点除了可能会导致性能有抖动之外，可用性和正确性理论上不会受影响，但是挂掉 2 个副本，一些 region 就会不可用，而且如果这 2 个副本无法完整地找回，存在永久丢失部分数据的可能。
    - 为行文方便，以下假设 TiKV 集群都配置为默认的 3 副本，更多副本数的情况在理解原理的情况下，请自行推断处理。

2. TiKV 异常发生场景

    - 机房掉电，再恢复 : tikv 2 块磁盘同时降速，致使 raft 状态机无法驱动，等同于同时掉 2 个 tikv；
    - TiKV 多实例部署，未正确配置 host label 的情况下，即使一台 TiKV 服务器宕机，也可能会出现同一份数据的两个副本丢失；
    - 非规范操作 : 如同时 scale-in --force 2 台 tikv instance，致使 raft group 无法多数派选举；
    - 等等与上述情况类似，可以明确异常的情况。

## 二、v6.1.0 版本以下 unsafe recover 分类及处理思路

1. 故障节点数小于副本数的一半导致的集群不可用 **(数据不会丢失)**

    一个典型的例子是 3 副本 TiKV 集群，未配置 sync-log，有一个实例掉电重启之后一直 panic。对此，要做的第一步是让无法启动的 TiKV 启动起来，具体的步骤是：  

    - 滚动重启所有可以正常启动的 TiKV，先恢复 commit index is out of range 的错误；
    - 尝试启动之前无法启动的 TiKV，并观察日志确定是哪种类型的数据损坏。
        - 如果从 panic 日志中看到 last index，commit index 等相关描述，则可以确定是 Raft 状态机的损坏;
        - 如果有 “Sst file size mismatch” 等错误，则可以确定是 RocksDB 在 Apply Snapshot 时出现了损坏；
    - 根据不同的损坏类型，进行不同的修复：
        - 对于 Raft 状态机损坏：
            - 把 Raft 状态机损坏的 Region 找出来；
            - 把 Region 设置为 tombstone 状态；
        - 对于 RocksDB 的损坏，目前并没有集成到 TiDB 组件中的修复工具可以处理，意味着需要放弃这个实例上的全部数据。
            - 放弃这些实例是否会导致丢数据呢？答案是不会的，因为故障节点小于副本数的一半，而 Raft 协议在确认时需要副本数一半以上的节点参与。

2. 故障节点数大于或等于副本数的一半 导致的集群不可用 **(数据会丢失)**

    - 可能的情况包括但不限于：
        - 部分 Region 的全部副本都在故障中丢掉了；
        - 部分 Region 的过半副本在故障中丢掉了，剩下的副本没收到最近的写入，因为无法完成 raft group 多数派投票无 leader 处理请求。

    - 恢复的过程中，需随时记录相关  Region 的 ID，以便评估损失：

        - 使用 pd-ctl 检查大于等于一半副本数在故障节点上的 Region，并记录 ID；
        - 在所有未发生掉电故障的实例上，对所有 Region 移除掉所有位于故障节点上的 Peer；
        - 在上述两步后，理论上所有仍然有副本健在的 Region 都可以选出 Leader ，这时可以重启 PD， TiKV 集群

    - 最后，处理那些所有副本都没有的 Region：

        - 使用 pd-ctl 检查没有 Leader 的 Region；
        - 根据 Region ID 确认 Region 属于哪些表的数据；
        - 如果用户可以接受这部分 Region 数据丢失，则创建空 Region 解决 Unavailable 报错；
        - 评估所有副本丢失情况的处理方法
            - 除了上面介绍的根据 Region ID 找到对应的表，创建空 Region 解决 Unavailable 报错。
            - 实践中往往更有效的手段是直接询问用户在故障发生前的某一段时间内（比如 5min），大概写入了哪些数据表，是否有 DDL 操作，是否可以重新消费更上游的数据来再次写入等等，重导是最简单、最完美的处理方式。

## 三、unsafe recover 操作步骤

1. 禁用调度

    在恢复之前，首先应该通过 pd-ctl 调整 PD 的配置，禁用相关的调度，将恢复过程中可能的异常情况降至最少。包括：

    ```yaml
    config set region-schedule-limit 0
    config set replica-schedule-limit 0
    config set leader-schedule-limit 0
    config set merge-schedule-limit 0
    ```

2. 检查副本

    使用 pd-ctl 检查大于等于一半副本数在故障节点上的 Region；   
    要求：PD 处于运行状态；

    ```jaml
    故障节点为 15,22,43,45,46
    pd-ctl -u <endpoint> -d region --jq=’.regions[] | {id: .id, peer_stores: [.peers[]. store_id] | select(length as $total | map(if .==(15,22,43,45,46) then . else empty end) |    length>=$total-length) }’
    ```

3. 移除故障副本

    在所有未发生掉电故障的实例上，对所有 Region 移除掉所有位于故障节点上的 Peer；  
    要求：在所有未发生掉电故障的机器上运行，且需要关闭 TiKV 节点；  

    ```yaml
    tikv-ctl --db /path/to/tikv-data/db unsafe-recover remove-fail-stores -s <s1,s2,....>   --all-regions

    ## 这里 store id 为 30725906

    tikv-ctl --db /path/to/tikv-data/db unsafe-recover remove-fail-stores -s 30725906   --all-regions
    ```

4. 检查 Region Leader

    使用 pd-ctl 检查没有 Leader 的 Region ;
    要求：PD 处于运行状态；

    ```yaml
    pd-ctl -u <endpoint> -d region --jq '.regions[]|select(has("leader")|not)|{id: .id, peer_stores: [.peers[].store_id]}'
    ```

5. 故障 Region 归属表检查

    根据 Region ID 确认 Region 属于哪些表的数据;
    要求：PD、TiKV、TiDB 处于运行状态

    ```yaml
    curl <http://{TiDBIP}:10080/regions/{regionID>}
    ```

6. 解决 Unavailable 报错

    创建空 Region 解决 Unavailable 报错；  
    要求：PD、TiKV 处于运行状态；

    ```yaml
    tikv-ctl --db /path/to/tikv-data/db recreate-region --pd <endpoint> -r <region_id>
    ```

7. 数据一致性检测

    检查数据索引一致性，要求：PD、TiKV、TiDB 处于运行状态;

    ```sql
    -- 例如：检查表：c1、索引：idx_name
    select count(*) from table as c1;
    select count(*) from table force index `idx_name` as c2;
    select c1 = c2;
    admin check table table_name;
    ```

8. 恢复调度

    ```
    config set region-schedule-limit {default-value}
    config set replica-schedule-limit {default-value}
    config set leader-schedule-limit {default-value}
    config set merge-schedule-limit {default-value}
    ```

## 二、v6.1.0 版本以上 Online Unsafe Recovery处理

针对 Unsafe Recovery 场景，自 v6.1.0 版本开始引入，但在 TiDB v6.1 以下版本为实验特性请谨慎使用，详情参考[官方文档步骤](https://docs.pingcap.com/zh/tidb/stable/online-unsafe-recovery#online-unsafe-recovery-%E4%BD%BF%E7%94%A8%E6%96%87%E6%A1%A3)。
