TiDB 备份恢复

读取历史数据

TiDB 使用 MVCC 管理版本,当更新/删除数据时,不会做真正的数据删除,只会添加一个新版本数据,所以可以保留历史数据。历史数据不会全部保留,超过一定时间的历史数据会被彻底删除,以减小空间占用以及避免历史版本过多引入的性能开销。
TiDB 使用周期性运行的 GC(Garbage Collection,垃圾回收)来进行清理,关于 GC 的详细介绍参见 TiDB 垃圾回收 (GC)。
本文后面介绍的几种数据恢复场景都是基于历史数据还没有被GC的前提下

GC

  • 通过下面命令可以查看GC相关的配置
MySQL [(none)]> select VARIABLE_NAME, VARIABLE_VALUE from mysql.tidb;
+--------------------------+---------------------------------------------------------------------------------------------------+
| VARIABLE_NAME            | VARIABLE_VALUE                                                                                    |
+--------------------------+---------------------------------------------------------------------------------------------------+
| bootstrapped             | True                                                                                              |
| tidb_server_version      | 47                                                                                                |
| system_tz                | Asia/Shanghai                                                                                     |
| new_collation_enabled    | False                                                                                             |
| tikv_gc_leader_uuid      | 5d536216420001b                                                                                   |
| tikv_gc_leader_desc      | host:db-kafka-2-ptest, pid:30863, start at 2020-10-22 07:39:42.749752108 +0800 CST m=+1.208132766 |
| tikv_gc_leader_lease     | 20201116-16:24:42 +0800                                                                           |
| tikv_gc_enable           | true                                                                                              |
| tikv_gc_run_interval     | 10m0s                                                                                             |
| tikv_gc_life_time        | 10m0s                                                                                             |
| tikv_gc_last_run_time    | 20201116-16:22:42 +0800                                                                           |
| tikv_gc_safe_point       | 20201023-10:17:15 +0800                                                                           |
| tikv_gc_auto_concurrency | true                                                                                              |
| tikv_gc_mode             | distributed                                                                                       |
+--------------------------+---------------------------------------------------------------------------------------------------+
14 rows in set (0.02 sec)

关于GC的配置参数,我们只需要调整tikv_gc_life_time,GC的保留时间,默认是10分钟,我们调整成3天,这个参数调大会对范围查询的效率有所影响,并且对磁盘的使用量也会增多,所以一般不建议设置太大

备份

目前官方有mydumper、dumpling、BR三个备份工具,其中mydumper目前已经不再维护,BR我们后面会单独介绍,这里主要写的是dumpling

工具安装

  • 自动安装
tiup  install dumpling
  • 手动安装
wget https://download.pingcap.org/tidb-toolkit-v4.0.8-linux-amd64.tar.gz
tar -zxvf tidb-toolkit-v4.0.8-linux-amd64.tar.gz
  • 备份
/dumpling -h 172.21.xx.xx -P 4000 -uroot -p xxx   --filetype sql -t 32 -F 64MiB  -B test -T test.t --snapshot "2020-11-16 15:56:12" -o ./test1
  • 主要参数详解
