Durability & Consistency: lời hứa của `acknowledged: true`

33 phút đọcSeries: MongoDB: từ gốc đến internals

Ba bài đã qua để lại cùng một câu hỏi, mỗi bài hỏi từ một phía. Bài CRUD & Query Model dừng ở { acknowledged: true }: dữ liệu đã bền tới đâu, đã vào journal chưa, đã sang secondary chưa? Bài Transactions & Atomicity commit bằng w: "majority" mà chưa nói từng mức write concern hứa gì. Bài Isolation & Snapshot cho thấy read concern majority có thể cũ hơn những gì primary đã áp dụng, và để ngỏ chuyện đọc lại lệnh ghi của chính mình qua secondary hay qua failover.

Bài này trả lời cả ba bằng thí nghiệm: đo cái giá của từng mức, làm mất một lệnh ghi đã được xác nhận rồi tìm nó trong file rollback, đọc từ một secondary chậm 1 giây để thấy lệnh ghi của mình biến mất, và bắt một lệnh ghi bị timeout rồi được driver gửi lại mà vẫn chỉ chạy một lần.

Bài này nằm ở đâu

  • Cần biết trước: Transactions & Atomicity (replica set 3 node, majority là 2, session, w: "majority" khi commit), Isolation & Snapshot (read concern local/majority/snapshot, majority không phải snapshot), CRUD & Query Model (kết quả acknowledged)
  • Giới thiệu: write concern (w, j, wtimeout) và mặc định của nó, lỗi timeout, rollback nhìn từ phía ứng dụng, read concern majority đi cùng write concern, causal consistency qua session (afterClusterTime), retryable writes và reads, synchronous_commit của PostgreSQL, chọn mức theo loại dữ liệu
  • Không dạy ở đây: journal hoạt động thế nào để giữ lời hứa j: true (bài Journal & Checkpoint); majority commit point di chuyển ra sao khi failover (bài Elections, Failover & Majority). Bài này chỉ nói chúng hứa gì với ứng dụng và trỏ sang đó
  • Dẫn tới: Write Conflicts, Retry & Idempotency

Môi trường lab: MongoDB 8.3.11 trong Docker (image mongo:8), replica set 3 node tên rs18: ba container mongo-rs18-a / -b / -c trên một máy host Apple M4, mỗi container 1 CPU, 1 GB RAM, WiredTiger cache 0,25 GB, database lab18; a có priority 2 nên là primary lúc bắt đầu. Client là mongosh 2.12.0 trong các container ngắn hạn cùng network. Ba node chung một máy nên mạng giữa chúng gần như bằng không: mọi số đo có chờ majority là cận dưới, và ổ đĩa là ổ Docker trên macOS, không phải ổ production. Mọi số trong bài đều đo thật và có file output thô kèm theo.

Nhãn: [tài liệu] theo tài liệu chính thức; [quan sát] đo trong lab này (MongoDB 8.3.11); [suy luận] rút ra từ quan sát, không kiểm tra riêng; [chi tiết cài đặt] cách server đang làm, không phải cam kết API; [hình dung] mô hình để dễ nhớ.

Giải thích trong 30 giây

acknowledged: true chỉ nói rằng server đã làm xong phần việc mà bạn yêu cầu trước khi trả lời. Yêu cầu càng ít thì lời hứa càng yếu. Với w: 1, phần việc đó chỉ là "primary đã áp dụng lệnh ghi"; nếu primary mất ngay sau đó, lệnh ghi có thể biến mất dù ứng dụng đã nhận acknowledged: true.

Chữ D (durability) trong ACID là một lời hứa nguyên khối ở database một máy. MongoDB tách nó thành ba núm bạn tự chọn: bao nhiêu node đã có dữ liệu (w), node đó đã ghi xuống journal trên đĩa chưa (j), và chờ tối đa bao lâu (wtimeout). Phía đọc có núm riêng (read concern), và ở giữa là causal consistency để "đọc lại điều mình vừa ghi" không phụ thuộc may rủi. Cuối cùng, mạng có thể đứt đúng lúc lệnh ghi đã chạy mà câu trả lời chưa tới: retryable writes giải quyết đúng chỗ đó.

Hình dung trước: gửi một lá thư quan trọng

Bạn gửi một lá thư quan trọng. Công ty có ba bưu cục: một bưu cục chính nhận thư, hai chi nhánh nhận bản sao sau đó. Bạn có thể dặn nhân viên ở quầy:

  • "Báo tôi khi thư đã nằm trên bàn anh." Bưu cục chính cháy ngay lúc đó thì thư mất.
  • "Ghi thư vào sổ nhật ký rồi hãy báo." Mất điện rồi bật lại vẫn tra ra thư, nhưng cháy bưu cục thì sổ cũng cháy.
  • "Chỉ báo khi thư đã tới ít nhất hai trong ba bưu cục." Chậm hơn chút, nhưng một bưu cục cháy thì thư vẫn còn nơi khác.
  • "Chỉ báo khi cả ba đã có thư." An toàn nhất, nhưng một chi nhánh đóng cửa là bạn đứng chờ mãi.
Bưu điện                                MongoDB
─────────────────────────────────       ───────────────────────────────────────
nhân viên nói "xong rồi"                acknowledged: true
thư nằm trên bàn nhân viên              primary đã áp dụng (w: 1)
ghi vào sổ nhật ký                      journal trên đĩa (j: true)
thư tới 2 trong 3 bưu cục               w: "majority" (3 node: primary + 1 secondary)
thư tới cả 3 bưu cục                    w: 3
chờ tối đa 30 phút rồi báo "chưa kịp"   wtimeout
bưu cục chính cháy                      primary mất, secondary lên thay

[hình dung] Đây chỉ là cách hình dung. Thư thật chỉ ở một nơi; primary giữ bản của nó và chép tiếp sang secondary. "Báo chưa kịp" không huỷ thư: lệnh ghi vẫn nằm đó và có thể tới nơi sau (lab 2). Còn khi bưu cục chính cháy rồi quay lại, thư chỉ có ở đó bị dọn khỏi dữ liệu chính và đặt vào một file riêng (lab 3). Ghi sổ nhật ký ở MongoDB là một cơ chế riêng với giá riêng, nằm ở bài Journal & Checkpoint.

Write concern: ba núm vặn

Write concern là tham số của lệnh ghi: "hãy chỉ trả lời khi đã làm được ngần này việc". Dạng đầy đủ:

{ w: <số node | "majority">, j: <true | false>, wtimeout: <ms> }

w: bao nhiêu node

[tài liệu]

wServer trả lời khiGhi chú
0không chờ xác nhậnlỗi chỉ lộ ra nếu là lỗi mạng; không retry được (mục "Retryable writes và retryable reads")
1primary đã áp dụng lệnh ghidữ liệu có thể bị rollback nếu primary mất trước khi chép sang secondary nào
2, 3, ...primary và đủ secondary mang dữ liệu để đạt con số đów: 3 trên 3 node: mất một node là lệnh ghi không bao giờ xong
"majority"đa số các member có quyền bầu và mang dữ liệu đã có lệnh ghi3 node: 2; không bị rollback khi failover

