背景

      08年左右,阿里巴巴开始尝试MySQL的相关研究,并开发了基于MySQL分库分表技术的相关产品,Cobar/TDDL(目前为阿里云DRDS产品),解决了单机Oracle无法满足的扩展性问题,当时也掀起一股去IOE项目的浪潮,愚公这项目因此而诞生,其要解决的目标就是帮助用户完成从Oracle数据迁移到MySQL上,完成去IOE的第一步. 


项目介绍

名称:   yugong

译意:   愚公移山

语言:   纯java开发

定位:   数据库迁移 (目前主要支持oracle / mysql / DRDS)


环境要求

操作系统

a. 纯java开发,有bat和shell脚本,windows/linux均可支持.
b. jdk建议使用1.6.25以上的版本,稳定可靠,目前阿里巴巴使用基本为此版本.


数据库

a. 源库为oracle,目标库可为mysql/drds/oracle. 基于标准jdbc协议开发,对数据库暂无版本要求
需要的数据库账户权限:
1. 源库(oracle)

GRANT SELECT,INSERT,UPDATE,DELETE ON XXX TO XXX; #常见CRUD权限
GRANT CREATE ANY MATERIALIZED VIEW TO XXX;
GRANT DROP ANY MATERIALIZED VIEW TO XXX;

2. 目标库(mysql/oracle)

GRANT SELECT,INSERT,UPDATE,DELETE ON XXX TO XXX;

迁移方案

整个迁移方案,分为两部分:

  1. 全量迁移
  2. 增量迁移

过程描述:

  1. 增量数据收集 (创建oracle表的增量物化视图)
  2. 进行全量复制
  3. 进行增量复制 (并行进行数据校验)
  4. 原库停写,切到新库

回滚方案:

  1. 开启新库到老库的数据回流

部署

下载

1. 源码编译

github地址: https://github.com/alibaba/yugong

git clone https://github.com/alibaba/yugong.git

下载后在yugong目录下,执行

mvn clean install -Dmaven.test.skip -Denv=release

会在dist目录下生成yugong-x.y.z.tar.gz文件

2. 二进制包下载

下载地址: https://github.com/alibaba/yugong/releases


目录结构

解压缩发布包后,可得如下目录结构:

drwxr-xr-x 2 jianghang jianghang  136 2013-09-29 17:19 bin
drwxr-xr-x 3 jianghang jianghang  152 2013-09-29 17:19 conf
drwxr-xr-x 2 jianghang jianghang 1640 2013-09-29 17:19 lib
drwxr-xr-x 2 jianghang jianghang   48 2013-09-29 11:57 logs

修改配置

正常情况下,只需修改下yugong.database的源库和目标库的地址信息,通过yugong.table.white定义本次需要迁移的表,通过yugong.table.mode定义要执行的操作,是全量还是增量等,其他的可以使用默认值.

默认值    :


启动停止

linux启动 :

sh startup.sh

linux带debug方式启动:(默认使用suspend=n,可设置为y,阻塞等待你remote debug链接成功) 

sh startup.sh debug 9099 

linux停止:

sh stop.sh  

几点注意:

  1. linux启动完成后,会在bin目录下生成yugong.pid,stop.sh会读取yugong.pid进行进程关闭
  2. startup.sh默认读取系统环境变量中的which java获得JAVA执行路径,需要设置PATH=$JAVA_HOME/bin环境变量

windows启动:

startup.bat

windows停止:直接关闭终端即可


查看日志

对应日志结构为:

logs/
  - yugong/  #系统根日志
     - table.log
  - ${table}/  #每张同步表的日志信息
     - table.log
     - extractor.log
     - applier.log
     - check.log

全量完成的日志:(会在yugong/table.log 和 ${table}/table.log中出现记录)

table[OTTER2.TEST_ALL_ONE_PK] is end!

增量日志:(会在${table}/table.log中出现记录)

table[OTTER2.TEST_ALL_ONE_PK] now is CATCH_UP ... #代表已经追上,最后一次增量数据小于onceCrawNum数量
table[OTTER2.TEST_ALL_ONE_PK] now is NO_UPDATE ... #代表最近一次无增量数据

ALL(全量+增量)模式日志: (会在${table}/table.log中出现记录)

table [OTTER2.TEST_ALL_ONE_PK] full extractor is end , next auto start inc extractor #出现这条代表全量已经完成,进入增量模式

CHECK日志: (会在${table}/check.log中出现diff记录)

-----------------
- Schema: yugong , Table: test_all_one_pk
-----------------
---Pks
        ColumnValue[column=ColumnMeta[index=0,name=ID,type=3],value=2576]
