2021 iThome 鐵人賽 - DAY19 MongoDB Oplog 到底是什麼?

2021 iThome 鐵人賽 MongoDB披荊斬棘之路


DAY19 MongoDB Oplog 到底是什麼?

oplog 是什麼?

如果你的 MongoDB 是使用 replication,那你會需要知道什麼是 oplog;如果你的 MongoDB 是只有單一個節點,那就暫時還不需要理解。

oplog 是用來同步主要節點與次節點資料用的。例如我們寫入了一筆資料,而這筆資料的異動資訊也會被寫入 oplog 中,次節點會主動去主節點的 oplog collection 內執行find, getMore 拿到需要同步的資料,再透過後續抄寫機制回次節點,並告訴主節點最後同步時間。

ChainingAllowed

MongoDB 同步機制中,來源不一定是主節點,例如這是我們預期的

  1. 主 -> 次1
  2. 主 -> 次2

但實際上可以是

  1. 主 -> 次1
  2. 次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 有何變化。

以下內容的操作步驟:

  1. 使用 ith2021
  2. 直接寫入一筆資料進 ironman
  3. 查看 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。

  • 作者: MingYi Chou
  • 版權聲明: 轉載不用問,但請註明出處!本網誌均採用 BY-NC-SA 許可協議。