MongoDB復制集由一組MongoDB實例(進程)組成,包含一個Primary節點和多個Secondary節點,MongoDB Driver(客戶端)的所有數據都寫入Primary,Secondary從Primary同步寫入的數據,以保持復制集內所有成員存儲相同的數據集,提供數據的高可用。
下圖(圖片源于MongoDB官方文檔)是一個典型的MongoDB復制集,包含一個Primary節點和兩個Secondary節點。
Primary選舉(一)
復制集通過replSetInitiate
命令(或mongoshell的rs.initiate()
)進行初始化,初始化后各個成員間開始發送心跳消息,并發起Primary選舉操作,獲得大多數成員投票支持的節點,會成為Primary,其余節點成為Secondary。
初始化復制集
config = {
_id : "my_replica_set",
members : [
{_id : 0, host : "rs1.example.net:27017"},
{_id : 1, host : "rs2.example.net:27017"},
{_id : 2, host : "rs3.example.net:27017"},
]
}
rs.initiate(config)
“大多數”的定義
假設復制集內投票成員(后續介紹)數量為N,則大多數為N/2 + 1,當復制集內存活成員數量不足大多數時,整個復制集將無法選舉出Primary,復制集將無法提供寫服務,處于只讀狀態。
投票成員數 | 大多數 | 容忍失效數 |
1 | 1 | 0 |
2 | 2 | 0 |
3 | 2 | 1 |
4 | 3 | 1 |
5 | 3 | 2 |
6 | 4 | 2 |
7 | 4 | 3 |
通常建議將復制集成員數量設置為奇數,從上表可以看出3個節點和4個節點的復制集都只能容忍1個節點失效,從服務可用性的角度看,其效果是一樣的,但無疑4個節點能提供更可靠的數據存儲。
特殊的Secondary節點
正常情況下,復制集的Secondary會參與Primary選舉(自身也可能會被選為Primary),并從Primary同步最新寫入的數據,以保證與Primary存儲相同的數據。
Secondary可以提供讀服務,增加Secondary節點可以提供復制集的讀服務能力,同時提升復制集的可用性。另外,MongoDB支持對復制集的Secondary節點進行靈活的配置,以適應多種場景的需求。
Arbiter
Arbiter節點只參與投票,不能被選為Primary,并且不從Primary同步數據。
比如你部署了一個2個節點的復制集,1個Primary,1個Secondary,任意節點宕機,復制集將不能提供服務了(無法選出Primary),這時可以給復制集添加一個Arbiter節點,即使有節點宕機,仍能選出Primary。
Arbiter本身不存儲數據,是非常輕量級的服務,當復制集成員為偶數時,最好加入一個Arbiter節點,以提升復制集可用性。
Priority0
Priority0節點的選舉優先級為0,不會被選舉為Primary。
比如你跨機房A、B部署了一個復制集,并且想指定Primary必須在A機房,這時可以將B機房的復制集成員Priority設置為0,這樣Primary就一定會是A機房的成員。
說明如果這樣部署,最好將大多數節點部署在A機房,否則網絡分區時可能無法選出Primary。
Vote0
MongoDB 3.0里,復制集成員最多50個,參與Primary選舉投票的成員最多7個,其他成員(Vote0)的vote屬性必須設置為0,即不參與投票。
Hidden
Hidden節點不能被選為主(Priority為0),并且對Driver不可見。
因Hidden節點不會接受Driver的請求,可使用Hidden節點做一些數據備份、離線計算的任務,不會影響復制集的服務。
Delayed
Delayed節點必須是Hidden節點,并且其數據落后于Primary一段時間(可配置,比如1個小時)。
因Delayed節點的數據比Primary落后一段時間,當錯誤或者無效的數據寫入Primary時,可通過Delayed節點的數據來恢復到之前的時間點。
Primary選舉 (二)
Primary選舉除了在復制集初始化時發生,還有如下場景:
復制集被reconfig
Secondary節點檢測到Primary宕機時,會觸發新Primary的選舉,當有Primary節點主動StepDown(主動降級為Secondary)時,也會觸發新的Primary選舉。Primary的選舉受節點間心跳、優先級、最新的oplog時間等多種因素影響。
節點優先級
每個節點都傾向于投票給優先級最高的節點。優先級為0的節點不會主動發起Primary選舉。當Primary發現有優先級更高Secondary,并且該Secondary的數據落后在10秒內,則Primary會主動降級,讓優先級更高的Secondary有成為Primary的機會。
Optime
擁有最新optime(最近一條oplog的時間戳)的節點才能被選為Primary。
網絡分區
只有在大多數投票節點間保持網絡連通,才有機會被選Primary;如果Primary與大多數的節點斷開連接,Primary會主動降級為Secondary。當發生網絡分區時,可能在短時間內出現多個Primary,所以Driver在寫入時,最好設置大多數成功的策略,這樣即使出現多個Primary,也只有一個Primary能成功寫入大多數。
數據同步
Primary與Secondary之間通過oplog來同步數據,Primary上的寫操作完成后,會向特殊的local.oplog.rs集合寫入一條oplog,Secondary不斷的從Primary獲取新的oplog并應用。
因oplog的數據會不斷增加,local.oplog.rs被設置成為一個capped集合,當容量達到配置上限時,會將最舊的數據刪除掉。另外考慮到oplog在Secondary上可能重復應用,oplog必須具有冪等性,即重復應用也會得到相同的結果。
如下oplog的格式,包含ts、h、op、ns、o等字段。
{
"ts" : Timestamp(1446011584, 2),
"h" : NumberLong("1687359108795812092"),
"v" : 2,
"op" : "i",
"ns" : "test.nosql",
"o" : { "_id" : ObjectId("563062c0b085733f34ab4129"), "name" : "mongodb", "score" : "100" }
}
字段說明如下:
ts:操作時間,當前timestamp + 計數器,計數器每秒都被重置。
h:操作的全局唯一標識。
v:oplog版本信息。
op:操作類型,取值說明:
i:插入操作。
u:更新操作。
d:刪除操作。
c:執行命令(如createDatabase,dropDatabase)。
n:空操作,特殊用途。
ns:操作針對的集合。
o:操作內容。
o2:操作查詢條件,僅update操作包含該字段。
Secondary初次同步數據時,會先執行init sync
,從Primary(或其他數據更新的Secondary)同步全量數據,然后不斷通過執行tailable cursor
從Primary的local.oplog.rs集合里查詢最新的oplog并應用到自身。
init sync過程:
T1時間,從Primary同步所有數據庫的數據(local除外),通過
listDatabases
+listCollections
+cloneCollection
命令組合完成,假設T2時間完成所有操作。從Primary應用T1到T2時間段內的所有oplog,可能部分操作已經包含在步驟1,但由于oplog的冪等性,可重復應用。
根據Primary各集合的index設置,在Secondary上為相應集合創建index。(每個集合_id的index已在步驟1中完成)。
說明oplog集合的大小應根據DB規模及應用寫入需求合理配置,配置得太大,會造成存儲空間的浪費;配置得太小,可能造成Secondary的init sync一直無法成功。比如在步驟1里由于DB數據太多、并且oplog配置太小,導致oplog不足以存儲T1和T2時間內的所有oplog,這就使得Secondary無法從Primary上同步完整的數據集。
修改復制集配置
當需要修改復制集時,比如增加成員、刪除成員、或者修改成員配置(如priority、vote、hidden、delayed等屬性),可通過replSetReconfig
命令(rs.reconfig()
)對復制集進行重新配置。
比如將復制集的第2個成員的priority屬性設置為2,可執行如下命令:
cfg = rs.conf();
cfg.members[1].priority = 2;
rs.reconfig(cfg);
異常處理(rollback)
當Primary宕機時,如果有數據未同步到Secondary,并且在Primary重新加入時,新的Primary上已經發生了寫操作,則舊Primary需要回滾部分操作,以保證數據集與新的Primary一致。
舊Primary將回滾的數據寫到單獨的rollback目錄下,數據庫管理員可根據需要使用mongorestore
進行恢復。
復制集的讀寫設置
Read Preference
默認情況下,復制集的所有讀請求都發到Primary,Driver可通過設置Read Preference來將讀請求路由到其他的節點。
primary:默認規則,所有讀請求發到Primary。
primaryPreferred:Primary優先,如果Primary不可達,請求Secondary。
secondary:所有的讀請求都發到Secondary。
secondaryPreferred:Secondary優先,當所有Secondary不可達時,請求Primary。
nearest:讀請求發送到最近的可達節點上(通過
ping
探測得出最近的節點)。
Write Concern
默認情況下,Primary完成寫操作即返回,Driver可通過配置Write Concern來設置寫成功的規則,詳情請參見Write Concern。
如下的write concern規則設置寫必須在大多數節點上成功,超時時間為5秒。
db.products.insert( { item: "envelopes", qty : 100, type: "Clasp" }, { writeConcern: { w: "majority", wtimeout: 5000 } } )
上面的設置方式是針對單個請求的,也可以修改副本集默認的write concern,這樣就不用單獨設置每個請求。
cfg = rs.conf() cfg.settings = {} cfg.settings.getLastErrorDefaults = { w: "majority", wtimeout: 5000 } rs.reconfig(cfg)