2021 iThome 鐵人賽 MongoDB披荊斬棘之路
DAY19 MongoDB Oplog 到底是什麼?
oplog 是什麼?
如果你的 MongoDB 是使用 replication,那你會需要知道什麼是 oplog
;如果你的 MongoDB 是只有單一個節點,那就暫時還不需要理解。
oplog 是用來同步主要節點與次節點資料用的。例如我們寫入了一筆資料,而這筆資料的異動資訊也會被寫入 oplog 中,次節點會主動去主節點的 oplog collection 內執行find
, getMore
拿到需要同步的資料,再透過後續抄寫機制回次節點,並告訴主節點最後同步時間。
ChainingAllowed
MongoDB 同步機制中,來源不一定是主節點,例如這是我們預期的
- 主 -> 次1
- 主 -> 次2
但實際上可以是
- 主 -> 次1
- 次1 -> 次2
這樣做的目的是減輕主節點的壓力。這個功能叫做 chainingAllowed
,在 rs.conf()
可以看到,預設為 true
。
各個次節點會以 heartbeat
方式確認互相彼此存活,以便進行資料同步。
oplog 怎麼查看
首先你必須於本機建立 replica set,可以參考之前的文章來建立測試環境。
連上任一節點後輸入 rs.printReplicationInfo()
,預期會取得以下結果:
1 2 3 4 5 6 7
| 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)
|
- oplog size: 可以參閱官網,預設是硬碟的 5%; macOS 則是 192MB。這些都可以再調整。
- log length start to end: 按照目前增長速度,oplog 甚麼時候會被寫滿,以上面的例子就是 1.72 hrs 後就會滿了。這個值僅供參考,因為寫入量隨時都在改變,要特別注意尖峰時刻就是了。
查看次節點的同步狀況
輸入以下指令: db.printSlaveReplicationInfo()
1 2 3 4 5 6 7
| 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
這段話的意思是跟主節點資料落差有多少秒,以上面的例子來說就是完全同步的意思。在後面我們會有一些測試方式來看數據的變化。
oplog
查看 oplog 容量
完整資訊是
1 2 3 4 5 6 7 8 9 10
| 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 (台北標準時間)' }
|
- logSizeMB 就是該節點目前預先 allocate 的容量
- usedMB 是目前使用的
Note: 主節點與次節點預設的 logSizeMB
是不同大小的
Note2: db.getReplicationInfo()
, db.printReplicationInfo()
結果都是一樣的,只是顯示格式不同
修改 oplog 大小
第一個方法是於啟動的時候加上設定值,而單位是 MB
,如下:
--oplogSize = 10
這樣就是設定為 10MB 大小。
第二個方法是啟動後的修改,輸入以下指令:
db.adminCommand({replSetResizeOplog:1, size: 123456})
單位是 MB
修改比預設還小的值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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,但並沒有任何改變。
修改比預設還大的值
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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 滿了會發生什麼事
首先,oplog 滿了,會先把最舊的刪除,就像是 FIFO 的概念。
所謂的滿,有可能是容量滿,或是檔案數超過喔!
當 oplog 滿了,會觸發 full resync,強制讓次節點完全同步 oplog 內容後,才會從 recovering
狀態變回正常。
oplog 長什麼樣子?
這篇文章將實際對 MongoDB 進行一些操作,接著查看 oplog 有何變化。
以下內容的操作步驟:
- 使用
ith2021
- 直接寫入一筆資料進
ironman
- 查看 oplog.rs,條件為
{"op": "i"}
(細節後談)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| 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
,還有以下操作:
- “i”:insert
- “u”:update
- “d”:delete
- “c”:資料庫相關指令
- “n”:no op,從 msg 可以得知是定期執行的指令,確保其運作。
使用 Update 更新
這邊我們將第一個欄位更新(update)其內容,再查看 oplog:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| 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 更新
再使用 Replace 語法去做更新,查看 oplog..
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| 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。