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/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,458评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,454评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,171评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,062评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,440评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,661评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,906评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,609评论 0 200
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,379评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,600评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,085评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,409评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,072评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,088评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,860评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,704评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,608评论 2 270

推荐阅读更多精彩内容

  • 【MySQL】Linux下MySQL 5.5、5.6和5.7的RPM、二进制和源码安装 1.1BLOG文档结构图 ...
    小麦苗DB宝阅读 10,362评论 0 31
  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 10,508评论 6 13
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,100评论 18 139
  • 1.小白兔有一家糖果铺,小老虎有一个冰淇淋机。兔妈妈告诉小白兔,如果你喜欢一个人呐,就给一颗糖他。小白兔喜欢上了小...
    九马阅读 430评论 0 2
  • 2015,变数很大的一年。 现在的我力不从心。 本以为找到了一份较满意的工作,开始自己的工作生涯。结果,现实给了狠...
    冰河木马阅读 164评论 0 1