MySQL · 2017-09-23 1

mysqlpump两个疑问点

一.先扯几句
关于mysqlpump,相信大多数同学都已经玩得溜溜的。很多大牛已经写了华丽文章赞许,我就不多美言了。

关于这个新玩意,虽然官方号称,并行备份,牛X,但是大家还是有很多疑问:
1.这个并行的东西为啥不像oracle expdp一样,导出来多个文件,而是一个文件?那么导进去岂不是又回到了单线程?
2.导出来写到一个文件里面,那么磁盘写入会不会是一个瓶颈呢?
3.还有,大家都说这个东西没办法保证数据一致性,这个不是很坑爹嘛,数据都不一致了,并行有什么用?
4.据说导出来还没有pos点,如果想用这个玩意儿搭建个备库,岂不是奢望?

这些疑问我刚开始也有,下面一一列出我的见解:
1.虽然mysqlpump导出来是单文件,导进去又是单线程,导进去岂不是很慢。我们要知道备份的需求,一般来讲,用户都会选择在晚上备份,而且晚上数据库一般都要承担跑批任务,一般都将备份和其他批作业错开,所以备份可以提速,就为其他任务赢取了宝贵时间。个人认为对于mysql来讲,导出提速的诉求大于导入提速的诉求。有同学可能会说,导进去单线程慢,怎么办?一般来讲,如果你对数据迁移有时间要求,那么都会选择xtrbackup进行物理备份传输,直接拷贝文件比mysqlpump这种逻辑备份快很多,几乎可以跑到你顺序IO极限。
2.关于写入一个文件会不会称为瓶颈,亲测,远程备份,普通的千兆网卡,可以跑满,还有这种备份文件的写入都是顺序IO,普通机械盘都可以达到100MB/s,再则如今机械盘都做了RAID,不再是一块机械盘在战斗,如果你感觉机械盘不能满足,还有SATA 口SSD,或者nvme接口的,或者3D XPOINT。亲,不要担心写不下去,如今的存储已经今非昔比,目前市场上比较流行的豪配存储,都是infiniband搭配全闪存,超百万IOPS,IO延时几乎和本地IO一样。
关于第三第四个问题,这个只有亲测,实验,看代码才能有说服力。

二.导出来到底有没有POS点
关于这个问题,做做实验不就知道了吗?首先起一个8.0.3的进程(用用最新版,很舒服的):

[mysql]
user=root
[mysqld]
datadir=/home/xxx/mysql-8.0.2/data/
socket=/home/xxx/mysql-8.0.2/data//mysql.sock
user=mysql
# Disabling symbolic-links is recommended to prevent assorted security risks
symbolic-links=0
pid-file=/home/xxx/mysql-8.0.2/data//mysqld.pid
log-error=/home/xxx/mysql-8.0.2/data//mysqld.log
log_bin=mysql_px
server-id=1

建一张表,插一行数据,备份搞起来:

