MongoDB丟數據問題的分析


坊間有很多傳說MongoDB會丟數據。特別是最近有一個InfoQ翻譯的Sven的一篇水文(為什么叫做水文?因為里面並沒有他自己的原創,只是搜羅了一些網上的博客,炒了些冷飯吃),其中又提到了丟數據的事情。大家知道作為一個數據庫來說,數據的持久性基本上是數據庫的最低要求了。如果MongoDB真的有那么糟糕的數據安全問題,它早就在技術選擇眾多的今天被無情地淘汰掉了。那么真相到底如何呢?

實事求是地來說,MongoDB確實在其發展的過程中,有一些數據持久化的問題沒有處理好,特別是一些默認值的選定上。大部分用戶會拿來就用,直到遇到問題之后才發現他們應該在開始的時候做一些必要的配置。但是,所有這些已經被發現的問題也好,默認設置也好,已經在 MongoDB 2.6以后得到了妥善的解決。我可以負責任地告訴你,你看到的數據安全問題,基本上都是2.4或者之前版本的問題或者是用戶配置的問題。接下來我們來仔細分析一下MongoDB的數據安全機制,通過這個分析來更好地理解為什么有丟數據問題的說法,以及如何來正確的配置MongoDB來保證數據的安全。

MongoDB的數據安全包括以下幾個概念:

  • 恢復日志(Journal)
  • 寫關注(Write Concern)

恢復日志

在MySQL, PostgreSQL,Oracle等關系型數據庫里都有一個Write Ahead Log(Redo Log)的機制用來解決因為系統掉電或者崩潰時導致內存數據丟失問題。MongoDB 的journal就是實現這個目的的一種WAL日志。在MongoDB 2.0之前,Journal沒有被支持或者不是一個默認開的選項。所以當你進行寫入操作時。在沒有Journal的情況下,MongoDB是這樣保存數據的:

無日志

簡單來說,數據在寫入內存之后即刻返回給應用程序。而數據刷盤動作則在后台由操作系統來進行。MongoDB會每隔60秒強制把數據刷到磁盤上。那么大家可以想象得到,如果這個時候發生了系統崩潰或者掉電,那么未刷盤的數據就會徹底丟失了。如果大家看到的博客是2011年左右的,那基本上是碰到了這種情況。

自從2.0開始,MongoDB已經把Journal日志設為默認開啟。

有日志

在上圖的情況下,MongoDB會先把數據更新寫入到Journal Buffer里面然后再更新內存數據,然后再返回給應用端。Journal會以100ms的間隔批量刷到盤上。這樣的情況下,即使出現斷電數據尚未保存到文件,由於有Journal文件的存在,MongoDB會自動根據Journal里面的操作歷史記錄來對數據文件重新進行追加。

有細心的同學可能注意到,Journal文件是100ms 刷盤一次。那么要是系統掉電正好發生在上一次刷journal的50ms之后呢?這個時候,我們就可以來看一下MongoDB持久化的下一個概念了:寫關注

寫關注(Write Concern)

寫關注(或翻譯為寫安全機制)是MongoDB特有的一個功能。它可以讓你靈活地指定你寫操作的持久化設定。這是一個在性能和可靠性之間的一個權衡。 寫關注有以下幾個級別:

{w: 0} Unacknowledged

Unacknowledged

Unacknowledged指的是對每一個寫入操作,MongoDB並不會返回一個是否成功的狀態值。這個級別是寫入性能最好但也是最不安全的級別。比如說,你試圖插入一個違反了唯一性的文檔(重復的身份證號),那么MongoDB會拒絕寫入並報錯。但是由於驅動端並沒有在乎你的報錯,應用程序還滿心歡喜以為一切都沒問題,下回再來查詢那條數據的時候就會出現數據缺失的情況。

有不少時候MongoDB用來保存一些監控和程序日志數據,這個時候如果你有1、2條數據丟失,是不會對應用程序有什么影響的。基於這些MongoDB早些時候不成熟考量,MongoDB在2.2之前的默認設置就是 {w:0}。這是個讓MongoDB 悔恨無比的選擇,因為這個是很多人覺得MongoDB數據不安全的根本原因。

在MongoDB 2.4,這個設置已經被改為下面的 {w:1}

{w: 1} Acknowledged

Unacknowledged

Acknowledged 的意思就是對每一個寫入MongoDB都會確認操作的完成狀態,不管是成功還是失敗。當然這個確認只是基於主節點的內存寫入。但就是這個級別,可以偵測到重復主鍵, 網絡錯誤,系統故障或者是無效數據等錯誤。

自2.4版本起,MongoDB的默認寫安全設置就是 {w:1} Acknowledged。在這種情況下,出現因為系統故障掉電原因而導致的數據丟失只會是我們早些提到的日志沒有及時刷盤的情況。如果你不能接受因為系統崩潰而引起的可能的100ms的數據損失,那么你可以選用下一個級別: {j:1} Journaled