Hai điều hay bị bỏ sót. Một: member ẩn, delayed hay priority 0 (có quyền bầu) vẫn xác nhận được majority; delayed secondary chỉ xác nhận được sau ít nhất secondaryDelaySecs. Hai: ở P-S-A (primary, secondary, arbiter), majority bằng đúng số node mang dữ liệu, nên khi một node dữ liệu mất, w: "majority" có thể timeout hoặc không bao giờ xong vì arbiter không giữ dữ liệu.

j: đã ghi journal chưa

[tài liệu] j: true yêu cầu các node được đếm trong w đã ghi lệnh ghi xuống journal trên đĩa trước khi xác nhận. Bản cũ chỉ đòi primary; bản hiện tại đòi mọi node được đếm trong w. Tài liệu viết rõ: j: true một mình không bảo đảm lệnh ghi không bị rollback khi primary failover. Journal bảo vệ trước việc một node tắt đột ngột; majority bảo vệ trước việc một node biến mất. Journal hoạt động ra sao: bài Journal & Checkpoint. Hệ quả với j, theo bảng của tài liệu:

wj không ghij: truej: false
1ghi vào bộ nhớ rồi xác nhậnjournal trên đĩabộ nhớ
"majority"journal trên đĩa (nếu writeConcernMajorityJournalDefault: true, là mặc định)journal trên đĩabộ nhớ; nhưng nếu writeConcernMajorityJournalDefault: true thì lệnh ghi majority vẫn chỉ được xác nhận sau khi journal đã flush

Tức là với cấu hình mặc định, w: "majority" đã ngầm có j: true. Nếu ai đó đặt writeConcernMajorityJournalDefault: false, lệnh ghi majority có thể bị rollback khi đa số node cùng crash rồi khởi động lại.

wtimeout: chờ bao lâu

[tài liệu] wtimeout là thời gian tối đa (ms) để lệnh ghi lan tới đủ node, tính sau khi lệnh ghi đã thành công trên primary. Không đặt wtimeout mà mức write concern không thể đạt thì lệnh ghi chờ vô hạn. wtimeout không áp dụng cho w <= 1. Điều tài liệu nhấn mạnh và lab 2 sẽ đo: khi hết giờ, MongoDB trả lỗi write concern nhưng không huỷ những gì đã ghi.

Mặc định, và những ngoại lệ phụ thuộc phiên bản

[tài liệu] Từ MongoDB 5.0, write concern mặc định ngầm của replica set và sharded cluster là { w: "majority" }. Ngoại lệ: nếu có arbiter và số node mang dữ liệu không lớn hơn majority của các node có quyền bầu, mặc định rơi về { w: 1 }. Ví dụ tài liệu đưa ra: 2 node dữ liệu + 1 arbiter → w: 1; 4 node dữ liệu + 1 arbiter → "majority". Có thể thay bằng setDefaultRWConcern; nếu đặt thì bắt buộc phải có w.

[quan sát] Lab đọc đúng như vậy (e0-config.out):

version 8.3.11
writeConcernMajorityJournalDefault true | settings.getLastErrorDefaults {"w":1,"wtimeout":0}
{
  w: 'majority',
  wtimeout: 0
}
defaultWriteConcernSource implicit
writeMajorityCount 2 votingMembersCount 3

w:1        -> {"acknowledged":true,"insertedId":1}
w:majority -> {"acknowledged":true,"insertedId":2}
j:true w:1 -> {"acknowledged":true,"insertedId":3}
default    -> {"acknowledged":true,"insertedId":4}

Bốn lệnh ghi, bốn lần acknowledged: true, giống hệt nhau. Ứng dụng không thể phân biệt lời hứa từ phản hồi; lời hứa nằm ở tham số bạn đã truyền. Nguồn của mặc định hiện hành xem bằng getDefaultRWConcern (implicit khi chưa ai đặt, global sau khi đặt bằng setDefaultRWConcern).

Bảng lời hứa tóm tắt, để các bài sau quay lại:

Cấu hìnhKhi acknowledged: true thìCòn có thể mất khi
w: 1primary đã áp dụng (trong bộ nhớ)primary tắt đột ngột (chưa journal) hoặc failover trước khi chép sang secondary
w: 1, j: trueprimary đã ghi journal trên đĩafailover trước khi chép sang secondary (lab 3)
w: "majority" (mặc định)đa số node đã có và đã ghi journalgần như không: phải mất cả đa số node cùng lúc và cả đĩa của chúng
w: 3 (3 node)cả ba node đã áp dụng (không đặt j thì chưa chắc đã journal)hiếm, nhưng chỉ cần một node mất là lệnh ghi treo

Setup và dataset

MongoDB version : 8.3.11 (mongo:8), replica set 3 node rs18, primary lúc đầu = mongo-rs18-a, FCV 8.3
Hardware        : Apple M4 host; mỗi node 1 CPU, 1 GB RAM
Configuration   : --replSet rs18 --wiredTigerCacheSizeGB 0.25; write concern mặc định ngầm w: "majority"
Dataset         : lab18.lat  insert 1 document nhỏ mỗi lần (tenantId, userId, status, createdAt, amount), index { tenantId: 1, createdAt: -1 }
                  lab18.wt, rb, cc, rcm, sd, rt: vài document cho từng kịch bản
Client          : mongosh 2.12.0, một client tuần tự, trừ khi ghi khác

Mọi script và output thô của bài nằm cạnh nhau trong thư mục lab (lab18/); tên file được ghi ở từng mục.

Lab 1: cái giá của từng mức

Một client tuần tự insertOne một document nhỏ, mỗi cấu hình 200 lần làm nóng rồi 1.000 lần đo, 5 lượt xen kẽ các cấu hình, bảng ghi median của p50 và p95 qua 5 lượt (e1-latency.js, e1-latency.out):

Write concernp50p95
w: 10,215 ms0,525 ms
w: 1, j: true0,495 ms0,981 ms
w: 20,558 ms0,846 ms
w: 30,579 ms0,710 ms
w: "majority", j: false0,605 ms0,877 ms
w: "majority" (không ghi j)0,618 ms0,955 ms
w: "majority", j: true0,668 ms1,189 ms
không đặt write concern (mặc định)0,627 ms0,908 ms

[quan sát]

  • w: 1 rẻ nhất: 0,215 ms. Thêm j: true cộng khoảng 0,28 ms ở p50: lệnh ghi chờ một lần ghi journal xuống đĩa của máy này.
  • Chờ thêm một secondary (w: 2) hoặc majority cộng khoảng 0,35–0,40 ms so với w: 1 ở đây, nơi ba node chung một máy và mạng gần bằng không. Trên production mỗi lần chờ majority còn cộng ít nhất một vòng mạng giữa các node.
  • Không đặt write concern cho kết quả như majority (0,627 so với 0,605–0,668): đúng như tài liệu nói về mặc định ngầm.
  • Không rút ra được j khác nhau thế nào trong majority. Hiệu số 0,605 / 0,618 / 0,668 nhỏ hơn dao động giữa các lượt của chính một cấu hình (p50 của majority không ghi j dao động 0,605–0,824), và j: false còn bị writeConcernMajorityJournalDefault: true ghi đè. Đừng đọc bảng thành "j: true cộng 0,05 ms cho majority". Thứ tự giữa w: 2, w: 3 và majority (0,558–0,668) cũng nằm trong dao động đó.