---diff
        ColumnMeta[index=3,name=AMOUNT,type=3] , values : [0] vs [0.0]

同步过程数据日志:会通过extractor.log/applier.log分别记录extractor和applier的数据记录,因为有DataTranslator的存在,两者记录可能不一致,所以分开两份记录.

统计信息:

  • progress统计,会在主日志下,输出当前全量/增量/异常表的数据,可通过该日志,全局把握整个迁移任务的进度,输出类似:
    {未启动:0,全量中:2,增量中:3,已追上:3,异常数:0}
  • stat统计,会在每个表迁移日志下,输出当前迁移的tps信息
    {总记录数:180000,采样记录数:5000,同步TPS:4681,最长时间:215,最小时间:212,平均时间:213} 

切换流程

  1. 当任务处于追上状态时候,表示已经处于实时同步状态
  2. 后续通过源数据库进行停写,稍等1-2分钟后(保证延时的数据最终得到同步,此时源库和目标库当前数据是完全一致的)
  3. 检查增量持续处于NO_UPDATE状态,可关闭该迁移任务(sh stop.sh),即可发布新程序,使用新的数据库,完成切换的流程.

自定义数据转换

 如果要迁移的oracle和mysql的表结构不同,比如表名,字段名有差异,字段类型不兼容,需要使用自定义数据转换。如果完全相同那就可以跳过此章节

整个数据流为:DB -> Extractor -> DataTranslator -> Applier -> DB,本程序预留DataTranslator接口,允许外部用户自定义数据处理逻辑,比如:

  1. 表名不同
  2. 字段名不同
  3. 字段类型不同
  4. 字段个数不同
  5. 运行过程join其他表的数据做计算等

例子:

/**
 * 一个迁移的例子,涵盖一些基本转换操作
 *
 * <pre>
 * 例子包含特性:
 * 1. schema/table名不同. oracle中为otter2.yugong_example_oracle,mysql中为test.yugong_example_mysql
 * 2. 字段名字不同.  oracle中的name字段,映射到mysql的display_name
 * 3. 字段逻辑处理.  mysql的display_name字段数据来源为oracle库的:name+'('alias_name+')'
 * 4. 字段类型不同.  oracle中的amount为number类型,映射到mysql的amount为varchar文本型
 * 5. 源库多一个字段. oracle中多了一个alias_name字段
 * 6. 目标库多了一个字段. mysql中多了一个gmt_move字段,(简单的用迁移时的当前时间进行填充)
 *
 * 测试的表结构:
 * // oracle表
 * create table otter2.yugong_example_oracle
 * (
 *     id NUMBER(11)  ,
 *     name varchar2(32) ,
 *     alias_name  char(32) default ' ' not null,
 *     amount number(11,2),
 *     score  number(20),
 *     text_b blob,
 *     text_c clob,
 *     gmt_create date not null,
 *     gmt_modified date not null,
 *     CONSTRAINT yugong_example_oracle_pk_id  PRIMARY   KEY (id)
 * );
 *
 * // mysql表
 * create table test.yugong_example_mysql
 * (
 *     id bigint(20) unsigned auto_increment,
 *     display_name varchar(128) ,
 *     amount varchar(32),
 *     score bigint(20) unsigned ,
 *     text_b blob,
 *     text_c text,
 *     gmt_create timestamp not null,
 *     gmt_modified timestamp not null,
 *     gmt_move timestamp not null,
 *     CONSTRAINT yugong_example_mysql_pk_id  PRIMARY KEY (id)
 * );
 * </pre>
 *
 * @author jianghang 2013-10-10 下午3:28:33
 */
public class YugongExampleOracleDataTranslator extends AbstractDataTranslator implements DataTranslator {
public boolean translator(Record record) {
    // 1. schema/table名不同
    // record.setSchemaName("test");
    record.setTableName("yugong_example_mysql");

    if (record instanceof IncrementRecord) {
        if (IncrementOpType.D == ((IncrementRecord) record).getOpType()) {
            // 忽略delete
            return super.translator(record);
        }
    }

    // 2. 字段名字不同
    ColumnValue nameColumn = record.getColumnByName("name");
    nameColumn.getColumn().setName("display_name");

    // 3. 字段逻辑处理
    ColumnValue aliasNameColumn = record.getColumnByName("alias_name");
    StringBuilder displayNameValue = new StringBuilder(64);
    displayNameValue.append(ObjectUtils.toString(nameColumn.getValue()))
        .append('(')
        .append(ObjectUtils.toString(aliasNameColumn.getValue()))
        .append(')');
    nameColumn.setValue(displayNameValue.toString());

    // 4. 字段类型不同
    ColumnValue amountColumn = record.getColumnByName("amount");
    amountColumn.getColumn().setType(Types.VARCHAR);
    amountColumn.setValue(ObjectUtils.toString(amountColumn.getValue()));

    // 5. 源库多一个字段
    record.getColumns().remove(aliasNameColumn);

    // 6. 目标库多了一个字段
    ColumnMeta gmtMoveMeta = new ColumnMeta("gmt_move", Types.TIMESTAMP);
    ColumnValue gmtMoveColumn = new ColumnValue(gmtMoveMeta, new Date());
    record.addColumn(gmtMoveColumn);

    // ColumnValue text_c = record.getColumnByName("text_c");
    // try {
    // text_c.setValue(new String((byte[]) text_c.getValue(), "GBK"));
    // } catch (UnsupportedEncodingException e) {
    // e.printStackTrace();
    // }
    return super.translator(record);
}

}

