異構(gòu)數(shù)據(jù)庫遷移的九個(gè)大坑怎么躲開
“貳過”,重犯同一過失的意思,語出《論語》,完整句子為“不遷怒不貳過”,乃仲尼對其不幸早逝的弟子顏回的極高評價(jià)。就it項(xiàng)目而言,從管理者的角度來說,一個(gè)錯(cuò)誤犯兩次,別說是同一個(gè)人了,哪怕是同一團(tuán)隊(duì)的不同人也不可接受。說到這里,不由得腦補(bǔ)出一位平時(shí)慈眉善目的老領(lǐng)導(dǎo)作嗔怒狀,大喊“干掉干掉”的恐怖場面。不重犯同一錯(cuò)誤真的很難嗎,實(shí)話實(shí)說,確實(shí)難,君不聞,人類從歷史中得到的最大教訓(xùn)就是從不吸取教訓(xùn)。
筆者上述感嘆的背后是有故事的。那是本世紀(jì)最“2”的一天,筆者在排查Oracle到ADB(此處指ADB PG,全稱AnalyticDB PostgreSQL版,是阿里的MPP架構(gòu)的數(shù)據(jù)庫)之間數(shù)據(jù)實(shí)時(shí)同步過程中,突然發(fā)現(xiàn)自己埋了個(gè)大雷——沒區(qū)分空值和空字符串,導(dǎo)致同樣的字段值在全量同步階段標(biāo)志為空字符串,增量同步階段則標(biāo)志為空值,這顯然不是一種合乎邏輯的行為。
其實(shí)在發(fā)現(xiàn)這個(gè)問題前,筆者已經(jīng)在這項(xiàng)目中苦苦掙扎了一個(gè)月,在此之前的兩天,筆者自以為數(shù)據(jù)質(zhì)量已經(jīng)完全沒問題,數(shù)據(jù)同步達(dá)到“又不是不能用”的境界了(筆者注:此處能用是真的能用,非為上線而上線那種能用)。Oracle DB本身確實(shí)不區(qū)分空值和空字符串,對于搞Oracle出身的工程師,沒意識到這個(gè)似乎情有可原,然而筆者作為搬磚佬,已經(jīng)前前后后搬了十幾年,數(shù)據(jù)庫異構(gòu)遷移項(xiàng)目也認(rèn)認(rèn)真真地做過好幾個(gè),這件事對筆者而言,其實(shí)無異于“低級錯(cuò)誤”。
回想起來,這個(gè)項(xiàng)目筆者做得甚是狼狽,這里面固然有進(jìn)度緊以及新工具、新技術(shù)引入需要邊學(xué)邊用的客觀因素,但這不足以平息筆者對自己的憤怒。問題來了,在開源技術(shù)使用日益廣泛的今天,筆者也可能突然被要求用一個(gè)新工具同步數(shù)據(jù)到一個(gè)新數(shù)據(jù)庫,時(shí)間還可能更緊迫。到時(shí)怎么辦呢?再憤怒一次嗎?不了不了,還是腳踏實(shí)地總結(jié)一下,記下這些坑,日后類似項(xiàng)目,哪怕被拿著槍指著頭也好,下述問題都要在前期階段予以考慮。
一、空值和空字符串
對于Oracle DB??,空值和空字符串是同?回事,其中判斷某個(gè)值是否為空值/空字符串統(tǒng)?使?"IS NULL"即可,?于=''是?效的。
這點(diǎn)對于其余數(shù)據(jù)庫來說,可真不?定了。就源端為Oracle DB,?標(biāo)端為其余數(shù)據(jù)庫的項(xiàng)???,我們需要與開發(fā)商確認(rèn)好,?標(biāo)端究竟?哪種值表?源端的空值/空字符。?對于實(shí)時(shí)同步的項(xiàng)?來說,還需要在全量同步以及增量同步期間保持?jǐn)?shù)據(jù)的?致。
除了空值和空字符串以外,其實(shí)還有?個(gè)隱藏的?boss——固定?度類型中的空格值。以O(shè)racle DB為例,char類型?于存放固定?度的字符串,??度不?的字符串則會被?動補(bǔ)充空格,對于這種值,?論是char_col=''還是char_col is null均?法識別,只能通過trim(char_col) is null識別,對此值使?length函數(shù)會返回本字段的?度,?個(gè)?0值。?這種數(shù)據(jù)同步到adb后則表現(xiàn)有?定差異了,char_col=''可以識別這種數(shù)據(jù)(哪怕ADB中也是?了固定?度的數(shù)據(jù)類型),然?對這個(gè)值使?char_length函數(shù),結(jié)果會返回0,如果實(shí)在要把這種數(shù)據(jù)抓出來,我們還得加?個(gè)條件oct_length(char_col)>0。很明顯,這?是?個(gè)坑!
二、數(shù)據(jù)類型轉(zhuǎn)換問題
筆者的朋友阿強(qiáng)一直立志做一個(gè)溫文儒雅的人,嗯,前提是不要讓他“寫材料”、“擦屁股”以及“估算工作量”!沒錯(cuò),“估算工作量”對他這種以txt狂魔自稱的人來說是一種煎熬,光說起這個(gè)詞,他已經(jīng)腦補(bǔ)出把pm按在地上,一邊揍一邊說,“我想做個(gè)好人,為什么要逼我”?問題來了,如果讓阿強(qiáng)估算這個(gè)數(shù)據(jù)類型轉(zhuǎn)換的工作量,估計(jì)他連40米的刀都可以拿出來了。
跨數(shù)據(jù)庫的表字段數(shù)據(jù)類型轉(zhuǎn)換工作主要涉及“精度”、“效率”、以及“兼容性”等方面。部分?jǐn)?shù)據(jù)同步軟件確實(shí)具備自動轉(zhuǎn)換的功能,然而這種情況,僅對于管理規(guī)范的數(shù)據(jù)庫適用,不規(guī)范的庫則需要耗費(fèi)較大精力了,下面舉幾個(gè)典型的例子說明這個(gè)問題:
以從Oracle DB到PG的同步為例,整型的值固然可以通過number類型(不設(shè)定精度和小數(shù)位)存放到源端Oracle數(shù)據(jù)庫中,問題來了,PG中應(yīng)該用啥字段對應(yīng)了,僅僅安全起見,免得精度丟失,那肯定是numeric類型,然而這毫無疑問存在性能損失。Bigint等類型自然是更好的選擇,可是這又牽涉到與開發(fā)商溝通這樣有沒有可能導(dǎo)致應(yīng)用報(bào)錯(cuò)等情況了,這又是一個(gè)工作量。
以從Oracle DB到DB2的同步為例,DB2有一個(gè)隱性要求,組成主鍵的字段值前后不能包含空格,否則會被過濾掉,這樣會造成一些在源端Oracle數(shù)據(jù)庫本來就不相同的兩條記錄的在目標(biāo)端DB2庫被誤判為同一條記錄進(jìn)而引發(fā)數(shù)據(jù)沖突,影響數(shù)據(jù)同步。
不要問為什么要在id類字段加空格:
- 這是合法的;
- 這未嘗不是一種有創(chuàng)意的備份數(shù)據(jù)方法。
這么干,真的,沒毛病!
以從Oracle DB到ADB的同步為例,由于MPP架構(gòu)需要,我們需要額外指定DISTRIBUTED列。對于ADB而言,這里還有一個(gè)附帶的要求,這個(gè)列應(yīng)當(dāng)為主鍵的一部分。
以O(shè)racle DB到HBase的同步為例,HBase是強(qiáng)制要求有主鍵的,否則不能同步。之前的項(xiàng)目中,筆者被迫無奈拿Oracle的轉(zhuǎn)換后的ROWID作為HBase的rowkey,滿足其同步前置條件。
為什么是轉(zhuǎn)換后?這又是另一個(gè)故事了,這里就不展開了,只提示個(gè)關(guān)鍵詞,“預(yù)分區(qū)”。
嗯嗯,這明顯是個(gè)坑。那種由開發(fā)商定表結(jié)構(gòu)的項(xiàng)目得心存感激,真的!
三、字符集轉(zhuǎn)換問題
字符集轉(zhuǎn)換深究起來其實(shí)并不是容易的事情,幾年前筆者所參與的一個(gè)O2O同步(即Oracle到Oracle的同步,下同) 遷移項(xiàng)目中,涉及了BIG5到UTF8的轉(zhuǎn)換,當(dāng)時(shí)的同步工具為OGG。這個(gè)項(xiàng)目中各種亂七八糟的數(shù)據(jù)至今仍然對筆者歷歷在目,當(dāng)然,這也讓筆者能更有經(jīng)驗(yàn)地處理異構(gòu)數(shù)據(jù)庫同步中的字符集轉(zhuǎn)換問題。
跨字符集轉(zhuǎn)換的工作其實(shí)陷阱不少,因此,條件允許的話,筆者建議盡量規(guī)避。當(dāng)然,遇到PG這種服務(wù)器端不支持GBK,而源端Oracle DB偏偏是GBK的情況,那只好迎難而上了,下面為筆者的建議:
- 涉及中文的情況,所見非所得,判斷一條中文記錄是否正常的依據(jù),應(yīng)該為其十六進(jìn)制編碼是否正常。對于Oracle DB而言,我們可以用dump函數(shù),其余DB需要找到類似的。
- 中文為多字節(jié)字符,在不同的字符集下占用的字節(jié)數(shù)并不一致,典型例子為GBK為2字節(jié),UTF8為3字節(jié)。目標(biāo)端環(huán)境可能需要相應(yīng)調(diào)整字節(jié)寬度。
- 不同字符集所涵蓋的漢字是不一樣的,例如BIG5中就沒有“邨”字。
- 字符集中有一個(gè)“自定義”區(qū)域,如不進(jìn)行特殊處理,有可能導(dǎo)致數(shù)據(jù)丟失。
“亂碼”會造成很大的困擾,對于Oracle DB而言,大致有如下幾種情況:
- 無法通過Oracle自帶convert函數(shù)轉(zhuǎn)換為目標(biāo)庫編碼,以ADB為例,這部分?jǐn)?shù)據(jù)會導(dǎo)致GPFDIST導(dǎo)入失敗,進(jìn)而影響數(shù)據(jù)全量同步。
- 可以通過Oracle自帶convert函數(shù)轉(zhuǎn)換為目標(biāo)庫編碼,但無法重新轉(zhuǎn)換為原有數(shù)據(jù),這部分?jǐn)?shù)據(jù)會有潛在的數(shù)據(jù)丟失影響,對于遷移類項(xiàng)目需要重點(diǎn)關(guān)注是否涉及“自定義”字符區(qū)域。
- 含義不明的單字節(jié)字符,如chr(0)、chr(255),這些字符往往夾雜在正常的字符中,以ADB為例,涉及dts( Data Transmission Service, 數(shù)據(jù)傳輸服務(wù),系阿里的數(shù)據(jù)同步工具/服務(wù))增量同步的話,相應(yīng)記錄有數(shù)據(jù)一致性風(fēng)險(xiǎn)。
四、特殊字符處理
對于數(shù)據(jù)庫異構(gòu)同步而言,特殊的字符,諸如單引號、雙引號、換行、斜杠、反斜杠等等也是一個(gè)困擾項(xiàng),這一點(diǎn)在數(shù)據(jù)全量同步階段尤其明顯。
對于文本方式的全量數(shù)據(jù)同步來說,我們可以考慮下述幾種方式:
- 使用CSV格式;
- 使用多字節(jié)分隔符;
- 進(jìn)行數(shù)據(jù)清洗;
- 僅同步“正?!睌?shù)據(jù),“特殊”數(shù)據(jù)另行處理。
五、異常記錄處理
這里的異常記錄,指的是那種本身就違反數(shù)據(jù)庫規(guī)范,不應(yīng)該插入到數(shù)據(jù)庫中的記錄。
以O(shè)racle DB為例,筆者遇到的記錄有異常日期格式以及異常數(shù)值兩類。
異常日期格式,典型例子有"0000-00-00 00:00:00"和"2022-02-30 00:00:00"。筆者在好幾個(gè)客戶環(huán)境都遇過這種數(shù)據(jù),以至于筆者覺得這很“常見”,需要加到測試項(xiàng)目里面。筆者這段時(shí)間做的Oracle到ADB同步項(xiàng)目確實(shí)遇到這種數(shù)據(jù)了,后者還造成dts的增量同步中斷,風(fēng)險(xiǎn)很高。所幸筆者的dts源端庫是基于OGG的目標(biāo)庫部署的,Oracle自己的OGG工具也不能同步這種數(shù)據(jù),這間接地?fù)趿诉@部分異常的增量數(shù)據(jù)。在此基礎(chǔ)上,筆者只需要修復(fù)已有的異常數(shù)據(jù)即可,修復(fù)方法也很簡單,先+1再-1能修復(fù)大部分?jǐn)?shù)據(jù),至于不能修復(fù)的只能和業(yè)務(wù)協(xié)商重置一個(gè)值了。
異常數(shù)值類型,典型例子為NaN(即Not a Number),筆者僅在一個(gè)客戶環(huán)境中遇到,當(dāng)時(shí)的場景為O2O同步 ,比較可怕的是連基本“來者不拒”的數(shù)據(jù)泵都無法同步這種數(shù)據(jù)??紤]到這個(gè)環(huán)境沒遇過這種數(shù)據(jù),筆者這次偷懶了,沒做相應(yīng)的測試。
六、全量同步測試
通常情況下,各種數(shù)據(jù)同步軟件都會自帶全量數(shù)據(jù)同步的功能,至于這個(gè)功能的效率、資源消耗、空間占用等項(xiàng)目需要進(jìn)行評估。如果其不能滿足需求,則可能需要考慮替代的手段。
在選取測試表的時(shí)候,筆者考慮綜合下面幾個(gè)因素選擇幾個(gè)測試表:
- 需要包括大表,大表往往是個(gè)瓶頸項(xiàng);
- 需要囊括本次同步涉及表的字段類型;
- 如果環(huán)境中存在中文等多字節(jié)數(shù)據(jù),則建議包含這種表;
- 建議找靜態(tài)的表或者準(zhǔn)靜態(tài)進(jìn)行測試,以方便核對數(shù)據(jù)一致性。
七、增量同步測試
作為數(shù)據(jù)同步項(xiàng)目,同步效率是一個(gè)重要因素,筆者建議在搭建完整的同步鏈路之前,拿數(shù)據(jù)變更頻繁的關(guān)鍵表進(jìn)行測試,通過單表單進(jìn)程的方式,剔除潛在的配置不當(dāng)風(fēng)險(xiǎn)。
對于這方面,筆者建議如下:
- 盡量使用真實(shí)的數(shù)據(jù);
- 筆者這次測試通過Ogg同步增量數(shù)據(jù),比較切合生產(chǎn)實(shí)際變更情況,這種方式可以參考。
增量同步發(fā)起后,在目標(biāo)數(shù)據(jù)庫后臺觀察對應(yīng)的SQL語句。以筆者本次項(xiàng)目為例,這個(gè)階段發(fā)現(xiàn)了兩個(gè)問題:
- 由于大小寫敏感問題,dts目標(biāo)側(cè)未成功識別出主鍵,導(dǎo)致所有字段被加到where條件,影響效率,此問題后來通過修改同步配置解決。
- 筆者觀察到dts側(cè)雖然設(shè)置了高并發(fā)度,但實(shí)際運(yùn)行中,僅個(gè)別進(jìn)程工作,其余處于空閑狀態(tài),無法充分利用資源。此問題后來通過修改配置參數(shù)解決。
八、數(shù)據(jù)一致性測試
數(shù)據(jù)一致性又是一個(gè)可以另外寫一篇文章的話題,對此,筆者建議如下:
- 對比靜態(tài)或者準(zhǔn)靜態(tài)的數(shù)據(jù)。很顯然,筆者這次使用的ogg中間庫方案很切合這個(gè)主題,如果沒這個(gè),筆者只能通過停止同步進(jìn)程后反查其停在哪個(gè)點(diǎn),再用這個(gè)時(shí)間點(diǎn)做檢驗(yàn)了。這個(gè)想法理論上可行,然而以筆者對dts的淺薄理解,這條路并不通,原因在于dts所停的時(shí)間點(diǎn)并不完全準(zhǔn)確。
- 活用md5函數(shù)。大部分正經(jīng)的數(shù)據(jù)庫均包含內(nèi)置的md5函數(shù)(PS:無意內(nèi)涵DB2,真不是故意的),這可以將一個(gè)復(fù)雜的字符串簡化,以便用于運(yùn)算確認(rèn)兩端的數(shù)據(jù)一致性。
九、軟件局限性
“越是漂亮的女人就越會騙人,記住啊!”
“不光是漂亮的女人不能相信,連貌似忠良的男人也不能相信?!?/p>
我覺得這段對話充分展示了一個(gè)產(chǎn)品的售前與售后的結(jié)局——殊途同歸。對于售前來說,拼指標(biāo)、造場景、講故事等等手段都是為了證明我家產(chǎn)品很棒,快來買買買;就售后而言,找到產(chǎn)品的痛點(diǎn),予以規(guī)避,以達(dá)到保證工作順利開展,避免一口大鍋從天而降的目的。大家都是靠博弈而生的,沒什么兩樣,手里的牌均是對技術(shù)的了解。
扯遠(yuǎn)了,回到it項(xiàng)目中,異構(gòu)數(shù)據(jù)庫的同步往往是邏輯的同步方式,這種方式必然有各種瓶頸的。對售后來說,再怎么謾罵售前“管殺不管埋”也無濟(jì)于事,最現(xiàn)實(shí)的做法莫過于:找到短板,通過改善流程、優(yōu)化需求甚至協(xié)同開發(fā)商改造應(yīng)用的方式保證軟件的穩(wěn)定運(yùn)行。
這里先講個(gè)故事。Timesten是Oracle的內(nèi)存數(shù)據(jù)庫,其Cachegroup功能可以實(shí)現(xiàn)從物理庫(即Oracle DB)到內(nèi)存庫的實(shí)時(shí)數(shù)據(jù)同步,而這個(gè)同步延遲對業(yè)務(wù)穩(wěn)定運(yùn)行是非常關(guān)鍵的。在實(shí)際使用中,運(yùn)維人員總結(jié)出的經(jīng)驗(yàn)就是得規(guī)避大事務(wù)變更,最終他們與開發(fā)商達(dá)成相應(yīng)的操作規(guī)范,無論是業(yè)務(wù)變更需求也好,數(shù)據(jù)庫運(yùn)維發(fā)起的清理作業(yè)也罷,如涉及Timestens同步的表,都得遵循變更量達(dá)10w萬就得分批提交,每個(gè)批次2萬條記錄,每批次之間sleep 30秒的硬性規(guī)定。我覺得這個(gè)故事的結(jié)局很完滿了,真的,要是換成非得揪著Timesten不放,意圖純粹靠軟件解決問題的話,那才是妥妥的災(zāi)難現(xiàn)場呢,畢竟其基于trigger的同步機(jī)制從原理上就對大事務(wù)極不友好……
問題來了,如何找到軟件的短板呢?
閱讀官方文檔自然是一個(gè)渠道,當(dāng)然,閱讀也是有“技巧” 的 :
我們支持xx指標(biāo)以內(nèi)的場景,這句話可以理解為超過這個(gè)您就得想想辦法了,同時(shí),這個(gè)值也許是要打個(gè)折的,畢竟環(huán)境不一樣,存在差異也是很合理的。
我們支持功能a,也支持功能b,這都是實(shí)話,至于同時(shí)支持功能a和b是您自己認(rèn)為的,我可沒說。
這個(gè)嘛,春秋筆法是有的,這種玩法自古就有了。陳壽不也沒在《三國志》里面明說司馬昭弒君嗎,后來大家不都知道了嗎?
除了文檔以外,我們還可以考慮結(jié)合自身經(jīng)驗(yàn)考慮下述點(diǎn)。
大事務(wù)測試
分別對同步范圍內(nèi)外的對象做批量操作,加大數(shù)據(jù)庫日志量,觀察其對數(shù)據(jù)同步以及系統(tǒng)的影響,具體包括cpu、內(nèi)存、io、空間等資源消耗以及同步延遲等。
以dts為例,源端oracle數(shù)據(jù)庫產(chǎn)生的所有數(shù)據(jù)均會被拉到dts的庫中分析,哪怕這數(shù)據(jù)與我們的同步策略無關(guān)。
目前有個(gè)黑名單功能可以繞過這問題。
長事務(wù)測試
包含啟動增量同步前開啟的事務(wù)能否正常同步、長時(shí)間未提交的事務(wù)是否影響同步進(jìn)程重啟等維度。
很明顯, 這是被ogg嚇到的結(jié)果。
頻繁事務(wù)測試
筆者曾在O2O同步環(huán)境中遇到某應(yīng)用使用了大量with as語法,后者隱式開啟了大量的短事務(wù),進(jìn)而短時(shí)間內(nèi)事務(wù)量暴漲,進(jìn)而 導(dǎo)致同步軟件Ogg抽取進(jìn)程出現(xiàn)延遲。這個(gè)問題后來找開發(fā)商修改語句就解決了,然而其對筆者的心理陰影一直都在,以至于每遇到一個(gè)新場景,均會想想會不會遇到類似的問題。
事務(wù)順序
這個(gè)探究的是,軟件同步是否能保持事務(wù)的順序,如不能保持,那就得多留個(gè)心眼了,這種情況輕則導(dǎo)致同步延遲誤判,重則導(dǎo)致舊 數(shù)據(jù)覆蓋新數(shù)據(jù),影響數(shù)據(jù)一致性。
一個(gè)簡單的測試樣例為,創(chuàng)建一個(gè)周期性(如每分鐘)更新的時(shí)間戳表,這個(gè)表的記錄數(shù)與源端Oracle DB的節(jié)點(diǎn)數(shù)一致。定時(shí)腳本依次連接各個(gè)實(shí)例并以當(dāng)前時(shí)間更新相應(yīng)的字段,在目標(biāo)端,我們可以通過查詢時(shí)間點(diǎn)表觀察會不會出現(xiàn)下面兩種情況:
- 后更新的數(shù)據(jù)是否會先被查詢出來;
- 同步出現(xiàn)延遲的話,時(shí)間戳表記錄的時(shí)間會不會與同步進(jìn)程的時(shí)間戳保持一致。
批量ddl測試
對于基于數(shù)據(jù)庫日志的同步工具,大批量的DDL語句很可能會觸發(fā)源端解析緩慢的情況,畢竟這涉及與數(shù)據(jù)字典的交互。
筆者曾遇過某個(gè)基于Ogg的O2O同步環(huán)境遇到這種瓶頸,而最終的解決方案為調(diào)整開發(fā)商版本上載的腳本,加大DDL語句之間等待時(shí)間間隔。
基于同步原理短板的探究
對于基于rowid的同步方案,沒啥好說的,只能從規(guī)范上減少move、shrink等改變r(jià)owid操作,實(shí)在要操作的話,需要重新同步相應(yīng)的數(shù)據(jù)。
對于基于主鍵的同步方案,則重點(diǎn)考慮如何處理無主鍵表如何處理。
進(jìn)程重啟測試
包含兩種場景,正常重啟以及異常重啟。
異常重啟即高可用方面的,具體不展開了;至于正常重啟的話,需要觀察進(jìn)程的一些自定義參數(shù)會不會被重置。