一、背景
實際生產中,發現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/