背景
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;
遷移方案
整個遷移方案,分爲兩部分:
- 全量遷移
- 增量遷移
過程描述:
- 增量數據收集 (創建oracle表的增量物化視圖)
- 進行全量複製
- 進行增量複製 (並行進行數據校驗)
- 原庫停寫,切到新庫
回滾方案:
- 開啓新庫到老庫的數據迴流
部署
下載
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
幾點注意:
- linux啓動完成後,會在bin目錄下生成yugong.pid,stop.sh會讀取yugong.pid進行進程關閉
- 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分鐘後(保證延時的數據最終得到同步,此時源庫和目標庫當前數據是完全一致的)
- 檢查增量持續處於NO_UPDATE狀態,可關閉該遷移任務(sh stop.sh),即可發佈新程序,使用新的數據庫,完成切換的流程.
自定義數據轉換
如果要遷移的oracle和mysql的表結構不同,比如表名,字段名有差異,字段類型不兼容,需要使用自定義數據轉換。如果完全相同那就可以跳過此章節
整個數據流爲:DB -> Extractor -> DataTranslator -> Applier -> DB,本程序預留DataTranslator接口,允許外部用戶自定義數據處理邏輯,比如:
- 表名不同
- 字段名不同
- 字段類型不同
- 字段個數不同
- 運行過程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);
}
}
幾點說明:
- DataTranslator目前僅支持java擴展,允許用戶完成類實現後,將類源文件放置到conf/translator/目錄下,yugong啓動後會進行動態編譯.
- DataTranslator目前查找規則會根據表名自動查找,比如需要處理的表爲otter2.test_all_one_pk,查找的時候會將test_all_one_pk轉化爲TestAllOnePk + 固定DataTranslator後綴. (如果當前classpath中存在,優先使用classpath,如果不存在,則到conf/translator中查找該名字的java文件進行動態編譯)
- 目前提供了幾個樣例,可參見解壓後的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)
全量模式,顧名思議即爲對源表進行一次全量操作,遍歷源表所有的數據後,插入目標表.
全量有兩種處理方式:
- 分頁處理:如果源表存在主鍵,只有一個主鍵字段,並且主鍵字段類型爲Number類型,默認會選擇該分頁處理模式. 優點:支持斷點續做,對源庫壓力相對較小。 缺點:遷移速度慢
- once處理:通過select * from訪問整個源表的某一個mvcc版本的數據,通過cursor.next遍歷整個結果集. 優點:遷移速度快,爲分頁處理的5倍左右。 缺點:源庫壓力大,如果源庫併發修改量大,會導致數據庫MVCC版本過多,出現棧錯誤. 還有就是不支持斷點續做.
特別注意
如果全量模式運行過程中,源庫有變化時,不能保證源庫最近變化的數據能同步到目標表,這時需要配合增量模式. 具體操作就是:在運行全量模式之前,先開啓增量模式的記錄日誌功能,然後開啓全量模式,完成後,再將最近變化的數據通過增量模式同步到目標表。
增量模式(INC)
全量模式,顧名思議即爲對源表增量變化的數據插入目標表,增量模式依賴記錄日誌功能.
目前增量模式的記錄日誌功能,是通過oracle的物化視圖功能。
創建物化視圖
CREATE MATERIALIZED VIEW LOG ON ${tableName} with primary key.
- 運行增量模式之前,需要先開啓記錄日誌的功能,即預先創建物化視圖. 特別是配合全量模式時,創建物化視圖的時間點要早於運行全量之前,這樣纔可以保證數據能全部同步到目標表
- 增量模式沒有完成的概念,它只有追上的概念,具體的停止需有業務進行判斷,可以看一下切換流程
自動模式(ALL)
自動模式,是對全量+增量模式的一種組合,自動化運行,減少操作成本.
自動模式的內部實現步驟:
- 開啓記錄日誌功能. (創建物化視圖)
- 運行全量同步模式. (全量完成後,自動進入下一步)
- 運行增量同步模式. (增量模式,沒有完成的概念,所以也就不會自動退出,需要業務判斷是否可以退出,可以看一下切換流程)
對比模式(CHECK)
對比模式,即爲對源庫和目標庫的數據進行一次全量對比,驗證一下遷移結果. 對比模式爲一種可選運行,做完全量/增量/自動模式後,可選擇性的運行對比模式,來確保本次遷移的正確性.