MongoDB數據安全機制

在MongoDB發展的早期階段,確實有一些數據持久化的問題沒有處理好,特別是一些默認值的選定上。隨着mongoDB的發展和社區的不斷貢獻,journal和replicaSet等功能也是愈來愈完善,到version 2.6以後,就已經可以使用在企業級的生產環境,也有大量的成功案例。尤爲是version 3.2默認採用新的存儲引擎WiredTiger,性能更是明顯提高。java

歷史發展問題

在早期版本中,坊間有不少傳說MongoDB會丟數據,也常常會據說mongoDB丟數據的案例。這其中有mongoDB早期版本的不完善的緣由,也有很多是因爲用戶對mongoDB的安全機制的不瞭解致使。mongodb

Journal配置

在MongoDB 2.0以前,Journal沒有被支持或者不是一個默認開的選項。在MySQL,PostgreSQL,Oracle等關係型數據庫裏都有一個Write Ahead Log(RedoLog)的機制用來解決由於系統掉電或者崩潰時致使內存數據丟失問題。MongoDB的journal就是實現這個目的的一種WAL日誌。在MongoDB2.0以前,Journal沒有被支持或者不是一個默認開的選項。因此當你進行寫入操做時。在沒有Journal的狀況下,數據在寫入內存以後即刻返回給應用程序。而數據刷盤動做則在後臺由操做系統來進行。MongoDB會每隔60秒強制把數據刷到磁盤上。那麼你們能夠想象獲得,若是這個時候發生了系統崩潰或者掉電,那麼未刷盤的數據就會完全丟失了。在大數據量的應用中,出現數據丟失的機率也就很是大了。自從2.0開始,MongoDB已經把Journal日誌設爲默認開啓。shell

getLastError命令

Mongodb的寫操做默認是沒有任何返回值的,這減小了寫操做的等待時間,同時也帶來了安全隱患。無論有沒有寫入到磁盤或者有沒有遇到網絡錯誤錯誤,它都不會報錯,客戶端發送寫命令以後就返回,不會等服務器返回操做的結果。 以java爲例,舉個例子:當咱們爲字段創建了一個惟一索引,針對這個字段咱們插入兩條相同的數據,不設置WriterConcern或者設置WriterConcern.NORMAL模式,這時候即使拋出異常,也不會獲得任何錯誤。insert()函數在java中的返回值是WriteResult類,數據庫

WriteResult( CommandResult o , WriteConcern concern ){
        _lastErrorResult = o;
        _lastConcern = concern;
        _lazy = false;
        _port = null;
        _db = null;
    }

這個類實際上包裝了getlastError的返回值,可是這時候WriteResult的_lastErrorResult屬性其實是空的。由於dup key錯誤是server error,只有在WriterConcern.SAFE或更高級別的模式下,纔會獲得server error。 MongoDB的讀寫操做是沒有事務,也不徹底有順序的。在多線程模式下讀寫Mongodb的時候,若是這些讀寫操做是有邏輯順序的,那麼這時候也有必要調用getlasterror命令,用以確保上個操做執行完下個操做才能執行。在實際開發中,咱們都是經過驅動程序操做MongoDB,而大多數驅動程序都是使用鏈接池去鏈接mongodb,這致使在多線程的程序中,頗有可能採用的不是同一個鏈接。經過不一樣鏈接發送的操做命令,MongoDB服務器並不徹底按照接收的順序來執行。這就有可能致使前面插入的數據,後面查詢不到的現象,尤爲是高併發程序中。安全