几点说明:

  1. DataTranslator目前仅支持java扩展,允许用户完成类实现后,将类源文件放置到conf/translator/目录下,yugong启动后会进行动态编译.
  2. DataTranslator目前查找规则会根据表名自动查找,比如需要处理的表为otter2.test_all_one_pk,查找的时候会将test_all_one_pk转化为TestAllOnePk + 固定DataTranslator后缀. (如果当前classpath中存在,优先使用classpath,如果不存在,则到conf/translator中查找该名字的java文件进行动态编译)
  3. 目前提供了几个样例,可参见解压后的conf/translator/目录
    a. YugongExampleOracleDataTranslator  (当前例子,介绍oracle一张表和mysql一张表之间的转换处理)
    b. YugongExampleJoinDataTranslator  (介绍oracle多张表和mysql一张表之间的转换处理,oracle中会通过一张表为主表,运行时join查询出其他表数据,合并同步到mysql)
    c. YugongExampleTwoDataTranslator (介绍oracle一张表和mysql多张表之间的转换处理,oracle的一张大表数据,可运行时拆分后输出到多张mysql表上)

运行模式详细介绍

MARK模式(MARK)

开启增量日志的记录,如果是oracle就是创建物化视图

CLEAR模式(CLEAR)

清理增量日志的记录,如果是oracle就是删除物化视图

全量模式(FULL)

全量模式,顾名思议即为对源表进行一次全量操作,遍历源表所有的数据后,插入目标表.
全量有两种处理方式:

  1. 分页处理:如果源表存在主键,只有一个主键字段,并且主键字段类型为Number类型,默认会选择该分页处理模式. 优点:支持断点续做,对源库压力相对较小。 缺点:迁移速度慢
  2. once处理:通过select * from访问整个源表的某一个mvcc版本的数据,通过cursor.next遍历整个结果集. 优点:迁移速度快,为分页处理的5倍左右。 缺点:源库压力大,如果源库并发修改量大,会导致数据库MVCC版本过多,出现栈错误. 还有就是不支持断点续做.

 特别注意
如果全量模式运行过程中,源库有变化时,不能保证源库最近变化的数据能同步到目标表,这时需要配合增量模式. 具体操作就是:在运行全量模式之前,先开启增量模式的记录日志功能,然后开启全量模式,完成后,再将最近变化的数据通过增量模式同步到目标表。

增量模式(INC)

全量模式,顾名思议即为对源表增量变化的数据插入目标表,增量模式依赖记录日志功能.
目前增量模式的记录日志功能,是通过oracle的物化视图功能。

 创建物化视图

CREATE MATERIALIZED VIEW LOG ON ${tableName} with primary key.

 

  • 运行增量模式之前,需要先开启记录日志的功能,即预先创建物化视图. 特别是配合全量模式时,创建物化视图的时间点要早于运行全量之前,这样才可以保证数据能全部同步到目标表
  • 增量模式没有完成的概念,它只有追上的概念,具体的停止需有业务进行判断,可以看一下切换流程

自动模式(ALL)

自动模式,是对全量+增量模式的一种组合,自动化运行,减少操作成本.
自动模式的内部实现步骤:

  1. 开启记录日志功能. (创建物化视图)
  2. 运行全量同步模式. (全量完成后,自动进入下一步)
  3. 运行增量同步模式. (增量模式,没有完成的概念,所以也就不会自动退出,需要业务判断是否可以退出,可以看一下切换流程)

对比模式(CHECK)

对比模式,即为对源库和目标库的数据进行一次全量对比,验证一下迁移结果. 对比模式为一种可选运行,做完全量/增量/自动模式后,可选择性的运行对比模式,来确保本次迁移的正确性.

 

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