Hệ thống Dữ liệu Phân tán/Đồng hồ không tin cậy — clock skew, monotonic vs wall clock
28/38
Bài 28 / 38~12 phútRắc rối của hệ phân tánMiễn phí lượt xem

Đồng hồ không tin cậy — clock skew, monotonic vs wall clock

Đồng hồ các node lệch nhau (clock skew) và nhảy (NTP). Vì sao 'timestamp lớn hơn = xảy ra sau' sai, và monotonic clock khác wall clock thế nào.

TL;DR:bài trước, mạng không cho bạn suy luận thứ tự sự kiện đáng tin. Lối thoát tự nhiên: gắn timestamp cho mỗi sự kiện rồi so cái nào lớn hơn thì xảy ra sau. Bài này cho thấy lối thoát đó vỡ, vì đồng hồ cũng không tin cậy: mỗi node có đồng hồ riêng chạy lệch nhau (clock skew), và wall clock (time-of-day) còn có thể nhảy — kể cả nhảy lùi — khi NTP đồng bộ lại. Ta phân biệt wall clock (đo mấy giờ, có thể nhảy) với monotonic clock (chỉ tăng, đo khoảng thời gian), rồi xem chiến lược Last-Write-Wins dựa timestamp đứng trên giả định gì khi đồng hồ lệch.

Bạn đã học ở bài 01 rằng không thể tin mạng để biết cái gì tới trước. Câu hỏi tiếp theo rất tự nhiên: "Vậy gắn đồng hồ cho mỗi sự kiện, rồi so giờ thì sao?". Cả bài này là câu trả lời "không được" — và hiểu vì sao không được là điều kiện để bài 03 giới thiệu một cách suy luận thứ tự không cần đồng hồ vật lý.

1. Analogy — một phòng đầy đồng hồ treo tường lệch giờ

Tưởng tượng một toà nhà mỗi phòng treo một đồng hồ, và không phòng nào chỉ đúng cùng một giờ: phòng A chỉ 10:00:03, phòng B chỉ 09:59:58, phòng C chỉ 10:00:01. Giờ hai người ở hai phòng khác nhau cùng ghi lại "tôi hành động lúc mấy giờ" rồi so xem ai làm trước. Con số họ ghi không so sánh được, vì mỗi cái đo bằng một đồng hồ lệch riêng. Tệ hơn: thỉnh thoảng bảo vệ đi chỉnh lại từng đồng hồ cho "đúng giờ chuẩn" — nên một đồng hồ có thể đột ngột nhảy vài giây tới hoặc lùi ngay giữa lúc bạn đang dùng nó.

Đó chính là đồng hồ trong một cụm máy chủ.

Toà nhà nhiều đồng hồHệ phân tán
Mỗi phòng một đồng hồMỗi node một đồng hồ thạch anh riêng
Các đồng hồ chỉ lệch nhauClock skew giữa các node
Đồng hồ chạy nhanh/chậm dầnClock drift (thạch anh không hoàn hảo)
Bảo vệ chỉnh đồng hồ đột ngộtNTP đồng bộ lại → đồng hồ nhảy
Nhảy lùi vài giâyWall clock có thể lùi về quá khứ
So giờ hai phòng để biết ai trướcSo timestamp hai node để suy thứ tự (sai)
💡 Cách nhớ

"Mấy giờ rồi?" và "bao lâu đã trôi qua?" là hai câu hỏi khác nhau, cần hai loại đồng hồ khác nhau. Lẫn lộn chúng là gốc của phần lớn bug liên quan thời gian.

2. Hai loại đồng hồ: wall clock và monotonic clock khác nhau thế nào?

