1. 背景
(1) 我们所使用的 TiDB 目前采用 3 副本的部署方式,其中一份数据同时存储在多个节点中。然而,如果一个 Region 的多数或全部副本在短时间内全部下线,该 Region 将无法进行读写操作,对业务造成严重影响。
(2)在实际生产过程中,我们也曾遇到同一台交换机发生灾难性故障,如机房断电等情况,其下的多个 TIKV 宕机,也会造成多副本或全部副本丢失,导致业务不可用的情况。
(3)社区当前多数文章只针对某一个版本在该场景下进行了故障恢复。而在 V6.1.0 正式引入 Online Unsafe Recovery 功能,所以本文将整合 V6.1.0 版本及其之前和之后的两个版本在该场景下故障解决方案。
2. 原理介绍
对于 PD 来说,是如何获取某个 Region 的状态?
对于每个 Raft Group 的 Leader 和 PD 之间都存在心跳包,用于汇报这个 Region 的状态。心跳包中主要包括:
- Leader 的位置
- Followers 的位置
- 掉线副本的个数
- 数据写入/读取的速度
除此之外,PD 还会收集每个 TiKV 节点(Store)发送的心跳包。通过这两种心跳包得到的信息,PD 会通过一些策略来制定具体的调度计划。
而调度的基本操作大致分为:
- 增加一个副本(addReplica)
- 删除一个副本(RemoveReplica)
- 将 Leader 角色在一个 Raft Group 的不同副本之间进行迁移(TransferLeader)
假设我们现在有若干个 Region 分布在 5 个 Store 上,并且每个 Region 有3个副本,我们进行实验,手动挂掉3个 Store 。若剩余副本数等于 2,PD 可以通过调度进行 Leader 的重新选举以及副本的补充。但是若只剩余 1 个副本,那么就需要人工介入进行恢复。
3. 实验模拟
首先,使用 tiup-bench 工具来准备一些测试数据:
tiup bench tpcc prepare --warehouses 4 --db xxx -H xxxx -P 4000 -pxxx
3.1 V6.1.0 以下版本(3副本丢失2副本)
集群 store 情况:
id | address |
---|---|
1 | IP1 |
2 | IP2 |
3 | IP3 |
10 | IP10 |
11 | IP11 |
这里用 IP+store id 代替真实 IP 地址。
Region 分布情况:
表名 | Region id | Region leader | Region分布 | Region leader 所在地址 |
---|---|---|---|---|
customer | 34768 | 11 | [11,10,2] | IP11 |
customer | 34780 | 1 | [11,1,3] | IP1 |
customer | 34772 | 10 | [10,2,1] | IP10 |
customer | 34776 | 11 | [11,2,1] | IP11 |
district | 34756 | 10 | [10,2,3] | IP10 |
district | 34764 | 1 | [11,1,10] | IP1 |
district | 34752 | 10 | [1,10,2] | IP10 |
district | 34760 | 1 | [1,10,2] | IP1 |
history | 34792 | 10 | [10,3,2] | IP10 |
history | 34799 | 1 | [1,2,3] | IP1 |
history | 34784 | 1 | [1,10,2] | IP1 |
history | 34788 | 2 | [11,2,1] | IP2 |
item | 70 | 10 | [11,10,2] | IP10 |
new_order | 34813 | 1 | [1,10,2] | IP1 |
new_order | 34826 | 11 | [11,3,1] | IP11 |
new_order | 34804 | 10 | [10,3,1] | IP10 |
new_order | 34808 | 2 | [2,3,1] | IP2 |
order_line | 34863 | 10 | [10,3,11] | IP10 |
order_line | 34859 | 11 | [11,10,3] | IP11 |
order_line | 34851 | 10 | [10,3,1] | IP10 |
order_line | 34855 | 1 | [11,1,10] | IP1 |
orders | 34839 | 1 | [11,1,10] | IP1 |
orders | 34830 | 11 | [11,3,1] | IP11 |
orders | 34834 | 2 | [11,2,3] | IP2 |
orders | 34847 | 1 | [1,3,11] | IP1 |
stock | 34867 | 1 | [1,2,11] | IP1 |
stock | 34879 | 1 | [1,2,11] | IP1 |
stock | 34871 | 1 | [11,1,10] | IP1 |
stock | 34875 | 1 | [1,10,11] | IP1 |
warehouse | 34748 | 10 | [10,2,3] | IP10 |
warehouse | 34743 | 3 | [3,1,11] | IP3 |
warehouse | 34733 | 3 | [1,3,10] | IP3 |
warehouse | 34738 | 3 | [3,1,10] | IP3 |
基于上述 Region 分布情况,假设我们关闭了 store 1 和 store 11 ,那么以下表将无法访问:customer、district、history、new_order、order_line、orders、stock 和 warehouse。而 item 表不受影响。
实验过程:
(1)集群正常状态
所有表均可访问
(2)在 store 1 和 11 上 mv data文件夹
移动后,store 1 和 11 两个 TiKV 节点状态变为 Disconnected。
(3)停止 store 1 和 11 的服务
这里要将 store 1 和 11 TiKV 服务不可用才能无法访问上述表。否则就会出现还能访问的情况。
TiKV 服务的配置文件一般在 /etc/systemd/system/tikv-端口号.service 中。
需要将 Restart 置为 no,因为 Restart=always: 只要不是通过 systemctl stop 来停止服务,任何情况下都必须要重启服务。
再将 TiKV 进程 KIll 掉。
(4)访问对应表
符合我们的预期,3 副本状态下挂掉 2 个副本,访问 item 表不受影响,访问其他表会报错 : Region is unavailable。
(5)待修复 Region
核心原理在于使用单副本数据进行多副本补充,如下表所示:
Region id | Region分布 | 可用Store |
---|---|---|
34867 | [1,2,11] | 2 |
34879 | [1,2,11] | 2 |
34788 | [11,2,1] | 2 |
34776 | [11,2,1] | 2 |
34780 | [11,1,3] | 3 |
34847 | [1,3,11] | 3 |
34743 | [3,1,11] | 3 |
34826 | [11,3,1] | 3 |
34830 | [11,3,1] | 3 |
34764 | [11,1,10] | 10 |
34855 | [11,1,10] | 10 |
34839 | [11,1,10] | 10 |
34871 | [11,1,10] | 10 |
34875 | [1,10,11] | 10 |
所以我们需要在正常的Store上对副本进行补充:
在 Store 2 上修复 Region 34867、34879、34788 和 34776。
在 Store 3 上修复 Region 34780、34847、34743、34826 和 34830。
在 Store 10 上修复 Region 34764、34855、34839、34871 和 34875。
(6)开始修复
停止 TiKV 角色
tiup cluster stop 集群名称 -R tikv
关闭 PD 调度
config set region-schedule-limit 0
config set replica-schedule-limit 0
config set leader-schedule-limit 0
config set merge-schedule-limit 0
修复 Region
这里使用 tikv-ctl 工具,需要到对应的 TiKV 节点去执行命令进行修复,如果是使用 TiUP 部署的集群,tikv-ctl 工具在 ~/.tiup/components/ctl/{VERSION}/ 目录下,拷贝到对应的TiKV节点(Store)上。
-s 是宕机的 TiKV 节点的 store id,-r 是我们要移除的 Region id(多个请以逗号分隔)。
Store 2:
tikv-ctl --data-dir /data/tidb/data unsafe-recover remove-fail-stores -s 1,11 -r 34867,34879,34788,34776
Store 3:
tikv-ctl --data-dir /data/tidb/data unsafe-recover remove-fail-stores -s 1,11 -r 34780,34847,34743,34826,34830
Store 10:
tikv-ctl --data-dir /data/tidb/data unsafe-recover remove-fail-stores -s 1,11 -r 34764,34855,34839,34871,34875
执行成功会显示
(7)启动集群,校验数据
tiup cluster start 集群名 -R tikv
这里注意,store 1 和 11 是无法启动的,因为数据目录已经被 mv 了。
经过修复,上述表可以正常访问。
(8) 节点下线
最后可以将 store 4、7 对应的 TiKV 节点强制下线
tiup cluster scale-in cluster-name -N IP1:20260,IP11:20260 --force
tiup cluster prune cluster-name
3.2 V6.1.0 以上版本(3副本丢失2副本)
在 V6.1.0 版本后,推出了在线有损恢复 Online Unsafe Recovery,现在,我们将使用这种方法进行故障恢复。
集群 store 情况:
id | address |
---|---|
1 | IP1 |
2 | IP2 |
3 | IP3 |
10 | IP10 |
11 | IP11 |
同上,这里用 IP+store id 代替真实 IP 地址。
Region 分布情况:
表名 | Region id | Region leader | Region分布 | Region leader 所在地址 |
---|---|---|---|---|
customer | 355 | 10 | [10,11,1] | IP10 |
customer | 367 | 10 | [10,3,1] | IP10 |
customer | 363 | 3 | [10,3,1] | IP3 |
customer | 359 | 2 | [10,2,1] | IP2 |
district | 339 | 3 | [10,3,1] | IP3 |
district | 343 | 10 | [10,1,3] | IP10 |
district | 351 | 2 | [10,2,3] | IP2 |
district | 347 | 10 | [10,3,1] | IP10 |
history | 371 | 10 | [10,3,11] | IP10 |
history | 383 | 10 | [10,3,11] | IP10 |
history | 379 | 10 | [10,3,11] | IP10 |
history | 375 | 10 | [10,11,1] | IP10 |
item | 30 | 2 | [10,2,3] | IP2 |
new_order | 387 | 2 | [2,3,1] | IP2 |
new_order | 391 | 10 | [10,3,1] | IP10 |
new_order | 395 | 10 | [10,1,3] | IP10 |
new_order | 399 | 2 | [10,2,3] | IP2 |
order_line | 427 | 2 | [10,2,3] | IP2 |
order_line | 431 | 2 | [2,1,11] | IP2 |
order_line | 423 | 2 | [2,3,1] | IP2 |
order_line | 419 | 2 | [10,2,1] | IP2 |
orders | 407 | 2 | [10,2,1] | IP2 |
orders | 415 | 2 | [2,11,10] | IP2 |
orders | 411 | 2 | [10,2,1] | IP2 |
orders | 403 | 2 | [10,2,1] | IP2 |
stock | 447 | 2 | [10,2,11] | IP2 |
stock | 435 | 2 | [2,3,10] | IP2 |
stock | 439 | 2 | [10,2,3] | IP2 |
stock | 443 | 2 | [2,3,1] | IP2 |
warehouse | 331 | 10 | [10,3,11] | IP10 |
warehouse | 323 | 3 | [10,3,1] | IP3 |
warehouse | 327 | 10 | [10,3,1] | IP10 |
warehouse | 335 | 10 | [10,1,3] | IP10 |
基于上述 Region 分布情况,假设我们关闭了 store 2 和 store 3 ,那么以下表将无法访问:district、item、new_order、order_line 和 stock。而不受影响的表包括:customer、history、orders 和 warehouse。
实验过程:
(1)集群正常状态
所有表均可访问
(2)在 store 2 和 3 上 mv data文件夹
(3)访问表(在线有损恢复,不需要停止 store 2 和 3 的服务,这里要注意)
(4)执行恢复命令
unsafe remove-failed-stores 2,3
执行成功如下:
(5)查看恢复进度
unsafe remove-failed-stores show
[
{
"info": "Unsafe recovery enters collect report stage",
"time": "2024-04-08 16:58:23.847",
"details": [
"Failed stores 2, 3"
]
},
{
"info": "Unsafe recovery enters force leader stage",
"time": "2024-04-08 16:58:24.157",
"actions": {
"store 1": [
"force leader on regions: 70, 351, 411"
],
"store 10": [
"force leader on regions: 30"
],
"store 11": [
"force leader on regions: 435"
]
}
},
{
"info": "Unsafe recovery enters demote Failed voter stage",
"time": "2024-04-08 16:58:44.664",
"actions": {
"store 1": [
"region 70 demotes peers { id:263 store_id:2 }, { id:296 store_id:3 }",
"region 351 demotes peers { id:353 store_id:2 }, { id:675 store_id:3 }",
"region 411 demotes peers { id:413 store_id:2 }, { id:680 store_id:3 }"
],
"store 10": [
"region 30 demotes peers { id:264 store_id:2 }, { id:622 store_id:3 }"
],
"store 11": [
"region 435 demotes peers { id:437 store_id:2 }, { id:438 store_id:3 }"
]
}
},
{
"info": "Unsafe recovery Finished",
"time": "2024-04-08 16:58:55.671",
"details": [
"affected table ids: 40, 114, 137, 147, 152",
"no newly created empty regions"
]
}
]
等待最后输出 "info": "Unsafe recovery Finished"
,即为恢复成功。
经查询,上述表可以进行访问。
(6) 检查数据索引一致性
即使表进行正常读写,但不代表数据没有丢失。
"affected table ids: 40, 114, 137, 147, 152"
,恢复完受影响的表id,我们根据表 id 进行查询:
SELECT TABLE_SCHEMA, TABLE_NAME, TIDB_TABLE_ID FROM INFORMATION_SCHEMA.TABLES WHERE TIDB_TABLE_ID IN (40, 114, 137, 147, 152);
现在我们上述进行检查:
admin check table 表名;
若有不一致的索引,通过以下步骤进行修复:
- 重命名索引:alter table 表名 rename 索引名 to 临时索引名
- 创建新索引:alter table 表名 add index 索引名(列名)
- 删除旧索引:alter table 表名 drop index 临时索引名
(7) 宕机的节点下线
tiup cluster scale-in cluster-name -N IP2:20260,IP3:20260 --force
tiup cluster prune cluster-name
3.3 V6.1.0以上版本 (3副本全部丢失)
经过在线有损恢复后,使用的感受非常良好。现在我们思考,如果3个副本全部丢失,是否可以使用这种方式来恢复我们的数据?
接下来一起测试下。
老方法,我们首先使用 tiup-bench prepare 数据:
集群 store 情况:
id | address |
---|---|
4 | IP2 |
1 | IP1 |
2 | IP7 |
10 | IP8 |
11 | IP9 |
Region 分布情况:
表名 | Region id | Region leader | Region分布 | Region leader 所在地址 |
---|---|---|---|---|
orders | 400 | 1 | [1, 10, 11] | IP1 |
order_line | 412 | 1 | [1, 10, 11] | IP1 |
stock | 430 | 1 | [1, 2, 10] | IP1 |
customer | 348 | 2 | [2, 10, 4] | IP2 |
item | 22 | 4 | [4, 10, 2] | IP4 |
warehouse | 314 | 4 | [1, 4, 2] | IP4 |
history | 361 | 4 | [4, 11, 10] | IP4 |
new_order | 369 | 4 | [4, 10, 11] | IP4 |
new_order | 377 | 4 | [4, 10, 11] | IP4 |
orders | 385 | 4 | [4, 11, 10] | IP4 |
order_line | 404 | 4 | [4, 2, 1] | IP4 |
order_line | 418 | 4 | [4, 10, 11] | IP4 |
stock | 422 | 4 | [4, 11, 2] | IP4 |
stock | 426 | 4 | [1, 4, 2] | IP4 |
stock | 434 | 4 | [4, 10, 2] | IP4 |
warehouse | 302 | 10 | [10, 2, 4] | IP10 |
warehouse | 306 | 10 | [1, 10, 4] | IP10 |
warehouse | 310 | 10 | [10, 11, 4] | IP10 |
district | 318 | 10 | [10, 2, 4] | IP10 |
district | 322 | 10 | [10, 2, 11] | IP10 |
district | 326 | 10 | [10, 2, 11] | IP10 |
district | 330 | 10 | [10, 11, 4] | IP10 |
customer | 334 | 10 | [1, 10, 11] | IP10 |
customer | 338 | 10 | [10, 2, 11] | IP10 |
customer | 342 | 10 | [10, 11, 4] | IP10 |
history | 352 | 10 | [10, 11, 2] | IP10 |
history | 356 | 10 | [10, 11, 1] | IP10 |
history | 365 | 10 | [10, 11, 2] | IP10 |
new_order | 373 | 10 | [10, 11, 2] | IP10 |
new_order | 381 | 10 | [1, 10, 2] | IP10 |
orders | 389 | 10 | [10, 2, 11] | IP10 |
orders | 393 | 10 | [10, 11, 1] | IP10 |
现在我们 mv 4,10,11 对应TiKV的 data文件夹,并打开防火墙 (关闭和 pd 的通信) 以及中控机 tiup 的 ssh 端口模拟宕机。
#!/bin/bash
mv /data_dir /data_dir_delete
iptables -A OUTPUT -d pd_host1 -j DROP # (pd)
iptables -A OUTPUT -d pd_host2 -j DROP # (pd)
iptables -A OUTPUT -d pd_host3 -j DROP # (pd)
iptables -A OUTPUT -d tiup_host -j DROP # (tiup)
iptables -A INPUT -s pd_host1 -j DROP # (pd)
iptables -A INPUT -s pd_host2 -j DROP # (pd)
iptables -A INPUT -s pd_host3 -j DROP # (pd)
iptables -A INPUT -s tiup_host -j DROP # (tiup)
之后观察报错情况:
- leader 和 region 节点监控消失,store size 为丢失了 3 副本
- 业务异常,日志报错: “9005: Region is unavailable”
- server report failure 出现错误报告
下面就到了激动人心的恢复时间了:
unsafe remove-faileds-stores 4,10,11
unsafe remove-faileds-stores show
最后一段输出信息:
{
"info": "Unsafe recovery Finished",
"time": "2024-03-27 10:29:41.471",
"details": [
"affected table ids: 147, 152, 119, 10, 26, 36, 123, 125, 135, 14, 62, 96, 138, 88, 50, 120, 126, 46, 64, 105, 106, 130, 107, 48, 117, 118, 281474976710649, 40, 42, 76, 74, 24, 54, 68, 28, 129, 86, 124, 136, 144, 20, 82, 114, 113, 131, 16, 66, 90, 4, 112, 281474976710653, 143, 150, 142, 22, 56, 94, 111, 137, 6, 60, 92, 100, 34, 70, 281474976710652",
"newly created empty regions: 568, 562, 564, 572, 582, 560, 574, 576, 578, 566, 570, 580, 584"
]
}
查询结果:
结果是,在三副本全部丢失的情况下,恢复成功了,这就比较令我感到诧异,随后团队将这个现象反应给了官方,也期待之后进一步完善。
4. 总结及思考
一般情况下,当 TiDB 集群发生故障后,只需进行重启即可完成多数派的选举,使集群能够正常对外提供服务。
然而,在极端情况下,例如 SST 文件损坏,就需要使用有损恢复的方式进行数据恢复。在这种情况下,恢复方案需要经过反复验证,以确保在线上的极端情况下能够成功完成数据恢复。
本文主要以 V6.1.0 为分界,针对 V6.1.0 版本及其之前和之后的两个版本,在 3 副本状态下多副本丢失的场景下进行故障模拟,并分别使用 tikv-ctl 手动恢复和在线有损恢复两种方式进行数据恢复。
值得注意的是,V6.1.0 之前的版本在线有损恢复功能为实验特性,因此对于小于 V6.1.0 的集群,我们可以考虑使用 tikv-ctl 手动恢复;而对于版本大于 V6.1.0 的集群,我们则可以考虑使用在线有损恢复方式。
同时,如果能够将恢复过程自动化,将极大地提高业务的稳定性。