天天看點

MongoDB實戰-複制(副本集的複制原理)

    學習了如何建立副本集,如何使用副本集,認知了其故障轉移政策。我們還需從複制的基礎原理了解副本集的工作方式。副本集主要依賴于兩個基礎機制:oplog和“心跳“。oplog讓資料的複制成為可能,而“心跳”則監控健康情況并觸發故障轉移,後續将看到這些機制時如何運作的。你應該已經逐漸開始了解并能預測副本集的行為了,尤其在故障發生的情況下。

1.關于oplog

    oplog是MongoDB複制的關鍵。oplog是一個固定集合,位于每個複制節點的local資料庫裡,記錄了所有對資料的變更。每次用戶端向主節點寫入資料,就會自動向主節點的oplog裡添加一個條目,其中包含了足夠的資訊來再現資料。一旦寫操作被複制到某個從節點上,從節點的oplog,從節點的oplog中也會有一條關于寫入的資料。每個oplog條目都是由一個BSON時間戳進行辨別的,所有的從節點都使用這個時間戳來追中他們最後應用的條目。

   為了更好的了解其原理,我們仔細看看真實的oplog以及其中的記錄。連接配接上主節點後,切換到local資料庫。local資料庫中儲存了所有的副本集中繼資料和oplog。當然,資料庫本身不能被複制。正如其名,local資料庫裡的資料對本地節點而言是唯一的,是以不該複制。檢視local資料庫,會發現一個叫oplog.rs的集合,每個副本集都會把oplog儲存在這個集合裡。如下圖所示:

MongoDB實戰-複制(副本集的複制原理)

    replset.minvalid包含了指定副本內建員的初始同步資訊。system.replset儲存了副本集配置文檔。me和slaves用來實作寫關注。startup_log是啟動資訊。還有replset.election包含了節點的選舉資訊。我們會将主要的精力集中在oplog上,查詢一條前面你插入資料的那個操作相關的op條目。執行:

db.oplog.rs.find({op:"i"})
           

在主從節點執行的結果相同,如下圖:

MongoDB實戰-複制(副本集的複制原理)
MongoDB實戰-複制(副本集的複制原理)

結果中,第一個字段為ts,儲存了該條目BSON時間戳。這裡要特别注意,shell使用TimeStamp對象來顯示時間戳的,包含兩個字段,前面一部分是從紀元開始的秒數,後面一個市計數器。h表示該操作的一個唯一的ID,每個操作的該字段都是一個不同的值。字段op表示操作碼,它告訴從節點該條目表示什麼操作,本例子中i表示插入。op後的ns标明了有關的命名空間(資料庫和集合)。o對插入操作而言包含了所插入的文檔的副本。

     在檢視oplog條目時,對于那些影響多個文檔的操作,oplog會将各個部分都分析到位。對于多項更新和大批量删除來說,會為每個影響到的文檔建立單獨的oplog條目。例如,你想集合中插入幾本狄更斯的數

db.books.insert({title:"A Tale of Two Cities"})
db.books.insert({title:"Great Expections"})
           
db.books.udpate({},{$set:{author:"Dickens"}},false,true)
           

執行完上述語句後,oplog中查詢op:"u"的選項,結果如下:

MongoDB實戰-複制(副本集的複制原理)
MongoDB實戰-複制(副本集的複制原理)

      如你所見,每個節點都有自己的oplog條目。這種正規化是更通用的一種政策中的一部分。它會保證從節點總是能和主節點擁有一樣的資料。要確定這一點,每次應用的操作就必須是幂等的--一個指定的oplog條目被應用多少次都無所謂:結果總是一樣的。其他文檔操作的行為是一樣的,比如删除,你可以試試不同的操作,檢視其在oplog中最終是什麼樣的。要取得oplog的目前狀态的基本資訊,可以運作

db.getReplicationInfo()
           
MongoDB實戰-複制(副本集的複制原理)

    這裡有oplog中第一條河最後一條時間戳,你可以使用$natural排序修飾符手工找到這些oplog條目。例如下面這句用于擷取最後一個條目

db.oplog.rs.find().sort({$natural:-1}).limit(1)
           
MongoDB實戰-複制(副本集的複制原理)

      關于複制,還有一件重要的事情。即從節點是如何确定它們在oplog裡的位置的。答案在于從節點自己也有一份oplog。這對主從複制的一項重大改進。是以值得深究其中的原理。假設向副本集的主節點發起寫操作,接下來會發生什麼?寫操作先被記錄下來,添加到主節點的oplog裡。與此同時,所有從節點複制oplog。是以,當某個從節點準備更新自己時,他做了三件事:首先,檢視自己oplog中最後一條的時間戳;其次,查詢主節點oplog中所有大于此時間戳的條目;最後,把那些條目添加到自己的oplog中并應用到自己的庫中。也就是說,萬一發生故障,任何被提升為主節點的從節點都會有一個oplog,其他從節點都能以他為複制進行複制。這項特性對副本集的恢複是必須的。

   從節點使用長輪詢(long polling)立即應用來自主節點oplog的新條目。是以從節點的資料通常是最新的。由于網絡分區或者從節點本身進行維護造成資料陳舊的,可以使用從節點oplog的最新的時間戳來檢測網絡延遲。

2. 停止複制

      如果從節點在主節點的oplog裡找不到它所同步的點,那麼就會永久停止複制。發生這種情況是,你會從日志中看到如下異常:

repl:replication data too stale,halting
Tus Aug 28 14:19:27[replsecondary]caught SyncException
           

     回憶一下,oplog是一個固定集合,也就是說集合裡的條目最終都會過期。一旦某個從節點沒能在主節點的oplog找到它已經同步的點,就無法保證這個從節點是主節點的完美副本了。因為修複停止複制的唯一途徑是重新完整同步一次主節點的資料,是以要竭盡全力避免這個狀态。為此,要監測從節點的延時情況,針對你的寫入要有足夠大的oplog。下面我們需要讨論oplog的大小設定。

3. 調整複制OPLOG大小

     因為oplog是一個固定集合,是以一旦建立就無法重新設定大小,為此要慎重選擇oplog的大小。預設的oplog大小會随着環境發生變化。在32系統上,oplog預設是50MB,而在64位系統上,oplog會增大到1GB或空餘磁盤空間的5%。對于多數部署環境,空閑磁盤空間的5%綽綽有餘,對于這種尺寸的oplog,要意識到一旦重寫20次,磁盤可能就滿了。

    是以預設大小并非适用所有應用程式。如果知道應用程式寫入量會很大,在部署之前就應該做些測試。配置好複制,然後以生産環境的寫入量向主節點發起寫操作,向這樣對伺服器施壓起碼一小時,完成後,連接配接到任意副本內建員上,擷取目前複制資訊:

db.getReplicationInfo()

    一旦了解了每小時會生成多少oplog,就能決定配置設定多少oplog空間了。你應該為從節點至少下線八小時做好準備。發生網絡故障或類似事件時,要避免任意節點重新同步完整資料,增加oplog的大小能夠為你争取更多時間。如果要修改預設的oplog的大小,必須每個成員節點首次啟動時使用mongod的-oplogSize選項,其值的機關是兆。可以像下面這樣啟動一個1GB oplog的mongod執行個體

mongod -replSet mySet -oplogSize 1024
           

4. "心跳"檢測和故障轉移

      副本集的“心跳”檢測有助于選舉和故障轉移。預設情況下,每個副本內建員每兩秒鐘ping一次其他所有成員。這樣一來,系統就可以弄清楚自己的健康狀況。在運作rs.status()時,你可以看到每個節點上次“心跳”檢測的時間戳和健康狀況(1表示健康,0表示沒有應答)

     隻要每個節點都保持健康且有應答,副本集就能快樂地工作下去。但如果哪個節點失去了相應,副本集就會采取措施。每個副本集都希望确認無論何時都恰好存在一個主節點。但這僅在大多數節點可見時才有可能。例如:如果殺掉從節點,大部分節點依然存在,副本集不會改變狀态,隻是簡單地等待從節點重新上線;如果殺掉主節點,大部分節點依然存在,但沒有主節點了。是以從節點自動提升為主節點,如果碰巧有多個從節點,那麼會推選狀态最新的從節點為主節點。

     但還有其他可能的場景,如果從節點和仲裁節點都被殺掉了,隻剩下主節點,但是沒有多數節點--原來的三個節點裡隻有一個節點仍處于健康狀态。在這種情況下,主節點的日志中會有如下資訊 

Tus Aug 28 19:19:27 [rs Manager] replSet can't see a majority of the set relinquishing primary
           
Tus Aug 28 19:19:27 [rs Manager] replSet relinquishing primary state
           
Tus Aug 28 19:19:27 [rs Manager] replSet SECONDARY
           
   沒有了多數節點,主節點會把自己降級為從節點。剛開始有點費解,但仔細想想,如果該節點仍然作為主節點存在的話會發生什麼情況呢?如果出于某些網絡原因心跳檢測失敗了,那麼其他節點仍然是線上的。如果仲裁節點和從節點依然健在,并能看到對方,那麼根據多數節點原則,剩下的從節點會自動變為主節點。要是原來的主節點沒有降級,那麼就會陷入不堪一擊的局面:副本集中有兩個主節點。如果應用程式繼續運作,就可能對兩個不同的主節點做讀寫操作。肯定會有不一緻,并伴随着奇怪的現象。是以,當主節點看不到多數節點時,必須降級為從節點。 5. 送出與復原   關于副本集,還有最後一部分需要了解,那就是送出的概念。本質上,你可以一直向主節點做寫操作,但是那些寫操作在被複制到大多數節點前,都不會被認為是已送出的。這裡說的已送出是什麼意思呢?舉例來說,你向主節點發起一系列寫操作,出于某些原因(連接配接問題、從節點為備份而下線、從節點有延遲等)沒有被複制到從節點。現在假設從節點突然被提升為主節點,你向新的主節點寫資料,而最終老的主節點再次上線,嘗試從新的主節點做複制。這裡的問題在于老的主節點裡有一系列寫操作并未出現在新的主節點的oplog中,這就會觸發復原。    在復原時,所有未複制到大多數節點的寫操作都會被撤銷。也就是說會将它們從從節點的oplog和它們所在的集合裡删掉。要是某個從節點登記了一條删除,那麼該節點會從其他副本裡找到被删除的文檔并進行恢複。删除集合以及更新文檔的情況也是一樣的。     相關節點資料路徑的rollback子目錄中儲存了被復原的寫操作。針對每個有復原寫操作的集合,會建立一個單獨的BSON檔案,檔案名裡包含了復原的時間。在需要恢複被復原的文檔時,可以用bsondump工具來檢視這些BSON檔案,并可以通過mongorestore手工進行恢複。     萬一你真的不得不恢複被復原的資料,你就會意識到應該避免這種情況。幸運的是,從某種程度上來說,這是可以辦到的。要是應用程式能夠容忍額外的寫延時,那麼就能用後面會介紹學習的寫關注,以此確定每次(也可能是每隔幾次)寫操作都能被複制到大多數節點上。使用寫關注,或者更通用一點,監控複制的延遲,能幫助你減輕甚至避免復原帶來的全部問題。