1. 程式人生 > >mongodb如何判斷一次操作是否成功

mongodb如何判斷一次操作是否成功

MongoDB核心開發工程師 Kristina Chodorow(@kchodorow) 最近在她的部落格上表示,她會發表一系列關於MongoDB Replica Sets相關的文章,向大家詳細的進行一次 Replica Sets的知識培訓。其系列文章內容包括下面一些章節:

  • Creating a set(建立一個replica sets)

  • Initial Sync(初始化同步)

  • Rollback(資料回滾)

  • Authentication(許可權控制)

  • Debugging(故障排除)

本文主要對Replica Sets節點間的同步機制和同步協議進行了深入講解。

同步過程

一個健康的secondary在執行時,會選擇一個離自己最近的,資料比自己新的節點進行資料同步。選定節點後,它會從這個節點拉取oplog同步日誌,具體流程是這樣的:

  1. 執行這個op日誌

  2. 將這個op日誌寫入到自己的oplog中(local.oplog.rs)

  3. 再請求下一個op日誌

如果同步操作在第1步和第2步之間出現問題宕機,那麼secondary再重新恢復後,會檢查自己這邊最新的oplog,由於第2步還沒有執行,所以自己這邊還沒有這條寫操作的日誌。這時候他會再把剛才執行過的那個操作執行一次。那對同一個寫操作執行兩次會不會有問題呢?MongoDB在設計oplog時就考慮到了這一點,所以所有的oplog都是可以重複執行的,比如你執行 {$inc:{counter:1}} 對counter欄位加1,counter欄位在加1 後值為2,那麼在oplog裡並不會記錄 {$inc:{counter:1}} 這個操作,而是記錄 {$set:{counter:2}}這個操作。所以無論多少次執行同一個寫操作,都不會出現問題。

w引數

當我們在MongoDB時執行一個寫操作時,預設會直接返回成功,同時也可以通過設定w引數,指定這個寫操作同步到幾個節點後才返回成功。如下:

db.foo.runCommand({getLastError:1, w:2})

上面例子就是執行getLastError命令,使其在上一個寫操作同步到兩個節點上後再返回。不同的客戶端可能在寫法上不太一樣,不過這個功能應該都是有的。對於重要資料,可以考慮採用這樣的方式,通過犧牲一部分寫效能來提升資料的安全性。

這個功能是如何實現的呢,primary節點是如何知道資料同步了幾份呢?

在呼叫上面命令時,實際上MongoDB內部執行了如下的一些流程:

  1. 在primary上完成寫操作

  2. 在primary上記錄一條oplog日誌,日誌中包含一個ts欄位,值為寫操作執行的時間,比如本例中記為t

  3. 客戶端呼叫{getLastError:1, w:2}命令等待primary返回結果

  4. secondary從primary拉取oplog,獲取到剛才那一次寫操作的日誌

  5. secondary按獲取到的日誌執行相應的寫操作

  6. 執行完成後,secondary再獲取新的日誌,其向primary上拉取oplog的條件為{ts:{$gt:t}}

  7. primary此時收到secondary的請求,瞭解到secondary在請求時間大於t的寫操作日誌,所以他知道操作在t之前的日誌都已經成功執行了

  8. 這時候getLastError命令檢測到primary與secondary都完成了這次寫操作,於是 w:2 的條件滿足了,返回給客戶端成功

啟動初始化

當一個新節點啟動並加入到現在的Replica Sets中時,這時候新啟動的節點會檢視自己的oplog,通過一個叫 lastOpTimeWritten 的命令查詢到它最近的一條寫操作。這個命令你也可以隨便在命令列執行:

> rs.debug.getLastOpWritten()

這個命令會返回一條oplog記錄,其中的ts欄位就是最近一次寫操作的時間了。

如果你這個節點是全新的,沒有資料,那麼oplog裡也沒有資料,這時候節點會選擇執行一次全量的同步。本文暫時不對全量同步的方法進行描述。

選擇同步源節點

Replica Sets中的節點之間總在同步資料,但是他們不是通過傳統的一主多從的方式來同步的。MongoDB的策略是選擇一個合適的節點作為資料來源。

首先secondary節點會通過ping的時間來確定其它節點與它的距離。時間越長的識為距離越遠。然後通過下面方法確定其源節點:

for each member that is healthy: if member[state] == PRIMARY add to set of possible sync targets if member[lastOpTimeWritten] > our[lastOpTimeWritten] add to set of possible sync targetssync target = member with the min ping time from the possible sync targets

對於節點是否healthy的判斷,各個版本不同,但是其目的都是找出正常運轉的節點。在2.0版本中,它的判斷還包括了salve delay這個因素。

你可以通過執行db.adminCommand({replSetGetStatus:1})命令來檢視當前的節點狀況,在secondary上執行這個命令,你能看到syncingTo這個欄位,這個欄位的值就是這個secondary的同步源。(其實名字應該是叫syncingFrom,但是由於版本相容的原因,沿用了這個錯誤的名字)

鏈式同步結構

上面對w引數的實現,講解上比較簡單,只講了w為2的情況,但是當w更大時,由於我們並不是採用一主多從的方式進行同步。所以情況會複雜一些。

比如我們有節點A,為primary節點,然後B節點為secondary節點,它從A節點同步資料,同時又有secondary節點C,它從同是secondary的B節點同步資料。這樣A->B->C之間就形成了一個鏈式的同步結構。如果我們設定w為3,那麼A節點如何能知道C節點已經從B節點同步成功了呢?

這是通過oplog同步協議來實現的,我們用通俗的語言來解釋一下oplog的同步協議。

  • 當C從B同步資料時,C會在協議中對B說:我要從你這同步資料了,如果寫操作有w引數的話,我的同步也算上吧。

  • 然後B會回答說:我不是一個primary節點,我會把你的這個計數轉到我的同步源上去。

  • 然後B再對A開啟一個新的連線,並且對A說:這個連線你就當成是C的吧,也算一個計數在w裡。

  • 這時候在A看來,就有兩個連線連到他上面,一個是B,一個是虛擬的C,這兩個連線都能報告他說完成了同步操作。

當一個寫操作在A上執行後,B首先同步到這個操作的oplog,執行完後會告訴A,我執行完了。然後C同樣從B上獲取到B的oplog,也執行了這一條寫操作,然後他告訴B,我執行完了,B在收到這個響應後,會通過剛才開通的虛擬通道跟A說,我是虛擬的C節點,我也完成寫操作了。這時候A就知道,A、B、C三個節點都完成寫操作了。w:3的條件滿足,然後返回給呼叫getLastError的客戶端,完成這次操作。

具體三個節點間的連線如下圖:

C B A <====> <====> <---->

B和A之間有兩條通道,雙線那條是真正的同步連線,單線那條是一個虛擬連線。

新功能展望

上面就是當前的Replica Sets同步的內部實現,在後續這一塊還會進行一些新特性的開發。在2.2版本中,我們會提供replSetSyncFrom命令,讓使用者可以手動設定一個secondary的同步源。使用方法大概是這樣:

> db.adminCommand({replSetSyncFrom:"otherHost:27017"})