cassandra百億級數據庫遷移實踐

遷移背景

cassandra集群隔段時間出現rt飆高的問題,帶來的影響就是請求cassandra短時間內出現大量超時,這個問題發生已經達到了平均兩週一次的頻率,已經影響到正常業務了。而出現這些問題的原因主要有以下3點:
1.當初設計表的時候partition key設計的不是很合理,當數據量上去(最大的單錶行數達到百億級)之後,出現了一些數據量比較大的partition。單partition最多的數據量達到了上百萬行(cassandra不支持mysql的limit m, n的查詢),當查詢這個partition的數據時,會帶來比較大的壓力。
2.cassandra本身的墓碑機制,cassandra的一大特性就是快速寫入,如果遇到delete一條記錄時,cassandra並不會實時的對這條記錄做物理刪除,而是在這行記錄上添加一個邏輯刪除的標誌位,而下次查詢會load出這些已經刪除了的記錄,再做過濾。這樣就可能帶來及時某個partition的查詢出的數據量不大,但是墓碑比較多的時候會帶來嚴重的性能問題。
3.公司dba也不推薦使用cassandra,出現問題的時候,難於定位解決問題。所以決定將cassandra數據庫遷移至社區比較成熟的關係型數據庫mysql。

遷移方案

整個遷移方案主要分為以下5個步驟:

  1. 全量遷移:搬遷當前庫中所有的歷史數據(該過程會搬掉庫中大部分數據)
  2. 增量遷移:記錄全量遷移開始的時間,搬遷全量遷移過程中變更了的數據
  3. 數據比對:通過接口比對cassandra和mysql中的數據,最終數據一致性達到一定99.99%以上
  4. 開雙寫:通過數據比對確保全量遷移和增量遷移沒問題以後,打開雙寫。如果雙寫有問題,數據比對還可以發現雙寫中的問題。
  5. 切mysql讀:確保雙寫沒問題以後,然後根據服務的重要性級別,逐步按服務切mysql讀。所有服務切mysql讀以後,確保沒問題後關閉cassandra寫,最終下線cassandra。

mysql的分庫分表方案

  1. 分多少張表?在DBA的推薦下,單表的數據最好不要超過200w,估算了下最大一張表數據量100億左右,再考慮到數據未來數據增長的情況,最大的這張表分了8192張表,單表的數據量120w左右,總共分了4個物理庫,每個庫2048張表。
  2. 字段對應的問題? 這裡需要權衡一個問題,cassandra有List、Set、Map等結構,到mysql這邊怎麼存?這裡可以根據自己實際情況選擇,
  • 集合結構的轉成json之後長度都在1000個字符以內的,可以直接轉成json用varchar來保存,優點:處理起來簡單。缺點:需要考慮集合的數據增長問題。
  • 轉成json之後長度比較長,部分已經達到上萬個字符了,用單獨的一張表來保存。優點:不用考慮集合的數據增長問題。缺點:處理起來麻煩,需要額外維護新的表。
  1. mysql分片鍵的選擇,我們這裡直接採用的cassandra的partition key。
  2. mysql表的主鍵和cassandra表保持一致。

全量遷移方案調研

  1. copy導出:通過cqlsh提供的copy命令,把keyspace導出到文件。
  2. 缺陷:
  • 在測試過程中,導出速度大概4500行每秒,在導出過程中偶爾會有超時,導出如果中斷,不能從中斷處繼續。
  • 如果keyspace比較大,則生成的文件比較大。所以這種方式不考慮
  1. sstableloader方式:這種方式僅支持從一個cassandra集群遷移到另一個cassandra集群。所以該方式也不考慮
  2. token環遍歷方式:cassandra記錄的存儲原理是採用的一致性hash的策略
cassandra百億級數據庫遷移實踐

整個環的範圍是[Long.MIN_VALUE, Long.MAX_VALUE],表的每條記錄都是通過partition key進行hash計算,然後確定落到哪個位置。

  • 例如有這樣一張表:
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
  • 循環以上兩個過程,直到token(a, b) = LONG.MAX_VALUE,表示整個表遍歷完成。最終採用了該方式。以上幾個方案都有一個共同的問題,在遷移過程中,數據有變更,這種情況需要額外考慮。

全量遷移詳細過程