Mọi máy tính hiện đại có hai loại đồng hồ, phục vụ hai mục đích khác hẳn nhau:

  • Wall clock (time-of-day clock): trả lời "bây giờ là mấy giờ" theo lịch thực (ví dụ số mili-giây kể từ mốc 1970 — Unix epoch). Nó được đồng bộ với giờ chuẩn qua NTP (Network Time Protocol). Vì được chỉnh, nó có thể nhảy: nếu đồng hồ máy đang chạy nhanh 200ms, NTP có thể kéo lùi nó → thời điểm đọc sau nhỏ hơn thời điểm đọc trước.
  • Monotonic clock: trả lời "đã trôi qua bao lâu" tính từ một mốc tuỳ ý (thường là lúc máy khởi động). Nó được đảm bảo chỉ tăng, không bao giờ nhảy lùi. Giá trị tuyệt đối của nó vô nghĩa khi so sánh giữa hai máy (mốc khác nhau), nhưng hiệu hai lần đọc trên cùng một máy cho khoảng thời gian đáng tin.
flowchart TD
  Q1["Cau hoi: 'may gio roi?'<br/>(timestamp su kien, log, lich)"] --> WC["WALL CLOCK<br/>dong bo NTP, CO THE nhay"]
  Q2["Cau hoi: 'bao lau da troi qua?'<br/>(timeout, do hieu nang)"] --> MC["MONOTONIC CLOCK<br/>chi tang, khong nhay lui"]
  WC -.->|"KHONG dung de do khoang"| BUG["Bug: khoang am neu NTP keo lui"]
  MC -.->|"KHONG dung de so 2 may"| BUG2["Vo nghia: moc khac nhau"]

Quy tắc rút ra: đo khoảng thời gian (timeout ở bài 01, đo latency một thao tác) → dùng monotonic. Ghi mốc thời gian tuyệt đối (một sự kiện xảy ra lúc mấy giờ, để hiển thị/log) → dùng wall clock, nhưng đừng tin nó để so sánh thứ tự giữa các máy.

Nhớ lại Latency & RTT

Bạn đã đo RTT (round-trip time) — khoảng thời gian một gói đi và về. Đây đúng là trường hợp phải dùng monotonic clock: bạn cần "bao lâu đã trôi qua", đo trên cùng một máy (ghi mốc lúc gửi, trừ đi lúc nhận). Nếu lỡ dùng wall clock và NTP kéo lùi đồng hồ đúng lúc đó, RTT tính ra có thể âm — một con số vô nghĩa. RTT cũng chính là lý do vì sao đồng bộ đồng hồ qua mạng không bao giờ chính xác tuyệt đối: gói NTP mất một RTT không cố định để tới nơi.

3. Clock skew và NTP: vì sao wall clock nhảy

Đồng hồ máy tính dựa vào một bộ dao động thạch anh — không hoàn hảo. Nó chạy nhanh hoặc chậm hơn thời gian thật một chút, và mức lệch thay đổi theo nhiệt độ. Sai số điển hình cỡ vài chục phần triệu; nghe nhỏ, nhưng tích luỹ có thể lệch nhiều mili-giây tới vài giây mỗi ngày. Hai node khởi động cùng lúc, sau một ngày đồng hồ đã lệch nhau — đó là clock skew.

Để chống trôi, các máy chạy NTP: định kỳ hỏi một máy chủ thời gian rồi chỉnh đồng hồ local về gần giờ chuẩn. Chính hành động chỉnh này khiến wall clock nhảy:

  • Nếu đồng hồ local đang chậm, NTP đẩy nó tới → có một khoảng thời gian bị "nhảy cóc" qua (một số mốc thời gian không bao giờ được đọc).
  • Nếu đồng hồ local đang nhanh, NTP kéo nó lùi → thời gian lặp lại hoặc đi giật lùi. Đây là trường hợp nguy hiểm: code đọc wall clock hai lần liên tiếp có thể thấy lần sau nhỏ hơn lần trước.

Vì vậy wall clock không phải một dòng thời gian tăng đều. Nó là một con số được chỉnh liên tục để bám giờ chuẩn, và mỗi lần chỉnh là một lần nó nói dối về "trôi đều".

4. Vì sao Last-Write-Wins dựa timestamp lại mong manh?

Nhớ lại bài multi-leader ở Module 2: khi hai ghi xung đột, một chiến lược giải quyết là Last-Write-Wins (LWW) — gán mỗi ghi một wall-clock timestamp, giữ ghi có timestamp lớn nhất, vứt phần còn lại. Chiến lược này giả định "timestamp lớn hơn = xảy ra sau". Bài này vừa cho thấy giả định đó sai vì clock skew. Trước khi đọc tiếp, hãy thử lần theo một tình huống cụ thể.

Thử đoán trước

Hai node A và B nhận ghi cho cùng một key x. Đồng hồ node A đang chạy nhanh hơn node B 100ms. Diễn biến theo thời gian thật:

  • Lúc thật t = 0ms: A nhận ghi x = "An". A đóng dấu timestamp bằng đồng hồ của nó → 100 (vì A nhanh 100ms).
  • Lúc thật t = 50ms (tức sau đó 50ms): B nhận ghi x = "Bình". B đóng dấu bằng đồng hồ của nó → 50.

Cả hai ghi lan tới mọi replica. LWW giữ ghi có timestamp lớn nhất. Theo thời gian thật, ghi nào xảy ra sau, và LWW sẽ giữ ghi nào? Kết quả cuối của x là gì? Thử lần theo trước khi đọc tiếp.

Lần theo: theo thời gian thật, ghi x = "Bình" xảy ra sau (t=50ms so với t=0ms), nên trực giác "giữ cái mới nhất" phải giữ "Bình". Nhưng LWW chỉ nhìn con số timestamp: ghi "An" mang timestamp 100, ghi "Bình" mang 50. Vì 100 vượt 50, LWW giữ "An"vứt "Bình" — đúng ngược với thứ tự thật. Kết quả cuối x = "An", và ghi "Bình" (hợp lệ, mới hơn) biến mất âm thầm: không lỗi, không cảnh báo, chỉ là dữ liệu người dùng lặng lẽ mất.

Đây là mặt nguy hiểm của LWW-timestamp: nó trông đúng và chạy đúng khi đồng hồ trùng khớp, nhưng chỉ cần clock skew — thứ luôn tồn tại — là nó vứt nhầm ghi. Và vì đồng hồ lệch chỉ vài chục mili-giây, lỗi chỉ xảy ra với các ghi xung đột rất gần nhau về thời gian — hiếm, không tái hiện ổn định, cực khó debug. Bài học: timestamp vật lý không phải một thứ tự đáng tin trong hệ phân tán. Đó chính là động lực để bài 03 tìm một cách suy luận thứ tự không dựa đồng hồ.

5. Pitfall — tin wall clock để đo khoảng hoặc so thứ tự

Pitfall — dùng sai loại đồng hồ

Hai lỗi phổ biến, cùng gốc là lẫn lộn "mấy giờ" với "bao lâu":

❌ start = wallClock();
   ... lam viec ...
   duration = wallClock() - start;   // NTP keo lui giua chung
   // -> duration co the AM, hoac sai lech vai giay

✅ start = monotonicClock();
   duration = monotonicClock() - start;   // luon >= 0, dang tin

❌ if (writeA.wallTimestamp > writeB.wallTimestamp) keep(writeA);
   // -> clock skew giua node -> giu nham ghi, mat du lieu

✅ Dung logical clock / vector clock (bai 03) de suy thu tu nhan qua
✅ Hoac chap nhan LWW nhung HIEU no co the mat du lieu (chi dung khi
   mat mot ghi la chap nhan duoc)

Quy tắc: monotonic cho khoảng thời gian, wall clock chỉ để hiển thị/log — và không bao giờ dùng wall-clock timestamp làm căn cứ chắc chắn cho thứ tự nhân quả giữa các máy.

Hướng đúng: tách bạch hai loại đồng hồ theo câu hỏi cần trả lời. Khi cần thứ tự sự kiện đáng tin, đừng tìm một đồng hồ "chuẩn hơn" — hãy đổi công cụ: dùng đồng hồ logic (bài 03). Nếu buộc dùng LWW, hãy tài liệu hoá rõ rằng nó có thể mất dữ liệu và chỉ áp cho dữ liệu chấp nhận được điều đó.