CREATE TABLE `user1` (
`user_name` char(30) NOT NULL,
`pwd` varchar(20) NOT NULL,
`age` smallint(6) DEFAULT '12',
`accout` int(11) DEFAULT NULL,
`birthday` timestamp NULL DEFAULT NULL,
`article` text,
`height` float DEFAULT NULL,
PRIMARY KEY (`user_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
insert into user1(user_name,pwd,age,accout) values('lujianxing','ttttttt',9,9797)

好了,直接用mysqlpump搞一把,看看有没有pos点相关信息,或者GTID嘛。

mysqlpump -uload_data -pload_data -h10.210.181.82 -P3306 --single-transaction --default-character-set=utf8 --default-parallelism=2 -B mypy > ~/mysqlpump.sql

我们看看导出来的是些什么东西呢?

-- Dump created by MySQL pump utility, version: 8.0.2-dmr, linux-glibc2.12 (x86_64)
-- Dump start time: Fri Sep 22 23:50:13 2017
-- Server version: 8.0.2

SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0;
SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0;
SET @OLD_SQL_MODE=@@SQL_MODE;
SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO";
SET @@SESSION.SQL_LOG_BIN= 0;
SET @OLD_TIME_ZONE=@@TIME_ZONE;
SET TIME_ZONE='+00:00';
SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT;
SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS;
SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION;
SET NAMES utf8;
CREATE DATABASE /*!32312 IF NOT EXISTS*/ `mypy` /*!40100 DEFAULT CHARACTER SET utf8mb4 */;
CREATE TABLE `mypy`.`user1` (
`user_name` char(30) NOT NULL,
`pwd` varchar(20) NOT NULL,
`age` smallint(6) DEFAULT '12',
`accout` int(11) DEFAULT NULL,
`birthday` timestamp NULL DEFAULT NULL,
`article` text,
`height` float DEFAULT NULL,
PRIMARY KEY (`user_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
;
INSERT INTO `mypy`.`user1` VALUES ("lujianxing","ttttttt",0,97999,NULL,NULL,NULL);
SET TIME_ZONE=@OLD_TIME_ZONE;
SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT;
SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS;
SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION;
SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS;
SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS;
SET SQL_MODE=@OLD_SQL_MODE;
-- Dump end time: Fri Sep 22 23:50:14 2017

真的是望眼欲穿, 没有POS点相关信息,也没有GTID的任何信息,怎么办?要不先看看帮助或者官方文档,应该有提到到底支持不支持GTID或者POS点记录。
看到官方文档这里:
4.5.6 mysqlpump — A Database Backup Program

--set-gtid-purged=value

This option enables control over global transaction ID (GTID) information written to the dump file, by indicating whether to add a SET @@global.gtid_purged statement to the output. This option may also cause a statement to be written to the output that disables binary logging while the dump file is being reloaded.

The following table shows the permitted option values. The default value is AUTO.

Value   Meaning
OFF Add no SET statement to the output.
ON  Add a SET statement to the output. An error occurs if GTIDs are not enabled on the server.
AUTO    Add a SET statement to the output if GTIDs are enabled on the server.
The --set-gtid-purged option has the following effect on binary logging when the dump file is reloaded:

--set-gtid-purged=OFF: SET @@SESSION.SQL_LOG_BIN=0; is not added to the output.

--set-gtid-purged=ON: SET @@SESSION.SQL_LOG_BIN=0; is added to the output.

--set-gtid-purged=AUTO: SET @@SESSION.SQL_LOG_BIN=0; is added to the output if GTIDs are enabled on the server you are backing up (that is, if AUTO evaluates to ON).

很清楚很明了,打开GTID模式,并且,要在参数里面加上--set-gtid-purged=ON。加上GTID模式起库,然后再备一次,配置文件加上:

gtid_mode = on
enforce_gtid_consistency = 1

重启后,加上--set-gtid-purged=ON 再来备份一次,应该会成功了吧。

mysqlpump -uload_data -pload_data -h10.210.181.82 -P3306 --single-transaction --default-character-set=utf8  --default-parallelism=2 --set-gtid-purged=ON  -B mypy > ~/mysqlpump.sql
mysqlpump: [Warning] Using a password on the command line interface can be insecure.
mysqlpump: [ERROR] (1) Server has GTIDs disabled.

Dump process encountered error and will not continue.

shit,居然报错,什么鬼,去网上搜一把,大多数表示--set-gtid-purged=OFF就好了,这不扯吗?我就是要GTID,关了还有什么意义。无奈只能翻翻源代码关于这块儿怎么处理的。源码文件在:./client/dump/sql_formatter.cc,有兴趣各位自己翻一翻。
DingTalk20170923001633
代码片段解释:
第一个if很好理解,当备份任务开始的时候,会去检查数据库GTID是否打开,并且存在dump_start_dump_task->m_gtid_mode,命令行对GTID的需求放在 m_options->m_gtid_purged,很容易就知道,如果数据库都没有打开GTID,你还想要备份保存GTID,肯定直接报错。

那么第二个if呢,如果检测到数据库的GTID模式已经打开,并且命令行对于GTID的需求选择是
--set-gtid-purged=ON
或者是
--set-gtid-purged=AUTO
那么进入下面的逻辑

if (!m_mysqldump_tool_options->m_dump_all_databases)
      {
        m_options->m_mysql_chain_element_options->get_program()->error(
          Mysql::Tools::Base::Message_data(1,
          "A partial dump from a server that has GTIDs is not allowed.\n",
          Mysql::Tools::Base::Message_type_error));
        return;
      }

也就是说只有当我们选择备份全库的时候,GTID才有用。好了,解释清楚了,大家不要疑惑了,GTID是针对全库的,如果你只备份部分表,取了GTID也没什么用处,只有当我们备份全库,用在恢复到备库这种场景才会需要GTID,对吧。

三.并行导出来的数据是否是一致的
谈到一致性三个字,各位英雄好汉大有话语权,我就不多言了。用过mysqldump的同学都知道啊,只有innodb才会提供一致性备份啊,innodb备份还不会锁表锁库这些啦,是吧,说到底innodb都是用的undo机制,这里面也略深奥,这里不多言,有兴趣的同学看看圭多大师的文章,需要花费一定的精力理解。

那么mysqlpump并行备出来的数据到底是不是一致的,我们其实只需要知道,mysqlpump和mysql server之间建立的会话是不是拿到的同一个snapshot,说白就是,mysqlpump第一个会话和mysql建立连接到最后一个会话建立连接,数据库数据必须是一致的,这些会话拿到的事务id是同一个,他们读到的数据库任何数据都是一样的,不存在会话A读到一个字段为1,会话B读到同一个字段为2。带着这个理解,我们去翻阅代码,看看mysqlpump是如何建立连接的,代码位置./client/dump/single_transaction_connection_provider.cc:
DingTalk20170923004245
我们来解读一下这段代码:
/* create a pool of connections */这里就代表,开始建立所有会话了,首先看看这个for循环吧,

for (unsigned int conn_count = 1; conn_count <= m_connections; conn_count++) {
}

很简单吧,我们需要创建connections个连接,连接数怎么算出来,可以参考./client/dump/program.cc:int Program::get_total_connections(),这里有详细解释。
具体建立每一个连接,我们干了什么,细看循环体:
首先通过Abstract_connection_provider::create_new_runner创建一个普通连接,这其实就是封装一个工具类,没什么特殊的。

Mysql::Tools::Base::Mysql_query_runner *runner =
                Abstract_connection_provider::create_new_runner(message_handler);

这句代码结束,一个会话就建立了,建立好会话后,又做了什么处理呢,代码注释非常棒,我感觉后面不用解释了,哈哈哈。

        /*
         To get a consistent backup we lock the server and flush all the tables.
         This is done with FLUSH TABLES WITH READ LOCK (FTWRL).
         FTWRL does following:
           1. Acquire a global read lock so that other clients can still query the
              database.
           2. Close all open tables.
           3. No further commits is allowed.
         This will ensure that any further connections will view the same state
         of all the databases which is ideal state to take backup.
         However flush tables is needed only if the database we backup has non
         innodb tables.
        */

自己翻译,我们接着说下面几行,

if (conn_count == 1)
            runner->run_query("FLUSH TABLES WITH READ LOCK");

如果我们建立第一个连接,那么,就执行FLUSH TABLES WITH READ LOCK,加上只读锁,哈哈,懂了吧。对于其他连接(包含第一个连接),显示指定一下,我们是RR隔离级别,而且需要一致性快照读。

        runner->run_query(
                "SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ");
        runner->run_query(
                "START TRANSACTION WITH CONSISTENT SNAPSHOT");

最后一个连接就需要收尾了,你不可能一直锁库吧,最后一个连接,肯定是要释放只读锁啦。

if (conn_count == m_connections)
            m_runner_pool[0]->run_query("UNLOCK TABLES");

那么,一个会话就建立完毕,这个时候,我们把连接放入连接池里面,供后续逻辑使用。

m_runner_pool.push_back(runner);

关于上面几条命令,要说的太多太多,大家有空可以翻阅下官方文档,官方lab等等,很多经典解释。

四.留下一个疑问,大家思考
GTID是在建立会话之前,之中,还是之后取出来的,取到的GTID是否正确?如果在取GTID和建立连接之间发生了数据库写动作,那么取出来的GTID是不是无效?