什麼鬼!基於備份恢復的例項資料還能變多?
此文已由作者溫正湖授權網易雲社群釋出。
歡迎訪問網易雲社群,瞭解更多網易技術產品運營經驗。
對資料庫進行資料備份無非兩種方式,一種是邏輯備份,也就是直接連上資料庫匯出所有的資料,對於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老司機,最好悠著點玩。網易雲MongoDB服務新推出分片叢集例項,歡迎大家到時試用,讓我們為您填坑。
網易雲MongoDB 服務為開發者提供了一站式的 MongoDB 雲端解決方案,包括提供三節點複製集的高可用架構,故障切換,並提供專業的備份、監控以及效能優化方案,徹底免除開發者的運維煩惱,點選可免費試用。
網易雲免費體驗館,0成本體驗20+款雲產品!
更多網易技術、產品、運營經驗分享請點選。