[suy luận] Chi phí của j: true phụ thuộc hoàn toàn vào đĩa: đây là Docker trên SSD macOS, không đại diện cho đĩa của bạn. Một client tuần tự cũng không cho thấy throughput khi nhiều client cùng ghi (journal có thể gộp nhiều lệnh vào một lần flush, chuyện của bài Journal & Checkpoint). Bảng này nói về latency của một lệnh ghi, không phải throughput.

Lab 2: lỗi timeout không có nghĩa là lệnh ghi thất bại

Chặn cả hai secondary (docker pause), rồi ghi { w: "majority", wtimeout: 2000 } vào primary (e2-wtimeout.sh, e2-wtimeout.js, e2-wtimeout.out):

after 2016 ms ERROR name=MongoWriteConcernError code=64 codeName=WriteConcernTimeout
message: waiting for replication timed out
errorLabels: []
same client, read local   : [{"_id":"order-1","status":"paid"}]
same client, read majority: []
-- docker unpause b c
after unpause, read majority: [{"_id":"order-1","status":"paid"}]
b (secondary) has it:       [{"_id":"order-1","status":"paid"}]

[quan sát] Lỗi trả về sau 2.016 ms với WriteConcernTimeout (code 64) và không có error label nào (errorLabels: []; error label là chuỗi server gắn kèm lỗi để cho biết nên thử lại hay không). Document thì đang nằm trên primary (đọc local thấy), chưa an toàn (đọc majority chưa thấy). Khi secondary hoạt động lại, nó được chép sang và majority thấy nó. Đúng như tài liệu: "MongoDB không huỷ thay đổi đã thành công trước khi hết wtimeout".

Hệ quả: gặp WriteConcernTimeout, trạng thái lệnh ghi là chưa biết: có thể sẽ tới nơi (lab này), có thể bị rollback (lab 3), và không phải "đã thất bại". Retry bằng _id mới là cách chắc chắn tạo bản trùng (lab 5b).

Lab 3: majority bảo vệ điều gì

Câu hỏi: w: 1 nhận acknowledged: true, rồi primary mất. Lệnh ghi đi đâu? Kịch bản (e3-rollback.sh, chạy 3 lần):

1. ghi T-M với w:"majority" khi cả ba node khoẻ                    → có trên ít nhất 2 node
2. "cắt dây": chuyển primary a sang một Docker network khác        → a không còn thấy b và c
3. ghi T-W1 với w:1 vào a                                          → acknowledged
4. ghi T-WM với w:"majority", wtimeout 1500 vào a                  → timeout, chưa có đa số
5. docker kill a
6. b và c bầu primary mới, ghi T-AFTER với w:"majority"
7. khởi động lại a, nối lại network: a trở về làm secondary

Lần thử đầu tôi dùng docker pause cho hai secondary thay vì cắt mạng, và kết quả sai bài toán: primary mới vẫn có T-W1 và T-WM (e3-pause-attempt.out). [suy luận] docker pause đóng băng tiến trình chứ không đóng băng mạng của kernel; nhiều khả năng dữ liệu đã nằm sẵn trong bộ đệm mạng của hai secondary, nên chúng đọc được sau khi tỉnh dậy, kể cả khi primary đã bị kill. Lab không kiểm tra riêng điều này. Muốn mô phỏng "chưa kịp chép" phải cắt đường mạng thật; mô phỏng sai là cách nhanh nhất để tin w: 1 an toàn hơn thực tế.

[quan sát] Trial 1 (e3-rollback.out; trial 2 và 3 trong e3-rollback-t2.out, e3-rollback-t3.out cho cùng kết quả):

3. w:1 insert  acknowledged: true after 5 ms
4. w:majority insert -> WriteConcernTimeout after 1515 ms (not acknowledged)
   old primary a, read local   : ["T1-M","T1-W1","T1-WM"]
   old primary a, read majority: ["T1-M"]
5. docker kill a
6. new primary elected among b,c after ~1 s: mongo-rs18-c:27017
   new primary read (local): ["T1-M"]
8. final read (majority): ["T1-AFTER","T1-M"]
   node a (direct, local read): ["T1-AFTER","T1-M"]

w: 1 trả acknowledged: true sau 5 ms (4–5 ms ở cả ba trial), nhưng node mới lên không có T1-W1; khi a quay lại, nó cũng không giữ: dữ liệu của nó bị kéo về cho khớp với lịch sử của đa số. Lệnh majority đã xác nhận lúc cả cụm khoẻ (T1-M) sống sót. Ngay cả lệnh w: "majority" bị timeout cũng mất.

Đây là rollback, và nó không xoá âm thầm. [quan sát] bsondump cho thấy mỗi trial để lại một file /data/db/rollback/<collectionUUID>/removed.<timestamp>.bson chứa đúng hai document bị cuốn đi (e3-bsondump.out); log của a ghi Operations reverted by rollback với insert: 2, rollback của trial 1 kéo dài 152 ms.

== .../removed.2026-10-09T07-25-13.1.bson
{"_id":"T1-W1","note":"w:1"}
{"_id":"T1-WM","note":"w:majority wtimeout"}

[tài liệu] File rollback mặc định được tạo (createRollbackDataFiles), nhưng không chứa thao tác xoá document hay drop collection. Tài liệu cũng khuyên dùng w: "majority" để tránh rollback dữ liệu đã xác nhận, và nói rõ dữ liệu của transaction commit bằng w: 1 cũng có thể bị rollback: lý do bài Transactions commit bằng majority.

w:1          acknowledged ✓ → mất ✗   (lab: 3/3 trial)
w:majority   timeout       → mất ✗   (chưa có đa số: vẫn có thể bị rollback)
w:majority   acknowledged ✓ → còn ✓   (lab: T-M, 3/3 trial)

Không phải "primary sập là mất dữ liệu": mất xảy ra với lệnh ghi chưa tới đa số lúc primary biến mất, và lab cố tình dựng khoảng hở đó. [tài liệu] Rollback không xảy ra nếu lệnh ghi đã kịp tới một member khác và member đó vẫn liên lạc được với đa số; một lệnh w: 1 thường được chép đi rất nhanh, nhưng w: 1 không hứa điều đó. Còn timeout vừa không phải thất bại (lab 2) vừa không phải an toàn. Thứ tự sự kiện bên trong cụm (ai bầu ai, điểm chung của rollback tìm ra sao, majority commit point nhích thế nào) là chuyện của bài Elections, Failover & Majority.

Phía đọc: đọc lại lệnh ghi của chính mình

Write concern nói về độ bền của lệnh ghi. Nhưng ứng dụng còn một câu hỏi khác: ghi xong, đọc lại có thấy không? Câu trả lời phụ thuộc vào hai thứ ghép lại: đọc ở đâu và đọc với mức nào.

readPreference    = đọc TỪ ĐÂU      (primary, secondary, nearest...)
read concern      = đọc ở MỨC AN TOÀN nào  (local, majority, snapshot...)
causal consistency = đọc theo THỨ TỰ nào    (sau lệnh ghi của tôi, sau lần đọc trước của tôi)

Lab 4a: read concern majority không đi một mình

[tài liệu] "majority" bảo đảm dữ liệu đọc ra đã được đa số xác nhận và không bị rollback, và hiệu năng ngang các mức khác (lab của bài Isolation cũng đo vậy). Trang tài liệu nói thêm: "dù ở mức nào, dữ liệu mới nhất trên một node không nhất thiết là phiên bản mới nhất của hệ thống". Trong transaction, majority chỉ có hiệu lực nếu commit bằng w: "majority".

