什麼鬼!基於備份恢復的數據還能變多?

對資料庫進行數據備份無非兩種方式,一種是邏輯備份,也就是直接連上資料庫導出所有的數據,對於MySQL,就是通過MySQL客戶端或JDBC等MySQL驅動進行全表Select,將查詢結果轉化為Insert語句並保存到文件中,實際場景下,一般使用mysqldump或mydumper等工具來實現。

同樣的,對於MongoDB也是如此,可以藉助mongodump工具來進行數據邏輯備份;另一種是物理備份,通俗地講,就是通過dd或cp等方式直接拷貝資料庫文件, 對於MySQL,可以使用Percona Xtrabackup工具,對於MongoDB,目前還沒有成熟的物理備份工具,不過目前Percona正在開發中,相信很快就能用上。目前主流的MongoDB數據物理備份方式還是基於塊設備或文件系統的snapshot機制。相比邏輯備份,物理備份在性能上更好,對資料庫的侵入性小,不足之處在於,相比邏輯備份,物理備份數據的跨版本兼容性較弱,備份所佔的空間較大。一般建議在備份大庫時,採用物理備份更佳。本文的重點不在對比兩種備份方式的優劣,所以在此不展開分析。

本文要介紹和分析的問題發現在使用lvm snapshot對MongoDB實例進行物理備份的場景。更確切的說,是在恢復MongoDB sharding集群的物理備份時發生的。在此還得簡單介紹幾句MongoDB sharding cluster,sharding cluster又稱分片集群。資料庫一旦成為瓶頸就需要進行擴展,擴展包括垂直擴展,如下:

另一種是水平擴展,將一個複製集實例擴展為一個集群,原來的複製集變為其下的一個shard,分片集群就是MongoDB官方提供的數據在線水平擴展方案,可以理解為MongoDB線上部署的終極方式:

如下圖所示,集群由三個部分組成,分別是mongos(Router)、config server和shard組成。

mongos是用戶訪問分片集群中數據的入口,該節點不會持久化數據,僅緩存config server上的元數據,比如查詢的路由表(Router Table)等,用戶發起的讀寫操作通過mongos路由到一個或多個Shard上。

config server是集群的大腦,保存了集群相關的元數據信息,這些信息包括在admin和config庫下。其中admin保存賬號和許可權定義相關信息,config保存集群的mongos、shard節點信息,資料庫和集合信息,如集群中保存的所有資料庫,集群中啟用分片的資料庫,啟用分片的集合等,還保存了每個數據chunk在shard上的分布信息。資料庫、集合和chunk信息組成了查詢的路由表,該路由表再mongos、config server和每個shard上均會緩存。

shard是真正存儲數據的組件,熟悉MySQL的同學可以將每個shard想像成MySQL的分區表。MongoDB中對集合啟用分片就相當於MySQL中對某個表進行了分區。每個shard對應一個分區。shard上的數據是由一系列chunk組成的,每個chunk默認64MB。

chunk在某些場景下會進行分裂,由mongos將一個chunk分裂為2個chunk。chunk也會由config server發起在不同的shard上進行遷移。在MongoDB 3.4中,config server和每個shard一般部署為3個節點的MongoDB複製集。chunk的遷移操作由config server複製集的Primary節點上的balancer線程來執行。通過分裂和遷移,實現MongoDB分片集群中數據在各個shard上的均勻分布。

做了上面這麼多鋪墊,終於可以開始詳細描述問題了。首先對這個分片集群使用ycsb工具壓入一定量的數據,確保能夠觸發其chunk分裂條件。在config server完成chunk分裂,balancer線程running狀態為false後,在mongos節點使用db.find().count()命令獲取usertable集合文檔個數。然後使用lvm snapshot分別在config server和各shard上進行數據備份。備份前,已將balancer disable掉,確保在備份期間不會發生chunk遷移導致config server上的chunk元數據和shard上的chunk信息不一致。備份完成後,基於集群備份恢復出一個新的分片集群,再用count()命令在mongos上做查詢,發現usertable集合的文檔個數變多了。需要說明的是,上面的這一系列不是我操作的。而是我們外號架構師的QA MM操作的。發現這個現象後,她跟我說:「基於集群備份恢復出來的實例,數據變多了!」。