最終採用了以上方案3,通過遍歷cassandra表的token環的方式,遍歷表的所有數據,把數據搬到mysql中。具體如下:

  1. 把整個token環分為2048段,這麼做的目的是為了,把每張表的一個大的遷移任務,劃分為2048個小任務,當單個遷移任務出現問題的時候,不用所有數據重頭再來,
  2. 只需要把出問題的一個小任務重跑就好了。這裡採用多線程。
  3. 遷移模式:主要有single和batch兩種模式:
  • single模式:逐一insert至mysql。數據量不大的情況選擇,單表億級別以下選擇,在64個線程情況下,16個線程讀cassandra的情況下,速度可以達到1.5w行每秒。
  • batch模式:batch insert至mysql。數據量比較大的情況下選擇,單表過億的情況下選擇。最大的一張100億數據量的表,遷移過程實際上峰值速度只有1.6w行每秒的速度。這是因為cassandra讀這部分達到瓶頸了。本身線上應用耗掉了部分資源。如果cassandra讀沒有達到瓶頸,速度翻倍是沒問題的。
  1. 遷移性能問題:這時候cassandra和mysql和應用機器本身都可能成為瓶頸點,數據量比較大,儘量採用性能好一點的機器。我們當時遷移的時候,採用的一臺40核、100G+內存的機器。
  2. 該過程遇到的一些問題:
  • 異常處理問題:由於本身cassandra和mysql的字段限制有一定區別。在這個過程肯定會遇到部分記錄因為某列不符合mysql列的限制,導致寫入失敗,寫入失敗的記錄會記錄到文件。這一過程最好是在測試過程中覆蓋的越全越好。具體的一些case如下:
  • cassandra text長度超過mysql的限制長度
  • cassandra為null的情況,mysql字段設置為is not null(這種情況需要創建表的時候多考慮)
  • cassandra的timestamp類型超過了mysql的datetime的範圍(eg:1693106-07-01 00:00:00)
  • cassandra的decimail類型超過了mysql的decimail範圍(eg:6232182630000136384.0)
  • 數據遺漏問題:由於部分表的字段比較多,代碼中字段轉換的時候最好仔細一點。我們這邊遇到過字段錯亂、字段漏掉等問題。再加上該過程沒有測試接入,自己開發上線了,數據遷移完成後才發現字段漏掉,然後又重頭再來,其中最大的一張表,從頭遷一次差不多需要花掉2周的時間。現在回過頭去看,這張表當初遷移的時候,還不止返工一次。這個過程實際上是非常浪費時間的。
  • 慢查詢問題:在最大的一張表的遷移過程中,超時比其他小表要嚴重一些。並且在跑的過程中發現,速度越跑越慢,排查發現是部分線程遇到了某個token查詢始終超時的情況。然後線程一直死循環查詢查token。當把cassandra超時時間設置為30s時,這種情況有所改善,但還存在極個別token存在該問題。此處有一點奇怪的是,通過登錄到線上cassandra機器,通過cqlsh直接查詢,數據是能夠查詢出來的。最終處理方案是針對該token加了5次重試,如果還是不成功,則記錄日誌單獨處理。

增量遷移詳細過程

記錄全量遷移開始的時間,以及記錄這段時間所有變更的account(一個user包含多個account),把這部分數據發往kafka。再通過額外的增量遷移程序消費kakfa的方式把這部分數據搬到mysql,循環往復該過程,直到mysql中的數據追上cassandra中的數據。

  1. 消費兩個kafka隊列,一個為全量遷移這段時間離線變更的account隊列,另一個是當前業務實時變更的account隊列。
  2. 處理過程中需要考慮兩個隊列中account衝突的問題,可以根據accountid進行加鎖。
  3. 起初是按照user維度,進行增量遷移。實際上線後發現,按照user維度搬遷速度根本追不上正常業務數據變更的速度。然後選擇了比user低一個維度的account(一個user包含多個account)進行遷移。

數據比對

