阿裏雲MongoDB Sharding備份和恢複服務深度解密
大數據時代,數據保存的重要性不言而喻。在數據保存過程中,數據的備份更是一個值得深入研究的課題。在3月12日下午舉行的MongoDB杭州用戶交流會上,阿裏雲技術專家明儼分享了MongoDB Sharding備份和恢複的技術解密。他通過介紹不同的備份方法及備份的主要問題等方麵來闡述阿裏雲在MongoDB Sharding備份和恢複方麵所做的工作。
在“MongoDB Sharding杭州用戶交流會”上,阿裏雲技術專家明儼分享了MongoDB Sharding備份和恢複的技術解密。他通過介紹不同的備份方法及備份的主要問題等方麵來闡述MongoDB Sharding在備份和恢複方麵實施的解決方案。
他的演講內容主要分為三個方麵:
- MongoDB Sharding架構簡介
- MongoDB Sharding備份的主要問題
- 阿裏雲MongoDB Sharding備份
以下內容根據現場分享和幻燈片整理而成。
由於MongoDB Sharding是一種分布式集群架構,它的備份與傳統的單機數據庫相比更具有挑戰性。
MongoDB Sharding主要有三個組件。
1. shard。shard是保存集群數據的節點,它本身是一個副本集。一個sharding可以有多個shard。2. Config Servers。Config Server是用來保證集群元數據和配置信息的節點。在3.2版本之前,它是一個三鏡像的組成,3.2版本之後,也是一個普通的副本集。3. mongos。mongos是一個路由節點。用戶在使用的時候通過連接一個或多個mongos來訪問整個集群。
MongoDB Sharding是一個分片集群,在使用時需要先對數據進行分片。MongoDB分片的單元是集合,可以對集合製定分片的策略。MongoDB支持兩種分片策略:一是基於哈希的分片策略,二是基於range的分片策略。這兩種策略有各自適合的場景,可以根據業務的使用情況選擇。
數據分片後最主要問題就是如何找到分片,因為數據可能分布在所有shard上。這就是Config Server上保存的最重要的集群元數據。在訪問的時候,用戶通過mongos訪問,Mongos從Config Server獲取路由信息(某個分片數據在哪個shard上)並會緩存在本地。Config server主要保存路由信息之外還保存一些配置信息。比如哪些集合做了分片,分片的形式是如何,分片的規則又是如何。這些都是在config server上保存的。
還有兩個MongoDB Sharding相關的概念必須了解:
1. Shard key。Shard key就是分片時指定的片鍵。每個分片集合必須要指定一個shard key,根據這個Shard key對數據進行分片,接下來的寫入和讀取都需要通過shard key來訪問。2. Chunk。Chunk就是Shard key的值所在名字空間的一個小範圍的集合。MongoDB會為每個Shard key的值定義一個minKey和一個maxKey。如圖上的例子假設Shard key是一個整型字段x,x的值所在名字空間分成了4個chunk,其中minKey到-75之間是第一個chunk,-75到25之間是第二個chunk。25到175之間是第三個chunk,175到maxKey之間是第四個chunk。
總結一下,MongoDB內部把整個Shard key的值所在名字空間分成了若幹個chunk,每個shard上保存多個chunk。MongoDB的數據遷移是基於chunk來遷移的,同時Config Server維護的路由信息也是基於chunk的,即它是記錄哪一個shard上麵有哪幾個chunk。

現在我們再從數據層麵來回顧一下MongoDB Sharding的節點存的都是哪些數據。假設現在有一個MongoDB Sharding集群,包含兩個shard(shard1和shard2)以及一個config server。如果我們對test_db這個數據庫開啟分片,並按照range策略使用_id作為shard key對test_db裏的test_col這個集合開啟分片。那麼shard1,shard2上存放的是test_db裏test_col這個集合的數據。而在config server上則有config.shards、config.chunks等這些表。其中Config.shards表是記錄集群裏有哪些shard,每個shard的訪問地址是什麼。而Config.chunks表則記錄了每個shard對應存放哪些chunk的分布。

