狂贊!海量數據遷移方案,免費送給你

一、背景

在創業初期,爲了快速把項目搭建運行起來,往往不會過多地去考慮系統是否可以支持未來更大的數據吞吐量,所以往往不會分表或分庫。可當項目真正運行了一年兩年之後,會發現原來的單表已經存儲不了更多的數據了,或者查詢性能受到影響,此時就要考慮分庫或分表了。

一般涉及到分庫分表,數據遷移是必須要做的一個工作。那麼接下來,筆者就以自己親身實踐過的一次數據遷移經驗爲依據,向大家介紹一下,當數據量過億時,進行數據遷移,我們要做些什麼,有哪些坑(限於保密協議,筆者會對錶名及字段名做一定的脫敏,但不妨礙理解)。

假設這裏有兩個表,分別用於存儲用戶行爲數據和行爲狀態(你可以想像成是下單行爲,或者投稿行爲等等),目前已分了100張表。可是隨着用戶數據的暴增,且這種分表策略(user_id % 100)難以擴展(如果想擴展爲200張表,則需要將原來100張表所有數據重新散列到200張表中,工作量很大),於是現在採用了一致性哈希算法來重新分表。

以下是兩張表的表結構:

表1:user_action_0

表2:user_action_status_0

其中第一張表user_id和data_id爲一個唯一索引;第二張表中action_id與status爲唯一索引。其中action_id即爲第一個表的id。

新的分表策略採用一致性哈希算法,關於一致性哈希,網上有若干博文可以查閱,此處不展開來講,如:

公衆號:Java技術人五分鐘徹底理解一致性哈希算法

我們的實現思路是,將分表shard字段(這裏是user_id)作md5,結果是一個32位的16進制字符串,然後取其中4位,前3位轉爲數字,後一位轉爲數字,則前3位正好是163=4096種可能,後一位16種可能。4096種可能表現爲環形哈希的4096個桶,而這些桶最多可以存放4096個庫,初期我只有16個庫即可,後一位的16種可能,作爲一個庫中的16個分表。這樣下來,本次要將之前100張表的數據,洗到16*16=256個表中。

目前user_action_0-100表中有1.8億條數據;user_action_status_0-100中有3.5億條數據。共77G的數據。並且每天以百萬的量增加。

二、思路&方案

瞭解上面的背景以後,來看看如何設計方案。

  1. 建庫:創建新的數據庫與分表

  2. 重構項目:將現在的項目改爲新的分庫分表策略

  3. 全量遷移:從舊庫舊錶中全量移植數據到新庫新表

 

當移植數據時,user_action_n沒有什麼問題,只要從舊錶取出數據,根據user_id按照新的策略,將數據插入到新庫新表即可。但是user_action_status_n表中的action_id依賴於前表,在讀取了狀態數據以後,不能直接根據新策略插入到新表,因爲其中的action_id還是舊的。

關於這個依賴問題,可以有以下的方案:

    1)兩個表同時進行遷移,即插入action表,成功後獲取id;讀取以該條記錄爲id的status數據,查詢並更新其中的action_id,將狀態數據插入到新庫新表中。
    2)兩個表獨立插入,action表插入時,維護一箇中間表,保存新舊的action_id的映射關係,插入status表時,再從這個中間表獲取。
    3)兩個表獨立插入,action表插入時,主鍵不再用自增,而是讓主鍵加上一個偏移量,100張表的主鍵出現重複的可能,再插入status表時,更新其中的action_id時只需要一個很少的函數計算就能得到新的action_id。簡單說下這個偏移量的計算,假如有100張表,其中最大的表大小爲100萬,則第一張表主鍵id+0遷移,第二張表主鍵id+100萬,第三張表主鍵id+2*100萬...以此類推。這樣,將舊錶中數據重新散列到新庫以後,就不會出現主鍵的重複問題。

    4. 增量程序:背景中說過,數據並不是靜止的,每天都在發生變化,那就要有一個增量程序在全量結束以後,把新增加的數據遷入新庫,這個也可以有下面的方案:
    1)全量結束以後,從舊庫中掃表,將大於某個id或時間點的數據移植過來,但這個有個缺點,100個表如何去掃?掃完user_action_0再去掃user_action_1的時候,0可能又增加了不少數據。
    2)建一個change_log表,舊庫在線上寫入時,同時往這個表中記錄增刪改查的行爲(包括操作類型,操作表,主鍵id),如action表的insert、update。這樣全量以後,只要從這個表出發,就可以簡單而高效地全表掃描了。

    

    5、檢查:數據遷移完成之後,測試是難免的,讓QA找那麼20來個用戶數據進行驗證,但人的力量終究太小了。因此需要一個檢查程序
    1)讀取舊庫分表條目數加和,新庫的分表條目數加和,看差別的大小;
    2)從舊庫的每個表中取出1w條數據,與新庫中的數據進行按字段比較

