WiredTiger MVCC (P1/4): Bốn tầng transaction và timestamp
Bài này trả lời một câu: khi một document đổi, các version cũ của nó nằm ở đâu, và ai được thấy version nào. Phần này dựng bốn tầng transaction và timestamp; các phần sau đi vào update chain và history store, ba lab về snapshot bị giữ và so với PostgreSQL.
Ba bài trước nhắc tới "nhiều version của cùng một document" mà chưa chỉ chúng nằm đâu. Bài Isolation & Snapshot dựng mô hình "timestamp cộng nhiều version" rồi dừng ở đó. Bài Write Conflicts, Retry & Idempotency cho thấy hai lệnh ghi vào cùng một document thì một bên phải làm lại, nhưng chưa nói vì sao đơn vị xung đột là document chứ không phải field.
Bài này là bài về cơ chế: một lệnh updateOne đi qua mấy tầng transaction, version mới nằm ở đâu trên page, người đọc chọn version nào, version nào được giữ khi page được ghi xuống đĩa, và khi nào nó bị xoá hẳn. Đọc thấy gì (các mức read concern, write skew) vẫn thuộc bài Isolation & Snapshot; bài này không dạy lại.
Bài này nằm ở đâu
- Cần biết trước: Isolation & Snapshot (mô hình timestamp cộng nhiều version, first-writer-wins,
minSnapshotHistoryWindowInSeconds,SnapshotTooOld), Transactions & Atomicity (vòng đời, giới hạn 60 giây), Write Conflicts, Retry & Idempotency (WriteConflict), WiredTiger & Compression (page trong RAM và trên đĩa, reconciliation, file.wt) - Giới thiệu: bốn tầng transaction từ ứng dụng xuống WiredTiger; read timestamp, commit timestamp, oldest timestamp, stable timestamp và pinned timestamp (quan sát được bằng
serverStatusvà$currentOp); update chain trên page; cách phát hiện xung đột; update dạng delta so với diff trong oplog; history store (WiredTigerHS.wt) và các bộ đếm của nó; lab giữ một snapshot mở trong lúc dữ liệu liên tục đổi; so sánh với MVCC của PostgreSQL - Không dạy ở đây: cache và eviction (bài Cache, Eviction & Working Set, chỉ có bản xem trước ở đây); journal và checkpoint (bài Journal & Checkpoint); lock, latch và ticket (bài Concurrency); stable timestamp và majority commit point trong replica set (bài Replica Set & Oplog và bài Elections, Failover & Majority, chỉ có bản xem trước ở đây)
- Dẫn tới: Concurrency: Locks, Latches & Tickets
Môi trường lab: MongoDB 8.3.11 trong Docker (
mongo:8), WiredTiger 12.0.0 (cùng bản với bài WiredTiger & Compression). Transaction và read concern"snapshot"cần replica set, nên mỗi lab chạy trên replica set một node (--replSet, rồirs.initiate()), trong container riêng, volume mới, 2 CPU, 2 GB RAM, WiredTiger cache 1 GB (lab áp lực cache: 1 GB RAM, cache 0,25 GB). Host Apple M4; máy ảo Docker chạy chung với lab của các bài khác và vài container không liên quan, nên thời gian dao động mạnh và mọi số về độ trễ chỉ là tham khảo. Mọi số đo đều đo thật, có file output thô kèm theo (thư mục lablab23/). Để transaction được giữ mở đủ lâu, lab đặttransactionLifetimeLimitSecondslên 3600 (chỉ trong lab); Lab 3 hạ nó xuống 10 giây để xem giới hạn này hoạt động.minSnapshotHistoryWindowInSecondscũng được đổi ở vài lab (ghi rõ tại chỗ).Giới hạn của lab: không tách riêng được chi phí của update chain dài khỏi dao động của máy (Lab 2 ở phần sau (Ba lab về snapshot bị giữ) nói rõ chỗ nào tách được). Và ở quy mô một máy, giữ snapshot mở không làm độ trễ update hay cache xấu đi theo cách đo được; tác động đo được nằm ở history store và các bộ đếm timestamp (Lab 1).
Nhãn: [tài liệu] theo tài liệu chính thức (manual MongoDB hoặc tài liệu WiredTiger 12.0.0); [quan sát] đo trong lab này (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ớ.
Ý chính
Khi bạn sửa một document, WiredTiger không ghi đè giá trị cũ. Nó thêm một version (phiên bản) mới vào đầu một danh sách các version của document đó (gọi là update chain, nằm trên page đang ở trong cache). Mỗi transaction đọc dữ liệu qua một snapshot và một read timestamp: với mỗi document, nó đi từ version mới nhất xuống cho tới version đầu tiên nó được phép thấy.
Khi page được ghi xuống đĩa (lúc bị evict, tức là bỏ khỏi cache để lấy chỗ, hoặc lúc checkpoint), WiredTiger ghi version mới nhất vào file dữ liệu và chuyển các version cũ hơn mà có thể còn ai đó cần sang một file riêng gọi là history store (WiredTigerHS.wt). Version nào chắc chắn không còn ai cần (đã bị thay từ trước oldest timestamp, và không transaction nào đang đọc ở mốc cũ hơn) thì bị xoá. Hệ quả: một snapshot bị giữ mở lâu, hoặc một cửa sổ minSnapshotHistoryWindowInSeconds rộng, buộc WiredTiger giữ nhiều version hơn. Lab sẽ cho thấy cái giá đó cụ thể ở đâu và ở đâu thì chưa thấy.
Hình dung trước: thư viện giữ mọi ấn bản
Một thư viện có cuốn "Quy định nội bộ", cập nhật liên tục. Mỗi lần sửa, thư viện không tẩy chữ trên cuốn cũ; họ in một ấn bản mới, đóng dấu thời điểm in, và xếp lên đầu chồng. Ai đến mượn sẽ nhận ấn bản mới nhất tại thời điểm họ bước vào, và chị Hà cầm một ấn bản từ sáng sẽ đọc ấn bản đó đến hết ngày dù thư viện đã in thêm hàng chục bản.
Thư viện không thể giữ mọi ấn bản mãi. Thủ thư làm hai việc: chồng ấn bản gần nhất để ngay trên bàn (ai cũng với tới), còn ấn bản cũ hơn thì chuyển xuống kho ở tầng hầm. Và họ chỉ vứt một ấn bản khi không còn người đọc nào cầm ấn bản đó hoặc cũ hơn. Một người mượn ấn bản sáng rồi ngồi lì cả tuần thì cả chồng ấn bản từ sáng đến giờ phải ở lại.
Thư viện WiredTiger
───────────────────────────────────── ──────────────────────────────────────────
mỗi ấn bản có dấu thời điểm in mỗi version có timestamp (và transaction id)
chồng ấn bản trên bàn, mới nhất ở trên update chain trên page trong cache
nhận ấn bản mới nhất lúc bước vào read timestamp của transaction
chuyển ấn bản cũ xuống tầng hầm reconciliation chuyển version cũ sang history store
vứt ấn bản không ai cầm hoặc cũ hơn xoá version cũ hơn oldest timestamp và mọi read timestamp
người mượn ngồi lì cả tuần transaction bị giữ mở[hình dung] Đây chỉ là cách hình dung. WiredTiger không có thủ thư đi chuyển sách: version được chuyển xuống history store trong lúc ghi page xuống đĩa, và bị xoá khi page của history store được ghi lại hoặc checkpoint chạy.
Bốn tầng transaction
Một lệnh ghi hay một transaction của ứng dụng đi qua bốn tầng trước khi chạm tới dữ liệu:
Ứng dụng: withTransaction(...) hoặc một lệnh updateOne đơn lẻ
│
▼
MongoDB: transaction của session (readConcern, txnNumber, retry)
│ [bài Transactions & Atomicity]
▼
WiredTiger: một transaction của WiredTiger = transaction id
│ + snapshot (danh sách transaction đang chạy)
│ + read timestamp (và commit timestamp lúc ghi)
▼
Trong cache: page của B-tree → với mỗi key, một update chain
│ (version mới nhất ở đầu)
▼ evict / checkpoint (reconciliation)
Trên đĩa: file .wt = version mới nhất đã commit
WiredTigerHS.wt = các version cũ hơn mà ai đó có thể còn cần[tài liệu] Theo tài liệu WiredTiger, mọi thao tác đọc ghi đều chạy trong một transaction của WiredTiger; nếu không ai bắt đầu transaction tường minh thì WiredTiger tự tạo một cái cho riêng lệnh đó. Một lệnh updateOne ngoài transaction của MongoDB vẫn là một transaction của WiredTiger, chỉ ngắn. Mặc định WiredTiger chạy ở snapshot isolation; mỗi transaction có một transaction id duy nhất (cấp trước lần ghi đầu tiên) và ghi id đó vào mọi thao tác nó làm. Snapshot của transaction gồm danh sách id của các transaction đang chạy lúc snapshot được lấy; một update bị coi là không thấy được nếu id của nó nằm trong danh sách đó hoặc lớn hơn id lớn nhất snapshot biết. Đây là cách engine giấu thay đổi chưa commit: ai có snapshot lập khi A còn chạy thì bỏ qua update mang id của A, đi tiếp xuống version cũ hơn.
[tài liệu] Ngoài id, WiredTiger còn có timestamp: số 64 bit do ứng dụng của WiredTiger tự quản lý (ở đây ứng dụng đó là mongod). Một update chỉ thấy được khi cả transaction id lẫn timestamp của nó đều thấy được với người đọc.
Các timestamp mà MongoDB dùng
Trong mongod, các timestamp của WiredTiger chính là Timestamp(t, i) của MongoDB (t là giây, i là bộ đếm trong giây): WiredTiger lưu chúng thành một số 64 bit bằng (t << 32) | i. Năm cái cần biết:
| Timestamp | Nghĩa | Ai đặt |
|---|---|---|
| read timestamp | transaction này chỉ thấy các update có timestamp ≤ nó; cố định cả đời transaction | lúc transaction lấy snapshot |
| commit timestamp | timestamp gắn vào các update của transaction lúc nó ghi | mongod, lúc ghi |
| oldest timestamp | điểm xa nhất trong quá khứ mà người đọc mới còn được phép bắt đầu; đọc trước điểm này là lỗi | mongod, đi theo sau stable timestamp một khoảng bằng minSnapshotHistoryWindowInSeconds |
| stable timestamp | ranh giới giữa phần dữ liệu đã cố định, bền và phần còn tạm (phần sau stable có thể bị bỏ khi recovery hoặc rollback to stable) | mongod (xem đoạn về majority commit point bên dưới) |
| pinned timestamp | min(oldest timestamp, read timestamp của mọi transaction đang chạy); mốc thật để quyết định xoá version nào | WiredTiger tự tính |
Một reader ở đây là bất kỳ transaction nào của WiredTiger đang đọc. [tài liệu] Tài liệu timestamp của WiredTiger 12.0.0: oldest timestamp là "điểm sớm nhất mà ứng dụng có thể bắt đầu transaction đọc mới"; pinned timestamp là "cận dưới thật" cho mọi thao tác xoá dữ liệu đã cũ. Ở lớp MongoDB, minSnapshotHistoryWindowInSeconds (mặc định 300 giây, từ 5.0) là thời gian tối thiểu storage engine giữ snapshot history (các version cũ để đọc lại ở mốc trong quá khứ), và manual nói nó nằm ở file WiredTigerHS.wt.
Mối quan hệ giữa stable timestamp và majority commit point (điểm mà đa số node của replica set đã có) là chuyện của bài Replica Set & Oplog và bài Elections, Failover & Majority. [quan sát] Ở lab một node dưới đây, stable timestamp trùng đúng với lastCommittedOpTime. [suy luận] Với nhiều node, stable timestamp không thể vượt majority commit point; tôi chưa kiểm chứng điều đó ở bài này.
Quan sát chúng
Nguồn là serverStatus().wiredTiger.transaction (các trường transaction global ... timestamp), replSetGetStatus().optimes, và $currentOp cho transaction đang mở. Script e6-timestamps.js đặt minSnapshotHistoryWindowInSeconds = 30 (chỉ trong lab), mở một transaction snapshot, rồi để client khác update 40 giây. Mỗi dòng dưới đây đã đổi từ số 64 bit sang dạng giây|bộ đếm (e6-timestamps.out):
(1) chưa ai đọc (2) vừa mở snapshot (3) 40 s sau, snapshot còn mở (4) sau khi giải phóng
global oldest timestamp 1791550045|2 1791550045|2 1791550084|1 1791550084|1
global stable timestamp 1791550075|2 1791550075|2 1791550114|1 1791550114|1
global pinned timestamp 1791550045|2 1791550045|2 1791550075|2 <- bị giữ 1791550084|1
read timestamp of the oldest
active reader 0 1791550075|2 1791550075|2 0
replSetGetStatus
lastCommittedOpTime.ts 1791550075|2 1791550075|2 1791550114|1 1791550114|1// $currentOp, session đang rảnh nhưng giữ transaction:
{ type: 'idleSession',
transaction: { parameters: { txnNumber: Long('2'), autocommit: false,
readConcern: { level: 'snapshot', provenance: 'clientSupplied' } },
readTimestamp: Timestamp({ t: 1791550075, i: 2 }), timeOpenMicros: Long('18345') } }Đọc bảng này từ trên xuống thì cả cơ chế hiện ra:
- [quan sát] Read timestamp của transaction (cột 2) là
1791550075|2, đúng bằng stable timestamp vàlastCommittedOpTimelúc đó (ở một node, lệnh ghi mới nhất cũng ở mốc này). [tài liệu] Manual: read concern"snapshot"đọc dữ liệu majority-committed, không phải "bây giờ"; vì vậy ở bài Isolation & Snapshot cursor"snapshot"bịSnapshotTooOldkhi không có lệnh ghi nào trong cả cửa sổ. - [quan sát] Oldest timestamp luôn đi sau stable timestamp đúng 30 giây (45 so với 75; 84 so với 114): đó là
minSnapshotHistoryWindowInSecondsđang đặt. Với giá trị mặc định 300, khoảng cách là 5 phút. - [quan sát] Trong cột 3, oldest timestamp đã tiến tới
...084, nhưng pinned timestamp ở lại...075|2, đúng bằng read timestamp của transaction đang mở. Mọi version mới hơn pinned timestamp phải được giữ. Cột 4: giải phóng transaction thì pinned timestamp nhảy lên bằng oldest timestamp. - [quan sát]
$currentOpcho thấy transaction đang mở và read timestamp của nó: cách tìm ai đang giữ pinned timestamp.
Cột mốc: Bạn đã có thể kể bốn tầng transaction, nói mỗi timestamp đánh dấu mốc nào, và giải thích vì sao một snapshot cũ cần các version cũ còn sống. Tiếp theo: Update chain và history store.
Hỏi & đáp
Một thư viện giữ mọi ấn bản của cuốn quy định nội bộ. Chị Hà mượn ấn bản sáng nay và ngồi đọc cả tuần. Thư viện in thêm hàng chục ấn bản. Họ có thể bỏ ấn bản nào?
Oldest timestamp của một node đang ở mốc 84 và stable timestamp ở mốc 114 (tính bằng giây). Một transaction snapshot mở từ trước, read timestamp 75, vẫn chưa đóng. Pinned timestamp là bao nhiêu?