Vận hành MongoDB production và quyết định kiến trúc — metric, backup, upgrade, chi phí, SSPL và khi nào nên chọn thứ khác

Rolling upgrade 3 node từ 8.0.32 lên 9.0.4 rồi hạ lại, writer w:"majority" chạy suốt: 12 194 insert, 0 lỗi; setFeatureCompatibilityVersion mất 91 ms. Nhưng binary 8.0.32 trên data đã FCV 9.0 thoát ngay (ExitCode=100), nhảy cóc 7.0 → 9.0 cũng vậy (62), và mỗi node tắt sạch mất 15 s (quiesce) trong khi docker stop chỉ chờ 10 s. PITR khôi phục đúng 1 983/1 983 write trước lệnh deleteMany({}) nhầm, nhưng restore chậm ~6 lần dump và replay oplog chỉ ~280 entry/s; snapshot kiểu fsyncLock khoá ghi 30,2 s. 200 worker PHP khởi động mở 600 kết nối trong 1 s; primary chết thì PHP 2 000 request lỗi, Node 0 lỗi nhưng treo ~11 s. Đĩa đầy: mongod abort, xoá 40 MB ballast vẫn không lên lại được. FerretDB 2.7.0: 23/28 truy vấn chạy, không có transaction/change stream, và $jsonSchema được nhận nhưng không chặn gì. Bài này gom series về tầm vận hành và quyết định, link về chỗ đã đo thay vì giảng lại: operator injection — bài 1; cache, eviction — bài 6; failover, cửa sổ oplog, flow control — bài 7; side effect, laravel-mongodb báo thành công giả — bài 8; shard key, orphan — bài 9; outbox, HistoryLost — bài 10.

Thuật ngữ dùng trong bài
working set (dữ liệu + index hay đọc) · oplog window (khoảng thời gian oplog còn giữ) · commit point (vị trí oplog đa số node đã ghi) · FCV — featureCompatibilityVersion (công tắc bật định dạng/tính năng của bản mới, tách khỏi binary) · rolling upgrade (nâng từng node, cụm vẫn chạy) · quiesce mode (lúc tắt, mongod báo driver "sắp tắt" rồi chờ request đang chạy xong) · PITR — point-in-time recovery (khôi phục về một thời điểm bất kỳ) · RPO / RTO (mất tối đa bao nhiêu dữ liệu / mất bao lâu mới chạy lại) · connection pool (bể kết nối dùng lại) · connection storm (bão kết nối: rất nhiều client mở kết nối mới cùng lúc) · FTDC — full-time diagnostic data capture (mongod tự ghi metric mỗi giây ra đĩa) · exporter (process đọc metric rồi phơi cho Prometheus) · SCRAM (đăng nhập bằng mật khẩu) · RBAC (phân quyền theo role) · exit strategy (kế hoạch rời công nghệ/nhà cung cấp).
Kiểm chứng: Context7 (/websites/mongodb_manual, /mongodb/node-mongodb-native); release notes 9.0 (9.0.4 ngày 13/10/2026, không có 9.0.3), upgrade / downgrade 8.0 ↔ 9.0; nguồn khác link tại chỗ, tra ngày 18/10/2026. Output: labs/mongodb/out/11-*.txt. Máy lab dùng chung: số ms chỉ để so tỉ lệ.

Trả lời 30 giây

MongoDB production chạy ổn khi giữ được bốn điều: working set vừa cache, cửa sổ oplog dài hơn sự cố dài nhất, commit point không đứng yên, số kết nối có trần — mọi metric và cảnh báo là để canh bốn điều đó. Backup ba tầng: mongodump (restore chậm vì build lại index), snapshot đĩa (cần atomic hoặc fsyncLock), PITR = snapshot + oplog (Atlas, Ops Manager, Percona Backup); backup chưa restore thử thì chưa phải backup. Nâng cấp hai pha: thay binary từng node (lùi được), rồi mới đổi FCV (lùi khó). Image Docker không bật auth nếu thiếu biến môi trường, và readWrite drop được collection. Chọn MongoDB khi access pattern xoay quanh một aggregate và có người vận hành (hoặc Atlas); chọn PostgreSQL khi join, ràng buộc, transaction nhiều bảng là hằng ngày. SSPL không cấm dùng làm backend, nhưng đường lui là engine "tương thích" thiếu đúng thứ hay dùng — phải đo.

1. Bài toán gốc và bối cảnh ra đời

Mười bài trước trả lời "MongoDB chạy thế nào". Bài này trả lời câu tech lead bị hỏi nhiều hơn: chạy nó trong 3 năm thì tốn gì, hỏng ở đâu, và lúc nào nên chọn thứ khác. Phần vận hành của MongoDB có lịch sử riêng, và lịch sử đó giải thích vì sao Community thiếu nhiều công cụ:

Tối ưu cho: đội trả tiền Atlas, hoặc có người vận hành tự dựng backup/monitoring. Không tối ưu cho: đội nhỏ self-host Community không ai trực — đúng phần họ cần nhất (PITR, cảnh báo, nâng cấp tự động) là phần không có sẵn.

2. Mô hình tư duy cốt lõi

Vận hành MongoDB quy về bốn đèn. Mỗi sự cố đã đo trong series là một đèn chuyển đỏ — và mỗi metric ở mục 6 tồn tại để canh một đèn:

Bốn đèn vận hành MongoDB Bốn ô: cache, cửa sổ oplog, commit point, kết nối. Lần lượt từng ô chuyển từ xanh sang đỏ: working set vượt cache; cửa sổ oplog co lại ngắn hơn sự cố dài nhất; commit point đứng yên trong khi thời gian chạy tiếp; số kết nối vọt khi failover. 1. Working set vừa cache 2. Cửa sổ oplog ≥ sự cố dài nhất 3. Commit point không đứng yên 4. Kết nối có kiểm soát cache = 50% × (RAM − 1 GB) working set Bình thường: ít đọc đĩa, dirty dưới 5% Dirty chạm 20%: request tự evict, p99 ×2–4 sự cố dài nhất bây giờ Bình thường: node chết vài giờ vẫn bắt kịp Ghi dồn làm cửa sổ co lại → initial sync commit point bây giờ Bình thường: đa số ghi kịp, trễ vài ms Mất đa số: majority treo, stream đứng trần: RAM, maxConns Bình thường: số kết nối phẳng, có trần Deploy/failover: mọi process nối lại
Hình 1 — Bốn "đèn" để canh khi vận hành MongoDB. Mọi metric và cảnh báo ở mục 6 quy về một đèn; hình cho mỗi đèn chuyển đỏ theo đúng cơ chế đã đo ở các bài trước.
ĐènĐỏ nghĩa làĐã đo ở
1. Working set vừa cacheĐọc rơi xuống đĩa; dirty chạm 20% → request tự evict, p99 ×2–4; transaction lớn bị huỷ (388)bài 6, 5
2. Cửa sổ oplog ≥ sự cố dài nhấtSecondary rớt khỏi cửa sổ → initial sync; consumer change stream → ChangeStreamHistoryLost; PITR không còn oplog để replaybài 7, 10, mục 3.3
3. Commit point không đứng yênĐa số không ghi được: w:"majority" treo, flow control hãm ghi, history store phình, change stream đứngbài 7, 6, 10
4. Kết nối có kiểm soátFailover/deploy: mọi process mở lại kết nối cùng lúc; mỗi kết nối tốn RAM + CPU handshake; vượt maxConns thì bị từ chốimục 3.2, 5.2

Quyết định kiến trúc quy về bốn câu hỏi: (1) access pattern có xoay quanh một aggregate không (bài 2); (2) có cần join, ràng buộc, transaction nhiều bảng thường xuyên không (bài 5, 8); (3) đội có người vận hành hoặc ngân sách Atlas không (mục 6); (4) license, vendor có chấp nhận được không (mục 8). "Có" ở câu 1, "không" ở câu 2 thì MongoDB hợp.

3. Internals và quyết định thiết kế

3.1 Metric đến từ đâu: serverStatus, FTDC, mongostat/mongotop, exporter

Mọi công cụ đọc cùng một nguồn: lệnh serverStatus (cộng replSetGetStatus, $collStats). Khác nhau ở chỗ ai đọc, bao lâu một lần, và giữ ở đâu:

3.2 Kết nối: PHP-FPM không có pool, Node có pool theo từng node

ext-mongodb (PHP, libmongoc chế độ single-threaded) giữ MongoDB\Client và socket suốt đời process, mỗi process 1 kết nối tới mỗi node (vừa chạy lệnh vừa giám sát topology), không có pool. Driver Node có pool riêng cho từng node (maxPoolSize 100, minPoolSize 0, maxConnecting 2) + 1–2 kết nối giám sát mỗi node. Đếm bằng $currentOp (idleConnections: true) theo clientMetadata (out/11-conn.txt):

Clientr1 (primary)r2r3Ghi chú
Nền: kết nối nội bộ giữa các node161813heartbeat, OplogFetcher, ReplicaSetMonitor…
50 process PHP (giả 50 FPM worker)+50+50+50secondary cũng gánh đủ, dù app chỉ đọc primary
1 process Node, 1 lệnh ping+2+1+11 trong pool + kết nối giám sát
Node, 200 lệnh cùng lúc, maxPoolSize 100+99 trong 275 ms0maxConnecting 2 → ~360/s; 100 lệnh còn lại xếp hàng
Như trên, maxPoolSize 10+90latency như nhau (chặn vì khoá)
1 000 kết nối nhàn rỗi tới primaryRAM cgroup 148 → 458 MB: ~316 KB mỗi kết nối; connections.threaded = 1 020mỗi kết nối = 1 thread trong mongod

Ví dụ minh hoạ: 20 web server × 50 worker PHP-FPM = 1 000 process → 1 000 kết nối trên mỗi node, ~310 MB RAM mỗi node chỉ cho kết nối, trừ vào phần ngoài cache. Cùng tải bằng Node: 20 process × (đồng thời thật ~10–30 + 2 giám sát) ≈ 250–650 kết nối, dồn ở primary. Bẫy:

  1. minPoolSize là cho từng node: minPoolSize=1000 với URI replicaSet= mở 1 000 kết nối tới mỗi node (lab: 334/334/334 sau 36 s, vẫn tăng), nạp nền chỉ ~10 kết nối/s mỗi pool (minPoolSizeCheckFrequencyMS 100).
  2. Secondary gánh kết nối của app chỉ đọc primary (PHP: 50/50/50) — tính capacity theo từng node.
  3. maxPoolSize là trần mỗi process, mỗi node, không phải trần tổng; trần thật là net.maxIncomingConnections (mục 5.2) và RAM.
  4. PHP không có maxPoolSize: giảm kết nối bằng pm.max_children. Client được giữ theo hash của URI + mảng option (php.net): option khác nhau (vd appname theo tenant) là thêm một bộ kết nối tới mọi node trong mỗi worker.

3.3 Backup: logic, snapshot, và PITR = snapshot + oplog

CáchCơ chếĐo trong lab (1,7 GB data, 5,8 triệu document)
mongodump --oplogĐọc từng document qua query; --oplog chép thêm oplog phát sinh lúc dump → restore với --oplogReplay ra một thời điểm nhất quán (lúc dump xong) dump 14,6 s → 1,1 GB BSON + 377 entry oplog; restore lên node mới 84,9 s (nạp data 73 s, build lại 5 index 9,7 s)
Snapshot volume / chép thư mụcChép file .wt + journal. Snapshot atomic cả volume thì WiredTiger tự recovery như sau crash; chép từng file thì phải db.fsyncLock() fsyncLock + tar 1,7 GB: khoá ghi 30,2 s (đọc vẫn chạy, p99 4 ms); bản chép mở được, validate().valid = true
PITR liên tụcSnapshot định kỳ + chép oplog liên tục ra chỗ khác → về được bất kỳ giây nào trong cửa sổ Community không có sẵn: Atlas, Ops Manager (Enterprise), Percona Backup for MongoDB (miễn phí) — chưa dựng; bản làm tay ở dưới

Lab PITR (out/11-pitr.txt): writer ghi lab.pitr mỗi 20 ms; T0 dump; sau đó updateMany 23 955 đơn; T2 shop.customers.deleteMany({}) xoá 200 000 khách. Khôi phục: restore bản dump lên node mới, lấy đoạn local.oplog.rs có ts > T0 (45 979 entry), replay với --oplogLimit = timestamp của entry xoá đầu tiên.

Khôi phục về thời điểm ngay trước lệnh xoá nhầm Trục trên là node nguồn: mongodump --oplog lúc T0, oplog tiếp tục ghi, có một updateMany, rồi lúc T2 deleteMany xoá nhầm cả collection customers. Trục dưới là node khôi phục: restore bản dump, rồi replay oplog từ T0 tới ngay trước T2 bằng oplogLimit; các write sau T2 không được áp. Node nguồn — oplog chạy liên tục Node khôi phục (mới) — mongorestore T0: mongodump --oplog (1,1 GB, 14,6 s) updateMany 23 955 đơn T2: deleteMany({}) customers +566 write sau T2 restore bản full: 84,9 s --oplogLimit = ts(T2), loại trừ Kết quả: customers 200 000 · 23 955 đơn có promo · lab.pitr 1 983 / 1 983 write trước T2 Mất 566 write sau T2 nếu không replay chọn lọc · replay 25 378 entry oplog mất 90 s (~280 entry/s)
Hình 3 — PITR (point-in-time recovery, khôi phục về một thời điểm) đã chạy trong lab (out/11-pitr.txt): bản mongodump --oplog lúc T0 + đoạn local.oplog.rs từ T0 → restore lên node mới → replay tới ngay trước entry xoá nhầm. Cái giá: write hợp lệ sau T2 cũng mất, và replay chậm nên RTO phụ thuộc độ dài đoạn oplog.
mongodump  --uri "$SRC" --oplog --out /backup/full                              # T0
mongodump  --uri "$SRC" -d local -c oplog.rs --query '{"ts": {"$gt": {"$timestamp": {"t": 1792276521, "i": 5}}}}' --out /backup/oplog
mv /backup/oplog/local/oplog.rs.bson /backup/replay/oplog.bson                   # replay đòi đúng tên oplog.bson
mongorestore --uri "$NEW" --oplogReplay /backup/full                            # 84,9 s
mongorestore --uri "$NEW" --oplogReplay --oplogLimit 1792276567:1 /backup/replay # dừng NGAY TRƯỚC T2: 25 378 entry, 90 s

shop.customers=200000, đơn có promo=23955, lab.pitr=1983          ← bản khôi phục
nguồn: ghi trước T2 = 1983, sau T2 (mất nếu dừng ở T2) = 566

Bẫy, theo mức hay dính:

  1. Replay chậm hơn ghi gốc rất nhiều: ~280 entry/s, áp tuần tự. Ví dụ minh hoạ: oplog 1 ngày ở 300 write/s ≈ 26 triệu entry ≈ 26 giờ replay. RTO = restore + replay → công cụ PITR chụp snapshot dày.
  2. Tìm T2 phải tìm cả hai dạng entry: deleteMany lớn thành 20 025 entry applyOps trên admin.$cmd, nhưng entry đầu lúc là op: "d", lúc là applyOps (hai lần chạy, hai kết quả).
  3. Write hợp lệ sau T2 cũng mất (566 trong lab); giữ lại thì phải replay chọn lọc bằng tay.
  4. Restore vào DB đã có dữ liệu, quên --drop: 0 document(s) restored successfully. 100000 document(s) failed to restore.
  5. --oplog chỉ cho cả instance: Failed: bad option: --oplog mode only supported on full dumps; trên sharded cluster dump không nhất quán xuyên shard.
  6. Oplog phải còn từ T0 tới T2 (đèn 2): backup đêm qua + oplog chỉ giữ 6 giờ = không PITR được buổi sáng.

3.4 Nâng cấp: tách binary và FCV

Nâng cấp có hai pha cố ý tách nhau. Thay binary từng node: binary mới vẫn ghi định dạng cũ vì FCV còn cũ → lùi binary được. Đổi FCV: bật định dạng mới trên đĩa → binary cũ không mở được nữa; docs khuyên chờ "burn-in" giữa hai pha. Đường nâng đi từng bản: 8.0 hoặc 8.3 → 9.0 (8.1–8.3 là bản minor, không bắt buộc ghé).

Rolling upgrade 8.0.32 lên 9.0.4 rồi đổi FCV Ba node u1 primary, u2, u3 secondary chạy 8.0.32. Lần lượt thay u2 rồi u3 bằng 9.0.4; rs.stepDown chuyển primary sang u2; thay u1; cuối cùng đổi featureCompatibilityVersion từ 8.0 sang 9.0. Writer ghi majority suốt quá trình không gặp lỗi nào. Bắt đầu: 3 node 8.0.32, FCV 8.0, u1 là primary, writer ghi w:majority mỗi 20 ms 1. Thay secondary u2: docker stop (SIGTERM) → chạy image 9.0.4 trên cùng volume 2. Thay secondary u3 — lúc nào cũng còn 2/3 node, majority vẫn ghi được 3. rs.stepDown() trên u1 → u2 lên primary (ghi khựng 0,13–0,2 s) 4. Thay u1 (giờ là secondary). Cả 3 node 9.0.4, FCV vẫn 8.0: còn lùi được 5. setFCV "9.0", confirm: true (91 ms) — từ đây binary 8.0 không mở được data tmp-mdb-u1tmp-mdb-u2tmp-mdb-u3 PRIMARYSECONDARY SECONDARYPRIMARY SECONDARY mongod 8.0.32mongod 9.0.4 mongod 8.0.32mongod 9.0.4 mongod 8.0.32mongod 9.0.4 tắt sạch ~15 s (quiesce)chạy 9.0.4: 1,7–3,3 s tắt sạch ~15 s (quiesce)chạy 9.0.4: 1,7–3,3 s tắt sạch ~15 s (quiesce)chạy 9.0.4: 1,8 s writer (driver Node 7.7) → luôn ghi vào node PRIMARY, w:"majority" mỗi 20 ms FCV 8.0 FCV 9.0 Lab: 12 194 insert w:majority qua cả nâng lẫn hạ, 0 lỗi; khoảng không ack dài nhất ~1 s (lúc thay u1)
Hình 2 — Rolling upgrade đã chạy trong lab (out/11-upgrade.txt): secondary trước, rs.stepDown(), primary sau, rồi mới đổi FCV. Đổi binary (lùi được) tách khỏi đổi FCV (lùi khó). Mỗi node tắt sạch mất ~15 s vì quiesce; docker stop mặc định chỉ chờ 10 s (mục 5.4).
ThửKết quả thật (out/11-upgrade.txt, 11-skip.txt)
Thay 3 node 8.0.32 → 9.0.4, writer w:majority 20 ms/lần0 lỗi; mỗi node tắt sạch 15,4–16,0 s, lên lại SECONDARY sau 1,7–3,3 s; rs.stepDown(): khoảng không ack 133–203 ms; dài nhất ~1 s lúc thay node cuối
setFeatureCompatibilityVersion: "9.0" thiếu confirmLocation7369100: To proceed with the FCV upgrade, please re-run this command with 'confirm: true'.
Có confirm: true91 ms, writer khựng 38–41 ms. Time series tsup được chuyển sang dạng 9.0 (không còn system.buckets.tsup, thêm fixedBucketing)
Chạy lại 8.0.32 trên data FCV 9.0 có time seriesExitCode=100: IDLUnknownField: BSON field 'CollectionOptions::parse.fixedBucketing' is an unknown field.
Như trên, không có time seriesExitCode=62: UPGRADE PROBLEM: Found an invalid featureCompatibilityVersion document … Invalid feature compatibility version value '9.0'; expected '7.0' or '7.3' or '8.0'
Nhảy cóc 7.0.45 → 9.0.4ExitCode=62: … value '7.0'. Expected one of the following versions: ''9.0', '8.3', '8.0'
Đường lùi đúng: setFCV "8.0" rồi binary 8.0.32 từng nodeFCV 54 ms, system.buckets.tsup xuất hiện lại; cả 3 node chạy 8.0.32, đủ dữ liệu, writer 0 lỗi

Chính sách lùi: docs 8.0 ghi "Binary downgrades are not supported for MongoDB Community Edition"; từ 8.3 lùi được về bản liền trước (hạ FCV, gỡ tính năng không tương thích trước). Patch cùng nhánh (9.0.2 → 9.0.4) không đụng FCV, chỉ rolling restart; 9.0.4 (13/10/2026) sửa 5 CVE, SERVER-133518 (bài 9) và deadlock resharding + setFCV (SERVER-133988).

3.5 Bảo mật: mặc định của image và RBAC

Lab out/11-security.txt, image mongo:9.0.2:

ThửKết quả thật
docker run mongo:9.0.2, không đặt MONGO_INITDB_ROOT_*, nối từ container khác không mật khẩulistDatabases chạy; getCmdLineOpts: mongod --bind_ip_all → {"bindIp":"*"}; createUser role root từ client vô danh: ok: 1
Vì sao --bind_ip_alldocker-entrypoint.sh: # so if no "--bind_ip" is specified, let's add "--bind_ip_all"
Có MONGO_INITDB_ROOT_USERNAME/PASSWORD, client không đăng nhậpUnauthorized: Command listDatabases requires authentication
User app role readWrite trên shopinsert OK, createIndex OK, drop collection OK; dropDatabase và đọc lms: Unauthorized: not authorized on …
User app2 role tự định nghĩa appData (chỉ find/insert/update/remove)insert OK; createIndexes, drop, dropDatabase: Unauthorized
Nén mạng: hello xin ["zlib","zstd","snappy"]Server nhận cả ba: zlib bật sẵn (liên quan MongoBleed, mục 9)
200 worker PHP cùng mở kết nối: không auth vs SCRAM-SHA-256ping đầu p50 376 → 694 ms, p99 474 → 867 ms; CPU mongod 470 → 550 ms — phần lớn giá nằm ở client (băm mật khẩu) và thêm vòng mạng

Role cho app Laravel nên tự định nghĩa, không dùng readWrite trần:

db.getSiblingDB("shop").runCommand({createRole: "appData", roles: [],
  privileges: [{resource: {db: "shop", collection: ""}, actions: ["find", "insert", "update", "remove"]}]})
db.getSiblingDB("shop").createUser({user: "app", pwd: passwordPrompt(), roles: ["appData"]})
// migration (index, collection mới) chạy bằng user khác, có dbAdmin — chỉ trong pipeline deploy

Community có SCRAM, x.509, TLS, RBAC, mã hoá field phía client (tường minh). Không có: audit log, LDAP/Kerberos, encryption at rest — có ở Enterprise/Atlas; Percona Server for MongoDB cho miễn phí (theo Percona, chưa verify).

4. Đảm bảo và giới hạn

ThứĐảm bảoKhông đảm bảo / giới hạn
mongodump --oplogMột thời điểm nhất quán của cả replica set (lúc dump xong)Không nhất quán xuyên shard; không dump local; restore phải build lại index; RPO = khoảng giữa 2 lần dump
Snapshot volumeNhư sau crash: checkpoint + journal (bài 6)Chỉ khi atomic cả data lẫn journal; không thì fsyncLock
PITR bằng oplogDừng đúng trước một entry (--oplogLimit loại trừ entry có ts đó)Chỉ trong cửa sổ oplog; một applyOps là một khối, không cắt giữa; write sau mốc mất hết
Rolling upgradeCụm vẫn ghi được (lab: 0 lỗi w:majority)Chỉ đi từng bản lớn (7.0 → 9.0 bị chặn); lùi binary sau khi đổi FCV thì mongod không khởi động
Tắt mongod (SIGTERM)Quiesce 15 s (shutdownTimeoutMillisForSignaledShutdown) cho request đang chạy và driver dời điTrình quản lý process giết sớm hơn 15 s = tắt không sạch
Số kết nốinet.maxIncomingConnections: từ 8.1 (và 8.0.16, 7.0.31) mặc định Linux = (RLIMIT_NOFILE / 2) × 0,8 (docs)Vượt trần → server đóng kết nối, client chỉ thấy connection closed (mục 5.2); mỗi kết nối 1 thread, ~316 KB
FTDCMetric mỗi giây, xoay vòng trong 500 MBKhông phải hệ cảnh báo

Khung lý thuyết: backup là bài toán RPO/RTO. mongodump hằng đêm: RPO tới 24 giờ, RTO = restore + build index (lab 84,9 s cho 1,7 GB; ví dụ minh hoạ 500 GB tuyến tính ≈ 7 giờ, chưa verify). PITR: RPO vài giây, RTO = snapshot + replay. Replica set không phải backup: lệnh xoá nhầm được replicate trong vài ms; chỉ node trễ (secondaryDelaySecs, bài 7) cho thêm thời gian.

5. Failure modes

5.1 Đĩa đầy: mongod tự chết, và có thể không lên lại được

Lab out/11-diskfull-run1.txt: /data/db là tmpfs 400 MB, đặt trước file ballast 40 MB (file "đệm" để xoá khi cần); journal cấp sẵn đã chiếm 200 MB. Ghi 8 KB/document: sau 11 400 document đã ack, client nhận connection … closed rồi ECONNREFUSED. mongod không từ chối ghi cho lịch sự — nó abort:

"s":"E","c":"WT",…"error":28,…"session_name":"eviction-server",…"msg":"int __posix_file_write(…
"s":"E","c":"WT",…"error":-31804,…"msg":"int __evict_thread_run(…):121:the process must exit …
"s":"F","c":"ASSERT","id":23089,…"msg":"Fatal assertion","attr":{"msgid":50853,…wiredtiger_util.cpp:659:45…
"s":"F","c":"CONTROL","id":6384300,…"msg":"Writing fatal message","attr":{"message":"Got signal: 6 (Aborted).\n"}}

Lỗi 28 là ENOSPC: thread eviction không ghi được trang nên WiredTiger panic. Xoá ballast 40 MB rồi chạy lại: Detected unclean shutdown, Recovering data from the last clean checkpoint, rồi lại lỗi 28 và chết dù df chỉ 80% — khởi động cần chỗ cho journal mới cộng phần ghi của recovery. Lần 2 (out/11-diskfull.txt, tmpfs 600 MB, ballast 150 MB): chết sau 14 100 document đã ack; xoá ballast → mongod lên lại primary sau 2,3 s (recovery từ checkpoint + journal), còn 14 066 document — 34 write w:1 đã ack bị mất (cửa sổ journal ~100 ms, bài 6).

Bẫy: (1) không có chế độ "chỉ đọc khi gần đầy" — cảnh báo từ lúc còn 25 % trống; (2) ballast nhỏ hơn journal là vô dụng — tốt hơn là volume nở nóng được (EBS/LVM); (3) ba node cùng cỡ đĩa đầy gần cùng lúc, replica set không cứu; (4) nguồn phình hay quên: file tạm index build, spill của aggregation (bài 5), diagnostic.data 500 MB, log không xoay vòng, xoá không trả dung lượng (bài 6).

5.2 Bão kết nối

Kết nối trên từng node khi app khởi động và khi primary chết: PHP so với Node Biểu đồ trên: 200 worker PHP khởi động làm mỗi node nhận thêm 200 kết nối trong 1 giây; khi primary r1 bị kill, r2 không tăng kết nối nhưng 2 000 request lỗi; r1 sống lại gần như không ai nối lại. Biểu đồ dưới: 1 process Node, request chờ primary mới rồi pool mở thêm khoảng 22 kết nối vào r2 trong 2 giây, và lần nữa khi r1 giành lại primary. PHP: 200 worker, mỗi worker 1 client (out/11-storm.txt) Node: 1 process, maxPoolSize 100, 100 request đồng thời (out/11-storm-node.txt) 2000 500 kill -9 r1start r1 kill -9 r1start r1 r2r1 r2r1 12345 1 · 200 worker PHP khởi động: +200 kết nối trên MỖI node trong 1 s (600 cả cụm), ping đầu p50 492 ms 2 · kill -9 r1: r2, r3 không thêm kết nối (PHP đã nối sẵn mọi node) nhưng 2 000 request lỗi 3 · r1 sống lại: chỉ 8–17 kết nối — worker PHP chưa quét lại topology 4 · Node: request chờ ~11 s (0 lỗi) rồi ập vào primary mới r2: +22 kết nối trong 2 s, tổng +38 5 · r1 (priority 2) giành lại primary: pool mở thêm 28 kết nối vào r1 trong 4 s
Hình 4 — Bão kết nối có hai dạng, đo trên rs0 3 node. Với PHP-FPM, bão nằm ở lúc khởi động / deploy: mọi worker cùng mở kết nối tới mọi node; failover không thêm kết nối nhưng request lỗi ngay (serverSelectionTryOnce). Với Node, failover làm request dồn lại rồi pool mở hàng loạt kết nối vào primary mới — tỉ lệ với số request đồng thời, không với số process.

Với net.maxIncomingConnections = 40 (lab, node riêng) và 60 worker PHP cùng nối: 40 thành công, 20 lỗi. Phía server log "Connection refused because there are too many open connections" và connections.rejected = 20; phía PHP chỉ thấy:

MongoDB\Driver\Exception\ConnectionTimeoutException: No suitable servers found (`serverSelectionTryOnce` set):
  [connection closed calling hello on 'tmp-mdb-mc:27017']. Topology type: ReplicaSetNoPrimary

Phía app không có chữ "too many connections" nào — người trực dễ đi tìm lỗi mạng. Bẫy: (1) deploy cả fleet PHP cùng lúc = mọi worker × mọi node trong 1 s (lab 600), cộng CPU handshake TLS + SCRAM (mục 3.5) → deploy theo đợt; (2) PHP lỗi ngay khi không có primary (serverSelectionTryOnce): 2 000 request lỗi, còn Node chờ serverSelectionTimeoutMS nên 0 lỗi nhưng treo ~11 s (bài 7 mục 5.5); (3) maxConns thấp để "bảo vệ" DB mà quên kết nối nội bộ (13–18 mỗi node lúc rảnh) và kết nối giám sát của mọi client.

5.3 Nâng cấp hỏng giữa chừng

Lùi binary sau khi đổi FCV → mongod thoát (ExitCode=100/62); lùi đúng là hạ FCV trước (lab 54 ms). Nhảy cóc 7.0 → 9.0 → không khởi động: phải đi 7.0 → 8.0 → 9.0, mỗi bước có burn-in. Đổi FCV khi một node đang initial sync / ROLLBACK / RECOVERING bị docs cấm; setFCV còn chờ cấu hình mới tới đa số node. Driver phải nằm trong ma trận tương thích của server mới — kiểm trước.

5.4 Docker và Kubernetes giết mongod giữa lúc quiesce

Lab out/11-quiesce.txt (rs0 3 node): SIGTERM → "Entering quiesce mode for shutdown","attr":{"quiesceTimeMillis":14984}. docker stop mặc định chờ 10 s → SIGKILL, ExitCode=137, lần sau khởi động log … after unclean shutdown. Với -t 60: 15,5 s, exit 0 (quiesceModeMillis 14 999); primary chưa stepDown cũng 15,4 s. Replica set 1 node thì quiesce = 0 (tắt trong 0,4 s) — lab 1 node không bao giờ lộ bẫy này. Chữa: stop_grace_period / terminationGracePeriodSeconds ≥ 15 s + thời gian checkpoint cuối, hoặc hạ shutdownTimeoutMillisForSignaledShutdown có chủ đích. Tắt không sạch thì index build đang dở chạy lại từ đầu (bài 3).

5.5 Database mở ra internet

Hai lớp mặc định cộng lại thành sự cố 2017: image không auth (mục 3.5) + docker run -p 27017:27017 publish ra mọi interface của host — và "Docker and ufw use firewall rules in ways that make them incompatible with each other": traffic tới port đã publish bị chuyển hướng trước khi qua rule của ufw (Docker docs; chưa dựng lại trong lab vì phải mở port thật ra ngoài). Ai quét thấy port là đọc, xoá, và — như lab — tạo được user root của riêng họ, nên đặt mật khẩu sau đó cũng không đuổi được. Bẫy:

  1. Thêm MONGO_INITDB_ROOT_* cho volume đã có data: entrypoint vẫn bật --auth nhưng bỏ qua bước tạo user (chỉ chạy khi thư mục data chưa khởi tạo — docker-entrypoint.sh dòng 257–301) → không ai đăng nhập được bằng user mới; user "lạ" kẻ tấn công đã tạo thì vẫn còn (đọc từ script, chưa chạy riêng).
  2. readWrite drop được collection và tạo index trên production (lab). Một lỗi code / một injection đủ quyền xoá cả bảng.
  3. App nối bằng user root "cho tiện": lộ một chuỗi kết nối là mất cả cụm. Mở port "debug tạm": dùng -p 127.0.0.1:27017:27017 hoặc SSH tunnel.

