问题产生

在线上主从同步的slave数据库中产生了如下的错误:

1
2
ERROR 1787 (HY000): When @@GLOBAL.ENFORCE_GTID_CONSISTENCY = 1, the statements CREATE TEMPORARY TABLE and 
DROP TEMPORARY TABLE can be executed in a non-transactional context only, and require that AUTOCOMMIT = 1.

使用show slave status可以查看从库当前的同步状态,发现I/O状态已经变为NO,尝试使用:

1
set global sql_slave_skip_counter = 1;

之后产生如下的问题:

1
ERROR 1858 (HY000): sql_slave_skip_counter can not be set when the server is running with @@GLOBAL.GTID_MODE = ON. Instead, for each transaction that you want to skip, generate an empty transaction with the same GTID as the transaction

根据错误说明使用一个空事务来跳过异常,代码如下:

1
2
3
4
5
STOP SLAVE;
SET GTID_NEXT=# binlog中事务的下一个位置;
BEGIN; COMMIT; # 产生一个空事务并提交
SET GTID_NEXT="AUTOMATIC";
START SLAVE;

然而这样会导致主从的数据可能产生不一致的情况,这是我们想要避免的,并且希望主库可以不锁表导出数据即可进行备份.

Xtrabackup

查看阿里云的官方文档:RDS for MySQL 物理备份文件恢复到自建数据库发现可以使用Xtrabackup来进行恢复.

操作系统中已安装数据恢复工具Percona XtraBackup。MySQL 5.6及之前的版本需要安装 Percona XtraBackup 2.3。MySQL 5.7版本需要安装 Percona XtraBackup 2.4。可以从Percona XtraBackup官网下载安装,安装指导请参见官方文档 Percona XtraBackup 2.3、Percona XtraBackup 2.4。

Xtrabackup的单表恢复

但是上面的官方文档中仍旧有一个问题,在文档中写到:

说明 由于软件限制,目前只支持将云数据库MySQL的备份文件恢复到安装在Linux系统中的自建MySQL数据库中。

而在恢复数据的时候,我们想要只对业务用到的数据库进行恢复,mysql的配置信息库不希望恢复到从库中去(由于主从用户信息等都不相同).也就是说从备份的源上无法进行库的指定(Xtrabackup支持表的指定但是阿里云RDS没有这个选项).

在查看了Xtrabackup网站上的教程之后,发现Xtrabackup可以单独对一张表进行恢复,即在阿里云文档的:

1
2
3
4
## 解包
cat <数据备份文件名>_qp.xb | xbstream -x -v -C /home/mysql/data
## 解压
innobackupex --decompress --remove-original /home/mysql/data

操作也就是解压了需要同步的数据之后:

创建新表

使用sql语句创建对应需要恢复的表(没有数据库就先创建数据库):

