Mysql 5.6迁移至PostgreSQL 9.6的实践小结

一、背景

实际生产中,发现mysql查询性能存在抖动,同样的sql,正常执行时间是秒级,但是偶尔会有执行上百秒的情况出现,经过DBA的排查,并没有发现mysql的问题。考虑迁移一部分生成数据到PG中进行测试。(ps~个人觉得这个迁移背景有点牵强,还是应该先定位性能抖动的原因比较好)

二、迁移方案

迁移的大致步骤如下:

  1. 从生产环境的mysql备份中拉取一个备份出来

  2. 在测试机上通过备份恢复生产库

  3. 导出mysql的表定义和数据

  4. 通过自己开发的小工具,将mysql表定义语法转换至PG的表定义语法

  5. 在PG中创建表

  6. 将数据导入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

坑点如下

  1. \n换行符导致原本的一条记录分为2行
  2. \r是特殊字符,vi模式下就表示为^M
  3. datetime类型可以存储”0000-00-00 00:00:00″,但是官方手册上datetime的合法范围是’1000-01-0100:00:00′ to ‘9999-12-31 23:59:59’,感觉是bug。。
  4. 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语法转换的小工具

  1. 功能简述
    将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索引,这个在应用中意义不大,因为多数情况,索引是需要根据业务需求重新定义的

  1. 实现方式
    通过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/

    原文作者:yglh_gq
    原文地址: https://www.jianshu.com/p/ba48ff7ad5f9
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