cassandra百亿级数据库迁移实践
為什么80%的碼農(nóng)都做不了架構(gòu)師?>>> ??
遷移背景
cassandra集群隔段時(shí)間出現(xiàn)rt飆高的問題,帶來(lái)的影響就是請(qǐng)求cassandra短時(shí)間內(nèi)出現(xiàn)大量超時(shí),這個(gè)問題發(fā)生已經(jīng)達(dá)到了平均兩周一次的頻率,已經(jīng)影響到正常業(yè)務(wù)了。而出現(xiàn)這些問題的原因主要有以下3點(diǎn):
遷移方案
整個(gè)遷移方案主要分為以下5個(gè)步驟:
mysql的分庫(kù)分表方案
- 集合結(jié)構(gòu)的轉(zhuǎn)成json之后長(zhǎng)度都在1000個(gè)字符以內(nèi)的,可以直接轉(zhuǎn)成json用varchar來(lái)保存,優(yōu)點(diǎn):處理起來(lái)簡(jiǎn)單。缺點(diǎn):需要考慮集合的數(shù)據(jù)增長(zhǎng)問題。
- 轉(zhuǎn)成json之后長(zhǎng)度比較長(zhǎng),部分已經(jīng)達(dá)到上萬(wàn)個(gè)字符了,用單獨(dú)的一張表來(lái)保存。優(yōu)點(diǎn):不用考慮集合的數(shù)據(jù)增長(zhǎng)問題。缺點(diǎn):處理起來(lái)麻煩,需要額外維護(hù)新的表。
全量遷移方案調(diào)研
copy導(dǎo)出:通過(guò)cqlsh提供的copy命令,把keyspace導(dǎo)出到文件。 缺陷:
- 在測(cè)試過(guò)程中,導(dǎo)出速度大概4500行每秒,在導(dǎo)出過(guò)程中偶爾會(huì)有超時(shí),導(dǎo)出如果中斷,不能從中斷處繼續(xù)。
- 如果keyspace比較大,則生成的文件比較大。所以這種方式不考慮
sstableloader方式:這種方式僅支持從一個(gè)cassandra集群遷移到另一個(gè)cassandra集群。所以該方式也不考慮
token環(huán)遍歷方式:cassandra記錄的存儲(chǔ)原理是采用的一致性hash的策略<br>整個(gè)環(huán)的范圍是[Long.MIN_VALUE, Long.MAX_VALUE],表的每條記錄都是通過(guò)partition key進(jìn)行hash計(jì)算,然后確定落到哪個(gè)位置。
- 例如有這樣一張表:
- 通過(guò)以下兩個(gè)cql就可以遍歷該張表:
- 循環(huán)以上兩個(gè)過(guò)程,直到token(a, b) = LONG.MAX_VALUE,表示整個(gè)表遍歷完成。最終采用了該方式。以上幾個(gè)方案都有一個(gè)共同的問題,在遷移過(guò)程中,數(shù)據(jù)有變更,這種情況需要額外考慮。
全量遷移詳細(xì)過(guò)程
最終采用了以上方案3,通過(guò)遍歷cassandra表的token環(huán)的方式,遍歷表的所有數(shù)據(jù),把數(shù)據(jù)搬到mysql中。具體如下:
- single模式:逐一insert至mysql。數(shù)據(jù)量不大的情況選擇,單表億級(jí)別以下選擇,在64個(gè)線程情況下,16個(gè)線程讀cassandra的情況下,速度可以達(dá)到1.5w行每秒。
- batch模式:batch insert至mysql。數(shù)據(jù)量比較大的情況下選擇,單表過(guò)億的情況下選擇。最大的一張100億數(shù)據(jù)量的表,遷移過(guò)程實(shí)際上峰值速度只有1.6w行每秒的速度。這是因?yàn)閏assandra讀這部分達(dá)到瓶頸了。本身線上應(yīng)用耗掉了部分資源。如果cassandra讀沒有達(dá)到瓶頸,速度翻倍是沒問題的。
- 異常處理問題:由于本身cassandra和mysql的字段限制有一定區(qū)別。在這個(gè)過(guò)程肯定會(huì)遇到部分記錄因?yàn)槟沉胁环蟤ysql列的限制,導(dǎo)致寫入失敗,寫入失敗的記錄會(huì)記錄到文件。這一過(guò)程最好是在測(cè)試過(guò)程中覆蓋的越全越好。具體的一些case如下:
- cassandra text長(zhǎng)度超過(guò)mysql的限制長(zhǎng)度
- cassandra為null的情況,mysql字段設(shè)置為is not null(這種情況需要?jiǎng)?chuàng)建表的時(shí)候多考慮)
- cassandra的timestamp類型超過(guò)了mysql的datetime的范圍(eg:1693106-07-01 00:00:00)
- cassandra的decimail類型超過(guò)了mysql的decimail范圍(eg:6232182630000136384.0)
- 數(shù)據(jù)遺漏問題:由于部分表的字段比較多,代碼中字段轉(zhuǎn)換的時(shí)候最好仔細(xì)一點(diǎn)。我們這邊遇到過(guò)字段錯(cuò)亂、字段漏掉等問題。再加上該過(guò)程沒有測(cè)試接入,自己開發(fā)上線了,數(shù)據(jù)遷移完成后才發(fā)現(xiàn)字段漏掉,然后又重頭再來(lái),其中最大的一張表,從頭遷一次差不多需要花掉2周的時(shí)間。現(xiàn)在回過(guò)頭去看,這張表當(dāng)初遷移的時(shí)候,還不止返工一次。這個(gè)過(guò)程實(shí)際上是非常浪費(fèi)時(shí)間的。
- 慢查詢問題:在最大的一張表的遷移過(guò)程中,超時(shí)比其他小表要嚴(yán)重一些。并且在跑的過(guò)程中發(fā)現(xiàn),速度越跑越慢,排查發(fā)現(xiàn)是部分線程遇到了某個(gè)token查詢始終超時(shí)的情況。然后線程一直死循環(huán)查詢查token。當(dāng)把cassandra超時(shí)時(shí)間設(shè)置為30s時(shí),這種情況有所改善,但還存在極個(gè)別token存在該問題。此處有一點(diǎn)奇怪的是,通過(guò)登錄到線上cassandra機(jī)器,通過(guò)cqlsh直接查詢,數(shù)據(jù)是能夠查詢出來(lái)的。最終處理方案是針對(duì)該token加了5次重試,如果還是不成功,則記錄日志單獨(dú)處理。
增量遷移詳細(xì)過(guò)程
記錄全量遷移開始的時(shí)間,以及記錄這段時(shí)間所有變更的account(一個(gè)user包含多個(gè)account),把這部分?jǐn)?shù)據(jù)發(fā)往kafka。再通過(guò)額外的增量遷移程序消費(fèi)kakfa的方式把這部分?jǐn)?shù)據(jù)搬到mysql,循環(huán)往復(fù)該過(guò)程,直到mysql中的數(shù)據(jù)追上cassandra中的數(shù)據(jù)。
數(shù)據(jù)比對(duì)
為什么有該步驟?為了確保cassandra和mysql數(shù)據(jù)源盡可能的一致。
- 時(shí)間精度的問題:cassandra的timestamp時(shí)間戳精確到毫秒(cassandra的一個(gè)客戶端工具DevCenter查詢出來(lái)的時(shí)間只精確到秒,毫秒部分被截?cái)嗔?#xff0c;如果通過(guò)該工具肉眼比對(duì),不容易發(fā)現(xiàn)該問題),而mysql的datetime默認(rèn)條件只精確到了秒。
- decimal小數(shù)位問題:cassandra中采用的decimal,對(duì)應(yīng)mysql的字段類型是decimal(18,2),cassandra中如果是0或者0.000,遷移到mysql中會(huì)變成0.00,需要注意該精度問題。
- 兩張表來(lái)保存同一份數(shù)據(jù)導(dǎo)致臟數(shù)據(jù)問題:由于cassandra查詢有很多限制,為了支持多種查詢類型。創(chuàng)建了兩張字段一模一樣的表,除了primary key不一樣。然后每次增刪改的時(shí)候,兩張表分別都增刪改,雖然這種方式帶來(lái)了查詢上的遍歷,但是產(chǎn)生臟數(shù)據(jù)的幾率非常大。在比對(duì)的過(guò)程中,發(fā)現(xiàn)同一份數(shù)據(jù)兩張表的數(shù)據(jù)量相差不小,排查發(fā)現(xiàn)由于早期代碼bug導(dǎo)致表一寫成功,表二寫失敗這種情況(好在的是這些數(shù)據(jù)都是很早之前的數(shù)據(jù),所以直接忽略該問題)。而遷移至mysql,只遷移一張表過(guò)去。如果兩張表的數(shù)據(jù)不能完全一致,必然有接口表現(xiàn)不一致。我個(gè)人對(duì)這種一份數(shù)據(jù)保存兩份用法也是不推薦的,如果不在物理層做限制,只通過(guò)代碼邏輯層來(lái)保證數(shù)據(jù)的一致性,是幾乎不可能的事。
- 空字符和NULL的問題:cassandra中""空字符串的情況下轉(zhuǎn)換至mysql變?yōu)榱薔ULL,這種情況會(huì)帶來(lái)接口返回的數(shù)據(jù)不一致的問題,在不確定下游如何使用該數(shù)據(jù)的時(shí)候,最好保證完全一致。
- 字段漏掉的問題:比對(duì)發(fā)現(xiàn)有張表的一個(gè)字段漏掉了,根本沒有遷移過(guò)去,除了需要重新全量遷移該表。并且增量遷移也需要重頭再來(lái)(盡量避免該問題,該過(guò)程是非常耗時(shí)的)。
- cassandra數(shù)據(jù)不一致的問題:同一條select查詢語(yǔ)句,連續(xù)查詢兩次返回的結(jié)果數(shù)不一致。這個(gè)比例在萬(wàn)分之一-千分之一,帶來(lái)的問題就是有的數(shù)據(jù)始終是比較不過(guò)的。
- 應(yīng)用本地時(shí)鐘不一致導(dǎo)致的問題:現(xiàn)象就是隨著應(yīng)用的發(fā)版,某張表的lastModifyTime的時(shí)間,出現(xiàn)了cassandra比mysql小的情況,而從業(yè)務(wù)角度來(lái)說(shuō),mysql的時(shí)間是正確的。大概有5%的這種情況,并且不會(huì)降下去。可能隨著下一次發(fā)版,該問題就消失了。近10次發(fā)版有3次出現(xiàn)了該問題,最終排查發(fā)現(xiàn),由于部署線上應(yīng)用機(jī)器的本地時(shí)鐘相差3秒,而cassandra會(huì)依賴客戶端的時(shí)間,帶來(lái)的問題就是cassandra后提交的寫入,可能被先提交的寫入覆蓋。為什么該問題會(huì)隨著發(fā)版而偶然出現(xiàn)呢?因?yàn)閼?yīng)用是部署在容器中,每次發(fā)版都會(huì)分配新的容器。
開雙寫
經(jīng)過(guò)以上步驟,基本可以認(rèn)為cassandra和mysql的數(shù)據(jù)是一致的。然后打開雙寫,再關(guān)閉增量遷移。這時(shí)候如果雙寫有問題,通過(guò)比對(duì)程序也能夠發(fā)現(xiàn)。
切mysql讀
雙寫大概一周后,沒什么問題的話,就可以逐步按服務(wù)切mysql讀,然后就可以下線cassandra數(shù)據(jù)庫(kù)了。
總結(jié)
- 表的設(shè)計(jì):特別需要注意partition key的設(shè)計(jì),盡量要保證單個(gè)partition的數(shù)據(jù)量不要太大。
- 墓碑機(jī)制:需要注意cassandra的本身的墓碑機(jī)制,主要產(chǎn)生的墓碑的情況,主要是delete操作和insert null字段這兩種情況。我們這里曾經(jīng)因?yàn)槟硞€(gè)用戶頻繁操作自己app的某個(gè)動(dòng)作,導(dǎo)致數(shù)據(jù)庫(kù)這邊頻繁的對(duì)同一個(gè)partition key執(zhí)行delete操作再insert操作。用戶執(zhí)行操作接近上百次后,導(dǎo)致該partition產(chǎn)生大量墓碑,最終查詢請(qǐng)求打到該partition key。造成慢查詢,應(yīng)用超時(shí)重試,導(dǎo)致cassandra cpu飆升,最終導(dǎo)致其他partition key也受到影響,大量查詢超時(shí)。
- cassandra客戶端時(shí)鐘不一致的問題,可能導(dǎo)致寫入無(wú)效。
- 全量遷移和增量遷移,最好在上線之前測(cè)試充分,千萬(wàn)注意字段漏掉錯(cuò)位的問題,盡可能的讓測(cè)試參與。在正式遷移之前,最好在線上創(chuàng)建一個(gè)預(yù)備庫(kù),先可以預(yù)跑一次。盡可能的發(fā)現(xiàn)線上正式遷移時(shí)遇到的問題。否則正式遷移的時(shí)候遇問題的時(shí)候,修復(fù)是比較麻煩的。
- 在切或關(guān)閉讀寫過(guò)程中,一定要有回滾計(jì)劃。
版權(quán)聲明 作者:wycm
出處:https://my.oschina.net/wycm/blog/3046173
您的支持是對(duì)博主最大的鼓勵(lì),感謝您的認(rèn)真閱讀。
本文版權(quán)歸作者所有,歡迎轉(zhuǎn)載,但未經(jīng)作者同意必須保留此段聲明,且在文章頁(yè)面明顯位置給出原文連接,否則保留追究法律責(zé)任的權(quán)利。
轉(zhuǎn)載于:https://my.oschina.net/wycm/blog/3046173
總結(jié)
以上是生活随笔為你收集整理的cassandra百亿级数据库迁移实践的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python+selenium中webd
- 下一篇: loadrunner编写脚本常用策略,用