華文網

阿裡雲MongoDB Sharding備份和恢復服務深度解密

更多深度文章,請關注雲計算頻道:https://yq.aliyun.com/cloud

大資料時代,資料保存的重要性不言而喻。在資料保存過程中,資料的備份更是一個值得深入研究的課題。在3月12日下午舉行的MongoDB杭州用戶交流會上,

阿裡雲技術專家明儼分享了MongoDB Sharding備份和恢復的技術解密。他通過介紹不同的備份方法及備份的主要問題等方面來闡述阿裡雲在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的分佈。

Sharding備份形式

簡單介紹完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的備份策略

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

並按照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的分佈。

Sharding備份形式

簡單介紹完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的備份策略

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