簡單介紹完MongoDB Sharding的架構,我們來看看Sharding的備份形式。Sharding備份形式基本上可以分為兩種,分別是異構備份恢複和同構備份恢複。
異構備份恢複指通過備份恢複出來的形態和原來不同,在需要改變形態的情況下可以使用此備份形式。
異構備份恢複的基本方案是通過mongodump(官方的備份工具)連接mongos進行邏輯備份。這種備份形式和訪問mongod進行備份類似,它通過mongo提供的訪問接口把所有數據一條一條dump出來,因此它的效率比較低,隻適用於數據量比較小的場景。在恢複方麵,目前的mongorestore隻支持把數據無分片的恢複到Sharding的一個Shard節點上麵,需要重新對數據進行分片。這也是我把這種形式叫做異構備份恢複的主要原因,它提供了一定的靈活性,可以是你在想對數據重新進行分片時的一種選擇。

和異構備份恢複相對,同構備份恢複就是指恢複出來的形態與原來的架構完全一致,是一種一對一的恢複。比如原來的Sharding有兩個shard,一個config server,恢複後還是兩個shard和一個config server。同構備份恢複可能沒有異構備份恢複那麼靈活,但它最大優點就是業務上不需要做修改,原來數據怎麼訪問,恢複出來的數據也以同樣的方式訪問即可。同構備份恢複所涉及的問題和解決方案是此次分享的重點。

有效備份的理解
在講述同構備份恢複之前,我們先來思考一個問題,一個備份要怎樣才能稱作是有效的。在我看來,一個備份要稱為有效需要包含以下幾點:
- 備份能夠恢複出來,並且數據是正確可用的。如果一個備份恢複不出來,這個備份等於無效的。
- 備份能對應到某個時間點。如果一個備份無法確定它是哪一個時間點,這個備份也不能稱為一個有效的備份。備份在需要用來恢複之時,基本都是因為源數據已經出現問題。比如數據被誤刪,我們需要找到刪除數據的時間點之前的備份才能恢複數據。如果備份不能對應到某個時間點,那麼我們就無法確定這個備份恢複出來的數據到底是不是是自己所需要的,那這個備份也是無效的。
對MongoDB Sharding而言,最關鍵的問題是獲取能對應到某個時間點的一致的sharding備份,即該備份中,整個sharding集群數據與元數據都是對應到該時間點。同時,整個sharding集群的數據和元數據必須一致。我們知道,Sharding集群包含多個shard節點,以及一個config server節點。每個節點備份出來之間的數據都需要是同一個時間點,這樣整個Sharding備份才可以對應到這一個時間點。此外,如果某一個shard備份出來的數據跟config server備份出來的元數據中該shard所負責的chunk信息不一致,那麼這個Sharding備份的數據和元數據是不一致的。這可能導致恢複出來後找不到數據。
現在說下MognoDB Sharding同構備份恢複的方案,首先我們能想到的備份方法就是基於單節點備份的擴展。也就是依次為每個節點(包括shards、cs)都進行備份,然後把所有節點備份的集合作為整個Sharding集群的備份。這個方法的主要問題是很難取得一個一致的sharding備份,因為備份過程中有外部修改和內部遷移這兩個因素的影響。

影響因素1:外部修改
首先說下第一個影響因素,外部修改。在備份過程中,MongoDB集群持續對外服務。即不停的有新的寫入和修改。如下圖這個例子:

假設我們現在有一個Sharding集群,包含一個mongos、兩個shard和一個config server。並且我們的外部訪問情況是每個shard每秒有100個插入請求。假設我們使用邏輯備份,我們知道通過mongodump的『--oplog』選項可以在備份過程中將修改的oplog也一塊備份出來,這樣我們可以得到一個對應確定時間點的備份。現在假設在12點時我們讓每個shard和config server都同時開始備份。因為config server上存儲的元數據信息數據量通常比較小,假設隻需要5分鍾就可以備份完,那麼它的備份對應的時間點是12點05分;Shard1數據量也比較少,用8分鍾備完,備份的對應時間點是12點08分;Shard2則需要10分鍾備份完,備份的對應時間點是12點10分。由於在備份期間每個shard每秒有100個插入請求,這樣就會導致所有節點備份的數據無法對應到同一個時間。即,這時候備份出來的數據中,shard1比config server上多幾分鍾的寫入,同樣shard2比shard1還要多幾分鍾的寫入,所以整個Sharding備份不能稱作一致的備份。
因此外部修改的主要問題就是因每個節點的容量不同導致備份耗時不同,在外部有持續修改的情況下,無法為整個備份確定一個時間點。解決這個問題有一個很簡單的方案就是在備份期間停止外部修改。當然這明顯會嚴重影響服務的可用性。因此,可行的方案是在Secondary上做備份,在備份前同時把所有節點的Secondary節點摘掉或加寫鎖,使得這些Secondary節點暫時不接受同步。這樣在這些Secondary節點上統一備份的數據就是摘掉或加寫鎖時間之後的數據。這個方法最主要的問題一個是備節點在整個備份的過程中都需要斷開同步,導致備節點備份完之可能與主節點跟不上同步,另外一個是精確控製各節點同時操作的難度。
影響因素2:內部遷移
接下來我們說下第二個影響因素,內部遷移。因為Sharding數據分布在多個節點,節點會有增減,數據的分布可能會有不均衡的情況出現,所以肯定會發生內部的遷移。
內部遷移以chunk為單位進行遷移,發生chunk遷移原因主要有三點:
- 負載不均衡。Sharding集群會自發進行chunk遷移以使得負載變得均衡。在3.2版本上,每個mongos有一個負載負載均衡的進程叫balancer。balancer會定期檢查集群是否需要做負載均衡,它會根據一個算法(根據整個集群的總chunk數和各個shard的chunk數進行判斷)判斷當前是否需要進行chunk遷移,哪個shard需要遷移,以及遷移到哪裏。此外,用戶也可以通過moveChunk命令手動發起一個chunk遷移。
- RemoveShard操作需要數據遷移。比如整個集群的數據量變小了,不需要那麼多shard了,這時候可以通過RemoveShard操作去把某個Shard下線。下線操作發起後,mongoDB內部會自發地把這個shard上的數據全部遷移到其他的shard上,這個操作會觸發chunk遷移。
- MongoDB sharding的Shard Tag功能。Shard Tag功能可以理解為一個標簽,可以用來強製指定數據的分布規則。你可以為某個shard打標簽,再對shard key的某些分布範圍打上相同的標簽,這樣MongoDB會根據這些標簽把相同標簽的數據自動遷移到標簽所屬的shard上。這個功能通常可以用來實現異地分流訪問。

Chunk遷移流程介紹
簡單介紹一下chunk遷移的流程。剛剛說過Chunk遷移是由mongos接收到用戶發的moveChunk命令,或balancer主動發起的。這裏主要分為四個步驟:
第一步:Mongos發一個Movechunk命令給一個Source shard。第二步:Source shard通知Dest shard同步chunk數據。第三步:Dest shard不停地同步chunk數據,同步數據完成時通知Source shard現在同步已經完成了,可以把訪問切換到我這了。第四步:Source shard到config server上更新chunk的位置信息。它會告訴config server這個chunk已經遷移到另一個Dest shard上了。接下來的數據請求全部需要到那個shard上。第五步:Source shard刪除chunk數據,這個是異步做的。
以上就是一個chunk遷移的主要流程。它涉及到Source shard、Dest shard、config server上的修改,有多個數據修改,因此是比較複雜的一個過程。

內部遷移的影響
我們來看下內部遷移會有什麼影響。舉個例子,同樣是兩個shard,一個config server。他們的初始分布如下圖。即chunk1、chunk3、chunk4在shard1上,chunk2在shard2上,config server上記錄了chunk1,chunk2,chunk3,chunk4分別所處的位置。

假設現在沒有外部修改,我們開始給各個節點做備份。因為例子中第一個shard上有三個chunk,而另外一個shard上隻有一個,數據明顯是不均衡的,所以MongoDB可能在某個時間點把shard1上的某個chunk遷移到shard2上,使得數據可以均衡。
假設在備份過程中發生了將chunk1從shard1遷移到shard2的操作,這可能導致以下幾種結果:
- 備份出了重複數據。先看下config server的備份,因為遷移過程涉及config server的修改(更新chunk1的位置信息),而備份也在進行當中,所以備份出來的數據可能是修改之前的,也有可能是修改之後的。假設備份的數據是在修改前的,那麼config server的備份數據還是原來的樣子,即chunk1在shard1上。同時假設備份shard2的時候chunk1的數據已經遷移完了,那麼shard2的備份會包含chunk1和chunk2。Chunk1在遷移之後還有一個刪除動作,它會把自己從shard1上刪除。假設shard1上備份的時候chunk1未刪除,這時候shard1的備份上也還會有chunk1,chunk3,chunk4。這樣就會導致在整個Sharding備份看來,備份出來的數據包含兩份chunk1的數據。當然,這個影響並不是很大因為原來的數據都還能找到,而多出來的shard2上的chunk1是一個外部訪問不到的數據。因為備份恢複出來後,是按照config server上的路由表來訪問。它會認為chunk1這時候還在shard1上麵,接下來對chunk1的訪問全部還是在shard1上訪問。而shard2上的chunk1則是一個野chunk,對訪問並無影響。這種野chunk後續可以通過運維手段清除。
- 第二種結果就是備份出來的數據出現了丟失。如果在備份config server的時候,已經是一個修改後的數據,即此時,它已經認為chunk1是在shard2上麵了。而在備份shard2的時候,chunk1的數據還未完全拷貝完成,即shard2上麵其實還是隻有chunk2的數據。備份Shard1時還是chunk1,chunk3,chunk4。這樣就會導致恢複出來的數據丟失了chunk1這個數據。因為config server認為chunk1在shard2上麵,而shard2上麵並沒有chunk1這個數據。這時候,shard1上雖然有chunk1,但它也是找不到的。這時問題比較嚴重,因為造成了數據丟失。

綜上,在備份過程中如果發生了內部chunk遷移最主要的問題就是由於chunk遷移涉及多個節點的數據修改,而各個節點備份的時間不同,可能會導致shard備份的數據和config server備份的數據是不一致的,可能導致恢複出來的數據重複或丟失。
這個問題也有一個很簡單的解決方式,就是在備份過程中關掉balancer,並且禁止用戶發起內部遷移,這樣就可以安全地備份。但個解決方式還是不完美,如果備份的數據量大,備份的時間較長,長時間把balancer關掉,集群就無法負載均衡。另外,禁止用戶發起內部遷移需要做一些修改。事實上,對於一個雲服務提供者來說,禁止用戶做內部遷移是比較困難的,因為用戶確實會有遷移的需求,他們在某些情況下確實比係統更清楚數據需要遷移到什麼地方。
阿裏雲MongoDB Sharding備份的介紹
接下來介紹一下阿裏雲MongoDB Sharding的備份和恢複方案。阿裏雲MongoDB Sharding備份主要采用同構備份恢複的形式,因為異構備份恢複在用戶體驗上不如同構備份恢複。阿裏雲MongoDB Sharding備份恢複的方式是克隆一個新的實例出來,會跳到購買頁麵讓用戶重新選配。這樣這個新實例會和源實例擁有一模一樣的架構,你可以在新實例上對數據進行校驗,確認沒問題後將訪問切到新實例上。這裏有個前提是需要保證這個新實例的shard節點數大於或等於源shard節點數。比如原來有三個shard,新克隆的實例至少也要有3個shard,可以是4個shard,此時有個shard上的數據為空。
那麼阿裏雲是如何解決剛剛前麵提到的同構備份恢複的外部修改和內部遷移這兩個問題呢?首先,阿裏雲MongoDB Sharding備份通過換個角度來解決外部修改問題。既然我們很難在備份過程中保證數據備份出來是同一個時間點的,那我們可以選擇在恢複的時候,讓所有節點恢複到同一個時間點來實現。這要求具備實現恢複到任意時間點這個功能。而對於內部遷移的問題,阿裏雲MongoDB Sharding不希望停止balancer,也不希望禁止用戶進行內部遷移。我們采用的是犧牲一些恢複時間點的選擇,即對恢複時間點進行了一些限製,避開有內部遷移發生的時間段這種方式。這要求需要通過一些手段能夠知道Sharding集群在哪些時間段有發生內部遷移,然後禁止用戶恢複到這些時間段內的時間點。

所有節點恢複到同一個時間點
我們先說解決第一個問題的關鍵,恢複到同一個時間點。恢複到同一個時間點的產品定義是把所有實例數據恢複到具體某個時間點(精確到秒,包含該秒)的一個狀態,這可以通過定期進行全量備份和持續進行增量備份來實現。
全量備份可以是邏輯備份(通過mongodump),也可以是物理備份(文件係統、邏輯卷快照,或加鎖拷貝)。全量備份需要解決的主要問題是它也要能夠確定到一個對應的時間點,需要知道數據是屬於哪個時間點的。如果使用邏輯備份,mongodump有一個『--oplog】選項,會把備份過程中還在進行的外部修改(oplog)抓出來,進而得到某個一致時間點的備份。這時候你可以選取抓取到的最後一條oplog的時間戳作為這個全量備份的時間點,此時這個全量備份一定包含此時間點之前的所有數據。如果使用物理備份,可以取持久化快照前的最後一條oplog的時間戳作為時間點。
增量備份就是抓取oplog。恢複時選取一個全量備份進行恢複,然後在此基礎上進行一個oplog的重放,就可以實現重放到指定的某個時間點。
通過各節點定期的全量備份和持續的增量備份實現恢複到統一個時間點。采用這種方式有一個額外的要求,即各節點的時鍾不能相差太多,要有一個時鍾同步的機製。現在通常的NTP服務誤差基本都可以做到在100毫秒以內,所以可以放心地將各個節點都恢複到某一秒。