6. 📚 Đào sâu — nguồn gốc & tài liệu

📚 Đào sâu — Unreliable Clocks
  • Designing Data-Intensive Applications (Kleppmann) — Chương 8, mục "Unreliable Clocks" — phần "Monotonic Versus Time-of-Day Clocks" là nền cho mục 2, và "Clock Synchronization and Accuracy" giải thích vì sao NTP không bao giờ chính xác tuyệt đối.
  • DDIA — Chương 8, mục "Relying on Synchronized Clocks" — phân tích LWW và vì sao timestamp vật lý nguy hiểm cho việc giải xung đột (mục 4 của bài lấy từ đây).
  • NTP (Network Time Protocol) — giao thức đồng bộ đồng hồ qua mạng; đọc để hiểu vì sao độ chính xác bị giới hạn bởi RTT không cố định của gói tin.
  • Về đồng hồ chính xác cao: Google Spanner dùng TrueTime (đồng hồ nguyên tử + GPS) trả về một khoảng bất định thay vì một điểm — một cách kỹ thuật để bao clock uncertainty thay vì phủ nhận nó. DDIA mục "Relying on Synchronized Clocks" có bàn.

7. Liên hệ các bài khác

  • Bài 01 — Mạng không tin cậy: bài này đóng nốt lối thoát mà bài 01 mở ra — không tin mạng để biết thứ tự, cũng không tin đồng hồ để biết thứ tự.
  • Bài 03 — Causal order: vì đồng hồ vật lý bất khả tín, bài 03 dùng đồng hồ logic (happened-before, vector clock) để suy thứ tự nhân quả mà không cần đồng bộ thời gian.
  • Module 2 — Multi-leader replication: LWW là một chiến lược giải xung đột ở đó; bài này giải thích vì sao nó mong manh, còn vector clock (bài 03) là lựa chọn không mất dữ liệu.
  • Bài 04 — Partial failure & fencing token: process pause (GC) có thể khiến một node "ngủ" lâu rồi thức dậy với đồng hồ đã lệch — một biểu hiện khác của việc không tin được thời gian trên một node.

8. Tóm tắt

  • Mỗi node có đồng hồ thạch anh riêng, trôi nhanh/chậm khác nhau → clock skew: timestamp hai node không so sánh trực tiếp được.
  • Wall clock (time-of-day) trả lời "mấy giờ", đồng bộ NTP nên có thể nhảy — kể cả nhảy lùi. Không dùng để đo khoảng thời gian.
  • Monotonic clock chỉ tăng, không nhảy lùi → đúng để đo khoảng (timeout, latency) trên một máy; giá trị tuyệt đối vô nghĩa khi so hai máy.
  • NTP chỉnh đồng hồ định kỳ; hành động chỉnh chính là lúc wall clock nhảy tới/lùi.
  • Last-Write-Wins dựa wall-clock timestamp mong manh: clock skew có thể khiến nó vứt nhầm một ghi mới hơn, mất dữ liệu âm thầm — chỉ dùng khi mất một ghi là chấp nhận được.
  • Kết luận: timestamp vật lý không phải thứ tự đáng tin trong hệ phân tán → cần đồng hồ logic (bài 03).

9. Tự kiểm tra

Tự kiểm tra
Q1
Phân biệt wall clock và monotonic clock: mỗi loại trả lời câu hỏi gì, và loại nào có thể nhảy lùi?

Wall clock (time-of-day) trả lời "bây giờ là mấy giờ" theo lịch thực (vd mili-giây từ mốc 1970). Nó được NTP đồng bộ nên có thể nhảy tới hoặc lùi khi bị chỉnh. Dùng để ghi mốc thời gian tuyệt đối, hiển thị, log.

Monotonic clock trả lời "đã trôi qua bao lâu" từ một mốc tuỳ ý; nó được đảm bảo chỉ tăng, không bao giờ nhảy lùi. Đúng để đo khoảng thời gian (timeout, latency) trên cùng một máy. Giá trị tuyệt đối của nó vô nghĩa khi so sánh giữa hai máy vì mốc khác nhau.

Q2
Vì sao dùng wall clock để tính 'duration = end - start' của một thao tác là sai lầm nguy hiểm?

Vì wall clock có thể bị NTP kéo lùi ngay giữa lúc thao tác đang chạy. Khi đó thời điểm end đọc ra lại nhỏ hơn start, khiến duration tính ra bị âm hoặc sai lệch vài giây so với thực tế.

Đây là lỗi tinh vi vì nó chỉ xảy ra đúng vào những lần NTP chỉnh đồng hồ trùng với khoảng đo — hiếm và khó tái hiện. Cách đúng là dùng monotonic clock, vốn được đảm bảo chỉ tăng nên hiệu hai lần đọc luôn không âm và phản ánh đúng khoảng đã trôi.

Q3
Trong tình huống 'Thử đoán trước' ở mục 4, vì sao LWW giữ ghi "An""Bình" mới hơn theo thời gian thật? Đây minh hoạ vấn đề gì?

Vì đồng hồ node A nhanh hơn B 100ms, ghi "An" (xảy ra ở thời gian thật t=0) lại mang timestamp 100, còn "Bình" (thời gian thật t=50, tức mới hơn) chỉ mang timestamp 50. LWW chỉ so con số và giữ cái lớn hơn (100), nên nó giữ "An" và vứt "Bình" — ngược thứ tự thật.

Nó minh hoạ rằng clock skew làm timestamp vật lý không phản ánh đúng thứ tự nhân quả. Ghi "Bình" hợp lệ và mới hơn bị mất âm thầm — không lỗi, không cảnh báo. Vì skew chỉ vài chục ms, lỗi chỉ hiện với các ghi xung đột rất gần nhau, nên cực khó phát hiện.

Q4
NTP chống clock drift bằng cách chỉnh đồng hồ. Vậy vì sao chính NTP lại là nguồn khiến wall clock nhảy lùi?

Đồng hồ thạch anh trôi (nhanh hoặc chậm) theo thời gian và nhiệt độ. NTP định kỳ so với máy chủ thời gian rồi chỉnh đồng hồ local về giờ chuẩn. Nếu đồng hồ local đang chạy nhanh, NTP phải kéo nó lùi để về đúng — và đó chính là lúc thời gian đi giật lùi hoặc lặp lại.

Nói cách khác, NTP đánh đổi: nó giữ đồng hồ bám giờ chuẩn về lâu dài, nhưng phá vỡ tính "trôi đều" trong ngắn hạn. Vì vậy wall clock không phải một dòng thời gian tăng đều mà là một con số bị chỉnh liên tục, và không được dựa vào nó cho các phép đo khoảng hay so thứ tự.

Q5
Nếu clock skew luôn tồn tại, khi nào dùng LWW-timestamp vẫn chấp nhận được, và khi nào tuyệt đối không nên?

Chấp nhận được khi việc mất một ghi khi hai ghi xung đột gần nhau là không gây hại nghiệp vụ — ví dụ ghi trạng thái tạm, cache, "last seen" của người dùng: mất một cập nhật cũng chỉ hơi cũ dữ liệu, tự sửa ở lần ghi sau. Ưu điểm của LWW là cực đơn giản (chỉ so một số).

Tuyệt đối không nên cho dữ liệu mà mỗi ghi đều quý và không được mất: số dư tài khoản, tồn kho, đơn hàng, tin nhắn. Ở đó cần theo dõi nhân quả (vector clock — bài 03) để phát hiện ghi song song và gộp thay vì vứt, hoặc dùng cơ chế mạnh hơn (giao dịch, consensus). Điều quan trọng là hiểu rõ LWW có thể mất dữ liệu chứ không chọn nó vì tưởng nó "đúng".

Bài tiếp theo: Causal order — happened-before và vector clock

Bài này có giúp bạn hiểu bản chất không?

Hỏi đáp về bài này

Chưa có câu hỏi

Đặt câu hỏi

Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).

Đặt câu hỏi đầu tiên