Câu hỏi cho ứng dụng: nếu ghi bằng w: 1 rồi đọc majority ngay trên primary, có thấy lệnh ghi của mình không? [quan sát] e4b-majority-primary.out, 500 lần mỗi cách, đọc ngay sau khi nhận acknowledged:

primary: insertOne w:1        then find(readConcern majority) at once: not visible 498 / 500
primary: insertOne w:majority then find(readConcern majority) at once: not visible   0 / 500

Với w: 1, 498 trên 500 lần đọc majority ngay sau đó không thấy document vừa ghi, dù cùng một client, cùng một node. Với w: "majority" thì 0 trên 500. [suy luận] w: 1 trả lời trước khi majority commit point kịp nhích qua lệnh ghi (nhanh 0,2 ms so với 0,6 ms ở lab 1), còn w: "majority" chỉ trả lời khi nó đã nhích. Quy tắc dễ nhớ: muốn đọc majority thấy lệnh ghi của mình thì ghi cũng phải majority.

Lab 4b: đọc từ một secondary đã lệch

Replication bất đồng bộ, nên secondary luôn chậm hơn primary một chút. [tài liệu] Có một chi tiết theo phiên bản làm khe hở này tồn tại cả khi không có lag bất thường: từ MongoDB 8.0, w: "majority" trả lời khi đa số node đã ghi bền oplog entry, còn việc áp dụng vào collection diễn ra bất đồng bộ sau đó. Tài liệu viết thẳng: truy vấn trên secondary ngay sau một lệnh ghi majority có thể đọc trước khi secondary áp dụng, và nếu cần thấy ngay thì chạy trong session nhất quán nhân quả.

Để nhìn rõ khe hở đó, tôi phóng đại nó có chủ đích: đặt priority: 0 và secondaryDelaySecs: 1 cho c (e4-setup.js), tức c áp dụng mọi lệnh ghi trễ 1 giây. w: "majority" vẫn được a và b xác nhận ngay. Độ trễ này là tham số của lab, chọn để khe hở đủ lớn mà đo, không phải độ lệch điển hình của một secondary khoẻ.

Một phát hiện phụ quyết định cách dựng lab (e4a-hello.out):

hello on primary: hosts = ["mongo-rs18-a:27017","mongo-rs18-b:27017"] passives =
20 reads with readPreference secondary landed on 1 distinct host(s): ed9963a7df5d
hostname of b: ed9963a7df5d | hostname of c: eda8e0ee60d4

[quan sát] Primary không liệt kê c cho client, và 20 lần đọc readPreference: secondary chỉ rơi vào b. [suy luận] Driver không thấy member delayed nên không route tới nó. Vì vậy lab nối thẳng vào c (directConnection=true) để đọc, còn ghi đi qua replica set. Đó là điều kiện lab, không phải khuyến nghị.

Mỗi lần ghi một document với w: "majority" rồi ngay sau đó đọc đúng document ấy từ c, 30 lần mỗi cách (e4-causal.js, e4-causal.out):

Cách đọc từ cThấy lệnh ghi của mìnhThời gian đọc (p50)
Akhông session, read concern local0 / 300 ms
Bkhông session, read concern majority0 / 300 ms
Csession causalConsistency: false, majority0 / 300 ms
Dsession causalConsistency: true, majority30 / 301.003 ms (926–1.008)
Esession causalConsistency: true, local30 / 301.004 ms

Ba hàng đầu trả kết quả trống trong tối đa 1 ms: nhanh và sai. Ở D và E mỗi lần đọc chờ đúng khoảng c còn thiếu rồi trả kết quả đúng. Read concern majority không giúp (hàng B): nó hứa dữ liệu an toàn, không hứa dữ liệu mới tới mức bạn vừa ghi. Thứ giúp là causal consistency, và nó giúp bằng cách chờ.

Causal consistency

Causal consistency hứa: nếu thao tác B xảy ra sau thao tác A trong cùng một chuỗi nhân quả (cùng session), B không bao giờ thấy trạng thái cũ hơn A. Tài liệu liệt kê bốn lời hứa: read your writes, monotonic reads (lần đọc sau không cũ hơn lần đọc trước), monotonic writes và writes follow reads.

Cơ chế, hình dung bằng một "phiếu hẹn giờ": mỗi lệnh ghi trả về mốc operationTime; session nhớ nó; lần đọc sau mang mốc đó trong read concern dưới tên afterClusterTime, nghĩa là "chỉ trả lời khi dữ liệu đã tới mốc này". Node chưa tới thì chờ.

ghi (w: "majority")           đọc
   │ operationTime = T          │ readConcern: { level: "majority", afterClusterTime: T }
   ▼                            ▼
 primary ──────────────────► secondary c
                               ├── đã áp dụng tới T?  có   → trả ngay
                               └──                     chưa  → CHỜ tới khi tới T (hoặc hết maxTimeMS)

[quan sát] Làm thủ công, không qua session (e4-causal.out, e4c-maxtime.out):

find on c without afterClusterTime       : 0 docs in 4 ms
find on c with afterClusterTime = opTime : 1 docs in 999 ms
find on c, afterClusterTime, maxTimeMS 300: MaxTimeMSExpired code 50 after 304 ms

Không có afterClusterTime: 0 document sau 4 ms (nhanh và sai). Có: 1 document sau 999 ms (xấp xỉ khoảng lệch 1 giây). Cho ít thời gian hơn khoảng lệch thì MaxTimeMSExpired. Nên một lần đọc nhân quả ở secondary đang lag là một lần đọc chờ, và ứng dụng nên đặt maxTimeMS.

Lab dùng hai client nên phải báo mốc cho session người đọc bằng tay (advanceOperationTime, advanceClusterTime). Trong ứng dụng thường, một session trên một client lo việc đó: ghi và đọc qua cùng session có causalConsistency: true, driver tự đính afterClusterTime.

[tài liệu] Chỉ tổ hợp majority + majority bảo đảm cả bốn lời hứa, kể cả khi mạng chia và hai node cùng tưởng mình là primary; w: 1 cho tính nhân quả không có độ bền. Lab chỉ chạy khi không có sự cố, nên mọi hàng có session nhân quả, cả hai hàng w: 1 trong output (F, G), đều 30/30: đừng đọc đó là bảo đảm. Trường hợp failover xen giữa chuỗi đọc ghi lab không kiểm tra; nó dựa vào tài liệu và bài Elections, Failover & Majority.

Read concernWrite concernRead own writesGhi chú
majoritymajority✓ cả bốn lời hứa, kể cả khi mạng chiachậm nhất, đúng nhất
majorityw: 1không bảo đảm khi có sự cốlệnh ghi có thể rollback; không session (lab 4a): 498/500 không thấy
localmajoritychỉ khi không có sự cốđọc có thể thấy dữ liệu về sau bị rollback
localw: 1chỉ khi không có sự cốkhông bền, không an toàn

Cái giá và giới hạn

  • Đọc phải chờ. Chi phí bằng độ lệch của secondary lúc đọc: lab ép 1 giây nên mỗi lần đọc chờ khoảng 1 giây; ở cụm thật, lag tăng thì latency đọc tăng theo.
  • Mốc phải đi cùng người dùng. Ứng dụng nhiều instance, mỗi request tới một instance: operationTime phải đi theo request (cookie, header), nếu không instance thứ hai không biết phải chờ tới đâu. Session cũng không dùng song song từ nhiều luồng.
  • Đọc từ primary thì ít cần hơn. Khi không có failover, đọc local trên primary đã thấy lệnh ghi của mình. [tài liệu] Qua một cuộc bầu, lần đọc rơi vào primary cũ có thể không thấy lệnh ghi majority mà primary mới đã nhận, nên tài liệu khuyên ghi majority và đọc majority. Causal consistency đáng giá nhất khi bạn chủ động đọc từ secondary để giảm tải primary.
  • readPreference là bài khác. Chọn secondary nào, nearest, tag set và hành vi khi failover thuộc bài Elections, Failover & Majority. Ở đây chỉ cần nhớ ba núm: từ đâu, mức nào, thứ tự nào.

Retryable writes và retryable reads

Mạng có một kiểu lỗi đặc biệt khó xử lý: lệnh ghi đã chạy nhưng câu trả lời không về. Ứng dụng nhận lỗi timeout, và không biết có nên gửi lại. Gửi lại thì có thể ghi hai lần; không gửi thì có thể mất.

Cách MongoDB giải bài toán này

[tài liệu] Retryable writes để driver thử lại đúng một lần một số lệnh ghi sau lỗi mạng hoặc khi không tìm thấy primary khoẻ. Cần replica set hoặc sharded cluster (không phải standalone) với WiredTiger; driver tương thích MongoDB 4.2 trở lên và mongosh bật sẵn (retryWrites=true). Được retry (khi write concern có ack, tức không phải w: 0): insertOne, insertMany, updateOne, replaceOne, deleteOne, họ findAndModify, và bulkWrite chỉ gồm các thao tác trên một document. Không được retry: updateMany, deleteMany, w: 0, và các lệnh ghi bên trong transaction (riêng commitTransaction và abortTransaction thì driver retry một lần). Driver chờ tối đa serverSelectionTimeoutMS để tìm primary mới; failover lâu hơn thì lệnh ghi lỗi. Đặt timeoutMS thì driver có thể retry nhiều lần cho tới khi hết giờ.

Làm sao khỏi ghi hai lần? [chi tiết cài đặt] (mức khái niệm) Mỗi lệnh ghi retryable mang ID session (lsid) và số thứ tự của lệnh trong session. Server nhớ những lệnh nào của session đã thực thi; gặp lại đúng lệnh đó, nó không chạy lại mà trả kết quả lần đầu. Bằng chứng gián tiếp ở lab 5b: bộ đếm serverStatus().transactions.retriedCommandsCount tăng đúng 1 mỗi lần.

[tài liệu] Từ MongoDB 6.1, nếu cả lần đầu và lần retry đều lỗi mà không có lệnh ghi nào được thực hiện, lỗi mang error label NoWritesPerformed. Nếu client không phản hồi lâu hơn localLogicalSessionTimeoutMinutes, lệnh ghi có thể được áp dụng lại khi client tỉnh: hiếm nhưng có thật.

Retryable reads đơn giản hơn vì đọc không đổi dữ liệu. [tài liệu] Driver tương thích server 6.0 trở lên bật sẵn (retryReads=false để tắt) và retry một lần các lệnh như find, aggregate (không có $out/$merge), count, countDocuments, estimatedDocumentCount, distinct, listDatabases, listCollections, listIndexes và change stream; không retry getMore, mapReduce và lệnh gọi qua runCommand chung. mongosh không hỗ trợ retryable reads, nên bài này không thí nghiệm được chúng.

Lab 5a: step down giữa dòng lệnh ghi

Một client ghi liên tục 8 giây (mỗi vòng: insertOne và updateOne { $inc } lên một bộ đếm, cả hai w: "majority"), trong khi một client khác chạy rs.stepDown(30) trên primary khoảng giây thứ ba. Hai lần chạy cho mỗi cấu hình (e5a-stepdown.sh, e5a-stepdown.out, e5a-stepdown-run2.out):

retryWritesSố vòngLỗi ứng dụng nhìn thấyDocument / bộ đếm so với số lần được xác nhận
true (lần 1)4.93604.936 document, bộ đếm 4.936: khớp
true (lần 2)4.90604.906 document, bộ đếm 4.906: khớp
false (lần 1)4.4931: InterruptedDueToReplStateChange (code 11602) ở một $incbộ đếm 4.493 trong khi chỉ 4.492 lần $inc được xác nhận: lệnh báo lỗi vẫn được áp dụng
false (lần 2)4.0651: PrimarySteppedDown (code 189) ở một insertOne4.064 document: lệnh báo lỗi không có trong dữ liệu

[quan sát] Khi tắt retry, mỗi lần chạy ứng dụng thấy đúng một lỗi, cả hai đều tới dưới dạng MongoWriteConcernError không có error label, nhưng hậu quả ngược nhau: một lệnh ghi báo lỗi mà vẫn được áp dụng, một lệnh ghi báo lỗi mà không có trong dữ liệu. Chỉ nhìn lỗi, ứng dụng không phân biệt được. Khi bật retry (mặc định), 0 lỗi lọt ra ứng dụng ở cả hai lần, và số document, bộ đếm đều khớp số lần được xác nhận.

Lab không nhìn thấy trực tiếp lần retry nào ở 5a. retriedCommandsCount bằng 0 trên cả ba node (e5a2-counters.out), nhưng bộ đếm này chỉ tăng khi server gặp lại một lệnh đã chạy; một lần retry chạy lệnh lần đầu thì không được đếm. [suy luận] Hai lần tắt retry đều có một lệnh trúng lúc step down, nên nhiều khả năng ở hai lần bật retry driver cũng đã retry âm thầm, và lần retry đó không gặp lại lệnh nào đã chạy. Mỗi cấu hình chỉ 2 lần chạy: đủ để thấy hình dạng, chưa đủ để nói tần suất.

Lab 5b: lệnh đã chạy, câu trả lời không về

Đây là trường hợp khó thật. Cách dựng: chặn hai secondary, ghi w: "majority" với socketTimeoutMS=1500. Primary áp dụng lệnh ghi ngay rồi chờ đa số; client hết giờ socket sau 1,5 giây; 1,8 giây sau khi client bắt đầu gửi thì tôi mở lại hai secondary. Mỗi trường hợp reset collection trước (e5b.sh, e5b-timeout-retry.js, e5b-timeout-retry.out):

LệnhretryWritesỨng dụng nhìn thấyDữ liệu sau đóNếu ứng dụng tự gửi lại
insertOne (đơn mới)truethành công, sau 1.945 ms1 đơn; retriedCommandsCount +1không cần
insertOnefalseMongoNetworkTimeoutError sau 1.509 ms1 đơn (đã có)2 đơn paid
updateOne { $inc: { n: 1 } }truethành công, sau 1.949 msn = 1; retriedCommandsCount +1không cần
updateOne { $inc: { n: 1 } }falseMongoNetworkTimeoutError sau 1.506 msn = 1 (đã cộng)n = 2

[quan sát] Khi retryWrites=false, ứng dụng nhận lỗi timeout dù lệnh ghi đã có trong dữ liệu. Ứng dụng tự "thử lại cho chắc" bằng _id mới thì ra hai đơn hàng; với $inc thì bộ đếm cộng hai lần. Khi retryWrites=true, ứng dụng nhận thành công sau ~1,95 giây; dữ liệu có 1 đơn, n = 1, và retriedCommandsCount trên primary tăng 1: server đã gặp lại một lệnh đã chạy và không thực thi lần hai. [suy luận] Driver gửi lại khi socket hết giờ ở 1,5 giây, và lần retry chờ majority tới khi secondary mở lại (1,8 giây); lab không ghi thời điểm từng bước.

[suy luận] Retryable writes giảm chứ không loại bỏ rủi ro: chúng chỉ phủ lỗi trong một lần retry, chỉ cho các lệnh retry được (không có updateMany, deleteMany), và chỉ khi driver còn tìm được primary trong serverSelectionTimeoutMS. Khi cả lần retry cũng lỗi, ứng dụng quay về tình huống "không biết lệnh ghi đã chạy chưa" của lab 2. Ở đó mới cần idempotency: thiết kế lệnh ghi sao cho chạy hai lần cho kết quả như một lần (_id do client sinh ra, khoá idempotency, $set thay vì $inc, unique index). Phần đó là của bài Write Conflicts, Retry & Idempotency, nơi cũng so sánh retry của lệnh ghi lẻ với retry cả transaction (TransientTransactionError).

So với PostgreSQL

PostgreSQL gom phần lớn những gì bài này gọi là write concern vào một tham số synchronous_commit, đi kèm danh sách standby synchronous_standby_names. Phần này không chạy PostgreSQL: mọi nhận định lấy từ trang cấu hình WAL và replication của tài liệu PostgreSQL hiện hành, mang nhãn [tài liệu PostgreSQL], không phải số đo.

synchronous_commitCommit chờGần nhất trong MongoDB (lỏng, không tương đương)
offkhông chờ gì; trễ tới 3 lần wal_writer_delay (mặc định 200 ms). Crash có thể mất vài giao dịch cuối nhưng database vẫn nhất quánw: 1
localWAL đã flush xuống đĩa cục bộw: 1, j: true
remote_writestandby đã nhận và ghi vào file system (chưa chắc xuống đĩa)w: 2 không đặt j (bản sao mới ở bộ nhớ)
on (mặc định)flush cục bộ và, nếu có standby đồng bộ, standby đã nhận và flushw: "majority"
remote_applystandby đã áp dụng, nên truy vấn trên standby thấy ngaykhông có; MongoDB giải bài toán đó ở phía đọc bằng causal consistency

[tài liệu PostgreSQL] Nếu synchronous_standby_names rỗng, remote_apply, remote_write và local đều chỉ còn là flush cục bộ như on. Danh sách standby có thể theo ưu tiên FIRST n (...) hoặc theo quorum ANY n (...); ánh xạ sang majority chỉ lỏng vì MongoDB đếm member của replica set, PostgreSQL đếm standby có tên trong danh sách.

Hai khác biệt cần nhớ. Một: mặc định. PostgreSQL mặc định on nhưng chưa cấu hình standby đồng bộ thì chỉ đợi đĩa cục bộ, không đợi bản sao nào; MongoDB từ 5.0 đợi đa số (trừ ngoại lệ arbiter). Hai: PostgreSQL không có tham số tương đương read concern cho standby, nên "đọc thấy gì" ở standby là việc của remote_apply và của ứng dụng. Cả hai đều cho đổi mức theo từng giao dịch (SET LOCAL synchronous_commit TO OFF; write concern theo từng lệnh ghi). Đây là so sánh khái niệm, không phải so sánh tốc độ.

Chọn mức theo loại dữ liệu

Câu hỏi quyết định: nếu lệnh ghi này đã được xác nhận mà vẫn biến mất sau một failover, hậu quả là gì?

Loại dữ liệuGợi ýGiáPhải đi kèm
Sổ cái, thanh toán, đơn hàngw: "majority" (mặc định), retryWrites=true, có wtimeout hoặc timeoutMSlatency cao hơn (lab: 0,62 so với 0,22 ms ở mạng gần 0); timeout hoặc treo khi mất đa sốidempotency key để retry an toàn; WriteConcernTimeout xử lý như "chưa biết"
Cần đọc lại lệnh ghi của mình qua secondaryghi majority, đọc majority trong session causalConsistency: trueđọc chờ secondary đuổi kịpmaxTimeMS; mang mốc thời gian theo request
Event log, click, metric: mất vài giây cuối chấp nhận đượcw: 1mất lệnh chưa tới secondary khi failover (lab 3)chấp nhận rõ ràng; vẫn giữ retryWrites=true
Dữ liệu giống cache, dựng lại đượcw: 1như trêncó đường dựng lại; tránh w: 0 vì mất luôn thông báo lỗi và không retry được

Hai nguyên tắc. Đừng hạ mức để "nhanh hơn" khi chưa đo: ở mạng gần bằng 0, majority chỉ đắt hơn w: 1 khoảng 0,4 ms (lab 1); số thật của bạn nằm ở RTT giữa các node. Đừng nâng thành w: 3: nó đổi khả dụng lấy độ bền (mất một node là không ghi được), trong khi majority chịu được mất một node mà vẫn ghi. Còn w: 1, j: true chỉ chống tắt đột ngột một node, vẫn mất khi failover.

Những lỗi thường gặp

  • Tin acknowledged: true là "đã an toàn". Lab 3: w: 1 xác nhận sau 4–5 ms và mất ở 3/3 lần chạy. Lời hứa nằm ở write concern, không ở phản hồi.
  • Coi WriteConcernTimeout là thất bại. Lab 2: lệnh ghi nằm trên primary rồi tới nơi; lab 3: cũng có thể bị rollback. Trạng thái là chưa biết.
  • Retry bằng _id mới hoặc $inc mà không có idempotency. Lab 5b: một timeout, hai đơn hàng, bộ đếm cộng hai lần.
  • Nghĩ j: true là majority. Journal chống tắt đột ngột một node, không chống failover.
  • Đọc majority ngay sau w: 1 rồi tưởng sẽ thấy. Lab 4a: 498/500 lần không thấy. Muốn đọc lại lệnh ghi của mình, ghi và đọc phải cùng majority.
  • Đọc secondary mà không có session nhân quả. Lab 4b: 0/30 thấy lệnh ghi của mình; session nhân quả chờ ~1 giây và thấy 30/30.
  • Mô phỏng mất mạng bằng docker pause. Trong lab 3, lệnh ghi vẫn tới secondary dù chúng bị đóng băng, nên suýt kết luận sai. Hãy cắt mạng thật.
  • Đặt w: 3 để "chắc ăn", hoặc quên arbiter. Mất một node là lệnh ghi treo. Với P-S-A, mặc định ngầm rơi về w: 1 và majority có thể không bao giờ xong khi mất node mang dữ liệu.

Tóm tắt

  • acknowledged: true chỉ cam kết phần việc bạn yêu cầu. w = bao nhiêu node, j = đã ghi journal chưa, wtimeout = chờ bao lâu. Mặc định từ 5.0 là w: "majority" (trừ ngoại lệ arbiter), và majority ngầm có journal.
  • Cái giá (lab 1, mạng gần bằng 0): w: 1 0,215 ms; w: 1, j: true 0,495 ms; w: "majority" 0,618 ms; các biến thể j của majority không phân biệt được trong dao động.
  • WriteConcernTimeout (lab 2): lỗi sau 2.016 ms, không có error label, lệnh ghi vẫn nằm trên primary rồi tới nơi. Timeout là "chưa biết".
  • Rollback (lab 3): w: 1 xác nhận sau 4–5 ms rồi mất ở 3/3 trial khi primary bị tách và kill, lệnh majority timeout cũng mất, lệnh majority đã xác nhận còn. Dữ liệu bị cuốn đi nằm trong file removed.*.bson.
  • Đọc: ghi w: 1 rồi đọc majority ngay trên primary không thấy ở 498/500 lần. Đọc từ secondary lệch 1 giây: 0/30 không session, 30/30 với session nhân quả, đổi lại mỗi lần đọc chờ ~1 giây.
  • Retryable writes: driver retry một lần, server nhận ra lệnh đã chạy nên không áp dụng hai lần (1 đơn, n = 1); tắt retry rồi tự gửi lại thì 2 đơn, n = 2. mongosh không hỗ trợ retryable reads.
  • PostgreSQL gom vào synchronous_commit cộng danh sách standby; mặc định PostgreSQL không chờ bản sao nào, mặc định MongoDB chờ đa số. Chọn mức theo hậu quả của việc mất dữ liệu đó.

Hỏi & đáp

Một service ghi đơn hàng với { w: 1 } và nhận { acknowledged: true } sau 5 ms. Điều nào đúng về đơn hàng đó ngay lúc này?

  1. Đã được ghi vào đa số node, vì acknowledged: true nghĩa là server đã nhận và xử lý xong

    acknowledged: true chỉ nói rằng phần việc bạn yêu cầu đã xong. Với w: 1 phần việc đó chỉ là primary đã áp dụng. Lab: bốn write concern khác nhau cho cùng phản hồi acknowledged: true. Xem mục "Mặc định, và những ngoại lệ phụ thuộc phiên bản".

  2. Primary đã áp dụng, nhưng chưa chắc đã sang secondary nào; primary mất ngay thì đơn có thể bị rollback

    Đúng. Lab 3: w: 1 được xác nhận sau 4–5 ms, primary bị tách mạng rồi kill, và T-W1 biến mất ở 3/3 trial; nó chỉ còn trong file rollback của node cũ. Xem mục "Lab 3: majority bảo vệ điều gì".

  3. Đã được ghi xuống journal trên đĩa, nên chịu được cả mất điện lẫn failover của primary

    w: 1 không đòi journal (nó xác nhận khi dữ liệu mới ở bộ nhớ), và ngay cả j: true cũng không chống được failover: journal bảo vệ một node tắt đột ngột, majority bảo vệ khi một node biến mất. Xem mục "j: đã ghi journal chưa".

  4. Chưa chắc đã ghi: w: 1 chỉ gửi lệnh đi và không chờ server trả lời gì cả

    Đó là w: 0. w: 1 chờ primary áp dụng xong rồi mới trả lời. Xem mục "w: bao nhiêu node".

Hai secondary tạm mất. Bạn ghi { w: "majority", wtimeout: 2000 } và sau 2 giây nhận WriteConcernTimeout. Điều nào mô tả đúng trạng thái của lệnh ghi?

  1. Lệnh ghi đã bị huỷ: hết wtimeout thì MongoDB quay lui những gì primary đã làm cho lệnh này

    Tài liệu nói rõ MongoDB không huỷ thay đổi đã thành công. Lab 2: đọc local trên primary vẫn thấy document sau lỗi. Xem mục "Lab 2: lỗi timeout không có nghĩa là lệnh ghi thất bại".

  2. Lệnh ghi chắc chắn sẽ tới đa số khi secondary quay lại, nên ứng dụng không cần làm gì thêm cả

    Lab 2 đúng là như vậy, nhưng đó không phải bảo đảm. Lab 3: một lệnh majority timeout bị rollback khi primary mất trước khi có đa số. Xem mục "Lab 3: majority bảo vệ điều gì".

  3. Lệnh đầu đã thất bại, nên ứng dụng nên gửi lại ngay với một _id mới cho an toàn

    Lệnh đầu có thể đang nằm trên primary và về sau tới nơi, nên gửi lại bằng _id mới tạo ra bản trùng. Lab 5b: một timeout, hai đơn hàng. Xem mục "Lab 5b".

  4. Chưa biết: document nằm trên primary nhưng chưa an toàn, có thể tới nơi hoặc bị rollback

    Lab 2: sau 2.016 ms lỗi code 64 không có error label; local thấy, majority chưa thấy; mở lại secondary thì majority thấy. Lab 3 cho thấy nhánh kia: lệnh majority timeout bị rollback. Xem mục "Lab 2".

Primary bị tách khỏi mạng rồi bị kill; lúc đó nó vừa xác nhận một lệnh w: 1. Sau khi nó khởi động lại và nối mạng, document đó ở đâu?

  1. Bị rollback: chỉ còn trong file removed.*.bson trên node cũ

    Lab 3: node mới lên không có nó; a quay lại không giữ nó, và bsondump đọc được T1-W1 trong file rollback. Xem mục "Lab 3: majority bảo vệ điều gì".

  2. Vẫn còn ở cả ba node, vì primary cũ chép nó sang secondary khi khởi động lại và secondary nhận lại

    Chiều ngược lại mới đúng: node cũ phải khớp với lịch sử của đa số, không áp lịch sử của nó cho đa số. Lab: sau khi a quay lại, đọc majority và đọc thẳng trên a đều chỉ còn T1-AFTER và T1-M. Xem mục "Lab 3".

  3. Còn trên node cũ như một document bình thường, và hai node kia nhận nó khi đồng bộ

    Node cũ đã bị kéo về điểm chung với đa số; đọc local trực tiếp trên a sau khi rejoin ra T1-AFTER và T1-M, không có T1-W1. Xem mục "Lab 3".

  4. Còn trên node mới, vì node mới được bầu từ các secondary đã nhận nó

    Trong lab hai secondary bị tách khỏi primary trước khi lệnh ghi xảy ra, nên chúng chưa từng nhận nó; node mới lên không có T1-W1. Nếu nó đã kịp tới đa số thì mới không rollback, và đó là việc của majority. Xem mục "Lab 3".

Một trang chi tiết đơn hàng đọc từ secondary để giảm tải primary, nhưng đôi lúc khách vừa đặt xong không thấy đơn của mình. Thay đổi nào đúng nhất?

  1. Thêm j: true cho lệnh ghi để dữ liệu chắc chắn đã nằm trong journal trên đĩa

    j nói về độ bền trên node ghi, không nói secondary đã áp dụng tới đâu. Vấn đề ở đây là secondary chưa kịp áp dụng. Xem mục "j: đã ghi journal chưa" và "Đọc từ một secondary đã lệch".

  2. Chỉ đặt read concern majority cho lần đọc, vì nó chỉ trả dữ liệu đã an toàn

    Lab 4b hàng B: majority trên secondary lệch trả 0/30. majority hứa dữ liệu an toàn, không hứa dữ liệu mới tới mức bạn vừa ghi. Xem mục "Lab 4b: đọc từ một secondary đã lệch".

  3. Ghi majority, đọc majority, cùng một session nhân quả cho cả hai

    Lab 4b hàng D: 30/30 thấy lệnh ghi của mình, đổi lại mỗi lần đọc chờ secondary đuổi kịp (1.003 ms ở độ lệch 1 giây). Chỉ tổ hợp majority + majority bảo đảm cả khi có sự cố. Xem mục "Causal consistency".

  4. Tăng wtimeout để lệnh ghi chờ lâu hơn rồi mới trả lời cho khách

    wtimeout chỉ là giới hạn chờ khi lệnh ghi không đạt mức yêu cầu; nó không làm secondary đọc được dữ liệu mới. Xem mục "wtimeout: chờ bao lâu".

