cassandra百億級資料庫遷移實踐

臥顏沉默發表於2019-05-08

遷移背景

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之後長度比較長,部分已經達到上萬個字元了,用單獨的一張表來儲存。優點:不用考慮集合的資料增長問題。缺點:處理起來麻煩,需要額外維護新的表。
  3. mysql分片鍵的選擇,我們這裡直接採用的cassandra的partition key。
  4. mysql表的主鍵和cassandra表保持一致。

全量遷移方案調研

  1. copy匯出:通過cqlsh提供的copy命令,把keyspace匯出到檔案。 缺陷:

    • 在測試過程中,匯出速度大概4500行每秒,在匯出過程中偶爾會有超時,匯出如果中斷,不能從中斷處繼續。
    • 如果keyspace比較大,則生成的檔案比較大。所以這種方式不考慮
  2. sstableloader方式:這種方式僅支援從一個cassandra叢集遷移到另一個cassandra叢集。所以該方式也不考慮

  3. 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. 遷移模式:主要有single和batch兩種模式:
    • single模式:逐一insert至mysql。資料量不大的情況選擇,單表億級別以下選擇,在64個執行緒情況下,16個執行緒讀cassandra的情況下,速度可以達到1.5w行每秒。
    • batch模式:batch insert至mysql。資料量比較大的情況下選擇,單表過億的情況下選擇。最大的一張100億資料量的表,遷移過程實際上峰值速度只有1.6w行每秒的速度。這是因為cassandra讀這部分達到瓶頸了。本身線上應用耗掉了部分資源。如果cassandra讀沒有達到瓶頸,速度翻倍是沒問題的。
  3. 遷移效能問題:這時候cassandra和mysql和應用機器本身都可能成為瓶頸點,資料量比較大,儘量採用效能好一點的機器。我們當時遷移的時候,採用的一臺40核、100G+記憶體的機器。
  4. 該過程遇到的一些問題:
    • 異常處理問題:由於本身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客戶端時鐘不一致的問題,可能導致寫入無效。
  2. 遷移相關:
    • 全量遷移和增量遷移,最好在上線之前測試充分,千萬注意欄位漏掉錯位的問題,儘可能的讓測試參與。在正式遷移之前,最好線上上建立一個預備庫,先可以預跑一次。儘可能的發現線上正式遷移時遇到的問題。否則正式遷移的時候遇問題的時候,修復是比較麻煩的。
    • 在切或關閉讀寫過程中,一定要有回滾計劃。

版權宣告
作者:wycm
出處:juejin.im/post/5cd284…
您的支援是對博主最大的鼓勵,感謝您的認真閱讀。 本文版權歸作者所有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連線,否則保留追究法律責任的權利。

一個程式設計師日常分享,包括但不限於爬蟲、Java後端技術,歡迎關注

相關文章