如果你的 MongoDB 是使用 replication,那你會需要知道什麼是 oplog
;如果你的 MongoDB 是只有單一個節點,那就暫時還不需要理解。
oplog 是用來同步主要節點與次節點資料用的。例如我們寫入了一筆資料,而這筆資料的異動資訊也會被寫入 oplog 中,次節點會主動去主節點的 oplog collection 內執行find
, getMore
拿到需要同步的資料,再透過後續抄寫機制回次節點,並告訴主節點最後同步時間。
MongoDB 同步機制中,來源不一定是主節點,例如這是我們預期的
但實際上可以是
這樣做的目的是減輕主節點的壓力。這個功能叫做 chainingAllowed
,在 rs.conf()
可以看到,預設為 true
。
各個次節點會以 heartbeat
方式確認互相彼此存活,以便進行資料同步。
首先你必須於本機建立 replica set,可以參考之前的文章來建立測試環境。
連上任一節點後輸入 rs.printReplicationInfo()
,預期會取得以下結果:
ith2021-rs:SECONDARY> rs.printReplicationInfo()
configured oplog size: 2300.437744140625MB
log length start to end: 6203secs (1.72hrs)
oplog first event time: Sun Sep 12 2021 16:08:52 GMT+0800 (CST)
oplog last event time: Sun Sep 12 2021 17:52:15 GMT+0800 (CST)
now: Sun Sep 12 2021 17:52:24 GMT+0800 (CST)
輸入以下指令: db.printSlaveReplicationInfo()
ith2021-rs:SECONDARY> db.printSlaveReplicationInfo()
source: mongo_node1:27666
syncedTo: Sun Sep 12 2021 17:54:25 GMT+0800 (CST)
0 secs (0 hrs) behind the primary
source: mongo_node2:27667
syncedTo: Sun Sep 12 2021 17:54:25 GMT+0800 (CST)
0 secs (0 hrs) behind the primary
0 secs (0 hrs) behind the primary
這段話的意思是跟主節點資料落差有多少秒,以上面的例子來說就是完全同步的意思。在後面我們會有一些測試方式來看數據的變化。
完整資訊是
ith2021-rs [direct: primary] local> db.getReplicationInfo()
{
logSizeMB: 130000.0999994278,
usedMB: 0.1,
timeDiff: 8474,
timeDiffHours: 2.35,
tFirst: 'Sun Sep 12 2021 16:08:52 GMT+0800 (台北標準時間)',
tLast: 'Sun Sep 12 2021 18:30:06 GMT+0800 (台北標準時間)',
now: 'Sun Sep 12 2021 18:30:14 GMT+0800 (台北標準時間)'
}
*Note: 主節點與次節點預設的 logSizeMB
是不同大小的
*Note2: db.getReplicationInfo()
, db.printReplicationInfo()
結果都是一樣的,只是顯示格式不同
第一個方法是於啟動的時候加上設定值,而單位是 MB
,如下:--oplogSize = 10
這樣就是設定為 10MB 大小。
第二個方法是啟動後的修改,輸入以下指令:db.adminCommand({replSetResizeOplog:1, size: 123456})
單位是 MB
修改比預設還小的值
ith2021-rs [direct: primary] local> db.getReplicationInfo().logSizeMB
6340
ith2021-rs [direct: primary] local> db.adminCommand({replSetResizeOplog:1, size: 1000.1})
{
ok: 1,
'$clusterTime': {
clusterTime: Timestamp({ t: 1631443194, i: 1 }),
signature: {
hash: Binary(Buffer.from("0000000000000000000000000000000000000000", "hex"), 0),
keyId: Long("0")
}
},
operationTime: Timestamp({ t: 1631443194, i: 1 })
}
ith2021-rs [direct: primary] local> db.getReplicationInfo().logSizeMB
6649
可以看到回傳雖然 ok 為 1,但並沒有任何改變。
修改比預設還大的值
ith2021-rs [direct: primary] local> db.adminCommand({replSetResizeOplog:1, size: 7899.1})
{
ok: 1,
'$clusterTime': {
clusterTime: Timestamp({ t: 1631443324, i: 1 }),
signature: {
hash: Binary(Buffer.from("0000000000000000000000000000000000000000", "hex"), 0),
keyId: Long("0")
}
},
operationTime: Timestamp({ t: 1631443324, i: 1 })
}
ith2021-rs [direct: primary] local> db.getReplicationInfo().logSizeMB
7899.099999427795
首先,oplog 滿了,會先把最舊的刪除,就像是 FIFO 的概念。
所謂的滿,有可能是容量滿,或是檔案數超過喔!
當 oplog 滿了,會觸發 full resync,強制讓次節點完全同步 oplog 內容後,才會從 recovering
狀態變回正常。
這篇文章將實際對 MongoDB 進行一些操作,接著查看 oplog 有何變化。
以下內容的操作步驟:
ith2021
ironman
{"op": "i"}
(細節後談)ith2021-rs [direct: primary] local> use ith2021
switched to db ith2021
ith2021-rs [direct: primary] ith2021> db.ironman.insertOne({field:'iThome 2021 Winner'})
{
acknowledged: true,
insertedId: ObjectId("613ddc90a3c50f67ffc384cd")
}
ith2021-rs [direct: primary] ith2021> use local
switched to db local
ith2021-rs [direct: primary] local> db.oplog.rs.find({"op":"i"})
[
{
lsid: {
id: UUID("84a171ba-6dbe-47b0-8187-6775dacd1281"),
uid: Binary(Buffer.from("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "hex"), 0)
},
txnNumber: Long("2"),
op: 'i',
ns: 'ith2021.ironman',
ui: UUID("c227fbe2-bae6-4ee1-8a7a-8ea32c99edd3"),
o: {
_id: ObjectId("613ddc90a3c50f67ffc384cd"),
field: 'iThome 2021 Winner'
},
ts: Timestamp({ t: 1631444112, i: 1 }),
t: Long("1"),
v: Long("2"),
wall: ISODate("2021-09-12T10:55:12.886Z"),
stmtId: 0,
prevOpTime: { ts: Timestamp({ t: 0, i: 0 }), t: Long("-1") }
}
]
從上面的操作步驟可以看到,我們寫入一筆資料後,oplog 也會有有一筆相同內容資訊的紀錄,這個就是讓次節點拿去同步的。上面的查詢條件 op
是代表 operation 的意思,MongoDb很常使用,應該是不陌生;i
則代表 insert
,還有以下操作:
這邊我們將第一個欄位更新(update)其內容,再查看 oplog:
ith2021-rs [direct: primary] ith2021> db.ironman.updateOne({_id:ObjectId("613ddc90a3c50f67ffc384cd")}, {$set:{"field":"iThome 2021 Winner - eplis"}})
ith2021-rs [direct: primary] local> db.oplog.rs.find({"op":"u"})
[
{
lsid: {
id: UUID("129cd660-1da1-4a2a-94ae-569d0b406ec3"),
uid: Binary(Buffer.from("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "hex"), 0)
},
txnNumber: Long("2"),
op: 'u',
ns: 'ith2021.ironman',
ui: UUID("c227fbe2-bae6-4ee1-8a7a-8ea32c99edd3"),
o: { '$v': 2, diff: { u: { field: 'iThome 2021 Winner - eplis' } } },
o2: { _id: ObjectId("613ddc90a3c50f67ffc384cd") },
ts: Timestamp({ t: 1631444513, i: 1 }),
t: Long("1"),
v: Long("2"),
wall: ISODate("2021-09-12T11:01:53.945Z"),
stmtId: 0,
prevOpTime: { ts: Timestamp({ t: 0, i: 0 }), t: Long("-1") }
}
]
再使用 Replace 語法去做更新,查看 oplog..
ith2021-rs [direct: primary] ith2021> db.ironman.replaceOne({_id: ObjectId("613ddc90a3c50f67ffc384cd")}, {field: 'iThome Winner', year: 2021, winner: 'eplis'})
ith2021-rs [direct: primary] local> db.oplog.rs.find({"op":"u"})
[
{
lsid: {
id: UUID("129cd660-1da1-4a2a-94ae-569d0b406ec3"),
uid: Binary(Buffer.from("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "hex"), 0)
},
txnNumber: Long("3"),
op: 'u',
ns: 'ith2021.ironman',
ui: UUID("c227fbe2-bae6-4ee1-8a7a-8ea32c99edd3"),
o: {
_id: ObjectId("613ddc90a3c50f67ffc384cd"),
field: 'iThome Winner',
year: 2021,
winner: 'eplis'
},
o2: { _id: ObjectId("613ddc90a3c50f67ffc384cd") },
ts: Timestamp({ t: 1631444709, i: 1 }),
t: Long("1"),
v: Long("2"),
wall: ISODate("2021-09-12T11:05:09.177Z"),
stmtId: 0,
prevOpTime: { ts: Timestamp({ t: 0, i: 0 }), t: Long("-1") }
}
]
可以看到同樣屬於 update 操作,但是魔鬼藏在細節裡!!
update
只會存修改的欄位replace
整份文件都會寫入 oplog確實很合理,但是如果是大量使用 replace 或是一個文件大小很大的話,會造成 oplog 大量被占用記憶體,進而導致縮短可用時間,再繼續下去可能就會觸發次節點 full resync。所以使用上一定要特別斟酌是否需要 replace。
本系列文章會同步發表於我個人的部落格 Pie Note