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