{j:1} Journaled

Journaled

使用這種方式意味着每一次的寫操作會在MongoDB實實在在的把journal落盤以后才會返回。當然這並不意味着每一個寫操作就等於一個IO。MongoDB並不會對每一個操作都立即刷盤,而是會等最多30ms,把30ms內的寫操作集中到一起,采用順序追加的方式寫入到盤里。在這30ms內客戶端線程會處於等待狀態。這樣對於單個操作的總體響應時間將有所延長,但對於高並發的場景,綜合下來平均吞吐能力和響應時間不會有太大的影響。特別是你能給journal部署一個對順序寫有優化的IO帶寬足夠的專門的存儲系統的話,這個對性能的影響可以降到最低。

那么使用 {j:1} 是不是就100% 安全了呢?如果是單機版本,這個基本上就是可以確保的了(除非硬盤損壞)。可是在復制集的場景下,我們還需要來考慮一種更高的級別: {w: “majority”}

{w: “majority”} 寫到多數節點

MongoDB 的默認部署是至少3個節點的復制集(Replicaset)。使用復制集的好處很多,最關鍵的就是提高系統的高可用性。另外一個好處就是提供數據的持久性。在復制集下哪怕你的整個主機連內存帶硬盤壞掉,你的數據還是健康的存在在第二台或者第N台從節點上。但是復制集作為一種分布式的架構也對我們數據一致性提出了新的挑戰。以上述的{w: 1} 寫安全配置為例,我們來分析一種比較復雜的場景。

rollback

  • 01:00:00 網絡故障,主從之間網絡斷開
  • 01:00:01 應用寫入一個文檔: {ts: “01:00:01″} 注意這個文檔無法復制到B和C。此時主節點尚未完全確認網絡已故障,所以按照{w:1}規則繼續接受並確認寫入。
  • 01:00:02 主節點A意識到自己無法和從節點B,C 聯絡上,主動降級為從節點,停止接受寫操作
  • 01:00:05 B,C 選舉結果成功,B升級為主節點。B開始接受寫操作。{ts: “01:00:06″}
  • 01:00:08 網絡恢復,A重新加入集群。這個時候A的oplog 和B的oplog已經有不一致了。A會主動把B上面不存在的寫操作回滾掉(rollback),並寫入一個回滾文件。

在這個時候應用如果再去查詢 {ts: “01:00:01″}這個文檔,MongoDB 將會說文檔不存在!

怎么辦怎么辦? {w: “majority”} 就是我們的答案。 “majority” 指的是“大多數節點”。使用這個寫安全級別,MongoDB只有在數據已經被復制到多數個節點的情況下才會向客戶端返回確認。

Journaled

我們來看一下在使用 {w: “majaority”} 之后,剛才的情況就變成了:

  • 01:00:00 網絡故障,主從之間網絡斷開
  • 01:00:01 應用要求寫入一個文檔: {ts: “01:00:01″} 文檔會首先成功寫入主節點。但是由於網絡斷開這個文檔無法復制到B和C。因為無法滿足{w:”majority”}要求,從應用的角度這個文檔並沒有寫入成功。
  • 01:00:02 主節點A意識到自己無法和從節點B,C 聯絡上,主動降級為從節點,停止接受寫操作
  • 01:00:05 B,C 選舉結果成功,B升級為主節點。B開始接受寫操作。{ts: “01:00:06″}
  • 01:00:08 網絡恢復,A重新加入集群。這個時候A會產生回滾,把{ts: “01:00:01″}這個文檔刪除。 此時集群的數據狀態為一致和正確的。

至此,如果使用 {w: “majority”, j:1 }, 那么MongoDB可以滿足所有級別數據持久性的要求。值得注意的是在2013年5月Kyle Kingsly 發表了一篇博客 Call Me Maybe: http://aphyr.com/posts/284-call-me-maybe-mongodb 在這片文章里Kyle 匯報了一些關於 {w: “majority”} 的bug, 這些bug已經在2.6里被解決了。當然像Sven那樣的嘩眾取寵之流,估計並沒有去研究3.0里面是否真的有問題,而是隨便google了一下幾年前的東西來做文章。

總結

一般來說,MongoDB建議在集群中使用 {w: “majority”} 設置。在一個集群是健壯的部署的情況下(如:足夠網絡帶寬,機器沒有滿負荷),這個可以滿足絕大部分數據安全的要求,因為MongoDB的復制在正常情況下是毫秒級別的,往往在Journal刷盤之前已經復制到從節點了。如果你追求完美,那么可以再進一步使用{j:1} 。兩者相結合,

傳說中MongoDB 丟數據的事情,確實已經成為傳說了。

后記:在我寫這篇文章之時,社區又有人匯報數據丟失的問題。說的是每一萬條記錄就會丟一兩條記錄。對於這種情況,我的第一反應就是:查查你的代碼吧,很多時候往往問題出在程序上。果不其然,經過仔細檢查,原來是代碼的問題。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM