WiredTiger — cache, MVCC, eviction, checkpoint và journal: một lần ghi đi qua những đâu
Một client ghi w:1 liên tục, rồi docker kill mongod: 9–302 write đã được trả "OK" biến mất sau khi khởi động lại (9 lần kill, không lần nào 0). Cùng tải với j:true hoặc
w:"majority": 0 mất. 32 client update ngẫu nhiên làm findOne của một collection không liên quan chậm từ p99 32 ms lên 75 ms — tăng cache từ 256 MB lên 1 GB không chữa được. Xoá nửa collection làm
file to thêm 52 %. Bài này đi theo một lần ghi qua từng tầng của WiredTiger (cache, update chain, eviction, checkpoint, journal), đo từng tầng, và giải thích các hiện tượng đã gặp ở
bài 1, 2, 3, 5. Transaction ở mức API để bài 8; write concern
nhiều node để bài 7.
Thuật ngữ dùng trong bài
page (trang — đơn vị đọc/ghi của B-tree) · update chain (các phiên bản của một key trong RAM, mới nhất đứng đầu) · MVCC (giữ nhiều phiên bản để người đọc không chặn người ghi) · snapshot (ảnh dữ liệu tại một thời điểm) · stable / oldest timestamp (mốc đã majority commit / mốc cũ nhất còn đọc được) · history store (fileWiredTigerHS.wtchứa phiên bản cũ) · dirty (trang đã sửa, chưa ghi đĩa) · eviction (đẩy trang ra khỏi cache) · reconcile (chuyển trang từ dạng RAM sang dạng đĩa) · checkpoint (ghi một bản nhất quán của mọi trang bẩn xuống đĩa) · journal / WAL (write-ahead log — ghi log trước để khôi phục sau crash) · copy-on-write (sửa thì viết bản mới ở chỗ khác, không ghi đè) · ticket (vé vào storage engine, giới hạn số thao tác chạy cùng lúc).
Kiểm chứng: Context7 (/mongodb/mongo,/websites/mongodb_manual); source nhánh v9.0 (README củastorage/,storage/wiredtiger/,repl/,admission/execution_control/;wiredtiger_kv_engine.cpp,wiredtiger_util.cpp,wiredtiger_global_options.idl); WiredTiger architecture guide vàconfig_def.c; manual + release notes 9.0. Output:labs/mongodb/out/06-*.txt. MongoDB 9.0.2, container--memory 1g, cache 0,25 GB, máy 4 vCPU dùng chung — số ms chỉ để so tỉ lệ.
Trả lời 30 giây
WiredTiger là B-tree có phiên bản: trang trong cache đã giải nén, mỗi key sửa có update chain mang timestamp (MVCC); trên đĩa là ảnh nén viết theo copy-on-write, không ghi đè. Một write: thêm phiên bản vào update chain → chép entry oplog vào log buffer → ack (w:1ack ngay lúc này) → journal fsync mỗi 100 ms (j:true/majoritychờ tới đây) → checkpoint 60 s ghi trang xuống file, bản cũ còn cần đọc sang history store. Trên replica set journal chỉ chứa oplog; crash = mở stable checkpoint + áp lại oplog. Cache mặc định max(50 % (RAM − 1 GB), 256 MB) theo cgroup; dirty chạm 20 % thì thread của request phải tự evict → p99 vọt, và tăng cache không chữa được nghẽn ghi. Snapshot/transaction dài ghim phiên bản cũ; transaction phải vừa cache (no-steal); xoá không trả đĩa (cầncompact).
1. Bài toán gốc và bối cảnh ra đời
MongoDB 1.x–2.x chỉ có một storage engine, sau này gọi là MMAPv1: file dữ liệu được mmap vào bộ nhớ, hệ điều hành tự quyết trang nào ở RAM, trang nào
xuống đĩa. Đơn giản, nhưng có ba giới hạn cứng
(manual MMAPv1, bản 3.4, FAQ concurrency 3.4):
- Khoá thô: trước 2.2 cả server một khoá ghi, 2.2 theo database, 3.0 theo collection — hai request sửa hai document khác nhau vẫn xếp hàng.
- Document to ra phải dời chỗ: chỗ cấp theo luỹ thừa của 2; vượt chỗ thì chuyển document đi và sửa mọi index trỏ tới nó.
- Không nén, không MVCC: không có snapshot nhất quán cho đọc dài, read concern majority hay transaction.
WiredTiger do Keith Bostic và Michael Cahill (hai kiến trúc sư của Berkeley DB / Sleepycat) viết thành một storage engine độc lập. MongoDB mua công ty WiredTiger ngày
16/12/2014 (press release), đưa vào 3.0 (2015) dạng tuỳ chọn qua pluggable storage
engine API, đặt làm mặc định từ 3.2, deprecated MMAPv1 ở 4.0 và xoá hẳn ở 4.2. 4.4 thay bảng tràn cache WiredTigerLAS.wt (lookaside) bằng
history store bền (WiredTigerHS.wt) (F. Pachot, 2025). Những thứ MongoDB có về sau
dựa thẳng vào nó: document-level concurrency, nén, read concern majority (đọc tại một timestamp), transaction nhiều document (bài 8).
Tối ưu cho: nhiều luồng cùng đọc/ghi các document khác nhau (cache gần như không khoá, xung đột phát hiện lạc quan), đọc theo snapshot, tiết kiệm đĩa nhờ nén. Không tối ưu cho: update nhỏ ngẫu nhiên trên dữ liệu lớn hơn RAM (ghi trang là ghi cả trang — lab đo 34–43 KB ghi đĩa cho một update vài byte), transaction lớn (phải nằm trọn trong cache tới lúc commit), trả lại dung lượng sau khi xoá, và document nóng chịu hàng nghìn lần ghi mỗi giây.
2. Mô hình tư duy cốt lõi
Hai ý trung tâm:
- B-tree có phiên bản trong RAM, ảnh copy-on-write trên đĩa. Mỗi collection và mỗi index là một bảng B-tree key → value (bài 3). Trang trong cache là bản làm việc: đã giải nén, có mảng để binary search, và mỗi key bị sửa có một update chain (danh sách phiên bản, mới nhất đứng đầu, mỗi phiên bản mang transaction id + timestamp). Trang trên đĩa chỉ là ảnh nén, được viết ra ở chỗ mới mỗi khi trang được reconcile (chuyển từ dạng RAM sang dạng đĩa) — không bao giờ ghi đè khối cũ.
- Checkpoint là bản nhất quán trên đĩa; journal lấp khoảng giữa hai checkpoint. Mỗi 60 s, checkpoint ghi mọi trang bẩn thành một snapshot nhất quán tại stable timestamp. Thay đổi sau checkpoint chỉ sống qua crash nếu nằm trong journal (WAL). Trên replica set, journal chỉ chứa oplog — dữ liệu collection được dựng lại bằng cách áp lại oplog từ checkpoint.
Các hiện tượng ở bài trước đều là hệ quả của hai ý này:
| Hiện tượng | Vì sao |
|---|---|
| Tên field dài tốn RAM dù đĩa nén gần hết (bài 1), index 9,4 MB chiếm 25,7 MB cache (bài 3) | Cache giữ trang đã giải nén; nén chỉ có trên đĩa |
| History store +80 MB khi sửa mảng to (bài 2) | Phiên bản cũ phải giữ 300 s cho đọc snapshot; trang được reconcile thì bản cũ chuyển sang WiredTigerHS.wt |
| Xoá dữ liệu mà file không nhỏ đi | Copy-on-write: khối trống chỉ vào danh sách "dùng lại được"; file chỉ co khi khối trống nằm ở cuối |
| p99 vọt khi ghi nhiều | Trang bẩn phải reconcile + ghi đĩa rồi mới bỏ khỏi RAM; quá ngưỡng thì chính thread của request phải làm |
| Transaction lớn bị huỷ, transaction dài làm cache nặng | Update chưa commit không evict được; snapshot cũ ghim mọi phiên bản mới hơn nó |
Ghi w:1 đã ack vẫn mất khi crash | Ack trả về khi entry oplog còn nằm trong log buffer ở RAM; journal fsync mỗi 100 ms |
3. Internals và quyết định thiết kế
3.1 Cache: trang trong RAM không phải trang trên đĩa
Đọc một document mà trang chưa có trong cache: WiredTiger đi từ root page của B-tree xuống, gặp trang chưa nạp thì nhờ block manager đọc khối từ file, kiểm checksum, giải nén, rồi dựng mảng chỉ mục cho từng key để binary search. Lần đầu trang bị sửa, nó được gắn thêm cấu trúc theo dõi thay đổi: mỗi key có một update chain, key mới chèn vào skip list nằm giữa các key cũ (arch-cache). Hệ quả:
- Cache tính theo byte chưa nén cộng phần phụ: lab nén ở mục 3.7 — cùng 174 MB BSON, file 32–186 MB tuỳ thuật toán, trong cache luôn ~185 MB.
- Cache chỉ tính dữ liệu B-tree. Data handle, cursor, session, buffer đọc/ghi tạm, heap của truy vấn (bài 5) nằm ngoài cache — RSS luôn lớn hơn cache, và evict xong thì bộ nhớ trả về allocator chứ RSS không giảm.
Kích thước mặc định = max(50 % × (RAM − 1 GB), 256 MB) (WiredTigerUtil::getMainCacheSizeMB), "RAM" là RAM mongod thấy — giới hạn cgroup nếu có
(06-cachedef.txt, mongod rỗng, không truyền --wiredTigerCacheSizeGB):
| Container | memLimitMB mongod thấy | cache |
|---|---|---|
9.0.2 --memory 1g | 1 024 | 256 MB (sàn) |
9.0.2 và 8.0.32 --memory 2g | 2 048 | 512 MB |
9.0.2 --memory 4g | 4 096 | 1 536 MB |
| 9.0.2 không giới hạn (máy 7,9 GB) | 7 941 | 3 458 MB |
Bẫy: manual vẫn ghi "chạy trong container thì phải đặt--wiredTigerCacheSizeGB" — có cgroup limit thì 8.0/9.0 tự tính đúng. Nguy hiểm là container không có--memory: mỗi mongod lấy nửa RAM của cả máy (3,4 GB ở đây). Cache cũng chỉ là một phần:--memory 1g+ cache 256 MB vẫn bị OOM-kill ở bài 5 vì heap truy vấn. Cache > 80 % RAM chỉ bị cảnh báo lúc khởi động (log id 22300).
3.2 MVCC: update chain, timestamp và history store
Mỗi update tạo một phiên bản mới ở đầu update chain của key, mang transaction id (cho isolation giữa các storage transaction) và timestamp (MongoDB gắn timestamp của entry oplog). Đọc một key, WiredTiger đi theo thứ tự: update chain trong RAM → bản trên ảnh đĩa → history store, lấy bản đầu tiên mà snapshot của mình "thấy" (arch-transaction). MongoDB điều khiển hai mốc toàn cục:
- stable timestamp — mốc đã majority commit; checkpoint ghi đúng trạng thái tại mốc này (repl README).
- oldest timestamp = stable −
minSnapshotHistoryWindowInSeconds(300 s): không ai được đọc trước mốc này, nên phiên bản cũ hơn có thể bỏ. Transaction/snapshot đang mở ghim thêm mốc "oldest active read timestamp" của nó.
Khi trang bẩn được reconcile, bản mới nhất đã commit thành giá trị trên đĩa; các bản cũ hơn mà còn có thể được đọc đi vào WiredTigerHS.wt — một B-tree duy nhất cho mọi bảng, key =
(id của B-tree, key gốc, start timestamp, bộ đếm), value = (stop timestamp, durable timestamp, kiểu, giá trị) (arch-hs). Lab
06-mvcc.txt: 20 000 document ~1 KB, 4 worker $set một chuỗi 1 000 ký tự mới (~1 700 update/s), node mới:
out/06-mvcc.txt). Cửa sổ 300 s mặc định giữ mọi bản cũ 5 phút; cửa sổ 0 thì history store gần như đứng yên —
cho tới khi một transaction snapshot mở và để đó.- Cửa sổ 300 s mặc định: mọi bản cũ phải giữ 5 phút — ~3 200 lần ghi vào history store mỗi giây, file
WiredTigerHS.wt0 → 87 MB sau 40 s. Đây là nguồn của "+80 MB" ở bài 2: không cần reader nào, chỉ cần cửa sổ. - Cửa sổ 0: còn ~160/s — bản cũ không ai cần thì reconcile bỏ luôn.
- Cửa sổ 0 + một transaction
readConcern: "snapshot"đọc 1 document rồi để đó: lại ~3 600/s, "ghim bởi reader cũ nhất" tăng 4 → 64 s, file history store 91 → 243 MB. Transaction bị reaper huỷ sau ~65–69 s (giới hạn 60 s, reaper chạy mỗi 30 s); ngay sau đó còn ~120/s. File không co lại — khối trống được dùng lại về sau.
3.3 Eviction: ai dọn cache, và khi nào thread của request phải tự dọn
Eviction (đẩy trang ra khỏi cache) do một eviction server dò từng B-tree, chọn khoảng 1/3 trang lâu không dùng nhất (LRU xấp xỉ) cho vào hàng đợi, và 4 eviction worker thread
(MongoDB truyền eviction=(threads_min=4,threads_max=4), 06-params.txt) lấy ra xử lý. Trang sạch thì chỉ việc free. Trang bẩn phải reconcile: chọn phiên bản mới nhất đã commit
làm giá trị trên đĩa, đưa các phiên bản cũ còn cần đọc vào history store, nén, ghi khối mới (arch-eviction). Ngưỡng đọc từ thống kê WiredTiger của node lab:
| Ngưỡng | % cache | Ở cache 256 MB | Vượt ngưỡng thì |
|---|---|---|---|
eviction_target / eviction_trigger | 80 / 95 | 204,8 / 243,2 MB | worker bắt đầu dọn / thread của request bị kéo vào dọn |
eviction_dirty_target / _trigger | 5 / 20 | 12,8 / 51,2 MB | như trên, chỉ tính trang bẩn |
eviction_updates_target / _trigger | 2,5 / 10 | 6,4 / 25,6 MB | như trên, tính byte của update chain |
Lab 06-evict.txt: client updateOne ngẫu nhiên (w:1) lên 2 triệu đơn (682 MB BSON, gấp 2,7 lần cache), song song probe findOne theo _id trên
shop.products (nhỏ, luôn trong cache) và probe insertOne vào collection khác, 50 ms một lần; mỗi biến thể sau docker restart, tải 60 s:
| Biến thể | update/s | ghi đĩa / update | thread request tự evict | probe đọc p50 / p99 / max | probe ghi p99 |
|---|---|---|---|---|---|
| pha nghỉ (cả 3 lượt) | — | — | 0 | 2,8–4,0 / 17–41 / 77–119 ms | 18–26 ms |
| cache 0,25 GB, 32 client | 2 285 | 37,4 KB | 231 s | 8,7 / 74,8 / 347 ms | 92,5 ms |
| cache 0,25 GB, 4 client | 1 746 | 43,2 KB | 0,5 s | 2,9 / 45,6 / 304 ms | 32,6 ms |
| cache 1 GB (container 2 GB), 32 client | 2 193 | 34,2 KB | 132 s | 10,6 / 156,6 / 936 ms | 212,1 ms |
out/06-evict.txt, lượt đầu). Dirty chạm eviction_dirty_trigger 20 % thì 4 eviction worker không ghi kịp: thread phục vụ request bị kéo đi
reconcile + ghi trang (cột đỏ). Probe findOne trên collection khác, nằm sẵn trong cache, vẫn chậm theo: p99 32 → 75 ms, max 347 ms (máy dùng chung — so tỉ lệ).- Nghẽn ở băng thông ghi đĩa, không phải RAM. Một update vài byte (
$inc+$setngày) làm bẩn cả trang lá; reconcile ghi lại cả trang vào khối mới: 34–43 KB ghi đĩa mỗi update (~85 MB/s). Cache gấp 4 không giúp: ngưỡng dirty là phần trăm cache nên cache 1 GB vẫn chạm 20 % (ở 205 MB thay vì 51 MB), p99 còn tệ hơn. - Thêm client chỉ dời hàng đợi vào storage engine: 4 client đạt 76 % throughput của 32 client, gần như không thread request nào phải evict.
- Truy vấn không liên quan cũng chậm: thread nào cần chỗ trong cache đều có thể bị bắt evict hoặc phải chờ ("application thread time waiting for cache" cộng dồn 358 s trong 60 s),
thêm tranh CPU và đĩa —
findOnetrên trang nằm sẵn trong cache chậm 2–4 lần.
3.4 Checkpoint và block manager: không bao giờ ghi đè
Checkpoint (mỗi syncdelay = 60 s) chạy trong một transaction snapshot tại stable timestamp (mốc đã majority commit — bài 7):
nhờ eviction giảm bớt trang bẩn, reconcile mọi trang bẩn, checkpoint history store sau cùng (reconcile dữ liệu sinh thêm bản cũ cho nó), fsync, rồi ghi metadata và file "turtle" trỏ tới checkpoint mới (arch-checkpoint). Block manager không ghi đè: mỗi file có danh sách extent
alloc / avail / discard, khối cũ chỉ được dùng lại khi checkpoint cũ tham chiếu nó bị xoá (arch-block).
- Được: crash giữa lúc ghi không hỏng checkpoint trước — không có "torn page" (trang ghi dở), không cần thứ như
full_page_writescủa PostgreSQL; checkpoint nhất quán nên backup bằng snapshot file được. - Mất: sửa vài byte là viết cả trang ở chỗ mới (mục 3.3); xoá xong file có lỗ và còn to ra (mục 5.5); cần
compactđể trả dung lượng. - Checkpoint đi theo stable timestamp: nếu majority commit point đứng yên (replica set PSA mất một node có dữ liệu), checkpoint không theo kịp dữ liệu mới, history store và lượng oplog phải áp lại khi khởi động cứ thế phình (manual: PSA; chưa dựng lab ở bài này — xem bài 7).
- Lab: checkpoint giữa lúc tải mất 913 ms (vạch xanh ở Hình 3).
3.5 Journal: WAL có group commit — và trên replica set nó chỉ chứa oplog
WiredTiger ghi log không cần khoá: transaction lúc commit giành một phần của log slot (vùng đệm trong RAM, tổng 32 MB) bằng phép toán atomic rồi chép bản ghi vào — chưa có syscall
nào. Slot được đẩy xuống file khi đầy, khi có ai yêu cầu flush, hoặc theo nhịp của thread log_server (docs ghi 50 ms; lab mục 5.1 cho thấy write vẫn nằm trong RAM tới lần flush của MongoDB) (arch-logging).
MongoDB không bật sync khi commit; thread JournalFlusher gọi log_flush(sync=on) mỗi commitIntervalMs = 100 ms, hoặc ngay khi có write j:true đang chờ — nhiều commit
chung một lần fsync (group commit). File log tối đa 100 MB, nén snappy, bản ghi ≤ 128 byte không nén (manual journaling).
Điểm hay bị hiểu sai: trên replica set, bảng của collection và index không được journal. Chỉ oplog (và vài bảng của DB local) đi vào journal; document lên đĩa nhờ
checkpoint, crash thì được dựng lại từ oplog (storage README). Lab 06-jbytes.txt, 20 000 insert (~270 byte) và 5 000 lần $inc:
| Byte journal mỗi thao tác | Replica set 1 node | Standalone (không --replSet) |
|---|---|---|
| insert, 0 index phụ | 273 | 266 |
| insert, 4 index phụ | 273 — không đổi | 304 — index cũng được journal |
$inc 1 field trên document 10 KB | 387 (entry oplog 314 byte: {"$v":2,"diff":{"u":{"n":5}}}) | 128 (WiredTiger chỉ log phần sửa) |
Vì vậy j:true trên replica set nghĩa là "entry oplog của write đã fsync", không phải "trang dữ liệu đã fsync" — đủ để dựng lại write sau crash. Và journal tỉ lệ với số oplog,
không với kích thước document hay số index.
3.6 Crash recovery: mở checkpoint, replay journal, áp lại oplog
sequenceDiagram participant M as mongod khởi động participant WT as WiredTiger participant R as replication recovery M->>WT: mở checkpoint gần nhất (recoveryTimestamp = stable timestamp lúc đó) WT->>WT: replay journal từ LSN của checkpoint, oplog đầy tới lần fsync cuối M->>R: Recovering from stable timestamp, topOfOplog R->>R: cắt oplog ở oplogTruncateAfterPoint (bỏ lỗ ở cuối) R->>WT: áp lại entry oplog từ stable timestamp tới đỉnh oplog vào collection và index R-->>M: Completed oplog application for recovery, rồi SECONDARY, PRIMARY
Hai tầng khôi phục: WiredTiger chỉ replay journal (tức dựng lại oplog), còn replication recovery mới đưa dữ liệu collection từ stable checkpoint lên kịp oplog
(repl README: Startup Recovery). Trên primary, entry oplog có thể commit không theo thứ tự (oplog hole); điểm "không còn lỗ" được ghi riêng
(oplogTruncateAfterPoint, có journal) để lúc khởi động cắt phần đuôi có lỗ. Lab 06-recovery.txt: fsync tạo checkpoint, đặt syncdelay 3 600 lúc chạy, ghi w:1
T giây (~3 400 insert/s) rồi docker kill; số đọc từ log JSON của lần khởi động. Lượt 60 s và 180 s vẫn dính một checkpoint ở giây ~54 (đặt syncdelay lúc chạy chỉ có hiệu lực sau
lần checkpoint đang chờ) — nên đọc theo cột khoảng cách tới checkpoint:
| Ghi trước kill | stable checkpoint cách đỉnh oplog | entry oplog áp lại | WiredTiger opened | áp oplog (startUpReplCoord) | starting → PRIMARY |
|---|---|---|---|---|---|
| 10 s (28 970 insert) | 19 s | 28 889 | 766 ms | 293 ms | 1 216 ms |
| 60 s (205 990 insert) | 6 s | 17 626 | 616 ms | 147 ms | 929 ms |
| 180 s (615 709 insert) | 127 s | 441 154 | 4 415 ms | 5 957 ms | 10 537 ms |
Cả hai bước đều tăng theo lượng ghi kể từ stable checkpoint cuối: 441 nghìn entry (127 s ghi) cần 4,4 s replay journal + 6,0 s áp oplog, tổng 10,5 s mới nhận ghi. Bình thường khoảng cách này ≤ 60 s; nó dài ra khi checkpoint chậm, stable timestamp đứng (mục 3.4) hoặc node chết lặp lại (mục 5.4).
3.7 Nén: chỉ có trên đĩa, trả giá bằng CPU lúc đọc/ghi trang
Collection nén theo khối (block_compressor, mặc định snappy; time series mặc định zstd), index chỉ có prefix compression (bài 3), journal nén snappy. Nén xảy ra lúc
reconcile, giải nén lúc đọc trang vào cache — trang trong cache luôn là bản chưa nén. Đặt riêng từng collection:
createCollection(name, {storageEngine: {wiredTiger: {configString: "block_compressor=zstd"}}}). Lab 06-compress.txt: cùng 500 000 đơn (174 MB BSON) chép sang 4 collection,
lượt 2 (lượt 1 còn trả giá đọc nguồn từ đĩa):
| Nén | file (storageSize) | tỉ lệ | chép + fsync | quét trọn khi cache rỗng (3 vòng) | vào cache khi quét |
|---|---|---|---|---|---|
| snappy (mặc định) | 60,0 MB | 2,90× | 4,8 s | 0,8–1,4 s | 185 MB |
| zstd (mức 6) | 31,9 MB | 5,46× | 4,8 s | 1,1–1,9 s | 185 MB |
| zlib | 38,1 MB | 4,57× | 6,7 s | 1,8–6,8 s | 188 MB |
| không nén | 185,8 MB | 0,94× | 4,5 s | 1,7–2,5 s | 184 MB |
zstd cho file bằng nửa snappy mà thời gian ghi ngang nhau; quét chậm hơn một chút (giải nén tốn CPU hơn). Không nén không nhanh hơn: phải đọc gấp 3 lần byte (từ page cache của OS — lab không
xoá được cache của OS trên máy dùng chung, đĩa lạnh thật sẽ còn nghiêng về bản nén). Số ms nhiễu (zlib có vòng 6,8 s) — so tỉ lệ. Hai điều chắc chắn: cache không lợi gì từ nén (185 MB
dù file 32 hay 186 MB), và đổi compressor chỉ áp dụng cho collection tạo mới — dữ liệu cũ phải chép sang (hoặc initial sync node mới với --wiredTigerCollectionBlockCompressor).
3.8 Concurrency: khoá ý định, xung đột lạc quan, và ticket
MongoDB chỉ lấy intent lock (khoá ý định IS/IX) ở mức global, database, collection — chúng không chặn nhau. "Ai được sửa document nào" do WiredTiger quyết theo kiểu lạc quan: key đã có
update chưa commit của transaction khác, hoặc update mà snapshot của mình không thấy, thì trả WT_ROLLBACK; server đổi thành WriteConflictException, huỷ storage transaction và tự chạy
lại (writeConflictRetry, chờ tăng dần ~1,1×) — client không thấy, trừ trong transaction nhiều document (bài 8). Trước khi vào storage engine, mỗi thao tác còn phải lấy một ticket đọc hoặc ghi (execution control). Từ 7.0, số ticket tự điều chỉnh bằng
throughput probing: bắt đầu thấp (min 4), mỗi 100 ms nếu ticket đang cạn mà tăng 10 % làm throughput tăng thì giữ, không thì lùi; trần 128 (execution control README, 06-params.txt).
Lab 06-tickets.txt (w:1, kèm 1 probe insertOne vào collection khác mỗi 20 ms):
| Tải 30 s | update/s | writeConflicts | totalTickets (ghi) | tổng thời gian chờ ticket | probe p50 / p99 |
|---|---|---|---|---|---|
| 64 client, update rải rác 2 triệu đơn | 2 599 | 0 | 4 → 19 | 590 s | 22,8 / 152 ms |
50 client $inc cùng 1 document | 2 253 | 121 354 | 17 → 36 | 54 s | 18,4 / 245 ms |
| 50 client, mỗi client 1 document riêng | 3 284 | 0 | 17 → 36 | 23 s | 21,8 / 167 ms |
Đọc ra: (1) ticket giờ không mặc định 128 — bắt đầu từ 4 và chỉ tăng khi tăng thật sự giúp; 64 client phần lớn thời gian nằm chờ ticket (590 s cộng dồn trong 30 s) thay vì chen
nhau trong cache — "available thấp" không có nghĩa quá tải, hãy xem hàng đợi. (2) Document nóng: mỗi update thành công kèm ~1,8 WriteConflict, throughput thấp hơn 31 % so với ghi document riêng,
kết quả vẫn đúng (tổng views = số update thành công); lúc ngủ chờ retry thao tác vẫn giữ ticket (internalQueryEnableWriteConflictBackoffWithoutTicket mặc định false). 9.0 thêm
metric writeConflictRetryWaiters/writeConflictRetryLimitHit và giới hạn retry thích ứng (lỗi 512 WriteConflictRetryLimitExceeded) — mặc định tắt
(internalQueryWriteConflictRetryLimitMax: 0), server vẫn retry vô hạn.
4. Đảm bảo và giới hạn
| Thứ | Giá trị / hành vi | Nguồn |
|---|---|---|
| Atomic | Một document cùng mọi index của nó trong một storage transaction | storage README |
| Isolation ở tầng storage | Snapshot isolation (transaction id + timestamp); xung đột ghi phát hiện lạc quan → WriteConflict | arch-transaction |
| Độ bền khi được ack | w:1: chưa bền — mất 9–302 write đã ack mỗi lần docker kill (9 lần), cửa sổ ≤ commitIntervalMs. j:true / majority: 0 mất qua 6 lần kill | lab 06-journal |
| Checkpoint | Mỗi 60 s, tại stable timestamp, luôn nhất quán | arch-checkpoint |
| Đọc về quá khứ | ≤ minSnapshotHistoryWindowInSeconds (300 s); xa hơn → SnapshotTooOld (239) | lab 06-mvcc |
| Transaction nhiều document | Sống ≤ 60 s (thực tế tới ~90 s, lab bị huỷ ở ~65–69 s); dirty của nó ≤ 75 % dirty trong cache, không thì TransactionTooLargeForCache (388) — lab: ~20 MB với cache 256 MB | lab 06-mvcc, 06-toolarge |
| Cache | ≥ 256 MB; ngưỡng evict 80/95 %, dirty 5/20 %, update 2,5/10 % | 06-params |
| Ticket | 4–128 mỗi loại, tự điều chỉnh mỗi 100 ms | execution control README, lab 06-tickets |
| Journal | File 100 MB, xoá sau checkpoint; fsync ≤ 100 ms; trên replica set chỉ chứa oplog | manual, lab 06-jbytes |
Khung lý thuyết: theo thuật ngữ ARIES, WiredTiger là no-force (commit không ép trang xuống đĩa, chỉ journal) và no-steal (dữ liệu chưa commit không bao giờ vào file dữ liệu) — chỉ cần redo log, checkpoint luôn sạch, đổi lại transaction bị chặn bởi cache. PostgreSQL, InnoDB là steal. CAP/PACELC thuộc replica set (bài 7); ở một node, câu hỏi chỉ là "ack trước hay sau fsync".
5. Failure modes
5.1 docker kill giữa lúc ghi: write w:1 đã ack biến mất
Lab 06-journal.txt (node tmp-mdb-j): 16 worker insert tuần tự _id = w·10⁷ + n, client ghi lại thời điểm nhận ack của từng write; sau ~8 s docker kill (SIGKILL),
docker start, đếm write đã ack mà không có trong DB:
| Write concern | số lần kill | write đã ack mà MẤT | chúng được ack trong … cuối | vào DB mà chưa kịp ack |
|---|---|---|---|---|
w:1 (commitIntervalMs 100) | 3 | 171, 71, 12 | 44 ms, 14 ms, 6 ms | 0, 0, 0 |
w:1, commitIntervalMs 500 | 2 | 307, 810 | 80 ms, 262 ms | 0 |
w:1, commitIntervalMs 1 | 2 | 6, 1 | 5 ms, 1 ms | 0 |
j:true | 3 | 0, 0, 0 | —, —, — | 5, 2, 10 |
w:"majority" | 3 | 0, 0, 0 | —, —, — | 3, 3, 7 |
Gộp cả 6 lần kill w:1 ở lab crash recovery (mục 3.6): 9 lần, mất 9–302 write đã ack, không lần nào 0, tất cả được ack trong ≤ 71 ms cuối.
w:1 đến trước độ bền, ack của j:true/majority đến sau fsync journal (out/06-journal.txt, 06-recovery.txt). SIGKILL không xoá page cache của OS;
mất điện thật còn mất thêm phần chưa fsync (chưa verify).- Cửa sổ mất ≤
commitIntervalMs: 100 ms → mất write được ack trong 6–44 ms cuối (tuỳ lúc kill rơi vào đâu giữa hai lần flush); 500 ms → 80–262 ms cuối (307–810 write); 1 ms → gần 0. Writew:1nằm trong bộ nhớ mongod tới lần JournalFlusher kế tiếp — docs WiredTiger nóilog_serverđẩy slot xuống OS mỗi 50 ms, nhưng cửa sổ đo được vượt 50 ms (chưa đọc source để giải thích). - Bẫy "không rõ kết quả": với
j:true/majority, vài write đã vào DB mà client nhận lỗi mạng. Retry phải idempotent:_idsinh ở client (gặp E11000 là đã ghi) hoặc retryable writes (bài 7). - Giá của độ bền: cùng 16 worker tuần tự,
j:true1 240–1 604 insert/s so với 3 163–3 747 củaw:1. - SIGKILL ≠ mất điện: byte đã
write()xuống OS sống qua SIGKILL; mất điện còn mất phần chưa fsync, đĩa có cache ghi không chống mất điện có thể làm mất cảj:true(chưa verify). Server không có metric "vừa mất write" — chỉ phòng được bằng chính sách write concern.
5.2 Transaction hoặc snapshot để mở: history store phình, rồi bị huỷ
Phần C và D của 06-mvcc.txt (mục 3.2): transaction readConcern: "snapshot" đọc 1 document rồi không làm gì. Trong lúc nó mở, history store nhận ~3 600 bản/giây thay vì
~160; reaper huỷ nó sau ~65–69 s, lệnh kế tiếp của client nhận:
251 NoSuchTransaction — Transaction with { txnNumber: 1 } has been aborted.
Đọc tại một atClusterTime cũ hơn cửa sổ (đặt cửa sổ 0, đọc mốc 5 s trước):
239 SnapshotTooOld — Read timestamp Timestamp(1791570625, 530) is older than the oldest available timestamp.
Phát hiện: metric "pinned by the oldest active read timestamp" tăng đều; db.currentOp({"transaction": {$exists: true}}) cho timeOpenMicros. Hay gặp: transaction mở rồi gọi API
ngoài; cursor snapshot qua nhiều getMore; ETL đọc một snapshot nhiều phút. Ngoài reaper còn có cache pressure monitor huỷ transaction cũ nhất khi cache kẹt lâu (README; lab chưa tái hiện).
5.3 Transaction quá lớn cho cache: lỗi 388
Update chưa commit không được evict (no-steal). Lab 06-toolarge.txt, cache 256 MB, insert document 1 KB trong một transaction:
transaction, ngưỡng mặc định 0.75: lỗi sau 20400 document (~20.0 MB) trong 2422 ms, dirty cao nhất đo được 34.0 MB
388 — WiredTigerRecordStore::insertRecord -31800: transaction is too large and will not fit in the storage engine cache (Transaction has the oldest pinned transaction ID)
errorLabels: []
ngoài transaction: 200000 document (~196.5 MB) insert xong trong 7854 ms
Chưa tới 1/10 cache đã chết: giới hạn thật là ngưỡng dirty/update (20 % / 10 % cache), transaction bị huỷ khi phần dirty của nó vượt 75 % tổng dirty
(transactionTooLargeForCacheThreshold). Lỗi không có nhãn TransientTransactionError nên withTransaction không retry; đặt ngưỡng 1.0 thì thành WriteConflict mà README cảnh báo
"may result in drivers retrying an operation indefinitely". Chữa: chia lô.
5.4 Crash loop: lần khởi động sau lâu hơn lần trước
Node chết trước checkpoint đầu tiên sau khi khởi động thì stable checkpoint vẫn là cái cũ: lần sau phải áp lại cả phần oplog của lần chạy trước. Lab phần B (syncdelay mặc định 60 s, mỗi lần sống ~25 s):
| Lần chết | stable checkpoint cách đỉnh oplog | entry oplog áp lại | starting → PRIMARY |
|---|---|---|---|
| 1 | 30 s | 73 064 | 2 379 ms |
| 2 | 64 s | 127 475 | 3 089 ms |
| 3 | 96 s | 209 211 | 3 407 ms |
Một mongod bị OOM-kill lặp lại mỗi dưới 60 s (kiểu bài 5) vì thế ngày càng lâu mới lên PRIMARY. Phát hiện bằng log: Recovering from stable timestamp + numOpsApplied của
Completed oplog application for recovery.
5.5 Xoá không trả dung lượng — và còn làm file to ra
Lab 06-compact.txt: lab6.o_compact 1 000 000 đơn (snappy):
| Bước | count | storageSize | freeStorageSize | index |
|---|---|---|---|---|
| sau khi chép | 1 000 000 | 118,8 MB | 0 | 33,3 MB |
| xoá nửa cũ (3,8 s) | 500 000 | 180,5 MB | 58,9 MB | 58,3 MB |
| chèn 250 000 đơn mới | 750 000 | 180,5 MB | 28,4 MB | 58,3 MB |
compact, force: true (0,2 s, bytesFreed 18,0 MB) | 750 083 | 162,3 MB | 10,2 MB | 58,3 MB |
Xoá làm file to thêm 52 %: mỗi trang bị xoá bớt được viết lại ở chỗ mới (copy-on-write) trong khi khối cũ còn bị checkpoint trước giữ, index cũng vậy. Chỗ trống được dùng lại (chèn
250 000 đơn không làm file lớn thêm). compact trên primary bị từ chối nếu không có force:
20 IllegalOperation — will not run compact on an active replica set primary as this will slow down other running operations. use force:true to force
dryRun: true ước đúng 18,0 MB — chỉ 18 trên 28,4 MB trống: compact dời khối từ 10–20 % cuối file lên đầu rồi cắt đuôi, best-effort
(arch-compact); probe insert + find vào chính collection đó: 0 lỗi. autoCompact (8.0+,
runOnce: true, freeSpaceTargetMB: 1): lab6.o_auto (xoá nửa) 89,0 → 64,1 MB sau 5 s — nhưng nó chạy trên mọi collection của node: 41 file compact, 341 MB lấy lại. Tắt trước
khi backup (manual).
6. Vận hành trong production
Metric cần theo dõi (db.serverStatus(), FTDC lưu sẵn mỗi giây; tên field đúng bản 9.0.2):
| Metric | Đường dẫn | Đọc thế nào |
|---|---|---|
| Cache dùng / dirty / update | wiredTiger.cache["bytes currently in the cache"], ["tracked dirty bytes in the cache"], ["bytes allocated for updates"] ÷ ["maximum bytes configured"] | ~80 % dùng là bình thường; dirty dính 20 % / update 10 % = thread request đang bị kéo đi dọn |
| Thread request tự evict / chờ cache | cache["application thread time evicting (usecs)"], ["thread-yield"]["application thread time waiting for cache (usecs)"] | Tăng liên tục = p99 xấu |
| Checkpoint | checkpoint["most recent time (msecs)"] | Vài giây trở lên = đĩa không kịp |
| History store, snapshot bị ghim | cache["history store table on-disk size"], transaction["transaction range of timestamps pinned by the oldest active read timestamp"] | Tăng đều = reader ghim hoặc commit point đứng |
| Ticket, xung đột | queues.execution.write.normalPriority.queueLength, metrics.operation.writeConflicts | Hàng đợi dài kéo dài; conflict vọt = document nóng |
Tham số ảnh hưởng lớn (giá trị thật ở 06-params.txt): cacheSizeGB/cacheSizePct (đổi lúc chạy được qua
wiredTigerEngineRuntimeConfig: "cache_size=…"); wiredTigerEvictionThreadsMin/Max 4/4 (chỉ giúp khi còn CPU và đĩa); syncdelay 60 s (manual khuyên không đổi);
commitIntervalMs 100 (1–500, chính là cửa sổ mất của w:1); minSnapshotHistoryWindowInSeconds 300; transactionLifetimeLimitSeconds 60; blockCompressor snappy
(chỉ cho collection mới); executionControlConcurrencyAdjustmentAlgorithm throughputProbing (đặt cứng số ticket là chuyển sang fixed).
Capacity planning: RAM ≈ 2 × (working set chưa nén + index) + 1 GB, vì cache chỉ là 50 % (RAM − 1 GB); đĩa = dữ liệu nén + history store (tới ≈ số update/s × 300 s × cỡ document)
+ journal (xoá sau mỗi checkpoint) + chỗ trống cho compact; băng thông ghi = số update ngẫu nhiên/s × 35–45 KB. Upgrade: định dạng file theo FCV, đổi compressor hay cấu hình bảng thì
phải tạo lại collection hoặc initial sync node mới. Lúc 3 giờ sáng, p99 vọt: (1) dirty % và app thread evict (mục 3.3) → tìm job ghi hàng loạt, giảm luồng của nó; (2) checkpoint
time và đĩa (iostat); (3) history store + pinned timestamp → tìm transaction/cursor dài trong $currentOp, hoặc node secondary chết (commit point đứng); (4) hàng đợi ticket +
writeConflicts → document nóng.
7. So sánh và ma trận quyết định
MongoDB Community không còn engine nào khác để chọn (in-memory chỉ có ở Enterprise/Percona); câu hỏi thật là "MVCC của MongoDB khác PostgreSQL chỗ nào" (PostgreSQL: dead tuple và bloat, wraparound):
| Trục | WiredTiger (MongoDB 9.0) | PostgreSQL 18 (heap) |
|---|---|---|
| Phiên bản cũ nằm đâu | Update chain trong RAM; khi reconcile → WiredTigerHS.wt riêng | Ngay trong heap, cạnh bản mới (dead tuple) |
| Ai dọn bản cũ | Tự động: hết cửa sổ snapshot + không reader nào cần thì reconcile bỏ; trang HS rỗng bị checkpoint dọn | VACUUM / autovacuum; chậm thì bloat, wraparound |
| Một update nhỏ ghi gì xuống đĩa | Journal: entry oplog (~0,3–0,4 KB); file: cả trang khi reconcile (lab 34–43 KB/update ngẫu nhiên) | WAL + tuple mới; full page image lần đầu sau checkpoint; index mới trừ HOT update |
| Dữ liệu chưa commit ra đĩa? | Không (no-steal) → transaction phải vừa cache (lab: lỗi 388 ở ~20 MB với cache 256 MB) | Có (steal) → transaction lớn được, rollback chỉ là đánh dấu |
| Trả dung lượng sau xoá | compact / autoCompact (8.0+), không chặn CRUD, best-effort | VACUUM FULL (khoá bảng) / pg_repack |
| Hai writer cùng một bản ghi | Lạc quan: WriteConflict, server tự retry (giữ ticket) | Bi quan: writer sau chờ row lock |
Dùng (và để mặc định) khi: working set ở dạng chưa nén vừa cache, tốc độ ghi trang dưới băng thông ghi của đĩa, transaction ngắn và nhỏ, ghi phân tán trên nhiều document. Tránh hoặc phải thiết kế lại khi: update nhỏ ngẫu nhiên trên dữ liệu lớn gấp nhiều lần RAM mà vẫn đòi p99 thấp; một transaction gom hàng chục MB; cần đọc snapshot kéo dài hàng phút; xoá hàng loạt định kỳ mà ngân sách đĩa chật; counter nóng hàng nghìn lần ghi/giây trên một document.
8. Góc nhìn tech lead: tác động tổ chức
- Sizing RAM theo byte chưa nén (mục 6); RSS = cache + heap truy vấn + connection + data handle; container phải có giới hạn RAM.
- Sizing đĩa theo write amplification: update ngẫu nhiên/s × 35–45 KB (lab) + journal + checkpoint. Đĩa mạng nhỏ, HDD là nơi p99 chết trước.
- Write concern là quyết định kinh doanh. Mặc định
majority(kéo theo journal).w:1chỉ cho dữ liệu dựng lại được, ghi vào ADR: "chấp nhận mất tớicommitIntervalMs(100 ms) write đã ack khi mongod chết" — đính kèm lab mục 5.1. - Job ghi hàng loạt phải có ga: giới hạn luồng (4 luồng = 76 % throughput của 32 luồng mà không kéo p99 cả hệ thống), chạy ngoài giờ cao điểm, cảnh báo theo dirty %.
- Quy ước transaction/snapshot: vài giây, vài MB; không gọi API ngoài trong transaction; đọc dài dùng phân trang theo khoá (bài 4).
- Vòng đời dữ liệu từ đầu: xoá không trả đĩa — chấp nhận file giữ đỉnh, hoặc lên lịch
compacttừng secondary rồi step down, hoặc dựng lại node bằng initial sync. - Lock-in / exit strategy: file
.wtchỉ MongoDB (và công cụwt) đọc được; Community không có engine khác. Thoát ra = dump/export hoặc CDC (bài 10); Percona Server for MongoDB là đường lùi về license, cùng WiredTiger (bài 11).
9. Anti-patterns và bài học thực tế
- "Tăng cache là hết chậm." Chỉ đúng khi nghẽn là đọc; ghi ngẫu nhiên thì ngưỡng dirty tăng theo cache còn đĩa thì không — lab 1 GB p99 còn tệ hơn 256 MB.
w:1cho đơn hàng, tiền. Ack ≠ bền (mục 5.1); failover còn rollback (bài 7).- Hạ
minSnapshotHistoryWindowInSecondsvề 0 cho history store khỏi phình. Đọc snapshot về quá khứ hỏng (SnapshotTooOld), transaction dài vẫn ghim như cũ. Chữa bằng mô hình dữ liệu (bài 2) và transaction ngắn. - Bọc import/migration lớn trong một transaction — chết ở vài chục MB (mục 5.3).
- Xoá rồi đợi đĩa tự nhỏ;
compactgiờ cao điểm trên primary; bậtautoCompactrồi quên (nó quét cả node). - Counter nóng trong một document — tách nhiều document con rồi cộng, hoặc đệm ở Redis.
- Coi "available tickets thấp" là quá tải — từ 7.0 ticket bắt đầu ở 4; nhìn hàng đợi.
Sự cố công khai: CircleCI (26/03–10/04/2019): service khắp fleet gọi lại createIndex lúc khởi động (tưởng rẻ) làm cạn ticket WiredTiger, cộng áp lực bộ nhớ — đồ thị có
"application evicted pages", eviction thất bại, phải failover; khắc phục gồm bỏ lệnh tạo index thừa, chỉnh tcmalloc, nâng bản (postmortem).
Manual tự ghi: PSA mất một node có dữ liệu thì "the storage engine keeps all changes that happen after the commit point on disk", cache áp lực, oplog vượt cỡ
(manual). Trước 4.4, bảng tràn WiredTigerLAS.wt từng phình tới mức có tham số giới hạn riêng
maxCacheOverflowFileSizeGB — nay ghi "Deprecated. Has no effect" (wiredtiger_global_options.idl).
Docs không nói rõ: trên replica set journal chỉ là oplog, j:true chỉ cam kết entry oplog; node chết lặp lại trước checkpoint đầu tiên thì lần khởi động sau lâu hơn lần trước.
10. Câu hỏi phụ interviewer hay hỏi tiếp
- Vì sao cache WiredTiger mặc định chỉ khoảng 50 % RAM?
- Phần còn lại cho page cache của OS (giữ file đã nén — đọc lại không cần đĩa), heap truy vấn, connection, data handle. Công thức max(50 % × (RAM − 1 GB), 256 MB), "RAM" là giới hạn cgroup nếu có.
- Ở tầng storage,
w:1,j:truevàw:"majority"khác nhau thế nào? w:1: ack khi entry oplog vào log buffer trong RAM.j:true: ack sau fsync journal (group commit, ≤ 100 ms).majority: ack khi đa số node đã ghi và — dowriteConcernMajorityJournalDefault: true— đã journal. Labdocker kill: chỉw:1mất write đã ack.- Xoá nửa collection sao file không nhỏ đi?
- Copy-on-write: khối giải phóng chỉ vào danh sách dùng lại; file co khi khối trống nằm cuối file.
compact/autoCompactdời khối từ cuối lên đầu rồi cắt đuôi, best-effort. - Transaction (hay cursor snapshot) dài ảnh hưởng gì nếu nó chỉ đọc?
- Nó ghim snapshot: mọi phiên bản mới hơn không bỏ được, khi trang được reconcile chúng dồn vào history store, cache nặng thêm. Transaction bị huỷ sau 60 s (thực tế tới ~90 s, reaper chạy mỗi 30 s).
- Sao query đọc collection khác cũng chậm khi batch job ghi?
- Dirty chạm 20 % cache → thread của request (bất kỳ) bị kéo đi evict/chờ cache; thêm tranh ticket, CPU, đĩa. Lab: p99 của
findOnekhông liên quan tăng 2–4 lần. - Lỗi
TransactionTooLargeForCache(388) là gì, chữa sao? - No-steal: update chưa commit không evict được; dirty của transaction vượt 75 % dirty cache thì bị huỷ thay vì retry mãi (lab: ~20 MB ở cache 256 MB). Chia lô.
Phụ lục: lab đã chạy
Môi trường: MongoDB 9.0.2, replica set 1 node, cache 0,25 GB, --memory 1g: tmp-mdb-1 (bộ dữ liệu chung, js/seed.js 138 s) cho eviction, nén, compact, ticket;
tmp-mdb-j (collection nhỏ, bị docker kill nhiều lần) cho journal, recovery, MVCC, transaction lớn; tmp-mdb-sa standalone; mongod rỗng tạm (9.0.2, 8.0.32) đọc cache mặc định.
Client tải: Node 22 + driver mongodb 7.7.0 (node/06-*.mjs); còn lại mongosh (js/06-*.js).
cd labs/mongodb
./06-wiredtiger.sh # chạy hết (~35 phút): seed params cachedef evict jbytes journal recovery mvcc toolarge compress compact tickets
./06-wiredtiger.sh journal # 1 phần
(source lib.sh && mdb_down tmp-mdb-1 tmp-mdb-j) # dọn container
Bẫy khi chạy lại: setParameter: {syncdelay: 3600} lúc chạy chỉ có hiệu lực sau lần checkpoint đang chờ (muốn tắt hẳn: khởi động với --syncdelay); journalCommitInterval,
wiredTigerEngineRuntimeConfig đặt lúc chạy mất sau restart; lệnh đầu tiên của driver Node 7.7 từng chết với MongoTopologyClosedError: Topology is closed khi
serverSelectionTimeoutMS: 1500 — đã nâng lên 5 000 ms; timestamp 64 bit trong serverStatus về Node là number: giây = Math.floor(x / 2**32); "history store table on-disk
size" chỉ cập nhật sau checkpoint; biến thể cache 1 GB cần docker update --memory 2g rồi trả về 1g.
Chưa verify: mất điện thật (SIGKILL không xoá page cache của OS; đĩa có cache ghi không chống mất điện còn làm hỏng cả j:true); history store và checkpoint khi majority commit point
đứng yên (PSA — bài 7); cache pressure monitor tự huỷ transaction (PeriodicThreadToRollbackUnderCachePressure, chỉ đọc README); vì sao cửa sổ mất của w:1 vượt
50 ms dù docs WiredTiger nói log_server đẩy slot xuống OS mỗi 50 ms (đo được, chưa đọc tới source); đọc đĩa lạnh thật cho phần nén (không xoá được page cache trên máy dùng chung);
số đo ở RAM, đĩa và số core lớn hơn; Atlas.