以上就是這次遷移的整理思路,下面理一下先後順序:

1、建庫建表,建庫建表(包括change_log表)
2、將線上程序,在每次寫入action、status表數據時,插change_log表,上線以後觀察數據是否正確
3、編寫全量移植程序、增量移植程序、檢查程序
4、將線上程序,去掉寫入change_log表,改爲新的分庫策略(這是最終上線版本)
5、在線下進行全量、增量的程序測試,尤其是全量,要能根據線下估算出線上的大概執行時間,能否全部執行完成(會不會在中間因爲內存等問題歇菜),日誌是否足夠(日誌非常重要)。
6、測試沒問題後,在線上進行數據移植,最後上新程序。

 

三、代碼設計

根據上面的思路,來設計代碼,先看結構:

├── all        ———— 全量├── bean       ———— 存放用到的中間Java對象├── check      ———— 檢查程序├── common     ———— 存放一些如SQL語句,工具類等├── dao        ———— 數據操作層└── incr       ———— 增量

由於數據量比較大,單線程邊讀邊寫性能將非常差,因此這裏要用到多線程,使用線程池控制線程在合理的數量;另外遷移程序的邊讀邊寫非常適合用生產者-消費者模式來做,因此要用到阻塞隊列來保存中間數據。

private final LinkedBlockingQueue<OldEntity> dataQueue = new LinkedBlockingQueue<OldEntity>(1000);

    如上用LinkedBlockingQueue數據結構作爲數據池,讀取數據使用put()方法將數據放入隊列,當數據滿1000時,讀線程將被阻塞;寫庫線程從隊列中通過task()方法拿出數據,當了隊列爲空時,寫線程被阻塞。

    另外,爲了避免在數據遷移完時,寫線程無限等待下去,可以在讀取完所有的數據以後,在隊列中設置一定數量的“毒瘤”,如放置OldEntity時故意將id設置爲null,這樣在拿出數據時,檢查下這個狀態,如果拿到這樣的數據,就退出循環體。

    在線下測試發現,讀取數據是一個非常快的操作,而相對讀取,寫操作慢的不是一個檔次,因此在設計線程組的時候,要記住一讀多寫,具體多少個寫,要看你的線程數。多線程數的設計,以保證最小的線程切換,這塊可以用java/bin目錄下提供的jvisualvm來進行性能測試,如下圖:

圖中展示的就是在jvisualvm中看到線程使用情況,這裏我有了四組線程(1讀+n寫爲一組),每組有一個讀線和4個寫線程,可以看到,即使這樣,讀線程大部分時間仍然在等待(黃色),而寫線程一直在繁忙地寫庫。另外執行期間也要觀察內存狀態,尤其要確保沒有內存溢出情況發生,即old區不會漲:

如果發現old區域不斷上漲,有可能在諸如沒有關閉prepareStatement等引起的。

四、上線及問題

線程數不能太大:按最初的設想,是通過配置線程組和寫線程數來提高程序的執行速度,但是忘了線的數據庫還在支持線上業務,因此不能任性的使用太多線程,最終使用線下測試時1/4的線程數據,用38個小時完成了數據移植。

主從延遲:由於線上數據庫採用的是一主二從,爲了不影響線上的業務,從從庫中讀取數據寫入到新庫的主庫中。但是由於新建的庫與老庫在一個實例下面,導致從庫同步主庫的數據異常緩慢(因爲從讀同步主庫sql log以串行的方式寫入數據),大概延遲了10000s,即近一天的數據,這樣導致專門從從庫讀取數據的業務不能正常讀取到實時的數據.

五、總結

最後總結一下,上億數據的遷移,是如何做到7*24小時服務不中斷的呢?關鍵點就在於第4點增量程序,這裏隱含了一點是,增量程序必須比真實數據跑的更快,否則增量追不上正常入庫數據就麻煩了。

另外有完善的數據檢查程序也是非常必須的,否則不能保證數據完全遷移完成。

最後上線時,要注意,雖然我們的設計可以讓遷移程序在更短的時間內跑完,但如果線上資源有限,沒有多個實例或機器,這樣源庫和目標庫之間就會相互影響,所以我還反而要控制寫入速度。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章