數據庫會爲每個MongoDB數據庫鏈接建立一個隊列,存放這個鏈接的請求。當客戶端發送一個請求,會被放到隊列的末尾。只有隊列中的請求都執行完畢,後續的請求才會執行。因此從單個鏈接就能夠了解整個數據庫,而且它老是能讀到本身寫的東西。 注意,每一個鏈接都有獨立的隊列,要是打開兩個shell,就有兩個數據庫鏈接。在一個shell中執行插入,以後在另外一個shell中進行查詢不必定能獲得插入的文檔。然而,在同一個shell中,插入後再進行查詢是必定能查到的。手動復現這個行爲並不容易,可是在繁忙的服務器上,交錯的插入/查找就顯得稀鬆日常了。當開發者用一個線程插入數據,用另外一個線程檢查是否成功插入時,就會常常遇到這種問題。有那麼一兩秒鐘時間,好像根本就沒插入數據,但隨後數據又忽然冒出來。 使用Ruby、Python和Java驅動程序時要特別注意這種行爲,由於這幾個語言的驅動程序都使用了鏈接池。爲了提升效率,這些驅動程序都和服務器創建了多個鏈接(一個鏈接池),並將請求分散到這些鏈接中去。好在它們都提供一些機制來確保一系列的請求都由一個鏈接來處理。MongoDBwiki(http;//dochub.mongodb.org/drivers/connections)上有不一樣語言鏈接池的詳細信息。服務器

當前現狀

隨着MongoDB的發展和開源社區的貢獻,到當前3.2版本,數據安全的問題早已不復存在。Journal日誌早在2.0版本就爲默認開啓,在大多數的語言驅動中,插入、更新和刪除操做都會等待Server返回的確認信息。網絡

By default, all write operations will wait for acknowledgment by the server, as the default write concern is WriteConcern.ACKNOWLEDGED.多線程

那麼在開發中,MongoDB如何來確保咱們的數據是安全的呢?在google.groupuser上,mongo的開發者有一段這樣的解釋:併發

By default: Collection data (including oplog) is fsynced to disk every 60 seconds. Write operations are fsynced to journal file every 100 milliseconds. Note, oplog is available right away in memory for slaves to read. Oplog is a capped collection so a new oplog is never created, old data just rolls off. GetLastError with params: (no params) = return after data updated in memory. fsync: true: with --journal = wait for next fsync to journal file (up to 100 milliseconds); without --journal = force fsync of collection data to disk then return. w: 2 = wait for data to be updated in memory on at least two replicas.app

咱們能夠看到:

  1. 若是打開journal,那麼即便斷電也只會丟失100ms的數據,這對大多數應用來講均可以容忍了。從1.9.2+,mongodb都會默認打開journal功能,以確保數據安全。並且journal的刷新時間是能夠改變的,2-300ms的範圍,使用 --journalCommitInterval 命令能夠設置。
  2. Oplog和數據刷新到磁盤的時間是60s,對於複製來講,不用等到oplog刷新磁盤,在內存中就能夠直接複製到Sencondary節點。

以MongoDB Java Driver中的GetLastError命令爲例,咱們從驅動程序接口開始分析MongoDB對數據安全的實現機制。

WriteProtocol.java  
    private BsonDocument createGetLastErrorCommandDocument() {
        BsonDocument command = new BsonDocument("getlasterror", new BsonInt32(1));
        command.putAll(writeConcern.asDocument());
        return command;
    }
    
    
WriteConcern.java
    /**
     * Gets this write concern as a document.
     *
     * @return The write concern as a BsonDocument, even if {@code w <= 0}
     */
    public BsonDocument asDocument() {
        BsonDocument document = new BsonDocument();

        addW(document);

        addWTimeout(document);
        addFSync(document);
        addJ(document);

        return document;
    }

全部的插入、更新和刪除操做底層都會調用到createGetLastErrorCommandDocument方法獲得getlasterror命令的Document,該document包含Object w, Integer wTimeoutMS, Boolean fsync, Boolean journal四個屬性。因此經過分析Driver的接口實現,咱們知道客戶端就是經過這四個參數來調用MongoDB Server實現數據安全。 這裏,咱們先看一下WriteProtocol的構造函數。

WriteProtocol.java 

    // Private constructor for creating the "default" unacknowledged write concern.  Necessary because there already a no-args
    // constructor that means something else.
    private WriteConcern(final Object w, final Integer wTimeoutMS, final Boolean fsync, final Boolean journal) {
        if (w instanceof Integer) {
            isTrueArgument("w >= 0", ((Integer) w) >= 0);
        } else if (w != null) {
            isTrueArgument("w must be String or int", w instanceof String);
        }
        isTrueArgument("wtimeout >= 0", wTimeoutMS == null || wTimeoutMS >= 0);
        this.w = w;
        this.wTimeoutMS = wTimeoutMS;
        this.fsync = fsync;
        this.journal = journal;
    }

下面,咱們就分析一下這四個參數分別表示什麼。

  • w參數

When running with replication, this is the number of servers to replicate to before returning. A w value of 1 indicates the primary only. A w value of 2 includes the primary and at least one secondary, etc. In place of a number, you may also set w to majority to indicate that the command should wait until the latest write propagates to a majority of the voting replica set members. 在複製集環境中,w表示數據須要複製的服務器數量。1表示只寫入primary Server就返回,2表示寫入primary Server和至少一個secondary Server,其餘數字同理。除了數字以外,w的值也能夠被設置成「majority」,表示須要等到寫操做被同步到大多數的Server以後才返回。

w參數表示的是在複製集環境中,最後一個寫操做返回以前,須要等待寫命令同步到幾個Server(默認值爲1)。w默認值爲1,就兼顧了基本的數據安全和性能。
  1. w也能夠被設置爲0或者-1,表示每個寫入操做,MongoDB都不會返回一個是否成功的狀態值。這個級別是寫入性能最好但也是最不安全的級別。有很多時候MongoDB用來保存一些監控和程序日誌數據,這個時候若是你有一、2條數據丟失,是不會對應用程序有什麼影響的。這是version 2.2以前的默認設置,也是不少人以爲MongoDB數據不安全的主要緣由。

  2. w=1的意思就是對每個寫入MongoDB的操做都會確認操做的完成狀態,無論是成功仍是失敗。固然這個確認只是基於主節點的內存寫入。這樣,就能夠偵測到重複主鍵,網絡錯誤,系統故障,惟一索引或者是無效數據等大多數錯誤,這也是當前版本的默認設置。在這種狀況下,出現由於系統故障掉電緣由而致使的數據丟失只會是咱們以前提到的日誌沒有及時寫入磁盤的狀況。若是你不能接受由於停電或系統崩潰而引發的可能的100ms的數據損失,那麼你能夠選用更安全的設置。

  3. w=2,3,4...,此時表示除了能夠確保寫操做寫入了主節點的內存,還能夠確保寫操做被同步到了其餘2-1,3-1,4-1個Server上。假如環境配置了3臺Server,1主2從,w=2就能夠保證數據被寫入保證1臺primary Server和1臺secondary Server的內存。若是要是擔憂這時候primary Server不可用致使數據不一致,還能夠設置w=3來確保數據被同步到全部的Server。

  4. 還有一種狀況,假如Server數量不肯定或者可能有變化,咱們能夠設置w值爲「majority」。「majority」 指的是「大多數節點」,使用這個寫安全級別,MongoDB只有在數據已經被複制到多數個節點的狀況下才會向客戶端返回確認。

    通常來講,MongoDB建議在集羣中使用 {w: 「majority」} 設置。在一個集羣是健壯的部署的狀況下(如:足夠網絡帶寬,機器沒有滿負荷),這個能夠知足絕大部分數據安全的要求,由於MongoDB的複製在正常狀況下是毫秒級別的,每每在Journal刷盤以前已經複製到從節點了。 爲何要說絕大多數呢? 假如集羣配置1主2從,w=3 數據還在primary Server內存,寫操做的journal日誌也在內存中,寫操做也被同步到其中一臺secondary Server。這時primary Server瞬間停電,數據沒有入盤,journal日誌也還沒來得及入盤,剩下兩臺secondary Server一臺有數據,一臺無數據。此時以前的primary Server恢復可用,狀況變成了兩臺server無數據,一臺有數據,就致使了以前寫入的數據被回退了。若是寫入數據的時候,可以確保journal日誌入盤,這種丟數據的狀況就徹底能夠避免了。

  • wTimeoutMS

Specify a value in milliseconds to control how long to wait for write propagation to complete. If replication does not complete in the given timeframe, the getLastError command will return with an error status. 以毫秒爲單位指定一個值,用來控制等待寫傳播來完成傳播的時間。若是複製沒有在給定的時間內完成,則GetLastError函數命令將錯誤狀態返回。

等待超時時間就比較好理解,若是寫操做在指定的時間內尚未執行完或同步完,則將返回錯誤的狀態。wTimeoutMS的值要根據集羣的Server數量和所處的網絡環境來決定,wTimeoutMS值過高也有可能會致使鏈接數太高。
  • j參數

If true, wait for the next journal commit before returning, rather than waiting for a full disk flush. If mongod does not have journaling enabled, this option has no effect. If this option is enabled for a write operation, mongod will wait no more than 1/3 of the current commitIntervalMs before writing data to the journal. 若是參數j的值爲true,則只要等到下一次journal日誌提交(寫磁盤)就會返回,而不須要等待一個完整的磁盤刷新。若是的mongod未啓用日誌記錄,此選項沒有任何效果。若是這個選項被用於寫入操做,在最終寫入到日誌以前,mongod將最多等待commitIntervalMs(提交間隔)值的1/3。

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

The primary use of fsync is to flush and lock the database for backups. The fsync operation blocks all other write operations for a while it runs. 主服務器使用fsync 來強制寫入磁盤和備份時候的鎖住整個數據庫。 fsync運行時會阻塞數據庫的其餘寫操做。

嚴格來講,FSYNC是一個管理員命令,它迫使全部的數據刷新到磁盤。你不該該在你的代碼中使用它,至少不是常用,它經常使用於鎖定備份數據庫。MongoDB中的數據安全經過複製/分片/日誌來實現,而不是強制寫入,不然就違背了MongoDB的原始初衷了。在Java驅動程序包中的WriteConcern類,我基本上歷來不使用WriteConcern.FSYNC_SAFE和WriteConcern.FSYNCED。

總的來講,對於數據安全,MongoDB2.6+ 就有了很是完善的機制,並且還很靈活。從不返回結果、確認主服務器寫內存、確認主服務器寫journal日誌、確認同步到大多數服務器到強制寫入磁盤,MongoDB數據安全級別逐漸提升。開發者能夠根據不一樣的應用場景選擇適合的安全級別,在數據安全和寫操做的性能之間找到平衡。

最佳實踐

根據以往踩過的坑和同行經驗,總結出如下幾點建議。

  1. 若是沒有特殊要求,最低級別也要使用WriterConcern.SAFE,即w=1。
  2. 對於不重要的數據,好比log日誌,可使用WriterConcern.NONE或者WriterConcern.NORMAL,即w=-1或者w=0,省去等待網絡的時間。
  3. 通常來講,在集羣中使用WriterConcern.MAJORITY設置。在一個集羣是健壯的部署的狀況下(足夠網絡帶寬,機器沒有滿負荷),這個能夠知足絕大部分數據安全的要求。由於MongoDB的複製在正常狀況下是毫秒級別的,每每在Journal刷盤以前已經複製到從節點了。若是你追求完美,那麼能夠再進一步使用 {w: 「majority」,j:1} 。
  4. 對大量的不連續的數據寫入,若是每次寫入都調用getLastError會下降性能,由於等待網絡的時間太長,這種狀況下,能夠每過N次調用一下getLastError。可是在Shard結構上,這種方式不必定確保以前的寫入是成功的。
  5. 對連續的批量寫入(batchs of write),要在批量寫入結束的時候調用getlastError,這不只能確保最後一次寫入正確,並且也能確保全部的寫入都能到達服務器。若是連續寫入上萬條記錄而不調用getlastError,那麼不能確保在同一個TCP socket裏全部的寫入都成功。這在併發的狀況下可能就會有問題。避免這個併發問題,能夠參考如何在一個連接(請求)裏完成批量操做,MongoDB Java Driver併發
  6. 對數據安全要求很是高的的配置:j=true,w="majority",db.runCommand({getlasterror:1,j:true,w:'majority',wtimeout:10000})
  7. 大多數語言的驅動程序均可以在MongoOption中設置,MongoOption中的這些設置是全局的,對於單獨的一個鏈接或一次操做,還能夠分別設置。

傳說中MongoDB 丟數據的事情,已經成爲了歷史。


參考資料

MongoDB writeConcern原理解析 MongoDB Doc MongoDB中文社區

相關文章
相關標籤/搜索