1
2
3
4
5
6
7
8
9
10
11
-- 官方例子中用到的表
mysql> CREATE TABLE `name_p4` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` text NOT NULL,
`imdb_index` varchar(12) DEFAULT NULL,
`imdb_id` int(11) DEFAULT NULL,
`name_pcode_cf` varchar(5) DEFAULT NULL,
`name_pcode_nf` varchar(5) DEFAULT NULL,
`surname_pcode` varchar(5) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2812744 DEFAULT CHARSET=utf8

删除表空间

1
mysql>  ALTER TABLE name_p4 DISCARD TABLESPACE;

拷贝数据

1
2
3
# 将对应的数据库的对应的表(备份数据目录下)拷贝到mysql文件夹下同数据库的同表目录下
$ cp /mnt/backup/2012-08-28_10-29-09/imdb/name#P#p4.exp /var/lib/mysql/imdb/name_p4.exp
$ cp /mnt/backup/2012-08-28_10-29-09/imdb/name#P#p4.ibd /var/lib/mysql/imdb/name_p4.ibd

导入表空间

1
mysql>  ALTER TABLE name_p4 IMPORT TABLESPACE;

导入指定的库

知道了上面的对单表导入的原理,那么对指定的库的数据导入过程其实就是重复导入表的过程直至全部的表都恢复到库中为止.

mysqlfrm介绍

mysqlfrm 是一个恢复性质的工具,用来读取.frm文件并从该文件中找到表定义数据生成CREATE语句。在大多数情况下,生成的CREATE语句用于在另一个服务器上创建表或进行诊断等。

我们使用mysqlfrm来生成数据表创建语句.

恢复单库的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
#!/bin/bash
# 注意:只能用在InnoDB表引擎的数据库上
# Usage: inno_restore_database.sh <db_name_to_restore_to> <db_backup_directory> 末尾不需要 /

# mysql数据库磁盘位置
datadir=/var/lib/mysql
myport=3306
# 随意指定
mysqlfrmport=3310
#username=dbrestoredude
#password=dbrestoredudespasswd

#######################################################################################
# Gotta be root.
if [ $UID -ne 0 ]; then echo "Run this as root" ; exit ; fi

database=$1
restoredir=$2

# mysqlfrm检查
which mysqlfrm > /dev/null 2>&1
if [ $? -ne 0 ] ; then echo "Could not find the mysqlfrm utility. Install mysql-utilities."; exit ; fi
# mysql目录检查
if [ ! -f $datadir/mysql/user.frm ] ; then echo "MySQL datadir not correct" ; exit ; fi
# Check the restore directory, looking for a cfg/exp/idb for each frm.
if [ ! -f $restoredir/db.opt ] ; then echo "Restore directory invalid, couldn't find db.opt in it"; exit ; fi
stoperror=0
for restorename in $restoredir/*.frm
do
chkname=$(echo $restorename|sed s/.frm$//)
for exten in cfg exp ibd
do
if [ ! -f $chkname.$exten ] ; then stoperror=1 ; fi
done
done
if [ $stoperror -eq 1 ] ; then
echo "Could not file valid restore directory files (need a cfg, exp and ibd for each frm)"
echo "Did you specify a valid database directory within a backup?"
echo "Did you prepare or apply-log to the backup directory?"
exit
fi

# Get username and password if the fields are blank.
if [ -z $username ] ; then read -p "Username: " username ; fi
if [ -z $password ] ; then read -s -p "Password: " password ; echo ; fi
# Check mysql permissions for the given user.
stoperror=0
grants=$(mysql -B -u $username -p$password mysql -e "show grants for current_user"|grep 'ON *.* TO')
if [ $? -ne 0 ] ; then exit ; fi
if [[ $grants == *"ALL PRIVILEGES"* ]] ; then stoperror=1 ; fi
if [[ $grants == *CREATE* ]] && [[ $grants == *DROP* ]] && [[ $grants == *ALTER* ]] ; then stoperror=1 ; fi
if [ $stoperror != 1 ] ; then echo "User $username does not have global CREATE, DROP and ALTER" ; exit ; fi

################################################################################
# 删除存在的数据库
mysql -B -u $username -p$password -e "DROP DATABASE IF EXISTS $database"
# 创建数据库
mysql -B -u $username -p$password -e "CREATE DATABASE $database"
# get directory for the original DB name
backupdb=$(find $restoredir -maxdepth 0 -type d -printf "%f\n" |cut -d '/' -f 1)

# 使用mysqlfrm来生产建表语句并且执行 流程:
# mysqlfrm生成语句-管道->mysql
# | sed s/'ENGINE=InnoDB' / 'ENGINE=InnoDB ROW_FORMAT=compact' | 用来指定行格式
echo "Importing create table statements from frm files..." ; echo
mysqlfrm -q --user=root --server=$username:$password@localhost:$myport --port=$mysqlfrmport $restoredir |
grep -vE "^#|WARNING: Using a password on the command line interface can be insecure." |
sed s/^$/';'/ | sed s/^'CREATE TABLE `'$backupdb/'CREATE TABLE `'$database/ |
mysql -B -u $username -p$password
echo "Table structure imported."

# 主要代码,循环执行删除表空间,移动文件,恢复表空间
for frmname in $restoredir/*.frm
do
tablename=$(find $frmname -printf "%f\n"|sed s/.frm$//)
# ALTER TABLE ... DISCARD TABLESPACE
mysql -B -u $username -p$password $database -e "ALTER TABLE $tablename DISCARD TABLESPACE"
for exten in exp ibd #cfg :: exp ibd cfg 文件取决于数据库版本
do
which rsync > /dev/null 2>&1
if [ $? -eq 0 ]
then
rsync --progress $restoredir/$tablename.$exten $datadir/$database/$tablename.$exten
else
\cp -v $restoredir/$tablename.$exten $datadir/$database/$tablename.$exten
fi
chown $(find $datadir/$database/$tablename.frm -printf "%u.%g") $datadir/$database/$tablename.$exten
done
# ALTER TABLE ... IMPORT TABLESPACE
mysql -B -u $username -p$password $database -e "ALTER TABLE $tablename IMPORT TABLESPACE"
done
echo "All done"

注意

  1. 一开始由于部分表仍旧使用MyISAM引擎导致行格式不一致而使得表空间更改语句不生效,而MyISAM表也不支持compact,所以从库全部使用的InnoDB作为存储引擎.
  2. 如果数据库用户配置比较复杂,可以优先尝试mysql相关命令是否生效,不生效则进行调整.

启动主从同步

在启动之前,需要设置GTID相关信息(仅限于自己的生产服务器,文档中没有提到):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
slave1> reset master;
slave1 > show global variables like 'GTID_EXECUTED';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| gtid_executed | |
+---------------+-------+
slave1 > set global GTID_PURGED="9a511b7b-7059-11e2-9a24-08002762b8af:1-14";
slave1> start slave io_thread;
slave1> show slave status\G
[...]
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
[...]

然后使用CHANGE MASTER语句来进行主从同步设置.

参见How to create/restore a slave using GTID replication in MySQL 5.6

参考连接

xtrabackup restore specific database from a full backup(单库的恢复方法)

MySQL管理工具MySQL Utilities — mysqlfrm

Backing Up and Restoring Individual Partitions

14.3 InnoDB的多版本

InnoDB是一个多版本存储引擎:它会保留关于行变动的旧版本信息用来支持事务特性,例如并发和回滚(rollback).这些信息被称为回滚段(rollback segment),存储在表空间中.InnoDB使用回滚段中的信息来执行事务回滚中所需要的撤销操作.还使用该信息构建早期版本以进行一致的行读取.

在内部,InnoDB为存储在数据库的每一行添加三个字段.一个6位的DB_TRX_ID字段用来标识插入或更新行的最后一个事务.此外,删除在内部被视为更新,行的一个特殊位用来标记是否删除.每一行还包含一个7位的DB_ROLL_PTR字段叫做滚动指针(roll pointer).滚动指针指向写入回滚段的撤消日志记录.如果行被更新了,那么撤消日志记录了用来重建被更新之前的行内容所必要的信息.一个6位的DB_ROW_ID字段包含了一个行ID在插入的时候递增.如果InnoDB自动生成聚簇索引,这个索引会包含行ID的值.否则,DB_ROW_ID列不会出现在任何索引中.

回滚段中的撤销日志分为插入和更新的撤销日志(undo logs).插入的回滚日志只在事务回滚的时候需要并且在事务提交之后可以立即丢弃.更新的撤销日志也被用于一致性读,但是只有在InnoDB没有分配快照的事务之后才能丢弃它们,在一致读取中可能需要更新撤消日志中的信息来构建数据库行的早期版本.

定期提交你的事务,包括那些只有一致性读的事务.否则,InnoDB将不能从更新的撤销日志里丢弃数据使得回滚段变得过大而填满表空间.

回滚段中撤消日志记录的物理大小通常小于相应的插入或更新行.你可以使用此信息计算回滚段所需的空间.

在InnoDB多版本方案中,当你使用sql语句删除行的时候不会马上从数据库中物理删除.InnoDB只在丢弃为删除而写的更新撤销日志的时候才会物理的删除相应的行.这个删除操作叫做清除(purge),它很快,通常采用和sql语句删除时相同的顺序.

如果你在表中以相同的速率执行小批量的插入和删除操作,清除线程可能开始落后,因为那些”死掉”的行,表会变得越来越大,使得一切都受磁盘限制变得很慢.在这种情况下,通过调整innodb_max_purge_lag系统变量来限制新行操作,并为清除线程分配更多资源.

多版本和二级索引

InnoDB的多版本并发控制(MVCC)看待二级索引和聚簇索引是不同的.聚簇索引中的记录更新是立即的,它们的隐藏系统列指向撤消日志条目,从中可以重建早期版本的记录.与聚簇索引记录不同,二级索引记录不包含隐藏的系统列,也不会立即更新.

当一个二级索引列被更新的时候,旧的二级索引记录会被标记删除,新的记录插入,最终清除被标记删除的记录.当一个二级索引记录被标记删除或二级索引页面被一个新事务更新的时候,InnoDB在聚合索引中查找数据库记录.在聚合索引中,记录的DB_TRX_ID会被检查并且记录的正确版本会从撤销日志中恢复如果在读事务开始之后记录被修改的话.

如果二级索引被标记为缺失或者二级索引页被一个新事务更新了,索引覆盖技术不会使用.代替从索引结构中返回值,InnoDB会从聚合索引中查找记录.

然而,如果启用了索引条件下推(ICP)优化并且where条件中的部分可以只用索引字段计算的话,MySQL服务器仍然会下推这一部分的where条件到使用索引计算的存储引擎中去.如果没有找到匹配的记录则聚合索引的查找会被避免.如果找到匹配的记录,即使是被标记删除的记录,InnoDB也会到聚合索引中查找记录.

14.2 InnoDB 和 ACID 模型

ACID模型是一系列的数据库设计准则,强调对业务数据和关键应用非常重要的可靠性方面.MySQL包括了像InnoDB存储引擎这样遵守ACID模型的组件所以数据不会因为例如软件崩溃和硬件故障这样的意外情况而遭到破坏或者丢失.当你依赖ACID相关特性的时候,你不需要重新发明一致性检查和崩溃恢复机制.如果你有其他附加的软件保障,高可用的硬件或者一个可以容忍小部分数据的丢失或者不一致的话,你可以调整MySQL设置用ACID可靠性来交换更好的性能和吞吐量.

下面讨论MySQL的特性尤其是InnoDB存储引擎是如何与ACID模型关联的.

  • A: 原子性(atomicity).
  • C: 一致性(consistency).
  • I: 隔离性(isolation).
  • D: 持久性(durability).

原子性

主要涉及InnoDB事务,相关MySQL特性:

  • 自动提交(autocommit) 设置
  • COMMIT子句
  • ROLLBACK子句
  • INFORMATION_SCHEMA表中的操作数据

一致性

主要涉及InnoDB内部保护数据,相关特性:

  • InnoDB 双写缓存(doublewrite buffer)
  • InnoDB 崩溃恢复

隔离性

主要涉及InnoDB事务,特别是每个事务的隔离级别,相关特性:

  • 自动提交(autocommit) 设置
  • SET ISOLATION LEVEL子句
  • InnoDB锁的底层细节

持久性

主要涉及到和MySQL所在物理硬件相关配置,相关特性:

  • InnoDB 双写缓存(doublewrite buffer)
  • innodb_flush_log_at_trx_commit 配置选项
  • sync_binlog 选项
  • innodb_file_per_table 选项
  • 存储设备的写入缓存,例如磁盘驱动,SSD,RAID
  • 存储设备的电池备份
  • 用来运行MySQL的操作系统
  • 不间断电源服务(UPS)用来保护MySQL服务器
  • 你的备份策略
  • 对于分布式应用取决于MySQL服务器在数据中心扮演的角色和数据中心之间的网络连接.
0%