参数 用途 备注
-B 指定导出的数据库
-T 指定导出的表
-f 导出能匹配模式的表,语法可参考 table-filter . (导出所有库表
-t 备份的并发线程数
-r 将 table 划分成 row 行 数据,一般针对大表操作并发生成多个文件。
-s 控制 INSERT SQL 语句的大小,单位 bytes
-F 将 table 数据划分出来的文件大小,需指明单位(如 128B, 64KiB, 32MiB, 1.5GiB)
–snapshot 指定导出的表 snapshot tso,只在 consistency=snapshot 下生效
–tidb-mem-quota-query 单条 dumpling 命令导出 SQL 语句的内存限制,单位为 byte,默认为 32 GB 34359738368
–snapshot 指定导出的表

更多介绍请查看官方文档 dumpling使用

恢复

场景分析

  • 误drop库
  • 误truncate table
  • 误drop table
  • 误 delete、update 表

恢复数据

Tidb恢复误操作的数据的前提是历史操作还没有被GC,所以如果线上发生了这样的事情,一定要第一时间确认GC的时间是否安全,如果有必要可以临时先调大,能数据恢复完成在调整回去,调整GC时间点命令如下:
update mysql.tidb set VARIABLE_VALUE="96h" where VARIABLE_NAME="tikv_gc_life_time";

误drop库

  • 模拟删库
MySQL [xxx_ods]> select count(*) from xxx_comment;
+----------+
| count(*) |
+----------+
|    83970 |
+----------+
1 row in set (0.14 sec)

MySQL [xxx_ods]> drop database xxx_ods;
Query OK, 0 rows affected (1.02 sec)

MySQL [xxx_ods]> select count(*) from xxx_comment;
ERROR 1046 (3D000): No database selected
MySQL [xxx_ods]>  select now()
    -> ;
+---------------------+
| now()               |
+---------------------+
| 2020-11-17 08:26:45 |
+---------------------+
1 row in set (0.00 sec)

MySQL [xxx_ods]> show databases;
+--------------------+
| Database           |
+--------------------+
| INFORMATION_SCHEMA |
| METRICS_SCHEMA     |
| PERFORMANCE_SCHEMA |
| cdc_test           |
| dm_meta            |
| dmtest             |
| mysql              |
| test               |
| tidb_loader        |
+--------------------+
13 rows in set (0.01 sec)

MySQL [xxx_ods]> \s
--------------
mysql  Ver 15.1 Distrib 5.5.60-MariaDB, for Linux (x86_64) using readline 5.1

Connection id:		6950
Current database:	
Current user:		root@172.21.xx.xx
SSL:			Not in use
Current pager:		stdout
Using outfile:		''
Using delimiter:	;
Server:			MySQL
Server version:		5.7.25-TiDB-v4.0.7 TiDB Server (Apache License 2.0) Community Edition, MySQL 5.7 compatible
Protocol version:	10
Connection:		172.21.xx.xx via TCP/IP
Server characterset:	utf8mb4
Db     characterset:	utf8mb4
Client characterset:	utf8
Conn.  characterset:	utf8
TCP port:		4000
--------------
  • 确认删除时间
    删库这种操作权限一般只有管理员才会有,当然也不排除有部分开发人员申请了超级权限,如果这个事情发生了那么我们肯定是希望能精确找到删库的时间点这样可以减少数据丢失,好在Tidb记录了所以DDL操作,可以通过 admin show ddl jobs; 查看,找到删库的具体时间点
MySQL [xxx_ods]> admin show  ddl jobs;
+--------+-------------------+-----------------------------------------+----------------+--------------+-----------+----------+-----------+---------------------+---------------------+--------+
| JOB_ID | DB_NAME           | TABLE_NAME                              | JOB_TYPE       | SCHEMA_STATE | SCHEMA_ID | TABLE_ID | ROW_COUNT | START_TIME          | END_TIME            | STATE  |
+--------+-------------------+-----------------------------------------+----------------+--------------+-----------+----------+-----------+---------------------+---------------------+--------+
|    815 | xxx_ods    |                                         | drop schema    | none         |       811 |        0 |         0 | 2020-11-17 08:26:36 | 2020-11-17 08:26:36 | synced |
|    814 | xxx_ods    | xxx_comment                  | create table   | public       |       811 |      813 |         0 | 2020-11-17 08:24:20 | 2020-11-17 08:24:21 | synced |
|    812 | xxx_ods    |                                         | create schema  | public       |       811 |        0 |         0 | 2020-11-17 08:24:20 | 2020-11-17 08:24:20 | synced |
|    810 | xxx ods    |                                         | drop schema    | none         |       795 |        0 |         0 | 2020-11-17 08:11:57 | 2020-11-17 08:11:59 | synced |
|    809 | test              | t                                       | truncate table | public       |         1 |      799 |         0 | 2020-11-16 15:55:48 | 2020-11-16 15:55:48 | synced |
|    807 | test              | t                                       | recover table  | public       |         1 |      799 |         0 | 2020-11-16 15:23:16 | 2020-11-16 15:23:18 | synced |
|    806 | test              | t                                       | drop table     | none         |         1 |      799 |         0 | 2020-11-16 15:23:03 | 2020-11-16 15:23:03 | synced |
|    805 | xxxtidb_ptest | xxx_report_data_5_202011     | truncate table | public       |       502 |      802 |         0 | 2020-11-16 14:08:14 | 2020-11-16 14:08:15 | synced |
|    803 | xxxtidb_ptest | xxx_report_data_5_202011     | create table   | public       |       502 |      802 |         0 | 2020-11-16 13:59:40 | 2020-11-16 13:59:40 | synced |
|    801 | xxxtidb_ptest | xxx_report_data_5_202011_bak | rename table   | public       |       502 |      745 |         0 | 2020-11-16 13:59:35 | 2020-11-16 13:59:36 | synced |
+--------+-------------------+-----------------------------------------+----------------+--------------+-----------+----------+-----------+---------------------+---------------------+--------+
10 rows in set (0.11 sec)
  • 确认数据的有效性
    通过上面方法我们可以确认drop 库的操作是在 2020-11-17 08:26:36 ,那么我们需要这个时间点的前几秒的快照应该就有被我们删除的库,通过 set @@tidb_snapshot="xx-xx-xx xx:xx:xx"; 设置当前session查询该历史快照数据
MySQL [xxx_ods]>  set @@tidb_snapshot="2020-11-17 08:26:35";
Query OK, 0 rows affected (0.06 sec)

MySQL [xxx_ods]> use xxx_ods
Database changed
MySQL [xxx_ods]> select count(*) from xxx_comment;
+----------+
| count(*) |
+----------+
|    83970 |
+----------+
1 row in set (0.10 sec)
  • 备份数据
dumpling -h 172.21.xx.xx -P 4000 -uroot -p xxx  -t 32 -F 64MiB  -B xxx_ods  --snapshot "2020-11-17 08:26:35" -o ./da
  • 恢复数据
myloader -h 172.21.xx.xx -u root -P 4000 -t 32 -d ./da  -p xxx

误truncate table

  • 模拟truncate table
MySQL [xxx_ods]> select count(*) from xxx_comment ;
+----------+
| count(*) |
+----------+
|    83970 |
+----------+
1 row in set (0.16 sec)

MySQL [xxxr_ods]> truncate xxx_comment ;
Query OK, 0 rows affected (1.16 sec)

MySQL [xxx_ods]> select count(*) from xxx_comment ;
+----------+
| count(*) |
+----------+
|        0 |
+----------+
1 row in set (0.03 sec)
  • 确认操作时间
    通过 admin show ddl jobs 确认truncate的操作的开始时间
  • 确认数据有效性
    通过上面方法我们可以确认truncate的操作是在 2020-11-17 09:25:04 ,那么我们需要这个时间点的前几秒的快照应该就有被我们删除的库,通过 set @@tidb_snapshot="xx-xx-xx xx:xx:xx"; 设置当前session查询该历史快照数据
MySQL [xxxr_ods]> set @@tidb_snapshot="2020-11-17 09:25:03";
Query OK, 0 rows affected (0.06 sec)

MySQL [xxx_ods]> select count(*) from xxx_comment ;
+----------+
| count(*) |
+----------+
|    83970 |
+----------+
1 row in set (0.20 sec)

  • 找回数据
    如果是4.0及以上版本可以使用flashback table恢复数据,具体介绍查看flashback table工作原理
    如果是3.0及以前的版本,可以使用dumpling把误操作的数据找回来

4.0及更高的版本数据找回

MySQL [xxx_ods]> FLASHBACK TABLE xxx_comment TO  xxx_comment_bak_20201117;
Query OK, 0 rows affected (2.09 sec)

MySQL [xxx_ods]> select count(*) from  xxx_comment_bak_20201117;
+----------+
| count(*) |
+----------+
|    83970 |
+----------+
1 row in set (0.11 sec)

3.0及以前版本的数据找回

dumpling -h 172.21.xx.xx -P 4000 -uroot -p xxx  -t 32 -F 64MiB  -B xxx_ods -T  xxx_ods.xxx_comment --snapshot "2020-11-17 09:25:03" -o ./da
  • 恢复数据
    1.如果线上的这张表没有新数据写入或者新数据可以不要,那么可以这样操作:

对于FLASHBACK TABLE找回的数据恢复方法如下:

drop table xxx_comment ;
rename table xxx_comment_bak_20201117 to xxx_comment;

对于dumpling找回的数据恢复方法如下:

myloader -h 172.21.xx.xx -u root -P 4000 -t 32 -d ./da  -p xxx

2.如果线上的表还在继续有新数据写入并且不能破坏,那么可以把恢复出来的临时表的数据在写会到线上表:

insert into xxx_comment  select * from xxx_comment_bak_20201117;

上面这个操作可能会由于超过tidb的单个事物大小限制报错,可以拆分成多个小的inster分批进行

误drop table

  • 模拟drop table
MySQL [xxx_ods]> select count(*) from xxx_comment  ;
+----------+
| count(*) |
+----------+
|    83970 |
+----------+
1 row in set (0.14 sec)

MySQL [xxx_ods]> show tables;
+--------------------------+
| Tables_in_xxx_ods |
+--------------------------+
| xxx_comment   |
+--------------------------+
1 row in set (0.00 sec)

MySQL [xxxr_ods]> drop table xxx_comment ;
Query OK, 0 rows affected (2.03 sec)

  • 确认操作时间
    通过 admin show ddl jobs 确认truncate的操作的开始时间
MySQL [xxx_ods]> admin show ddl jobs 1;
+--------+----------------+------------------------+------------+--------------+-----------+----------+-----------+---------------------+---------------------+--------+
| JOB_ID | DB_NAME        | TABLE_NAME             | JOB_TYPE   | SCHEMA_STATE | SCHEMA_ID | TABLE_ID | ROW_COUNT | START_TIME          | END_TIME            | STATE  |
+--------+----------------+------------------------+------------+--------------+-----------+----------+-----------+---------------------+---------------------+--------+
|    825 | xxx_ods | xxx_comment | drop table | none         |       816 |      818 |         0 | 2020-11-17 10:04:50 | 2020-11-17 10:04:51 | synced |
+--------+----------------+------------------------+------------+--------------+-----------+----------+-----------+---------------------+---------------------+--------+
1 row in set (0.08 sec)
MySQL [xxx_ods]> RECOVER TABLE xxx_comment ;
Query OK, 0 rows affected (3.28 sec)

MySQL [xxx_ods]> select count(*) from xxx_comment  ;
+----------+
| count(*) |
+----------+
|    83970 |
+----------+
1 row in set (0.09 sec)

上面是通过表名恢复的,这种方式有两个前提条件:
最近 DDL JOB 历史中找到的第一个 DROP TABLE 操作,且 DROP TABLE 所删除的表的名称与 RECOVER TABLE 语句指定表名相同 ,如果这个表执行过多次删除再建的操作,你想恢复到第一次的删除操作之前的数据,可以通过 RECOVER TABLE BY JOB 827; 恢复,其中827是通过 admin show ddl jobs ; 确认的job id。

误 delete、update表

  • 模拟误操作
ySQL [xxxr_ods]> select count(*) from xxx_comment ;
+----------+
| count(*) |
+----------+
|    83970 |
+----------+
1 row in set (0.06 sec)

MySQL [xxx_ods]> delete from xxx_comment ;
Query OK, 83970 rows affected (5.82 sec)

MySQL [xxx_ods]> select count(*) from xxx_comment ;
+----------+
| count(*) |
+----------+
|        0 |
+----------+
1 row in set (0.09 sec)

  • 确认操作时间
    对于DML的误操作,如图Tidb集群没开启全日志,基本没办法从集群层面确认误操作时间了,需要从误操作发起端介入排查了。如果是管理员命令行误操作,可以通过堡垒机的操作记录去人;如果是程序bug可以通过排查程序的日志确认执行误操作的时间
  • 确认数据的有效性
    通过上面方法我们可以确认误操作是在 2020-11-17 10:28:25 ,那么我们需要这个时间点的前几秒的快照应该就有被我们删除的库,通过set @@tidb_snapshot=“xx-xx-xx xx:xx:xx”; 设置当前session查询该历史快照数据
set @@tidb_snapshot="2020-11-17 10:28:25";
Query OK, 0 rows affected (0.05 sec)

MySQL [xxxr_ods]> select count(*) from xxx_comment ;
+----------+
| count(*) |
+----------+
|    83970 |
+----------+
1 row in set (0.11 sec)

在实际的工作中,上面步骤确认操作时间可能并不是那么容易,所以我们可能需要不断的通过从大到小范围的set @@tidb_snapshot=“xx-xx-xx xx:xx:xx”; 来尝试,最终和RD确认一个最靠近的时间点。

  • 备份数据
    通过dumpling把上面确定的时间点的快照数据备份出来
dumpling -h 172.21.xx.xx -P 4000 -uroot -p xxx  -t 32 -F 64MiB  -B xxx_ods -T  xxx_ods.xxx_comment --snapshot "2020-11-17 09:55:00" -o ./da
  • 恢复数据
    把上面备份的数据导入到一个临时实例里面,确认下数据没问题可以在临时实例里面把这个表重命名,然后导入到线上库,最后把数据合并到线上的表里面
myloader -h 172.21.xx.xx -u root -P 4001 -t 32 -d ./da  -p xxx

更多学习和交流可以关注我公众号:
image.png

4 Likes

感谢分享:+1:

这个文章 必须点赞


你好,我的tidb版本是4.0.8,我尝试了这个方法,但是执行这个会报错,并没有像你实验显示的这样
image
请问这是什么问题