乍一聽這個這句話,我的表情是這樣的:

瞬間又變成下面這樣,聽說過基於備份恢復出來的資料庫實例少數據的情況,數據多出來卻很少見,為什麼憑空多出來呢:

是不是她搞錯了,問了句:「Are you Ok!」

不過作為資深的測試人員,相信我們的QA不會犯那種低級錯誤。既然出現這個情況,那麼肯定是有原因的。於是了解了整個測試的經過。經過初步分析,大致可以確定,問題雖然發生在備份恢復的時候,但癥結卻在執行lvm snapshot的那一刻就已經埋下了。那麼,這是我們那裡做錯了嗎?應該說這是我們還做得不夠好。這一切都跟MongoDB chunk遷移的機制有關係。

在前面我們提到,為了確保數據在各個shard上保存均衡。config server會將一個shard上的chunk遷移到另一個shard上。遷移有兩個重點關注的點:

1、對於不同的存儲引擎,遷移的行為是不一樣的,對於目前默認的WiredTiger存儲引擎,chunk數據默認僅寫到目標shard的Primary節點即返回(可以通過參數_secondaryThrottle來調整,但調後會影響遷移性能)。由於我們備份是在Secondary節點進行的,所以必須確保chunk數據已經從Primary複製到Secondary後才開始備份。這已經在我們的設計方案中得到保障。對於已經逐步棄用的MMAPv1存儲引擎,chunk數據會同時寫到Primary和Secondary上。2、遷移完成後,源shard上的這個chunk數據如何處理?MongoDB分片集群默認是在後台完成源shard上該chunk數據的刪除的。不需要等到數據刪除後才commit本次遷移。這麼設計的目的是為了提高chunk遷移的效率,因為MongoDB規定一個shard上同時只能存在一個正在進行的chunk遷移操作,這麼處理就無需等待本次chunk遷移完全結束即可開始下一個chunk遷移。

根據理論分析,本次出現的問題就是由於刪除源shard的已遷移chunk數據環節導致的。下圖截自MongoDB官方文檔:

這些待刪除的chunk會加入到位於shard複製集Primary節點上的一個隊列中,刪除線程逐個獲取隊列中的刪除請求並執行。但這個隊列是非持久化的,如果Primary節點Crash了,而Crash時隊列中還有請求。那麼這部分刪除請求再也得不到處理。在MongoDB 分配集群中,將這些未被刪除的冗餘數據成為orphan文檔。

但在備份前後,並沒有發生過任意一個節點Crash啊?扯了半天,似乎還沒有跟問題建立直接關係。其實對複製集節點做snapshot,在這個場景下,基本上可以等同為一次節點Crash,因為snapshot是對mongod數據盤做快照,這樣能夠確保硬碟上的數據不丟失,但此時內存中的信息是無法保存下來的。由於chunk數據刪除全過程都僅在內存中進行,不會將進展持久化到硬碟上,而且我們又是在Secondary節點上進行snapshot,所以這部分內存信息肯定是丟失的。

好了,接下來就是如何證明上面的推斷是對的。MongoDB官方針對orphan文檔專門提供了一個命令cleanupOrphaned來進行刪除。如下所示:

如果描述的問題確是該原因導致的,那麼執行該命令後,集合usertable的文檔數應該恢復到跟備份前的connt()一樣。顯然,答案是肯定的。相比MongoDB複製集實例,MongoDB分片集群複雜度提升數倍,在使用過程中需要關注的坑也較多,如果不是MongoDB老司機,最好悠著點玩。

PS:設計和實現分片集群服務過程中,遇到和解決了很多問題,有時間允許的話,都拿出來聊聊,這樣大家就有應對經驗了。網易雲MongoDB服務新推出分片集群實例,歡迎大家到時試用,讓我們為您填坑。
推薦閱讀:

簡析關係型資料庫和非關係型資料庫的比較(上)
簡析關係型資料庫和非關係型資料庫的比較(下)

TAG:資料庫 | 資料庫備份 |