6. Vận hành trong production

6.1 Bốn đèn thành cảnh báo

Tên metric lấy thật từ percona/mongodb_exporter 0.53.0 (out/11-exporter.txt); ngưỡng là điểm xuất phát, chỉnh theo số đo của bạn:

ĐènBiểu thức (PromQL)Cảnh báo khi
1. Cachemongodb_ss_wt_cache_tracked_dirty_bytes_in_the_cache / mongodb_ss_wt_cache_maximum_bytes_configured; rate(mongodb_ss_wt_cache_application_thread_time_evicting_usecs[5m])dirty > 0,15 trong 10 phút; thread request evict > 0 kéo dài (bài 6)
2. Cửa sổ oplogmongodb_mongod_replset_oplog_head_timestamp - mongodb_mongod_replset_oplog_tail_timestamp (giây)< 2 × (sự cố/bảo trì dài nhất + initial sync), đo ở giờ ghi cao nhất
3. Commit pointtime() * 1000 - mongodb_rs_optimes_lastCommittedWallTime; mongodb_ss_flowControl_isLagged> 10 000 ms trong 2 phút; isLagged = 1 (bài 7)
4. Kết nốimongodb_ss_connections{conn_type="current"} / (current + available); increase(…{conn_type="rejected"}[5m])> 0,7; rejected > 0; current tăng bậc thang sau mỗi deploy
Đĩa (node-exporter)node_filesystem_avail_bytes của volume data< 25 % — đủ thời gian nở volume trước khi mongod abort (mục 5.1)

Ví dụ đèn 2 trong chính lab: oplog 2 691 MB nhưng ngay sau khi seed + restore 5,8 triệu document, cửa sổ chỉ còn 0,27 giờ (964 s) — một job nạp dữ liệu lớn "ăn" cửa sổ của cả ngày. Cảnh báo theo giờ, không theo MB.

6.2 Capacity planning — công thức gom từ series

6.3 Nâng cấp và vá

Patch trong nhánh: đọc release notes (9.0.4 sửa 5 CVE), rolling restart, không đụng FCV. Bản lớn: staging có dữ liệu thật → kiểm driver, chạy lại explain các truy vấn chính (luật optimizer đổi theo bản — bài 5) → production theo Hình 2 với grace period ≥ 15 s → burn-in vài ngày tới vài tuần với FCV cũ → setFCV khi mọi node khoẻ. Sharded: config server → từng shard → mongos → FCV, dừng balancer khi nâng (bài 9).

6.4 Chi phí: self-host vs Atlas — ví dụ minh hoạ

Giá tra ngày 18/10/2026, AWS us-east-1, on-demand; để so cách tính, không phải báo giá:

KhoảnAtlas M30 (8 GB RAM, 2 vCPU, 40 GB)Self-host 3 × m6i.large (8 GiB, 2 vCPU)
Compute$0,54/giờ cho cả cụm 3 node ≈ $394/tháng (pricing; cụm M10+ = 3 instance, "72 server hours per calendar day" theo MongoDB trả lời trên forum) 3 × $0,096/giờ ≈ $210/tháng (vantage); reserved 1 năm $0,064/giờ
Đĩa, backup, trafficTính riêng; backup $0,14–0,19/GB-tháng trên AWS (Atlas billing)EBS × 3, snapshot, traffic giữa AZ — tự cộng
NgườiPITR, monitoring, nâng cấp có sẵnTự dựng PBM, exporter, runbook, người trực — thường là khoản lớn nhất

Chênh ~$180/tháng ở cỡ này — ít hơn một ngày công kỹ sư. Self-host thắng rõ khi cụm lớn và đã có người vận hành; cụm nhỏ, đội nhỏ thì managed thường rẻ hơn khi tính cả người.

6.5 Gánh nặng cho team

Người trực cần đọc được bốn đèn + FTDC, rs.status(), slow log và plan cache, oplog và rollback, và với sharded thì balancer/orphan. Runbook đã có: query chậm đột ngột (bài 4), p99 vọt (bài 6), mất primary / file rollback (bài 7), HistoryLost (bài 10); bài này thêm: đĩa đầy (nở volume, đừng chỉ xoá ballast), bão kết nối (deploy theo đợt), nâng cấp hỏng (hạ FCV trước khi lùi binary).

7. So sánh và ma trận quyết định

7.1 Chạy "MongoDB API" ở đâu

Lựa chọnEngineLicense / vendorTương thíchGhi chú vận hành
MongoDB Community self-hostMongoDBSSPL100 %Thiếu PITR, audit, encryption at rest
Percona Server for MongoDBDựng lại từ source MongoDBSSPL; hỗ trợ trả phí tuỳ chọnDrop-in (theo Percona)Audit, LDAP, encryption at rest, PBM cho PITR — chưa verify
AtlasMongoDBDịch vụ của MongoDB Inc.100 % + tính năng riêngLock-in ở tính năng riêng (mục 8)
AWS DocumentDB 8.0 (11/2025)Engine riêng của AWSĐóng"wire protocol compatibility with MongoDB 8.0" (AWS)Không có admin/local, $natural: -1 lỗi, retryable writes từ engine 8.0.2 (docs) — chưa verify
Azure DocumentDB (GA 18/11/2025, trước là Cosmos DB for MongoDB vCore)PostgreSQL + DocumentDB (MIT, Linux Foundation)Managed Azure"99.03% MongoDB compatibility" (Microsoft)Cùng engine với FerretDB — chưa verify
FerretDB 2.7.0 + DocumentDB 0.107PostgreSQL 17Apache 2.0; bản phát hành cuối 11/2025Lab: 23/28, 5 lỗi + 2 khác im lặngKhông transaction, change stream; RAM rảnh ~90 MB

Lab out/11-ferret.txt — cùng js/11-compat.js, MongoDB 9.0.2 được 28/28:

LỖI  transaction (bài 8) → CommandNotFound (59): no such command: 'commitTransaction'
LỖI  change stream watch() (bài 10) → CommandNotSupported (115): Stage $changeStream is not supported yet in native pipeline
LỖI  $where / $function (JS phía server) → CommandNotSupported (115): Operator $function not implemented yet
OK   $jsonSchema validator chặn ghi sai (bài 2) → KHÔNG chặn: insert sai kiểu vẫn vào
OK   capped collection → isCapped=false, còn 5/5 doc (Mongo: 3)
OK   explain dùng index? → {"queryPlanner":{"Plan":{"Node Type":"Seq Scan",…   ← plan của PostgreSQL

(Hai lỗi còn lại: replSetGetStatus, $queryStats — công cụ giám sát của mục 6 không dùng được.) Hai dòng "OK" cuối nguy hiểm hơn lỗi: lệnh được nhận, không báo gì, nhưng không làm điều nó hứa. CRUD, update operator, ngữ nghĩa mảng/null của bài 1, $lookup, $facet, $setWindowFields, $merge, unique/TTL/text/2dsphere đều chạy đúng.

7.2 MongoDB so với các họ database khác

TrụcMongoDBPostgreSQL + JSONBDynamoDBCassandraElasticsearch
Mạnh nhất ởĐọc/ghi trọn một aggregate, index phụ phong phú, shardQuan hệ, ràng buộc, transaction; JSONB cho phần độngKey-value ở mọi quy mô, latency ổn địnhGhi rất nhiều, nhiều DCFull-text, log, phân tích gần thời gian thực
Yếu ởJoin tuỳ ý, report ad-hoc (bài 5: 31–60 s vs 3,5 s)Scale ghi ngang (tự shard/Citus)Item ≤ 400 KB, 3 000 RCU / 1 000 WCU mỗi partition (docs)Không join, mô hình theo từng truy vấnKhông phải nơi lưu chính, không transaction
Độ phức tạp vận hànhTrung bình (replica set); cao khi shardThấp–trung bình, đội Laravel đã quenRất thấp (serverless) — đổi lại lock-in AWSCao (repair, compaction, tombstone)Cao (shard, heap JVM, mapping)
Độ trưởng thànhMở nguồn 2009; transaction từ 4.0 (2018)PostgreSQL từ 1996, JSONB từ 9.4 (2014)Dịch vụ AWS từ 2012Mở nguồn 2008Từ 2010
Chi phíAtlas hoặc người vận hànhThấp, nhiều nhà cung cấpTheo request — rẻ khi nhỏ, đắt khi scanNhiều node tối thiểuRAM/đĩa lớn
Hệ sinh thái Laravel/PHPlaravel-mongodb (có bẫy: bài 8)Hạng nhấtSDK AWS, ít ORMYếuScout/client chính thức
Tuyển người ở Việt Nam (nhận định)Dev dùng được nhiều; người vận hành sâu ítDễ nhấtCó ở công ty dùng AWSHiếmTrung bình

Dùng MongoDB khi: dữ liệu tự nhiên là document đọc/ghi trọn (catalog nhiều thuộc tính, giỏ hàng, hồ sơ, nội dung, event có cấu trúc thay đổi), access pattern biết trước, cần index phụ phong phú hơn key-value, có thể cần scale ghi ngang, và đội có người vận hành hoặc ngân sách Atlas. Tránh khi: nghiệp vụ xoay quanh join, ràng buộc và transaction nhiều thực thể (sổ cái, thanh toán, tồn kho đối soát), báo cáo ad-hoc là chính, đội đã mạnh PostgreSQL và JSONB đủ dùng, hoặc không ai trực mà cũng không trả tiền managed. Kiểu chia hay gặp nhất trong LMS + e-commerce: PostgreSQL cho đơn/thanh toán/tồn kho, MongoDB (hoặc JSONB) cho catalog và nội dung, đồng bộ bằng CDC (bài 10).

8. Góc nhìn tech lead: tác động tổ chức

ADR-xxx: Dùng MongoDB cho catalog sản phẩm và nội dung khoá học
Bối cảnh. Catalog 30 nhóm hàng, mỗi nhóm 3–6 thuộc tính riêng, đổi hằng tuần; trang sản phẩm đọc trọn 1 document. Đơn hàng, thanh toán, tồn kho cần transaction nhiều bảng và báo cáo kế toán.
Quyết định. Catalog + nội dung trên MongoDB (Atlas M30, 3 AZ); đơn hàng, thanh toán, tồn kho trên PostgreSQL; đồng bộ một chiều bằng outbox + change stream.
Hệ quả. Hai hệ thống phải trực; truy vấn MongoDB đi qua lớp repository; role app tự định nghĩa; cảnh báo bốn đèn + đĩa; mỗi quý restore thử một bản backup, đo RTO.
Exit strategy. Không đặt đường đi chính lên tính năng chỉ có trên Atlas; mỗi năm chạy 11-compat.js (mở rộng bằng truy vấn thật) trên ứng viên thay thế; rời đi = CDC sang đích mới, chạy song song, so số liệu, cắt dần theo collection.

9. Anti-patterns và bài học thực tế

10. Câu hỏi phụ interviewer hay hỏi tiếp

Tổng hợp cả series — mỗi câu trả lời ngắn, chi tiết và số đo ở bài được link.

Document model khác bảng quan hệ ở cơ chế nào?
Document là đơn vị đọc, ghi atomic, khoá và phân tán; embed, giới hạn 16 MB, transaction-là-ngoại-lệ đều là hệ quả. Thiết kế theo access pattern, không theo thực thể — bài 1, bài 2.
Vì sao {scores: {$gt: 5, $lt: 3}} khớp [1, 10]? {x: null} khớp gì?
Mỗi điều kiện trên mảng có thể do phần tử khác nhau thoả; muốn một phần tử thoả cả hai thì $elemMatch. null khớp cả field thiếu — bài 1.
Operator injection là gì, chặn ở đâu?
Input {"$ne": null} lọt vào filter thành toán tử; chặn bằng validate kiểu ở biên, không tin package — bài 1.
Embed hay reference?
Embed khi phần con luôn đọc cùng cha và có trần nhỏ; reference khi tăng không giới hạn hoặc sửa độc lập — bài 2.
Đổi schema khi không có ALTER TABLE?
Schema version trong document, code đọc hai dạng, backfill theo lô có hãm tốc (oplog!), đếm về 0 rồi mới xoá code cũ — bài 2.
ESR là gì, khi nào sai? Thêm index tốn gì?
Equality → Sort → Range để tránh sort trong RAM; sai khi range rất chọn lọc. Mỗi index thêm một lần chèn B-tree mỗi lần ghi: 10 index làm insert chậm 4,28 lần — bài 3.
Query hôm qua nhanh, hôm nay chậm?
Plan cache theo query shape; giá trị đến trước quyết định plan; restart/tạo index xoá cache. Ghim bằng query settings — bài 4.
Phân trang skip có vấn đề gì?
Skip vẫn đi qua N key (trang 100 000 đọc 2 triệu key) và trùng/sót khi dữ liệu đổi; dùng keyset (createdAt, _id) — bài 4.
$lookup chậm khi nào? Giới hạn 100 MB là gì?
Thiếu index ở foreignField → N×M; 100 MB là trần mỗi stage chặn trước khi spill, RAM thật có thể gấp ~6 — bài 5.
Vì sao p99 vọt khi có job ghi hàng loạt, dù đọc collection khác?
Dirty chạm 20 % cache → thread của mọi request bị kéo đi evict; cache to hơn chưa chắc cứu — bài 6.
w:1, j:true, majority khác gì khi node chết?
w:1 mất write đã ack (lab 9–302 khi kill; 34 khi đĩa đầy ở bài này); j:true/majority mất 0 — bài 6, bài 7.
Primary chết thì sao?
~10–11 s không ghi được; write w:1 chưa tới đa số bị rollback ra file .bson; app phải retry idempotent — bài 7.
PSA (primary-secondary-arbiter) mất một node?
Vẫn có primary nhưng majority không commit được: w:"majority" treo, flow control hãm ghi, change stream đứng — bài 7.
Snapshot isolation có chặn write skew không?
Không: hai transaction đọc chung điều kiện rồi ghi document khác nhau đều commit (lab 30 chỗ thành 31); PostgreSQL SERIALIZABLE chặn được — bài 8.
Side effect trong withTransaction?
Callback có thể chạy nhiều lần khi retry (lab 714 email cho 161 đơn); laravel-mongodb 5.11 còn báo thành công cho transaction đã huỷ — bài 8.
Chọn shard key cho orders? Khi nào không nên shard?
Theo truy vấn chính ({customerId, _id}); ObjectId tăng dần dồn 100 % insert vào một shard. Chưa shard khi dữ liệu dưới vài TB, chưa scale dọc hết — bài 9.
Change stream có exactly-once không? Outbox để làm gì?
At-least-once; token rơi khỏi oplog → ChangeStreamHistoryLost, phải đồng bộ lại. Dual write mất im lặng (7/3 000); outbox cùng transaction + relay idempotent thì không mất, có thể trùng — bài 10.
Theo dõi MongoDB production bằng metric nào?
Bốn đèn: cache dirty/evict, cửa sổ oplog theo giờ, độ trễ commit point, kết nối (current/available/rejected) — cộng đĩa trống. FTDC giữ số từng giây cho điều tra sau — mục 2, 6.1.
Chiến lược backup? RPO/RTO bao nhiêu?
Dump cho dữ liệu nhỏ; snapshot volume cho nhanh; PITR = snapshot + oplog. RTO phải đo: restore chậm ~6 lần dump, replay ~280 entry/s — mục 3.3.
Nâng 8.0 lên 9.0 thế nào, lùi được không?
Rolling: secondary, stepDown, primary, burn-in, rồi setFCV "9.0" + confirm. Lùi binary chỉ được khi FCV còn cũ; sau đó phải hạ FCV trước — mục 3.4.
Laravel trên PHP-FPM mở bao nhiêu kết nối?
Mỗi worker một kết nối tới mỗi node, giữ suốt đời process: 20 server × 50 worker = 1 000 kết nối mỗi node, ~316 KB mỗi cái. Bão nằm ở lúc deploy — mục 3.2, 5.2.
Đĩa đầy thì mongod làm gì?
Abort (lỗi 28, fassert) chứ không chuyển sang chỉ đọc; phải giải phóng đủ chỗ cho journal mới mới lên lại được; write w:1 cuối có thể mất — mục 5.1.
Bảo mật tối thiểu cho MongoDB của app?
Bật auth, không publish port ra ngoài, TLS, role tự định nghĩa (không readWrite — nó drop được collection), vá kịp (MongoBleed) — mục 3.5, 9.
SSPL ảnh hưởng gì? Exit strategy?
Dùng làm backend thì không sao; đường lui cùng code chỉ có Percona, còn lại là engine khác (DocumentDB, FerretDB: lab 23/28, thiếu transaction/change stream). Exit = repository layer + CDC + đo tương thích — mục 7, 8.
Atlas hay self-host?
Atlas khi đội nhỏ, không người trực (M30 ~$394/tháng vs ~$210 compute tự chạy — chênh ít hơn một ngày công); self-host khi cụm lớn và có người vận hành — mục 6.4.

Phụ lục: lab đã chạy

Môi trường: MongoDB 9.0.2 (bản chính của series), 9.0.4 (đích nâng cấp, ra 13/10/2026), 8.0.32, 7.0.45 (thử nhảy cóc); mỗi mongod --memory 1g, cache 0,25 GB, nofile 64 000; database tools 100.19.1; Node 22 + driver mongodb 7.7.0; PHP 8.3 + ext-mongodb 2.5.3 + mongodb/mongodb; percona/mongodb_exporter 0.53.0; FerretDB 2.7.0 + postgres-documentdb:17-0.107.0. Máy 4 vCPU dùng chung. Container tạm tmp-mdb-* trên network tmp-mdb-net, đã xoá hết sau lab. Chạy lại:

cd labs/mongodb
./11-van-hanh.sh                 # tất cả (~70 phút), hoặc từng phần:
./11-van-hanh.sh upgrade|skip|pitr|snapshot|metrics|exporter|quiesce|conn|storm|stormnode|diskfull|security|ferret
DF_SIZE=400m DF_BALLAST=40M ./11-van-hanh.sh diskfull    # lần 1: xoá ballast vẫn không lên lại

Output: out/11-*.txt (11-pitr-tools.txt: log đầy đủ mongodump/mongorestore; 11-diskfull-run1.txt: lần 1). Script: 11-van-hanh.sh, js/11-compat.js, js/11-fill.js, node/11-{writer,gaps,pool,connmon,ftdc}.mjs, laravel/11-conn.php.

Bẫy khi dựng lab: image mongo chạy lệnh mongo* bằng user mongodb → --entrypoint "" khi ghi volume của root; thay image mà giữ dữ liệu cần named volume; lần scrape đầu của exporter chỉ có 48 series; minPoolSize lớn mở kết nối tới mọi node; sửa file .sh đang chạy làm bash đọc lệch (gặp lại lỗi của bài 4).

Chưa verify: Percona Backup for MongoDB, Ops Manager, Atlas (giá, backup, tính năng); snapshot LVM/EBS thật (lab dùng fsyncLock + tar); AWS DocumentDB và Azure DocumentDB bản managed; Keyhole/thư viện Go đọc FTDC; nâng cấp sharded cluster; TLS (chỉ đo SCRAM); exploit MongoBleed (chỉ kiểm cấu hình zlib); quy mô lớn (restore nhiều trăm GB, hàng nghìn kết nối thật từ nhiều máy).