天天看點

Mongo的持久性

  持久性(durability)是操作被送出後可持久儲存在資料庫中的保證。從完全沒有保障到完全保證持久性,MongoDB可高度配置與持久性相關的設定。本章内容包括:

  • MongoDB如何保證持久性;
  • 如何配置應用和伺服器,進而獲得所需的持久性級别;
  • 運作時關閉日記系統(journaling)可能帶來的問題;
  • MongoDB不能保證的事項。

  如磁盤和軟體運作正常,則MongoDB能夠在系統崩潰或強制關閉後,確定資料的完整性。

  注意:關系型資料庫通常使用持久性一詞來描述資料庫事務(transaction)的持久儲存。由于MongoDB并不支援事務(4.0之前不支援),是以該詞義在這裡有些許不同。

1.日記系統的用途

  MongoDB會在進行寫入時建立一條日志(journal),日記中包含了此次寫入操作具體更改的磁盤位址和位元組。是以,一且伺服器突然停機,可在啟動時對日記進行重放(replay),進而重新執行那些停機前沒能夠重新整理(flush)到磁盤的寫入操作。

  資料檔案預設每60秒重新整理到磁盤一次,是以日記檔案隻需記錄約60秒的寫入資料。 日記系統為此預先配置設定了若幹個空檔案,這些檔案存放在/data/db/journal目錄中,檔案名為_j.0、_j.1等。

  長時間運作MongoDB後,日記目錄中會出現類似_j.6217、_j.6218和_j.6219的檔案。這些是目前的日記檔案。檔案名中的數字會随着MongoDB運作時間的增長而增大。資料庫正常關閉後,日記檔案則會被清除(因為正常關閉後就不再需要這些檔案了)。

  如發生系統崩潰,或使用kill -9指令強制終止資料庫的運作,mongod會在啟動時重放日記檔案,同時會顯示出大量的校驗資訊。這些資訊冗長且難懂,但其存在說明一切都在正常運作。可在開發時運作kill -9指令來終止mongod程序并重新啟動,這樣在生産環境中,如果發生相同狀況,也會明白此時顯示哪些資訊是正常的。

1.1 批量送出寫入操作

  MongoDB預設每隔100毫秒,或是寫入資料達到若幹兆位元組時,便會将這些操作寫入日記。這意味着MongoDB會成批量地送出更改,即每次寫入不會立即重新整理到磁盤。不過在預設設定下,系統發生崩潰時,不可能丢失間隔超過100毫秒的寫入資料。

  然而,對于一些應用而言,這一保障還不夠牢固,是以可通過若幹方式來獲得更強有力的持久性保證。可通過向getLastError傳遞j選項,來確定寫入操作的成功。getLastError會等待前一次寫入操作寫入到日記中,而日記在下一批操作寫入前,隻會等待30毫秒(而非100毫秒):

> db.foo.insert({"x" : 1})
> db.runCommand({"getLastError" : 1, "j" : true})
> // The {"x" : 1} document is      

  注意:這意味着如果在每次寫入操作中都使用了 "j":true選項,則寫入速度實際上會被限制為每秒33次:

(1次/30毫秒)x (1000毫秒/秒)=33.3次/秒      

  通常将資料重新整理到磁盤并不會耗費這麼長時間,是以如果允許MongoDB對大部分資料進行批量寫入而非每次都單獨送出,資料庫的性能則會更好。然而,重要的寫入操作還是會經常選用此選項。

  送出一次寫入操作,會同時送出這之前的所有寫入操作。是以,如果有50個重要的寫入操作,可使用“普通的” getLastError (不包括j選項),而在最後一次寫入後使用含有j選項的getLastError。如果成功的話,就可知道所有50次寫入操作都已安全重新整理到磁盤上。

  如果寫入操作含有很多連接配接,可通過并發寫入,來減少使用j選項所帶來的速度開銷。此種做法可增加資料吞吐量,但也會增加延遲。

1.2 設定送出時間間隔

  另一個減少日記被幹擾幾率的選項是,調整兩次送出間的時間間隔。運作setParameter指令,設定journalCommitInterval的值(最小為2毫秒,最大為500毫秒)。以下指令使得MongoDB每隔10毫秒便将資料送出到日記中一次:

> db.adminCommand({"setParameter" : 1, "journalCommitInterval"      

  也可使用指令行選項--journalCommitInterval來設定這一值。

  無論時間間隔設定為多少,使用帶有"j" : true的getLastError指令都會将該值減少到原來的三分之一。

  如用戶端的寫入速度超過了日記的重新整理速度,mongod則會限制寫入操作,直到日記完成到磁盤的寫入。這是mongod會限制寫入的唯一情況。

2.關閉日記系統

  對于所有生産環境的部署,都推薦使用日記系統,但有時我們可能需要關閉該系統。即使不附帶j選項,日記系統也會影響MongoDB的寫入速度。如果寫入資料的價值不及寫入速度降低帶來的損失,我們可能就會想要禁用日記系統。

  禁用日記系統的缺陷在于,MongoDB無法保證發生崩潰後資料的完整性。在沒有日記系統的前提下,一旦發生崩潰,那麼資料肯定會遭到損壞,進而需要對資料進行修複或替換。這種情況下遭到損壞的資料不應繼續投入使用,除非我們不在乎資料庫會突然停止工作。

  如果希望資料庫在崩潰後能夠繼續工作,有以下幾種做法。

2.1 替換資料檔案

  這是最佳選擇。删除資料目錄中的所有檔案,然後擷取新檔案:可從備份中恢複,使用確定正确的資料庫快照,如果是副本內建員的話,也可對其進行初始化同步。如果是一個資料量較小的副本集,重新同步可能是最好的選擇,即先停止此成員的運作(如果它還沒有停止運作的話),删除資料目錄中的所有内容,然後重新啟動它。 

2.2 修複資料檔案

  如果既沒有備份和複制,也沒有副本集中的其他成員,則需搶救所有可能的資料。需對資料庫使用一個”修複“工具,修複實質上是删除所有受損資料,不過可能不會留有太多完好的資料。

  mongod自帶了兩種修複工具,一種是mongod内置的,另一種是mongodump内置的。mongodump的修複更加接近底層,可能會找到更多的資料,但耗時要更長(而另一種自帶的修複方式也不見得很快)。另外,如使用mongodump的修複,在準備再次啟動前,依然需要恢複資料的操作。

  是以,應根據願意在資料恢複中消耗的時間長短來進行決定。

  要使用mongod内置的修複工具,需附帶--repair選項運作mongod:

$ mongod --dbpath /path/to/corrupt/data --repair      

  進行修複時,MongoDB不會開啟27017端口的監聽,但我們可通過査看日志(log) 的方式得知它正在做什麼。注意,修複過程會對資料進行一份完整的複制,是以如有80 GB的資料,則需80 GB的空閑磁盤空間。為盡量解決這一問題,修複工具提供了--repairpath選項。這一選項允許在主磁盤空間不足時挂載一個“緊急驅動器”,并使用它來進行修複操作。--repairpath選項的用法如下:

$ mongod --dbpath /path/to/corrupt/data \
> --repair --repairpath /media/external-hd/data/db      

  如果修複過程被強行終止,或者出現故障(如磁盤空間不足),至少不會使情況變得更糟。修複工具将所有的輸出都寫入新的檔案中,不會對原有檔案進行修改。是以原始資料檔案不會比開始修複時變得更糟。

  另一個選擇是使用mongodump的--repair選項,就像這樣:

$ mongodump --repair      

  這些選擇都不是特别好,但它們應該可以讓mongod重新運作在一個幹淨的資料集上。

2.3 關于mongod.lock檔案

  資料目錄中有一個名為mongod.lock的特殊檔案。該檔案在關閉日記系統運作時十分重要(如啟用了日記系統,則這一檔案不會出現)。

  當mongod正常退出時,會清除mongod.lock檔案,這樣在啟動時,mongod就會得知上一次是正常退出的。相反,如果該檔案沒被清除,mongod就會得知上一次是異常退出的。

  如果mongod監測到上一次是異常退出的,則會禁止再啟動,這樣我們就會意識到一份幹淨資料副本的需求。然而,有些人意識到可通過删除mongod.lock檔案來啟動mongod。請不要這麼做。通常,在啟動時删除這一檔案,意味着我們不知道也不在乎資料是否受損。除非如此,否則請不要這麼做。如果mongod.lock檔案阻止了 mongod的啟動,請對資料進行修複,而非删除該檔案。

2.4 隐蔽的異常退出

  不要删除鎖檔案的另一重要原因在于,我們甚至可能意識不到這是一次異常退出。假設我們需要重新開機機器進行例行維護。初始化腳本應負責在伺服器關閉之前關閉mongod。初始化腳本通常會先嘗試正常關閉程式,但如在若幹秒後依然沒有關閉的話,則會選擇強行關閉。在一個繁忙的系統上,MongoDB完全可能耗費30秒來結束運作,正常的初始化腳本不會等待它正常關閉。是以,異常退出的次數可能比我們知道的要多得多。

3.MongoDB無法保證的事項

  在硬體或檔案系統出現故障等情況下,MongoDB無法保證操作的持久性。尤其是在硬碟發生損壞的情況下,MongoDB根本無法保證資料安全。

  另外,不同的硬體和軟體對于持久性的保障可能有所不同。例如,一些破舊的硬碟會在寫入操作還在列隊中等待之際,便報告稱寫入成功。MongoDB無法防止這一層次的誤報,如果此時系統崩潰,資料就可能會發生丢失。

  基本上,MongoDB的安全性與其所基于的系統相同,MongoDB無法避免硬體或檔案系統導緻的資料損壞。可使用副本應對系統問題。如果一台機器發生了故障,還有另一台在正常工作。

4.檢驗資料損壞

  可使用validate指令,檢驗集合是否有損壞。如檢驗名為foo的集合,代碼如下:

> db.foo.validate()
{
    "ns" : "test.foo",
    "firstExtent" : "0:2000 ns:test.foo",
    "lastExtent" : "1:3eae000 ns:test.foo",
    "extentCount" : 11,
    "datasize" : 75960008,
    "nrecords" : 1000000,
    "lastExtentSize" : 37625856,
    "padding" : 1,
    "firstExtentDetails" : {
        "loc" : "0:2000",
        "xnext" : "0:f000",
        "xprev" : "null",
        "nsdiag" : "test.foo",
        "size" : 8192,
        "firstRecord" : "0:20b0",
        "lastRecord" : "0:3fa0"
    },
    "deletedCount" : 9,
    "deletedSize" : 31974824,
    "nIndexes" : 2,
    "keysPerIndex" : {
        "test.foo.$_id_" : 1000000,
        "test.foo.$str_1" : 1000000
    },
    "valid" : true,
    "errors" : [ ],
    "warning" : "Some checks omitted for speed. use {full:true} 
        option to do more thorough scan.",
    "ok" : 1
}      

  需重點注意的是結尾附近的valid字段,字段值為true。否則,輸出内容中會包含找到的資料損壞細節。

  輸出中的大部分内容,是有關集合的内部結構資訊,于調試而言沒有太大用處。

  • firstExtent (首區段)

  該集合首區段(extent)的磁盤偏移量(disk offset)。本例中位于檔案test.O處, 位元組偏移量(byte offset)為 0x2000。

  • lastExtent (尾區段)

  該集合尾區段的偏移量。本例中位于檔案test.1處,位元組偏移量為0x3eae000。

  • extentCount

  該集合所占區段數量。

  • lastExtentSize

  最近配置設定區段的位元組數量。區段大小随集合的增長而增長,最大可達2 GB。

  • firstExtentDetails

  描述集合中首區段的子對象。其中包含指向相鄰兩個區段的指針(xnext和 xprev)、區段的大小(注意,它比尾區段要小得多,通常首區段是很小的),以及指向區段中第一條和最後一條記錄(record)的指針。記錄是真正承載着文檔的結構。

  • deletedCount

  該集合從存在至今,共删除的文檔數目。

  • deletedSize

  該集合中空閑清單(freelist),即所有有效空餘空間的大小。不僅包括被删除文檔所占的空間,還包括已被預配置設定給該集合的空間。

  validate指令隻适用于集合,而不适用于索引。是以我們通常無法判斷索引是否被損壞,除非周遊檢查一遍,即査詢每個索引在集合中對應的文檔。通過周遊得出的結果即可判斷索引是否被損壞。

  如果程式提示了非法的BSON對象(invalidBSONObj), —般說明資料損壞了。最糟糕的錯誤則是提到了 pdfile的錯誤。pdfile可以說是MongoDB的資料存儲核心,源于pdfile的錯誤基本說明資料檔案已經損壞了。

  如果遇到了資料損壞,則可在日志中看到類似如下内容:

Tue Dec 20 01:12:09 [initandlisten] Assertion: 10334:
    Invalid BSONObj size: 285213831 (0x87040011) 
    first element: _id: ObjectId('4e5efa454b4ae20fa6000013')      

  如果顯示的第一個元素已經被廢棄,就沒什麼可做的了。如果第一個元素還是可見的(如上例中的ObjectId),也許可删除損壞文檔。可嘗試運作:

> db.remove({_id: ObjectId('4e5efa454b4ae20fa6000013')})      

  将其中的 _id 替換為日志中看到的對應_id。注意,如果資料損壞影響的不隻是該文檔,則這種技術可能不會奏效。這種情況下,我們可能仍需對資料進行修複。

5.副本集中的持久性

  副本章節中,曾讨論過副本集中的投票問題,即一次對副本集的寫入操作,在寫入副本集中的大多數成員中之前,可能先會進行復原(rollback)。可将與此相關的選項和之前提到的日記系統的選項結合起來使用:

> db.runCommand({"getLastError" : 1, "j" : true, "w" : "majority"})      

  進行這一操作後,可保證寫入操作寫入到了主節點和備份節點中,其中隻有對主節點的寫入可保證持久性。理論上來講,在進行寫入到記錄到日記内的100毫秒時間内,多數的伺服器同時崩潰也是有可能的,這種情況下資料庫會復原到目前主節點的狀态。雖然這是一種極端情況,但也說明其并非是完美的。遺憾的是要解決這一問題并不簡單,但目前MongoDB社群正嘗試改變這一情況。

作者:小家電維修

轉世燕還故榻,為你銜來二月的花。

繼續閱讀