這裏再說下全量備份中的邏輯備份和物理備份。邏輯備份很簡單,通過mongodump和mongorestore來實現。它存在如下幾個問題:
問題一:邏輯備份的效率比較低。在備份的過程中dump比較慢,因為它是一條一條數據讀出來的,恢複也較慢,需要一條一條往裏插。並且恢複還需要重建索引,如果一個索引的數量很大,單獨索引重建的時間就會很長,恢複個好幾天都是正常的。問題二:通過邏輯備份來獲取時間點快照需要使用【--oplog】選項。這個選項會在全量備份過程中的oplog抓下來。我們知道mongodb的oplog集合是固定大小的,當集合滿時會重複利用舊的數據所占的空間用來存放新的數據。因此如果備份時間很長,oplog增長又很快,很有可能會在備份過程中oplog被滾掉,導致備份失敗。這裏我們阿裏雲在內核上做了一些改進,能夠確保備份過程中oplog能夠被抓完。問題三:邏輯備份在某些場景可能備份失敗。第一個場景是如果備份過程中集合被drop掉,會導致備份失敗。第二個場景是對唯一索引的處理,如果在備份過程當中,連續delete/insert某個唯一索引的某一個相同的key,可能會導致恢複失敗。因為這時候mongodump可能會dump出相同的key,恢複出正確的數據依賴於這當中的oplog被正確回放(對這個相同的key先delete再insert,最後恢複出來還是隻有這1個key)。問題就在於mongorestore的行為是恢複完數據後先建索引,然後才重放oplog。這樣在建唯一索引的時候,這個地方就過不去了。這兩個問題都是我們在線上運維時真實遇到的,目前官方也未解決。
再來說下物理備份。物理備份就是直接拷貝數據文件,它最大的優點就是效率高,還可以解決上述邏輯備份中出現的所有問題。
官方物理備份的方法在官方文檔上有介紹。它要求在備份過程中先對節點加一個寫鎖,然後後才能安全地把數據拷貝走(或實施底層文件係統或邏輯卷的快照),之後再解鎖。這也有一個問題,就是備份過程全程加鎖,如果數據量大,也有可能發生Secondary節點oplog追丟的問題(加鎖通常不會在Primary節點上做)。這裏阿裏雲MongoDB對物理備份做了一些優化,不需要全程加鎖就可以實現備份。這個功能接下來也馬上就會在線上使用到。

避開內部的遷移操作的方法
接下來介紹Sharding同構備份恢複第二個內部chunk遷移問題的解決。前麵提到我們通過對恢複時間點進行限製,避開發生內部遷移的時間段來解決這個問題。我們是通過後台實時分析整個集群有哪些內部遷移操作來做到這一點的。我們記錄了所有內部遷移發生的時間段,以用來在恢複時間點選擇的時候進行判斷。如果恢複的時間點發生在某次內部遷移以內,則會禁止這個恢複操作。當然,由於MognoDB Sharding對chunk有大小限製(默認為64MB),通常一次chunk遷移涉及的時間都非常短,因此這對恢複時間點的選擇影響並不大。事實上我們還發現,不止chunk遷移會有影響,如果在備份過程中存在以下的這些操作都會存在一些問題,包括moveChunk、movePrimary、shardCollectio、dropDatabase、dropCollection等。這些操作都是相對比較複雜的操作,涉及到多個節點數據的修改,因此在恢複的時間點的選擇上我們會要求用戶避開選擇發生這些事件的時間範圍。

最後介紹一下阿裏雲MongoDB Sharding的備份策略,目前在我們在用戶創建好一個sharding實例後默認會為所有節點開啟定期的全量備份和持續的增量備份。備份默認保留七天,我們允許用戶自定義備份的周期和時間。基於我們的Sharding備份恢複的實現,阿裏雲建議sharding用戶根據業務行為自定義設置balancer的運行時間窗口。最好設定在業務的低峰期,比如在夜晚,這樣可以保證白天的大部分時間的都是可以恢複的。當然後續阿裏雲也會提供一個可恢複的時間點的選擇,讓用戶可以直接在控製台上看到具體哪些時間點是可以恢複的。

最後更新:2017-04-01 16:39:46