代码改变世界

gh-ost唯一索引变更可能导致表数据丢失

2017-08-30 18:38  Kevin.hhl  阅读(772)  评论(0)    收藏  举报

一.问题起因

    下午在给测试库加unique key发现gh-ost无法退出,即使我把postpone文件删掉,gh-ost还是无法正常退出,于是想仔细回溯gh-ost加unique key的整个流程,思考gh-ost和正常DDL的区别,然后在测试环境跑case,整个流程在下面会介绍。

 

二.案例及分析

 2.1 测试case

 因线上数据敏感性,故用测试环境来复现:

--测试表原结果
CREATE TABLE `t1` (
  `id` int(11) NOT NULL,
  `c1` varchar(4) DEFAULT NULL,
  `c2` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_c1` (`c1`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;


mysql> insert into t1 values(4,'ccc',3);
Query OK, 1 row affected (0.01 sec)

mysql> select * from t1;
+----+------+------+
| id | c1   | c2   |
+----+------+------+
|  1 | aaa  |    1 |
|  2 | bbb  |    2 |
|  3 | ccc  |    3 |
|  4 | ccc  |    3 |
+----+------+------+
4 rows in set (0.00 sec)

到此为t1表增加unique key uniq_c1c2(c1,c2),此时t1表有2条重复数据
然后执行gh-ost,脚本大致命令如下:
alter="add unique key uniq_c1c2(c1,c2);"
log="/tmp/${pid}.gh-ost_${h}_${p}_${d}_${t}.log";
postpone="/tmp/${pid}.gh-ost_${h}_${p}_${d}_${t}.postpone";
panic="/tmp/i${pid}.gh-ost_${h}_${p}_${d}_${t}.panic";

/ghost/gh-ost \
--host=${h} \
--port=${p} \
--conf=gh.cnf \
--database="${d}" \
--table="${t}" \
--alter="${alter}" \
--verbose \
--initially-drop-ghost-table  \
--assume-rbr \
--max-lag-millis=1000 \
--cut-over=default \
--exact-rowcount \
--concurrent-rowcount \
--default-retries=120 \
--initially-drop-old-table \
--panic-flag-file=${panic} --execute >${log} 2>&1

执行gh-ost 发现t1 少了一条重复数据,如下:
mysql> select * from t1;
+----+------+------+
| id | c1   | c2   |
+----+------+------+
|  1 | aaa  |    1 |
|  2 | bbb  |    2 |
|  3 | ccc  |    3 |
+----+------+------+
3 rows in set (0.00 sec)

2.2 原因分析

在master抓包分析发现insert到gho(临时中间表)是insert ignore .....  

insert /* gh-ost `ccut`.`t1` */ ignore into `ccut`.`_t1_gho` (`id`, `c1`, `c2`)
      (select `id`, `c1`, `c2` from `ccut`.`t1` force index (`PRIMARY`)
        where (((`id` > '1') or ((`id` = '1'))) and ((`id` < 4) or ((`id` = 4)))) lock in share mode
      );

验证了重复数据被ignore掉。

翻看gh-ost 代码:

-- go/sql/builder.go的第218-229行代码如下:
    if transactionalTable {
        transactionalClause = "lock in share mode"
    }
    result = fmt.Sprintf(`
      insert /* gh-ost %s.%s */ ignore into %s.%s (%s)
      (select %s from %s.%s force index (%s)
        where (%s and %s) %s
      )
    `, databaseName, originalTableName, databaseName, ghostTableName, mappedSharedColumnsListing,
        sharedColumnsListing, databaseName, originalTableName, uniqueKey,
        rangeStartComparison, rangeEndComparison, transactionalClause)
    return result, explodedArgs, nil

可以看到gh-ost确实用insert ignore处理。

 

三.总结

   给已有数据的表加unique key时,如果使用gh-ost,数据可能面临丢失。重复数据是否删除需要和业务方确认。

   新工具虽好,一些细节需要大量的测试再评估,避开可能存在的问题,例如加unique key使用其他方式来替代等。

   提Bug给gh-ost社区,希望可以推进gh-ost修复此问题。