Replica Set & Oplog (P3/3): Replication lag, flow control và initial sync
Ở phần trước: oplog là capped collection, mỗi entry idempotent. Window bằng kích thước chia tốc độ ghi; secondary vắng lâu hơn window thì bị stale và phải initial sync.
- Cần đọc trước: Oplog, idempotent và oplog window
- Dẫn tới: Elections, Failover & Majority
Ý chính
Replication lag là khoảng chậm giữa một thao tác trên primary và lúc secondary áp dụng nó. Khi lag nhỏ, secondary gần như là bản sao tức thời. Khi lag lớn, ba thứ xấu đi cùng lúc: đọc từ secondary trả dữ liệu cũ, w: "majority" chờ lâu, và node chậm khó được bầu làm primary (xem bài Elections, Failover & Majority). Lag cũng là tín hiệu sớm cho thấy secondary có thể sắp tụt lại xa hơn oplog window.
Lag xuất hiện khi secondary áp dụng chậm hơn primary ghi. Nguyên nhân thường gặp theo manual: độ trễ mạng, thông lượng đĩa của secondary, thao tác dài trên primary, và bulk load không chờ ack. Phần này đo lag, xem nó làm gì với đọc và ghi, và xem server tự bảo vệ bằng flow control ra sao.
Đo replication lag
| Công cụ | Chạy ở đâu | Cho biết gì |
|---|---|---|
rs.printSecondaryReplicationInfo() | primary | syncedTo và số giây chậm của từng secondary |
rs.status() (replSetGetStatus) | bất kỳ node | optimeDate của từng member, các mốc optimes |
serverStatus().metrics.repl | secondary | bộ đếm của fetcher, buffer và batch áp dụng |
serverStatus().flowControl | primary | flow control có đang kích hoạt không |
[quan sát] e10-prep.out, ngay sau khi xoá vài collection lớn và nạp 200.000 document vào primary:
source: mongo-rep25-b:27017 syncedTo: ... 02:31:39 replLag: '11 secs (0 hrs) behind the primary'
source: mongo-rep25-c:27017 syncedTo: ... 02:31:49 replLag: '1 secs (0 hrs) behind the primary'Cách tự tính từ rs.status(): lag của một secondary là optimeDate của primary trừ optimeDate của nó. Hai lưu ý. Độ phân giải là 1 giây, và con số của node khác đi qua heartbeat nên cũ tới vài giây. [tài liệu] Delayed member có thể hiện 0 secs khi primary không có hoạt động lâu hơn độ trễ của nó. Muốn biết lag có đang tăng không, hãy theo dõi theo thời gian.
Lab 1: gây lag, và lag làm gì với đọc và ghi
[quan sát] e10-lag.out. Collection hot có 200.000 document. Một client chạy updateMany({}, { $inc: { n: 1 } }) liên tục 40 giây (w: 1); mỗi vòng là 200.000 entry, và sau mỗi vòng client ghi số vòng vừa xong vào một document riêng. Trong lúc đó một client khác, mỗi lượt: đọc lag của b và c từ rs.status(); đo một insertOne với w: 1 và với w: "majority"; và đọc document ghi số vòng đó trên b. Từ giây 5 tôi giới hạn CPU của b còn 0,25 bằng docker update --cpus, rồi trả lại 2 CPU ở giây 41. Nhãn thời gian là lúc in dòng; mỗi lượt đo xong mới in, nên khi w: "majority" chậm thì các dòng thưa ra.
| Giây | Vòng đã xong trên primary | Lag b / c | Vòng mà b thấy | insertOne w: "majority" | isLagged |
|---|---|---|---|---|---|
| 5 | 1 | 2 / 2 s | vòng 1 | 289 ms | false |
| 11 | 3 | 4 / 3 s | vòng 1 | 2.498 ms | false |
| 17 | 5 | 7 / 3 s | vòng 1 | 4.338 ms | false |
| 26 | 7 | 13 / 5 s | vòng 1 | 7.916 ms | true |
| 39 | 10 | 21 / 9 s | vòng 1 | 12.516 ms | true |
| 58 | 11 | 34 / 14 s | vòng 4 | 17.614 ms | true |
Đọc bảng:
- Lag xuất hiện cả ở node không bị giới hạn.
cgiữ 2 CPU suốt lab mà lag vẫn tăng từ 2 lên 14 giây:updateManysinh mỗi document một entry (phần về oplog), và primary tạo entry nhanh hơn secondary áp dụng. Giới hạn CPU làmbtệ hơn nhiều (21 so với 9 giây ở giây 39). w: "majority"đi theo lag,w: 1thì không.insertOnevớiw: 1luôn trong khoảng 1 đến 113 ms; với"majority"nó tăng từ 289 ms lên 17,6 giây. Với ba node, majority cần primary và một secondary; nó chờ secondary nhanh hơn, tứcc.- Majority nhanh lại khi
cbắt kịp, dùbvẫn xa. Ngay sau dòng giây 58,insertOnemajority còn 43 ms rồi 2 đến 6 ms, và từ giây 60cbáo lag 0, trong khibvẫn chậm 42 đến 44 giây. Latency của majority theo secondary nhanh nhất, không theo secondary chậm nhất. - Đọc từ secondary thấy dữ liệu cũ. Từ giây 5 tới 39, một lệnh đọc trên
bluôn thấy vòng 1 trong khi primary đã ở vòng 10: dữ liệu cũ khoảng 30 giây. [suy luận] ĐọcreadConcern: "majority"trên chính secondary đó cũng không mới hơn những gì nó đã áp dụng (bài Durability: lab majority và phía đọc). - Flow control bật nhưng lag vẫn tăng.
isLaggedthànhtruetừ giây 26 (targetRateLimitxuống 151 rồi 100), vậy mà lag vẫn tăng tới giây 58. Lab 2 xem kỹ chuyện này. - Hết nghẽn chưa phải là hết lag. Client ghi dừng ở giây 40,
bđược trả CPU ở giây 41, nhưngbtới giây 84 mới thấy vòng 11 và lag về 0: khoảng 43 giây để áp dụng backlog (các entry chưa áp dụng), dù primary gần như không ghi thêm. Trong lúc đórs.status()báo lag củabđứng ở 41 đến 44 giây rồi rơi về 0 một lần. [suy luận] Con số lag cho biết secondary chậm bao xa, không cho biết nó cần bao lâu để theo kịp.
Lab 2: flow control
[tài liệu] Flow control cố giữ majority committed lag (khoảng chậm giữa thay đổi mới nhất trên primary và majority commit point, điểm mà đa số node đã có) dưới flowControlTargetLagSeconds (mặc định 10 giây). Khi lag này vượt một tỷ lệ nào đó của ngưỡng, lệnh ghi trên primary phải lấy ticket trước khi lấy lock, và số ticket cấp mỗi giây bị giới hạn. enableFlowControl mặc định true (tham số có thể đổi lúc chạy). serverStatus().flowControl.isLagged cho biết nó đang kích hoạt. Manual lưu ý lag vẫn có thể xảy ra mà không kích hoạt flow control (ví dụ một secondary không phản hồi).
[quan sát] e14-flow.out, e14-docker.out. 16 vòng lặp insertOne chạy song song trong một mongosh (w: 1, document khoảng 1 KB) trong 75 giây, mỗi giây đo số insert, lag và flowControl. Từ giây 8 tôi giới hạn CPU của cả b và c còn 0,1, giây 55 trả lại 2 CPU. Bộ nhớ mỗi node lúc này là 3 GB (xem mục "Giới hạn của lab").
| Giây | Insert mỗi giây | Lag b / c | isLagged | targetRateLimit |
|---|---|---|---|---|
| 1 đến 13 (trước và ngay sau khi giới hạn CPU) | 7.673 đến 17.544 (trung bình 12.835) | 0 đến 5 s | false | 1.000.000.000 |
| 14 | 18.350 | 6 / 6 s | true | 1.829 |
| 16 | 617 | 8 / 7 s | true | 100 |
| 25 | 100 | 16 / 15 s | true | 100 |
| 40 | 100 | 29 / 29 s | true | 100 |
| 55 (trả CPU) | 100 | 41 / 41 s | true | 100 |
| 57 | 2.176 | 0 / 0 s | false | 2.176 |
| 62 đến 75 | 5.312 đến 12.407 (trung bình 10.209) | 0 đến 1 s | false | tăng dần tới 34.760 |
- Flow control kích hoạt khi lag khoảng 6 giây, trước ngưỡng 10 giây, khớp với "một tỷ lệ của ngưỡng" trong manual. [suy luận] Lag trong bảng là lag áp dụng đọc từ
rs.status(), không phải chính majority committed lag; khi cả hai secondary cùng chậm, hai con số này gần nhau. - Thông lượng của client giảm mạnh. Từ giây 16 tới 55 trung vị chỉ 100 insert mỗi giây (22 trong 39 lần đo đúng 100), so với 12.835 trước đó: giảm khoảng 99%. Giá trị 100 là mức thấp nhất mà
targetRateLimitvề tới trên 8.3.11 trong lab; tôi không thấy manual nêu mức sàn này, nên coi nó là [chi tiết cài đặt]. Mỗi giây các luồng tổng cộng mất khoảng 15 giây chờ ticket (timeAcquiringMicros, cộng dồn qua 16 luồng): ứng dụng chờ chứ không lỗi. - Lag vẫn tăng tới 42 giây. Flow control chỉ làm primary chậm lại; nó không làm secondary nhanh lên. Ở đây hai secondary chỉ còn 0,1 CPU, và lag tăng gần đúng 1 giây mỗi giây: [suy luận] chúng gần như không áp dụng nổi cả 100 insert mỗi giây. Lab này không cho thấy flow control giữ được lag thấp.
- Hồi phục nhanh: lag về 0 ngay giây 57, hai giây sau khi trả CPU, và
targetRateLimitleo lại từ 2.176 lên 34.760 trong 18 giây. [suy luận] Có lẽ backlog nhỏ vì trong lúc nghẽn primary chỉ nhận vài trăm insert mỗi giây; đó là phần flow control thực sự giúp, dù lab không có lần chạy tắt flow control để so.
Cái giá là rõ ràng: flow control đổi latency và thông lượng của ứng dụng lấy việc primary không sinh backlog nhanh hơn nhiều so với mức secondary theo được; manual nêu lý do là lag lớn gây áp lực lên cache của primary (cache pressure). Khi secondary gần như đứng yên, như ở đây, nó chỉ làm ứng dụng chậm mà lag vẫn tăng. Đừng tắt hay chỉnh flowControlTargetLagSeconds nếu chưa có số đo của hệ thống bạn: thấy isLagged: true nghĩa là đi tìm secondary yếu, không phải chỉnh tham số.
Delayed member: lag cố ý
[tài liệu] Delayed member áp dụng thay đổi chậm secondaryDelaySecs giây; manual yêu cầu nó có priority: 0 và là hidden. Mục đích là giữ một bản dữ liệu cũ hơn để phục hồi sau lỗi do người (xoá nhầm, drop nhầm). [quan sát] e11-delayed.out: cấu hình c thành priority: 0, secondaryDelaySecs: 5 (lab bỏ qua hidden cho gọn; server vẫn nhận cấu hình).
| Tình huống | Kết quả |
|---|---|
a, b bình thường, c trễ 5 s | w: "majority" 1 đến 2 ms (primary + b); hello() trên c: secondary: true, secondaryDelaySecs: 5 |
Một document ghi trên a, dò liên tục trên c | c thấy sau 5.034 ms |
docker pause b: majority phải dựa vào c | w: "majority" mất 5.004 đến 5.007 ms (6 lần); w: 1 vẫn 1 đến 10 ms |
Hai bài học. Delayed member có votes: 1 thì vẫn có phiếu và vẫn đếm vào majority: khi b mất, majority chờ đúng bằng độ trễ cố ý, khớp với manual (delayed member không thể ack sớm hơn secondaryDelaySecs). Và nó chống lệnh nhầm theo cách mà secondary thường không làm được (ở phần đầu của bài, lệnh xoá nhầm tới mọi secondary trong 2 giây), nhưng chỉ khi bạn phát hiện lỗi trong khoảng độ trễ đó; nó không thay backup.
Initial sync, ở mức tài liệu
Initial sync là cách một node mới (hoặc node đã stale) lấy lại toàn bộ dữ liệu. [tài liệu] Logical initial sync: sao chép mọi database trừ local, build index song song với lúc chép, đồng thời lấy oplog mới về một collection tạm, rồi áp dụng chúng; xong thì node chuyển STARTUP2 thành SECONDARY. Nếu lúc áp dụng phát hiện thiếu một khoảng oplog so với nguồn, initial sync bắt đầu lại từ đầu; vì vậy oplog window phải đủ dài cho cả thời gian chép. Bản file-copy chỉ có ở Enterprise.
[quan sát] e9-initsync.out. Sau lần c bị stale (phần về oplog), tôi xoá container c, khởi động lại với thư mục dữ liệu trống. Primary có lab25 khoảng 6.029 MB dữ liệu, 6.725.314 document. Log của c: Initial sync done với durationSeconds: 25, method: "logical", approxTotalBytesCopied: 6316285796, và Finished cloning data. Beginning oplog replay, rồi chuyển STARTUP2 → RECOVERING → SECONDARY trong cùng một giây. Sau đó c chọn b, một secondary, làm sync source (log Changed sync source sang mongo-rep25-b): đây là chaining.
6,3 GB trong 25 giây là khoảng 250 MB mỗi giây, một lần đo, trong một máy với network ảo gần bằng không và không có tải ghi nào trên replica set. Đó là cận trên về tốc độ, không đại diện cho production: ba máy thật qua mạng thật, với index lớn và tải đang chạy, sẽ lâu hơn nhiều, và thời gian tăng theo dung lượng dữ liệu.
So với PostgreSQL
[tài liệu] PostgreSQL có hai họ replication. Streaming (physical) replication gửi dòng WAL từ primary sang standby, standby phát lại WAL; tài liệu chỉ rõ log shipping là bất đồng bộ nên có khoảng có thể mất dữ liệu khi primary hỏng. Logical replication theo mô hình publish và subscribe, nhân bản đối tượng theo replica identity (thường là khoá chính), trái với physical là địa chỉ block. Oplog của MongoDB gần logical hơn ở chỗ mô tả thay đổi dữ liệu, nhưng nó là cơ chế duy nhất của replica set.
| MongoDB replica set | PostgreSQL streaming replication | |
|---|---|---|
| Thứ được gửi | entry oplog, idempotent, mức MongoDB | WAL, mức block, phát lại ở standby |
| Giữ log cho node vắng mặt | oplog capped; hết window thì initial sync | wal_keep_size, hoặc replication slot giữ WAL tới khi standby nhận, có thể làm đầy đĩa (max_slot_wal_keep_size để chặn); nếu WAL bị xoá, standby phải khởi tạo lại từ base backup |
| Đo lag | optimeDate, printSecondaryReplicationInfo | pg_stat_replication: write_lag, flush_lag, replay_lag |
| Chờ replica khi commit | w: "majority" | synchronous_commit (remote_write, on, remote_apply...) |
| Đọc trên replica | secondary; đọc từ snapshot, không chờ batch | hot standby; truy vấn có thể bị huỷ khi xung đột với WAL đang phát lại (max_standby_streaming_delay) |
Hai điểm khác nhau về triết lý. Replication slot của PostgreSQL giữ WAL cho tới khi standby nhận: standby không bị thiếu WAL (trừ khi max_slot_wal_keep_size cắt bớt), nhưng một standby chết có thể làm primary đầy đĩa. Oplog không chờ ai: nó có kích thước cố định, đổi lại một node vắng quá lâu thì phải sync lại. Và synchronous_commit với w: "majority" không tương đương nhau: w còn nói về số node, và từ 8.0 majority nói về entry đã ghi, chưa chắc đã áp dụng.
Những lỗi thường gặp
- Đọc từ secondary mà không tính lag: dữ liệu có thể cũ cỡ chục giây khi một secondary quá tải.
- Đặt oplog theo mặc định rồi không đo window ở giờ ghi nặng nhất: window thật có thể chỉ vài chục giây.
- Bulk update hay delete hàng triệu document một lần: mỗi document là một thao tác phải chép đi, lag tăng, flow control kích hoạt và ứng dụng chậm.
- Chỉnh
flowControlTargetLagSecondshay tắt flow control để "hết chậm", thay vì tìm secondary yếu. - Nghĩ lag lớn của một secondary luôn làm
w: "majority"chậm: với ba node, nó chờ secondary nhanh nhất, như Lab 1.
Giới hạn của lab
Ba node chạy trong một máy với network ảo, nên không có độ trễ mạng; mọi số lag, majority và initial sync là cận dưới. docker update --cpus giới hạn CPU của cả container, kể cả thread không liên quan tới replication, nên không mô phỏng một đĩa chậm hay mạng chậm. Lần chạy đầu của Lab 1, ở 2 GB RAM mỗi node, c bị kill vì hết bộ nhớ (OOM): output nằm trong thư mục lab (e10-lag-run1.*), tôi không dùng số của nó, và từ lần chạy lại nâng lên 3 GB mỗi node (initial sync và các lab trước đó chạy ở 2 GB). Host dùng chung với các lab khác nên thời gian dao động, và mỗi lab chỉ chạy một lần: các con số là một mẫu, không phải phân phối.
Cột mốc: Bạn đã có thể đo replication lag, giải thích vì sao với ba node
w: "majority"chờ secondary nhanh nhất, nhận ra flow control đang kích hoạt và cái giá của nó, và nói initial sync làm gì. Bài này khép lại tại đây; tiếp theo là Elections, Failover & Majority.
Hỏi & đáp
Replica set 3 node, b đang lag 40 giây còn c lag 0. Một ứng dụng ghi với w: "majority". Điều gì nhiều khả năng đúng?
serverStatus().flowControl.isLagged trên primary là true, targetRateLimit là 100 và thông lượng ghi của ứng dụng sụt mạnh. Việc nên làm đầu tiên là gì?
Nếu bỏ hết thuật ngữ: một chuỗi cửa hàng có trụ sở và hai chi nhánh chép sổ cái từ trụ sở. Chi nhánh càng chậm thì khách hỏi chi nhánh càng nhận số cũ. Nếu trụ sở coi một giao dịch là xong khi có ít nhất một chi nhánh đã chép, giao dịch chỉ chờ chi nhánh nhanh hơn, không chờ chi nhánh chậm nhất. Khi các chi nhánh chậm quá, trụ sở có thể tự hạn chế nhận khách để chi nhánh theo kịp; đổi lại khách phải xếp hàng. Có thể cố ý để một chi nhánh chép chậm vài giờ, để còn bản sổ cái trước lúc ai đó xoá nhầm. Còn chi nhánh nghỉ lâu hơn khoảng thời gian mà cuốn sổ ghi thay đổi còn giữ được thì phải chép lại cả sổ cái gốc từ đầu.
Bài tiếp theo
Bài này nói secondary chép thế nào và lag là gì; chưa nói khi primary biến mất thì ai thay, bằng cách nào, và những lệnh ghi chỉ mới nằm trên primary cũ thì ra sao.
Elections, Failover & Majority trả lời: heartbeat và electionTimeoutMillis phát hiện primary mất ra sao, term và election hoạt động thế nào, majority commit point dịch chuyển khi failover, lệnh ghi w: 1 có thể bị rollback, và read preference cùng write concern hành xử ra sao trong lúc đó, với lab kill primary.
Tài liệu tham khảo
- MongoDB: Replication (replica set, asynchronous replication, flow control, automatic failover)
- MongoDB: Replica Set Oplog (capped collection, idempotent, oplog size, oplog window, minimum retention)
- MongoDB: Replica Set Data Synchronization (initial sync, streaming replication, multithreaded replication, sync source selection)
- MongoDB: Replica Set Members (primary, secondary, arbiter, member limits)
- MongoDB: Delayed Replica Set Members
- MongoDB: Troubleshoot Replica Sets (replication lag, causes, flow control status)
- MongoDB: Resync a Member of a Replica Set (stale members)
- MongoDB:
replSetResizeOplog(990 MB minimum,minRetentionHours) - MongoDB:
rs.printSecondaryReplicationInfo()vàrs.printReplicationInfo() - MongoDB:
replSetGetStatusvàserverStatus(flowControl,metrics.repl) - MongoDB: Parameters (
enableFlowControl,flowControlTargetLagSeconds,replWriterThreadCount,oplogFetcherUsesExhaust) - MongoDB: Release Notes for 8.0 (batched bulk inserts, majority acknowledgment on written entries, oplog write and apply threads)
- PostgreSQL: Log-Shipping Standby Servers (streaming replication, replication slots,
wal_keep_size) - PostgreSQL: Hot Standby (query conflicts,
max_standby_streaming_delay) - PostgreSQL: Logical Replication
- PostgreSQL: Cumulative Statistics (
pg_stat_replication:write_lag,flush_lag,replay_lag)