Một lệnh insertOne bị timeout socket sau khi primary đã áp dụng nó (secondary tạm chậm), retryWrites=true. Điều gì xảy ra?

  1. Driver gửi lại đúng lệnh đó; server nhận ra lệnh đã chạy nên không áp dụng lần hai, còn 1 document

    Lab 5b: ứng dụng nhận thành công sau 1.945 ms, có 1 đơn, và retriedCommandsCount tăng 1. Server nhớ lệnh nào của session đã thực thi. Xem mục "Lab 5b: lệnh đã chạy, câu trả lời không về".

  2. Driver thực thi lại lệnh như mới, nên có thể có 2 document nếu _id được server sinh

    Lab 5b: có 1 document. Retry dùng lại cùng định danh lệnh nên server không áp dụng lần hai. Bản trùng chỉ xuất hiện khi chính ứng dụng gửi lại bằng _id mới (lab 5b, retryWrites=false rồi tự gửi lại: 2 đơn). Xem mục "Lab 5b".

  3. Ứng dụng luôn nhận lỗi timeout vì retry chỉ áp dụng cho lỗi NotWritablePrimary

    Retry áp dụng cho lỗi mạng (kể cả timeout) lẫn lúc không tìm thấy primary khoẻ. Lab 5b: timeout, rồi thành công. Xem mục "Cách MongoDB giải bài toán này".

  4. Driver retry nhiều lần cho tới khi thành công, nên không cần quan tâm tới idempotency

    Mặc định chỉ một lần retry (trừ khi đặt timeoutMS). Khi cả lần retry cũng lỗi, ứng dụng quay về tình huống "không biết lệnh đã chạy chưa" và cần idempotency. Xem mục "Retryable writes và retryable reads".

Chọn write concern cho hai bảng: nhật ký click của người dùng (hàng chục nghìn bản ghi mỗi giây) và sổ cái thanh toán. Cách nào hợp lý?

  1. w: 0 cho click để ghi nhanh nhất, majority cho sổ cái

    w: 0 không chờ xác nhận nên mất luôn thông báo lỗi và không retry được. Với click, w: 1 đã rẻ (lab 1: 0,215 ms) mà vẫn báo lỗi khi ghi hỏng. Xem mục "Chọn mức theo loại dữ liệu".

  2. w: 1, j: true cho sổ cái vì journal trên đĩa là đủ

    Journal chống tắt đột ngột một node, không chống failover: lệnh ghi vẫn có thể bị rollback. Với sổ cái cần majority. Xem mục "j: đã ghi journal chưa".

  3. w: 3 cho sổ cái, vì cần chắc chắn cả ba node đều có

    Mất một node là mọi lệnh ghi sổ cái treo; majority bảo vệ như nhau trước một node mất mà vẫn ghi được. Xem mục "Chọn mức theo loại dữ liệu".

  4. w: 1 cho click, majority cho sổ cái thanh toán

    Mất vài click cuối khi failover chấp nhận được, mất giao dịch thanh toán thì không. Lab 1: 0,215 so với 0,618 ms; lab 3: w: 1 mất ở 3/3 trial. Xem mục "Chọn mức theo loại dữ liệu".

Bạn nhờ một nhân viên bưu điện gửi thư quan trọng. Anh ta nói "xong rồi" ngay khi đặt thư lên bàn mình, trước khi thư đi đâu. Điều nào hợp lý nhất để bạn làm?

  1. Tin anh ta, vì "xong rồi" nghĩa là thư đã an toàn trong tay bưu điện

    Lời của anh ta chỉ nói thư nằm trên bàn anh ta. Cũng như acknowledged: true ở w: 1: chỉ nói phần việc bạn yêu cầu đã xong. Xem mục "Hình dung trước: gửi một lá thư quan trọng".

  2. Dặn rõ: chỉ báo khi thư tới ít nhất hai trong ba bưu cục, và có hạn chờ

    Tương ứng w: "majority" với wtimeout: chậm hơn một chút nhưng thư sống sót khi một bưu cục cháy, và hạn chờ tránh treo khi bưu cục đóng cửa. Xem mục "Hình dung trước" và "Chọn mức theo loại dữ liệu".

  3. Bảo anh ta ghi thư vào sổ nhật ký rồi mới báo, vì sổ chống được mọi rủi ro

    Sổ nhật ký chống mất điện ở bưu cục đó; bưu cục cháy thì sổ cũng cháy. Tương ứng j: true: chống tắt đột ngột một node, không chống mất cả node. Xem mục "j: đã ghi journal chưa".

  4. Chỉ báo khi cả ba bưu cục đều có thư, vì như vậy là an toàn nhất

    Đúng là an toàn nhất, nhưng chỉ cần một bưu cục đóng cửa là bạn đứng đợi mãi; hai trên ba đã chịu được một bưu cục mất. Tương ứng w: 3 so với majority. Xem mục "Chọn mức theo loại dữ liệu".

Nếu bỏ hết thuật ngữ: khi bạn nhờ người khác lưu một thứ quan trọng, câu "xong rồi" chỉ có nghĩa là họ đã làm đúng việc bạn nhờ, không hơn. Nhờ "nhận thư" thì họ nhận; muốn thư không mất khi một nơi cháy, phải nhờ "đưa tới ít nhất hai nơi". Nếu người đưa tin mất liên lạc ngay lúc bạn không biết thư đã đi chưa, thì đừng viết lại một lá thư mới: hãy hỏi lại đúng lá thư đó, hoặc viết sao cho đọc hai lần cũng chỉ có một việc xảy ra. Và nếu bạn muốn đọc lại chính điều mình vừa gửi từ một chi nhánh, hãy mang theo biên nhận: chi nhánh nào chưa nhận tới số biên nhận đó thì phải đợi.

Bài tiếp theo

Bài này trả lời "lệnh ghi đã bền tới đâu" và "đọc lại lệnh ghi của mình bằng cách nào". Nhưng nó cũng để lại một vòng lặp mở: khi một lệnh ghi lỗi, ứng dụng làm gì? WriteConflict trong transaction, WriteConcernTimeout, timeout mạng, PrimarySteppedDown: lỗi nào retry được, retry bằng cách nào, và làm sao để chạy hai lần cũng không gây hại?

Write Conflicts, Retry & Idempotency trả lời những câu đó: vòng WriteConflict → abort → retry, sự khác nhau giữa retryable write (driver lo) và transaction retry (ứng dụng lo), cái gì phải idempotent, và cách giảm tranh chấp trên một document nóng. Còn journal giữ lời hứa j: true thế nào, và majority commit point nhích ra sao khi failover, là chuyện của bài Journal & Checkpoint và bài Elections, Failover & Majority.

Tài liệu tham khảo

Chủ đề

Bạn thấy bài này thế nào?