一、背景
实际生产中,发现mysql查询性能存在抖动,同样的sql,正常执行时间是秒级,但是偶尔会有执行上百秒的情况出现,经过DBA的排查,并没有发现mysql的问题。考虑迁移一部分生成数据到PG中进行测试。(ps~个人觉得这个迁移背景有点牵强,还是应该先定位性能抖动的原因比较好)
二、迁移方案
迁移的大致步骤如下:
从生产环境的mysql备份中拉取一个备份出来
在测试机上通过备份恢复生产库
导出mysql的表定义和数据
通过自己开发的小工具,将mysql表定义语法转换至PG的表定义语法
在PG中创建表
将数据导入PG
三、迁移步骤说明
3.1 拉取备份
这个没什么好说的,scp指定的备份文件到测试机即可
考虑是生产环境,有防火墙和权限等的限制,可以临时创建临时用户tmp,关闭防火墙,待拷贝完成,删除用户,重启防火墙
3.2 恢复生产库
生产上通过xtrabackup做的备份,恢复方法这里就不啰嗦了,不是本次的重点,自行百度~
3.3 导出mysql的表定义和数据
从这步开始就有坑了~
首先,导出表定义(只贴出测试数据)
# 将名为test_db的库中所有的ddl都导出到test_db.sql文件中
# 导出的定义以sql语句的形式写入文件
[mysql@sndsdevdb01 ~]$ mysqldump -h127.0.0.1 -uroot -ppassword -d test_db > /mysql/test_db.sql
[mysql@sndsdevdb01 ~]$ cat /mysql/test_db.sql
...
/* 下面是导出的表定义部分 */
DROP TABLE IF EXISTS `tb1`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `tb1` (
`c1` int(11) DEFAULT NULL,
`c2` char(5) DEFAULT NULL,
`c3` varchar(10) DEFAULT NULL,
`c4` datetime DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
/*!40101 SET character_set_client = @saved_cs_client */;
SET @@SESSION.SQL_LOG_BIN = @MYSQLDUMP_TEMP_LOG_BIN;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
...
导出表定义是为了之后人工检查mysql到PG的ddl语法转换的正确性
实际实施时,利用小工具直接连接mysql服务器即可完成mysql到PG的ddl语法转换
关于小工具的说明,请见附录~
然后,导出数据
考虑到数据格式,编码的问题,决定统一将数据导出为UTF8编码的csv文件
为了说明坑的地方,我插入了5条记录
mysql> delete from tb1;
Query OK, 3 rows affected (0.01 sec)
mysql> insert into tb1 values(1,'qqq','www',current_time);
Query OK, 1 row affected (0.02 sec)
mysql> insert into tb1 values(1,'qq\nq','www',current_time);
Query OK, 1 row affected (0.01 sec)
mysql> insert into tb1 values(1,'qq\r\nq','www',current_time);
Query OK, 1 row affected (0.00 sec)
mysql> insert into tb1 values(1,'qqq','www','0000-00-00 00:00:00');
Query OK, 1 row affected (0.00 sec)
mysql> insert into tb1 values(1,'qqq','www',null);
Query OK, 1 row affected (0.00 sec)
mysql> select * from tb1;
+------+-------+------+---------------------+
| c1 | c2 | c3 | c4 |
+------+-------+------+---------------------+
| 1 | qqq | www | 2017-07-14 17:36:25 |
| 1 | qq
q | www | 2017-07-14 17:36:30 |
| 1 | qq
q | www | 2017-07-14 17:36:36 |
| 1 | qqq | www | 0000-00-00 00:00:00 |
| 1 | qqq | www | NULL |
+------+-------+------+---------------------+
5 rows in set (0.00 sec)
mysql> select * from tb1 into outfile '/mysql/tb1.csv' fields terminated by ',' optionally enclosed by '"' escaped by '"' lines terminated by '\n';
其中第二条和第三条中,c2列分别包含了换行符和windows的特殊换行符
然后再通过vi 打开tb1.csv
1,"qqq","www","2017-07-14 17:36:25"
1,"qq"
q","www","2017-07-14 17:36:30"
1,"qq^M"
q","www","2017-07-14 17:36:36"
1,"qqq","www","0000-00-00 00:00:00"
1,"qqq","www","N
坑点如下
- \n换行符导致原本的一条记录分为2行
- \r是特殊字符,vi模式下就表示为^M
- datetime类型可以存储”0000-00-00 00:00:00″,但是官方手册上datetime的合法范围是’1000-01-0100:00:00′ to ‘9999-12-31 23:59:59’,感觉是bug。。
- NULL值会被转义为”N的形式
1和2两点,导致csv格式混乱,导入PG会出错;datetime对应PG的timestamp类型,而”0000-00-00 00:00:00″是不符合PG的时间戳类型的合法范围的;PG也不认识”N表示的NULL。。。
由于上述的坑都是在将数据导入PG的时候才发现的,所以我的做法是通过shell的sed,awk等命令,去人工替换这些内容。因为生产数据量很大,一个库大概200G,磁盘空间有限,加上导出数据需要较长时间,所以尽量不重复导数据
但是用shell处理大文件,效率也很低,150G的csv文件,遍历sed多次,往往超过1小时,而且存在正则表达式写的不精确,匹配出错的情况
所以我个人推荐,select导出数据时,通过where条件过滤,用replace函数将需要处理的列直接处理掉,可以省去后面的麻烦,但是前提条件是需要知道有哪些列存在这些问题(生成中的表往往列很多,几十甚至几百列)
3.3 在PG中创建表并导入数据
首先创建相应的业务库
postgres=# create database test_db;
CREATE DATABASE
postgres=# \c test_db
You are now connected to database "test_db" as user "postgres".
postgres=#\i /pgsql/pg.sql
# 执行转换后的ddl,定义表
...
postgres=#\copy tb1 from '/pgsql/tb1.csv' with(format csv,encoding 'UTF8',NULL 'null')
# 通过copy命令导入数据,通过指定NULL字符串来识别NULL值
如果导入过程不出现任何错误,那说明数据的迁移基本就完成了
3.4 其他
上述内容只是单纯的业务库的数据迁移,如果想完整的把整个业务系统迁移至PG,还有很多的别的迁移工作
例如表的索引
PG提供了丰富的索引类型,索引详情参考:
PG 9.6 手册 http://www.postgres.cn/docs/9.6/indexes.html
需要根据业务需求重新定制,例如AP型业务,gin索性就有很大的优势,除此之外,业务定义的存储过程,上层的增删改查接口等等也需要修改
另外,数据库的备份方案,日志归档设置,高可用方案的设计这些也需要定制
附录
关于DDL语法转换的小工具
- 功能简述
将mysql的表定义转换为PG对应的语法。主要完成数据类型的映射,列属性语法的转换,主键和部分类型索引的转换
1.1. 类型映射
case "tinyint":
case "tinyint unsigned":
case "smallint":
if (col_is_auto_increment.equals("YES")){//increment type
mysql_type.add("smallserial");
}else{
mysql_type.add("smallint");
}
break;
case "mediumint":
case "smallint unsigned":
case "mediumint unsigned":
case "integer":
case "int":
if (col_is_auto_increment.equals("YES")){//increment type
mysql_type.add("serial");
}else{
mysql_type.add("int");
}
break;
case "int unsigned":
case "bigint":
if (col_is_auto_increment.equals("YES")){//increment type
mysql_type.add("bigserial");
}else{
mysql_type.add("bigint");
}
break;
case "bigint unsigned":
mysql_type.add("decimal");
mysql_type.add("20");
mysql_type.add("0");
break;
case "double":
mysql_type.add("double precision");
break;
case "decimal":
mysql_type.add("decimal");
mysql_type.add(precision.toString());
mysql_type.add(scale.toString());
break;
case "float":
mysql_type.add("real");
break;
case "binary":
case "char":
mysql_type.add("char");
mysql_type.add(precision.toString());
break;
case "varbinary":
case "varchar":
mysql_type.add("varchar");
mysql_type.add(precision.toString());
break;
case "tinyblob":
case "mediumblob":
case "longblob":
case "blob":
mysql_type.add("bytea");
break;
case "date":
mysql_type.add("date");
break;
case "datetime":
case "year":
case "timestamp":
mysql_type.add("timestamp");
break;
case "time":
mysql_type.add("time");
break;
/*case "bit":
pg_type.add("bit");
break;*/
case "tinytext":
case "text":
case "mediumtext":
case "longtext":
mysql_type.add("text");
break;
default:
mysql_type.add("This type may be user deifned type,confirm for yourself please!");
break;
1.2. 列属性
* not null属性
* column注释
* 自增属性
1.3. 索引
统一将mysql的索引转换为PG的btree索引,这个在应用中意义不大,因为多数情况,索引是需要根据业务需求重新定义的
- 实现方式
通过JDBC连接mysql服务器,通过元数据(metadata)获取所有的表名,列名以及列的数据类型等等信息,然后在程序中做转换,最后写入sql文件
思考
其实这只是简单的迁移方案,目前也有一些商用或者开源的迁移工具,例如:
mysql2pg:https://sourceforge.net/projects/mysql2pg/
另外,关于迁移数据,用csv文件的方式,对磁盘空间的要求较高,而且有上述字符格式的问题。其实还可以考虑PG的插件mysql_fdw,可以直接用select into的方式将数据直接插入PG中,可以省去中间导出的步骤。但是9.6的PG,对foreign table的语法支持不完善,不支持like的方式建表,所以对宽表,create foreign table写起来就比较麻烦,可以考虑用脚本自动化。
另外,生产中往往mysql和PG不在一台机器上,mysql_fdw拉取和插入数据的效率还有待测试。我初步的尝试发现,速度是很慢的,不过没有深入调查原因,有可能是网络问题,也有可能是配置问题
mysql_fdw的说明参考德哥的博客:http://blog.163.com/digoal@126/blog/static/163877040201493145214445/