关于MongDB数据迁移方案的研究
如果說mongodb在設計上有什么缺陷,那數據遷移應該算是不小的一個,在集群內部,不同分片之間的auto-balance問題頻出,無法用于實際生產環境,而集群之間的數據遷移也沒有給出一個可行的方案.
對于集群內部的負載均衡,我們使用了pre-split,關閉了auto-balance,定期move chunk,并將move chunk分成了copy到目標分片,更新config路由與remove源數據三個步驟,原因在于mongodb原生的move chunk進行數據刪除的時候速度并不可控,在數據庫壓力本來就比較大的時候,高速的刪除往往使得數據庫的可用性變差,而我們自己進行數據刪除的時候可以控制速度.
之所以沒有直接修改mongod的代碼,直接限制刪除速度,是因為在一個mongod實例中中,move chunk是單任務的,數據刪除完成之前,無法進行下一個move chunk,而實際上對于應用來說,數據的刪除并不是必要的,我們完全可以在copy完數據之后再用較長的時間慢慢刪除,一切都是為了數據庫的穩定可靠.
當然,解決方案并不能停留在此,更徹底一些的兩種方案也被使用.
1. 開發可以并行的move chunk,我們既可以直接在move chunk的代碼中限制刪除數據的速度,又不影響其他move chunk的進度.
2. 對于數據刪除較慢的情況,開發了移走本分片所有數據,直接刪除數據文件,停掉數據庫,之后重啟數據庫的方案,效果也被證實是不錯的.
實際使用數據庫過程中,我們除了在分片之間進行數據遷移之外,還有在集群間進行數據遷移的需求,比如說,我們以性能作為區分搭建了super,middle與low三個數據庫,之前有一張業務表tieba建立在low集群中,后來tieba發展迅速,各種請求數量飛速增長,我們需要將tieba這張表遷移到super集群中,這時候問題就來了.
基本的思路是不復雜的,首先,我們拷貝所有的數據文件,使用mongo提供的mongodump或者mongoexport都可以,之后使用mongorestore或者mongoimport將數據導入,結束之后利用mongod存儲的oplog進行由dump開始到restore階段之間數據的更新.
在數據量與請求量比較小的時候,這個方案是沒有問題的,一切都很順利,我們也使用這種方案進行過表在不同集群的遷移,證實了方案的可行性,但是隨著數據量與請求量的變大,事情開始變得復雜.
出現的第一個問題是,在mongod中,oplog采用固定大小的磁盤空間進行歷史操作的記錄,使用的空間被占滿后,最舊的操作歷史記錄將被清掉,這就意味著,我們的dump與restore總的時間不能超過oplog可記錄的最大時間,否則之前的遷移策略將不再可用.
出現的第二個問題是,如果數據量本身不是很大,我們可以在oplog記錄的時間限制內完成基本數據的拷貝,進行oplog同步的時候,有可能我們的同步速度無法跟上源集群的操作速度,使得集群之間的數據永遠無法得到同步.
在第一個問題發生之前,我們意識到了這個問題,并決定采用按照shardkey的范圍進行數據遷移的方案,首先將數據進行全量導出(事實證明,數據的導出速度要遠遠大于數據的導入速度,在目前的數據量下,數據進行全量導出而不是部分導出,并沒有成為整個數據遷移過程中的瓶頸,同時未完成整張表的遷移,數據全部導出是必然要進行的操作,當然,如果在你進行操作時發現數據全部導出也花費了很客觀的時間,那也可以在導出階段便對數據進行過濾),按照chunk的范圍對導出的多個數據文件命名,之后選擇只導入選定范圍的數據,以縮短數據導入的時間.
然后,第二個問題出現了,源集群數據的操作太快,我們通過之前的oplog同步方案跟不上.
一開始,我們的同步方案是這樣,通過源集群的mongos查到所有的分片,并對每個分片起一個線程,逐條查詢位于某個時間點(這個時間點在進行數據導出時進行了記錄)之后的所有操作,同時向目標mongos進行同步,這樣同步有可能導致實際被執行的oplog順序與最初的oplog記錄順序不一致,但是可以保證對每個源分片,所有的oplog記錄都是有序的,而我們的操作本身并沒有多個分片數據之間的依賴性,因此這種亂序是可行的.
在發生oplog的同步速度無法跟上源集群的操作速度時,我們對同步所有的步驟進行了時間測試,發現有約95%的時間花費在了向目標集群進行寫的操作上,而這個寫的速度遠遠沒有達到目標集群可以承受的最大寫入速度,我們試圖將safe=True的寫入換成了safe=False,發現向目標集群的寫入速度提高了5倍左右,由于之前發現safe=False的寫入會造成約萬分之一的數據錯誤,不安全寫入是不能被采用的,但是我們發現了速度慢的問題之一所在,寫入的線程在等待目標集群的返回,而只有有限幾個線程在進行數據的寫入(等于源集群的分片數目),對這種情況,我們想出了幾個解決方案.
第一個解決方案基于使用批量操作減少耗時的前提,經過測試,在使用safe=True的批量操作時,在總數據條數為2w條時,使用1000條每次的批量操作速度是每次一條的五倍左右,在這個基礎之上,我們開發了使用buffer進行批量操作的方案.
一開始,我們分析oplog,認為insert操作可以亂序,update操作不可以亂序,并且只能在inser之后進行,remove操作可以亂序,但是只能在insert與update之后進行,基于這個假設,我們在同步oplog時破壞了讀源集群數據與寫目標集群數據的時間一致性與順序性,之前是讀一條源集群數據,操作到目標集群,循環往復,現在對每個線程,建立了三個buffer,為insert_buffer,update_buffer與remove_buffer,當讀到一條源集群操作記錄時,按照操作的種類,將記錄放到相應的buffer中,在buffer滿或者超過指定時間(防止有一部分數據永遠無法被操作到目標集群)時,將buffer中的記錄操作到目標集群,操作使用以下的過程:如果insert_buffer滿了,通過批量寫將操作一次性應用到目標集群;如果update_buffer滿了,首先將insert_buffer一次性操作至目標集群,之后將update_buffer中的數據一條條操作至目標集群;如果remove_buffer滿了,首先將insert_buffer一次性操作至目標集群,之后將update_buffer中的數據一條條操作至目標集群,最后將remove_buffer一次性操作至目標集群.
使用這個方案后,在寫入與刪除較多的情況下,數據的同步速度可以提高至原來的5倍以上,update較多的情況下速度沒有改善.
不過這個方案并不是完備的,方案有一個假設,即insert之間的操作是可以完全亂序的,實際上,由于唯一索引沖突的情況存在,各操作之間的順序并不是可以完全亂序的,這導致這種解決方案在insert有可能出現唯一索引沖突的情況有可能導致最終數據的不一致.舉個簡單的例子,有insert_1:{a:1,b:2},remove_1:{a:1,b:2},insert_2:{a:1,b:2}而{a:1,b:1}為唯一索引的情況下,最終的數據{a:1,b:2}是存在的,而被亂序加速之后,可能導致remove_1操作在insert_1與insert_2之后進行,而此時insert_2是失敗的,remove_1是成功的,最終{a:1,b:2}不存在.
修正策略在原理上也存在,即對唯一索引沖突增加兼容處理,在將操作分配至buffer之前,為每個操作添一個遞增的序列號,當發生操作失敗時,將失敗的操作記錄至redo_buffer,redo_buffer滿執行或者定期執行,且按照序列號從小到大依次執行即可.不過由于mongod在進行批量操作時并不能告知你哪一條操作成功或者失敗,所以先避過不談.(希望有一天數據庫不再支持唯一索引這種東西,由業務層保證即可,數據庫就應該是存數據,取數據的地方,沒有理由一條數據進來了,給你返回不能存.).
第二個解決方案是試圖通過某個隔離,使得同步操作可以多線程,其實之前對不同的分片已經實現了多線程,前提便是不同的分片之間操作的獨立性,而這種獨立性的根源便是不同shardkey之間操作的獨立性,基于這個前提,我們可以實現在同一個分片使用多線程同步oplog,設線程數為n,將shardkey一一映射為整數,之后對n取模得到i,由第i個線程處理這條記錄,為避免操作堵塞,我們設立n個隊列,之前從源集群的分片取到oplog操作之后應用到目標集群的線程現在做的事情是:一開始啟動n個線程,每個線程有一個負責監控的隊列,之后循環,從源集群取到oplog操作記錄,由剛剛提到的取模策略將操作記錄放到第i個隊列,由相應的線程取隊列的記錄進行實際的操作.
在進行全部為insert操作的測試中,這種取模多線程的方案最大可以將寫入速度提高到之前的4倍左右,提高的程度取決于線程數,并不是線程數越高速度越好,具體的線程數目需要實際去實踐決定,建議5-10個線程即可.
這個方案的問題在于,對于insert操作與不使用set方式的update操作,oplog的記錄總是存在shardkey字段的,但是對于remove操作與使用set方式的update操作,oplog是沒有shardkey字段的,我們不知道這條操作屬于哪一個shardkey,因此有六種解決方案.
1. 遇到沒有shardkey的oplog記錄,我們首先等待之前的所有隊列被取空之后,應用這條記錄,之后再啟動寫隊列-讀隊列的循環.
2. 雖然oplog的記錄沒有shardkey,但是我們可以查出來,對于remove操作,由于源集群已經沒有這條數據了,我們可以首先在目標集群進行查詢,如果查不到,這條記錄肯定就在我們存儲在本地還沒有應用到目標集群的隊列中,因此需要再向隊列查詢(需要一個可以遍歷的隊列...),查出shardkey之后再放到相應的隊列中,對于使用set方式進行更新的update操作,同樣的道理,可以在目標集群+本地隊列查找,肯定找得到,并且最終找到的肯定是insert記錄(_id是不會更新的),insert是帶有shardkey的,問題解決.
3. 第三種解決方案稍微不常規一些,即在使用set方式更新記錄時,api層面人工加上某個新的字段(shardkey是不能被更新的,哪怕與之前的一樣,數據庫直接拒絕操作而不進行判斷),比如_mk(_mk必須沒有被使用過),字段的值與shardkey的值一樣,以此當我們發現set方式的update時,可以查找_mk獲取shardkey,發現remove時,采取方案一.
4. 第四種解決方案可以說是上面三種方案的終極解決方案,原理很簡單,我們修改mongod的代碼,使其在進行oplog的所有操作的['o']字段加上shardkey即可.
5. 第五種方案簡單粗暴,首先復制源集群的config配置,之后按照分片對分片的方式進行更新,去掉了mongos的分配作用,也不用讓oplog再勉為其難做自己難以做到的路由分配的功能.
6. 第六種更為通用一些,可以實現任意操作的亂序,即將oplog取出來應用之前,先檢查已有的尚未被應用的隊列里是否有產生沖突的記錄,具體來說,對insert,查看是否有唯一索引沖突,對update,查看是否有對同一_id的之前的update,對remove,查看是否有同_id的insert與update,如果所有檢測都通過,放入隊列,如果有任意一項不通過,放入等待隊列,并由新的線程定時做重新檢測.
幾個方案都有可行性,按照最小的代價我們決定首先在方案一的基礎上使用方案三,以完成目前的數據遷移,之后為長遠考慮,會去實現其余的幾個方案.
通過之前的分析,我們可以得出一個很明顯的結論,mongod在進行數據庫設計時并沒有考慮集群間數據遷移這種情況,oplog只是為主從同步而存在,沒有絲毫路由分配的設計理念在里面,直接導致了在目前的oplog下使用多線程操作的復雜性.
希望新的版本發布時可以在集群間數據遷移做一些設計上的改進,比如說oplog的改進,等等.
與50位技術專家面對面20年技術見證,附贈技術全景圖總結
以上是生活随笔為你收集整理的关于MongDB数据迁移方案的研究的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: MongoDB 性能瓶颈分析
- 下一篇: MongoDB管理:慎用local、ad