為什麼有該步驟?為了確保cassandra和mysql數據源儘可能的一致。

  1. 在全量遷移完成以後,增量遷移過程中,便上線了該比對功能。如何比對?當線上業務產生了數據變更,根據accountid,把該accountid下的cassandra的所有數據和mysql的所有數據通過調接口的形式查詢出來進行比對。精確到具體字段的值
  2. 原本認為全量遷移和增量遷移基本沒什麼問題了,但是通過數據比對還發現了不少的數據不一致地方。排查發現有全量遷移過程導致的,也有增量遷移過程導致的,都是代碼bug導致。發現了問題如果某張表全量遷移過程都出了問題,除了需要重新全量遷移該表。並且增量遷移也需要重頭再來。
  3. 所有的比對結果存入數據庫,然後定時任務發現比對不過的數據,再按照account維度進行增量遷移。
  4. 遇到的主要問題如下:
  • 時間精度的問題:cassandra的timestamp時間戳精確到毫秒(cassandra的一個客戶端工具DevCenter查詢出來的時間只精確到秒,毫秒部分被截斷了,如果通過該工具肉眼比對,不容易發現該問題),而mysql的datetime默認條件只精確到了秒。
  • decimal小數位問題:cassandra中採用的decimal,對應mysql的字段類型是decimal(18,2),cassandra中如果是0或者0.000,遷移到mysql中會變成0.00,需要注意該精度問題。
  • 兩張表來保存同一份數據導致髒數據問題:由於cassandra查詢有很多限制,為了支持多種查詢類型。創建了兩張字段一模一樣的表,除了primary key不一樣。然後每次增刪改的時候,兩張表分別都增刪改,雖然這種方式帶來了查詢上的遍歷,但是產生髒數據的幾率非常大。在比對的過程中,發現同一份數據兩張表的數據量相差不小,排查發現由於早期代碼bug導致表一寫成功,表二寫失敗這種情況(好在的是這些數據都是很早之前的數據,所以直接忽略該問題)。而遷移至mysql,只遷移一張表過去。如果兩張表的數據不能完全一致,必然有接口表現不一致。我個人對這種一份數據保存兩份用法也是不推薦的,如果不在物理層做限制,只通過代碼邏輯層來保證數據的一致性,是幾乎不可能的事。
  • 空字符和NULL的問題:cassandra中""空字符串的情況下轉換至mysql變為了NULL,這種情況會帶來接口返回的數據不一致的問題,在不確定下游如何使用該數據的時候,最好保證完全一致。
  • 字段漏掉的問題:比對發現有張表的一個字段漏掉了,根本沒有遷移過去,除了需要重新全量遷移該表。並且增量遷移也需要重頭再來(儘量避免該問題,該過程是非常耗時的)。
  • cassandra數據不一致的問題:同一條select查詢語句,連續查詢兩次返回的結果數不一致。這個比例在萬分之一-千分之一,帶來的問題就是有的數據始終是比較不過的。
  • 應用本地時鐘不一致導致的問題:現象就是隨著應用的發版,某張表的lastModifyTime的時間,出現了cassandra比mysql小的情況,而從業務角度來說,mysql的時間是正確的。大概有5%的這種情況,並且不會降下去。可能隨著下一次發版,該問題就消失了。近10次發版有3次出現了該問題,最終排查發現,由於部署線上應用機器的本地時鐘相差3秒,而cassandra會依賴客戶端的時間,帶來的問題就是cassandra後提交的寫入,可能被先提交的寫入覆蓋。為什麼該問題會隨著發版而偶然出現呢?因為應用是部署在容器中,每次發版都會分配新的容器。

開雙寫

經過以上步驟,基本可以認為cassandra和mysql的數據是一致的。然後打開雙寫,再關閉增量遷移。這時候如果雙寫有問題,通過比對程序也能夠發現。

切mysql讀

雙寫大概一週後,沒什麼問題的話,就可以逐步按服務切mysql讀,然後就可以下線cassandra數據庫了。

總結

  1. cassandra的使用:
  • 表的設計:特別需要注意partition key的設計,儘量要保證單個partition的數據量不要太大。
  • 墓碑機制:需要注意cassandra的本身的墓碑機制,主要產生的墓碑的情況,主要是delete操作和insert null字段這兩種情況。我們這裡曾經因為某個用戶頻繁操作自己app的某個動作,導致數據庫這邊頻繁的對同一個partition key執行delete操作再insert操作。用戶執行操作接近上百次後,導致該partition產生大量墓碑,最終查詢請求打到該partition key。造成慢查詢,應用超時重試,導致cassandra cpu飆升,最終導致其他partition key也受到影響,大量查詢超時。
  • cassandra客戶端時鐘不一致的問題,可能導致寫入無效。
  1. 遷移相關:
  • 全量遷移和增量遷移,最好在上線之前測試充分,千萬注意字段漏掉錯位的問題,儘可能的讓測試參與。在正式遷移之前,最好在線上創建一個預備庫,先可以預跑一次。儘可能的發現線上正式遷移時遇到的問題。否則正式遷移的時候遇問題的時候,修復是比較麻煩的。
  • 在切或關閉讀寫過程中,一定要有回滾計劃。


分享到:


相關文章: