Hệ thống Dữ liệu Phân tán/Causal order — happened-before và vector clock
29/38
Bài 29 / 38~12 phútRắc rối của hệ phân tánMiễn phí lượt xem

Causal order — happened-before và vector clock

Không tin đồng hồ vật lý được, làm sao biết cái gì xảy ra trước? Happened-before (Lamport) và vector clock cho thứ tự nhân quả không cần đồng hồ.

TL;DR: Bài trước đóng lối "so timestamp vật lý để biết thứ tự". Bài này mở lối khác: suy luận thứ tự không cần đồng hồ vật lý, chỉ dựa vào quan hệ nhân quả. Leslie Lamport định nghĩa happened-before (a → b: a có thể ảnh hưởng b) — cùng node theo thứ tự thực thi, hoặc gửi rồi nhận một message. Nhiều cặp sự kiện không có quan hệ này — chúng song song (concurrent), nên nhân quả là thứ tự bộ phận, không toàn phần. Lamport clock (một số đếm) cho thứ tự tổng nhưng không phân biệt được song song; vector clock (một vector đếm mỗi node) thì phát hiện được.

Ở bài 01 mạng che mất thứ tự; ở bài 02 đồng hồ cũng không cứu được. Giải pháp không phải tìm một đồng hồ tốt hơn mà đổi câu hỏi: thay vì "cái nào xảy ra sau theo thời gian tuyệt đối?" (bất khả tín), hỏi "cái nào có thể ảnh hưởng tới cái nào?" — một câu hỏi trả lời được chỉ bằng thông tin trao đổi giữa các node, không cần đồng hồ.

1. Analogy — chuỗi email trả lời vs hai email độc lập

Trong hộp thư, bạn phân biệt được hai tình huống mà không cần nhìn giờ gửi. Một email B là trả lời (reply) email A: chắc chắn A đến trước, vì người gửi B đã đọc A rồi mới viết — B phụ thuộc nhân quả vào A. Ngược lại, hai người ở hai nơi cùng viết cho bạn hai email hoàn toàn khác chủ đề, không ai biết email của người kia: đó là hai email độc lập — không cái nào ảnh hưởng cái nào, dù một cái có thể tình cờ gửi trước vài giây. Với cặp reply, thứ tự là xác định (A trước B). Với cặp độc lập, hỏi "cái nào trước" là vô nghĩa về nhân quả — chúng song song.

Hộp thư emailHệ phân tán
Email A, rồi B trả lời ASự kiện a, rồi b (b đã "thấy" a)
B là reply → B phụ thuộc Aa → b (happened-before)
Hai email độc lập, khác chủ đềHai sự kiện concurrent
"Cái nào trước?" vô nghĩa với cặp độc lậpKhông có a → b lẫn b → a
Không cần nhìn giờ gửi để biết replySuy nhân quả không cần đồng hồ vật lý
💡 Cách nhớ

Nhân quả không hỏi "lúc mấy giờ" mà hỏi "cái này có thấy cái kia trước khi xảy ra không". Nếu có, thứ tự xác định. Nếu không thấy nhau, chúng song song — và đó là thông tin quan trọng, không phải thiếu sót.

2. Happened-before: vì sao nhân quả là thứ tự bộ phận?

Leslie Lamport (1978) định nghĩa quan hệ happened-before, ký hiệu a → b, đọc là "a xảy ra trước b" theo nghĩa a có thể ảnh hưởng tới b. Quan hệ này đúng trong ba trường hợp:

  1. Cùng một node: nếu ab xảy ra trên cùng một node và a chạy trước b theo thứ tự thực thi → a → b.
  2. Gửi và nhận: nếu a là sự kiện gửi một message và b là sự kiện nhận chính message đó (ở node khác) → a → b.
  3. Bắc cầu (transitive): nếu a → bb → c thì a → c.

Điểm mấu chốt: không phải cặp sự kiện nào cũng có quan hệ này. Nếu ab xảy ra trên hai node khác nhau và không có chuỗi message nào nối chúng, thì khônga → b lẫn b → a. Ta nói ab song song (concurrent), ký hiệu a ∥ b.

flowchart LR
  A1["a1: node A ghi x"] --> A2["a2: node A gui msg"]
  A2 -->|"message"| B2["b2: node B nhan msg"]
  B2 --> B3["b3: node B ghi y"]
  B1["b1: node B ghi z<br/>(truoc khi nhan)"] --> B2

Trong sơ đồ: a1 → a2 → b2 → b3 (chuỗi nhân quả qua message). Nhưng a1b1 song song — chúng xảy ra độc lập, không node nào thấy sự kiện của node kia trước khi hành động. Vì tồn tại các cặp song song không so được, nhân quả là một partial order (thứ tự bộ phận): một số cặp có thứ tự rõ, một số cặp thì không. Điều này khác total order (thứ tự toàn phần) — nơi mọi cặp đều so được (như số nguyên: hai số bất kỳ luôn có cái lớn hơn).

Sự phân biệt này không phải hình thức: hai ghi song song cho cùng một key chính là xung đột thật cần gộp (như bài multi-leader ở Module 2), còn hai ghi có quan hệ nhân quả thì cái sau đè cái trước một cách hợp lệ. Muốn giải xung đột đúng, hệ phải phân biệt được hai loại đó.

3. Lamport clock — một số đếm, và giới hạn của nó

Cách đơn giản nhất để đánh dấu nhân quả: mỗi node giữ một bộ đếm (một số nguyên). Quy tắc Lamport clock:

- Moi node co bien counter, khoi tao 0.
- Truoc moi su kien noi bo: counter <- counter + 1.
- Khi GUI message: counter <- counter + 1; dinh kem counter vao message.
- Khi NHAN message co timestamp t: counter <- max(counter, t) + 1.

Quy tắc max khi nhận là mấu chốt: nó đảm bảo bên nhận luôn có counter lớn hơn bên gửi tại thời điểm gửi. Kết quả: nếu a → b thì LC(a) < LC(b) — số Lamport tôn trọng nhân quả. Gán thêm một quy tắc phá hoà (ví dụ kèm node-id) là có một total order nhất quán với nhân quả.

Nhưng Lamport clock có một giới hạn nền tảng: chiều ngược lại không đúng. LC(a) < LC(b) không chứng minh a → b — hai sự kiện song song vẫn có hai số khác nhau, và bạn không cách nào biết số đó đến từ "sau thật" hay chỉ "song song mà tình cờ đếm nhiều hơn". Nói cách khác, Lamport clock nén một partial order thành một total order, và trong lúc nén nó xoá mất thông tin "cặp này song song". Với việc cần phát hiện xung đột (biết hai ghi có song song không), Lamport clock không đủ.

Nhớ lại Vector clock

Bạn đã học vector clock ở track thuật toán: mỗi node giữ một vector đếm — một ô cho mỗi node trong hệ, ghi lại "tôi đã thấy bao nhiêu sự kiện của từng node". So hai vector cho ta biết chính xác quan hệ nhân quả: một vector trội hoàn toàn vector kia → có ; không trội hết mà cũng không bị trội hết → song song. Đây đúng là mảnh còn thiếu của Lamport clock, và bài này cho thấy vì sao hệ phân tán cần tới nó.

4. Vector clock — phân biệt được nhân quả với song song

Vector clock khắc phục đúng giới hạn của Lamport clock. Thay vì một số, mỗi node giữ một vector V có một ô cho mỗi node. V[i] là "số sự kiện của node i mà node này đã biết". Quy tắc:

- Node i co vector V, khoi tao [0, 0, ..., 0].
- Truoc moi su kien noi bo cua node i: V[i] <- V[i] + 1.
- Khi GUI message: V[i] <- V[i] + 1; dinh kem CA vector V.
- Khi NHAN vector W: voi moi j: V[j] <- max(V[j], W[j]); roi V[i] <- V[i] + 1.

So sánh hai vector VW:

  • V → W (V xảy ra trước) nếu V[k] <= W[k] với mọi k, và có ít nhất một kV[k] < W[k].
  • V ∥ W (song song) nếu không cái nào trội hết cái kia — tồn tại kV[k] > W[k] mV[m] < W[m].

Nhờ giữ nguyên "ai đã thấy gì của ai", vector clock không nén mất thông tin song song như Lamport clock. Trước khi đọc phần giải, hãy tự áp quy tắc so sánh vào một cặp cụ thể.

Thử đoán trước

Hệ có ba node theo thứ tự ô vector [A, B, C]. Hai sự kiện mang hai vector clock:

  • Sự kiện P: [2, 1, 0]
  • Sự kiện Q: [2, 3, 1]

Áp quy tắc so sánh ở trên (so từng ô): quan hệ giữa P và Q là gì — P → Q, Q → P, hay P ∥ Q (song song)? Và một cặp khác:

  • Sự kiện R: [1, 2, 0]
  • Sự kiện S: [2, 1, 0]

R và S quan hệ gì? Với mỗi cặp, chỉ rõ ô nào quyết định. Thử tự so trước khi đọc tiếp.

Lần theo quy tắc:

  • P [2,1,0] vs Q [2,3,1]: so từng ô — 2<=2 (ô A), 1<=3 (ô B), 0<=1 (ô C): mọi ô của P nhỏ hơn hoặc bằng Q, và có ô lớn thật sự (B: 1 vượt lên 3). Vậy P → Q — P xảy ra trước Q (Q đã "thấy" P). Trực giác khớp: Q biết nhiều sự kiện của B và C hơn, còn của A thì bằng.
  • R [1,2,0] vs S [2,1,0]: ô A có R=1 < S=2 (S trội ở A) nhưng ô B có R=2 > S=1 (R trội ở B). Không cái nào trội hết → R ∥ S, song song. Đây chính là dấu hiệu xung đột: R và S là hai ghi độc lập, không cái nào thấy cái kia — nếu chúng ghi cùng một key thì hệ phải gộp chứ không được vứt một cái.

Đó là sức mạnh vector clock so với Lamport clock: nó không chỉ sắp thứ tự mà còn chỉ mặt cặp nào song song. Cái giá là bộ nhớ — vector dài bằng số node, và với hệ nhiều node biến động, việc quản lý các ô trở nên tốn kém (một vấn đề thực tế mà các hệ dùng nhiều biến thể để giảm nhẹ).

5. Pitfall — tưởng "số lớn hơn = xảy ra sau"

Pitfall — dùng Lamport clock để phát hiện xung đột

Cạm bẫy phổ biến là thấy Lamport clock cho hai số khác nhau rồi kết luận thứ tự nhân quả — trong khi nó không phát hiện được song song.

❌ if (LC(writeA) > LC(writeB)) keep(writeA);   // nhu LWW nhung dung so Lamport
   // -> hai ghi SONG SONG van co hai so khac nhau
   // -> van vut nham mot ghi hop le (xung dot bi bo qua)

✅ Dung VECTOR clock: neu writeA || writeB (song song) -> GOP, dung vut
✅ Chi khi mot vector troi hoan toan vector kia moi coi la de hop le

Nhớ: LC(a) < LC(b) chỉ suy ra được khi a → b thì số của a nhỏ hơnkhông phải chiều ngược lại. Dùng một con số để phát hiện xung đột là đánh mất đúng thông tin bạn cần (song song hay không).

Hướng đúng: chọn công cụ theo câu hỏi. Cần một total order để, ví dụ, sắp log toàn hệ → Lamport clock đủ và rẻ. Cần phát hiện ghi xung đột để gộp đúng → phải vector clock (hoặc biến thể), chấp nhận cái giá bộ nhớ. Và trong mọi trường hợp, đừng quay lại tin timestamp vật lý — đó là bài học của bài 02.

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

📚 Đào sâu — Causality & logical clocks

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

  • Bài 02 — Đồng hồ không tin cậy: bài này là câu trả lời cho bế tắc của bài 02 — suy thứ tự bằng nhân quả (logic), không bằng đồng hồ vật lý.
  • Module 2 — Multi-leader replication: "hai ghi song song cần gộp, hai ghi nhân quả thì đè" chính là bài toán giải xung đột ở đó; vector clock là công cụ phân biệt hai loại — không mất dữ liệu như LWW.
  • Module 6 — Causal consistency: causal order của bài này được nâng thành một mô hình nhất quán — hệ đảm bảo mọi node thấy các sự kiện theo đúng thứ tự nhân quả.
  • Bài 01 — Mạng không tin cậy: vì message là thứ duy nhất nối các node (mạng không tin cậy nhưng vẫn là kênh truyền), quan hệ gửi→nhận trở thành nền để định nghĩa nhân quả mà không cần đồng hồ.

8. Tóm tắt

  • Happened-before (a → b): a có thể ảnh hưởng b — cùng node theo thứ tự, hoặc gửi→nhận cùng message, hoặc bắc cầu.
  • Cặp không có a → b lẫn b → asong song (concurrent) → nhân quả là partial order, không phải total order.
  • Lamport clock (một số, quy tắc max khi nhận): a → bLC(a) < LC(b), cho total order nhất quán với nhân quả — nhưng không phát hiện được song song (chiều ngược không đúng).
  • Vector clock (một ô đếm mỗi node): so từng ô → xác định rõ a→b, b→a, hay song song; đủ để phát hiện ghi xung đột cần gộp. Cái giá: bộ nhớ tỉ lệ số node.
  • Chọn theo câu hỏi: cần total order rẻ → Lamport; cần phát hiện xung đột → vector clock. Tuyệt đối không quay lại timestamp vật lý.

9. Tự kiểm tra

Tự kiểm tra
Q1
Định nghĩa happened-before (a → b) qua ba trường hợp. Vì sao nó tạo ra một partial order chứ không phải total order?

a → b ("a có thể ảnh hưởng b") đúng khi: (1) a và b cùng một node và a chạy trước theo thứ tự thực thi; (2) a là gửi một message và b là nhận chính message đó; (3) bắc cầu: a → bb → c suy ra a → c.

Nó là partial order vì không phải cặp nào cũng có quan hệ: hai sự kiện ở hai node không nối bởi chuỗi message nào thì không có a → b lẫn b → a — chúng song song. Total order đòi mọi cặp đều so được (như số nguyên), còn nhân quả để lại các cặp song song không so được.

Q2
Lamport clock đảm bảo nếu a → b thì LC(a) < LC(b). Vì sao chiều ngược lại không đúng, và điều đó giới hạn Lamport clock ở đâu?

Quy tắc max khi nhận message đảm bảo bên nhận có số lớn hơn bên gửi, nên a → b kéo theo LC(a) < LC(b). Nhưng chiều ngược lại sai: hai sự kiện song song vẫn có hai số khác nhau (mỗi node đếm riêng), nên thấy LC(a) < LC(b) ta không biết đó là "b xảy ra sau thật" hay "hai cái song song mà b tình cờ đếm nhiều hơn".

Giới hạn: Lamport clock nén một partial order thành total order và trong lúc nén xoá mất thông tin "cặp này song song". Vì vậy nó không dùng để phát hiện xung đột (biết hai ghi có song song không) — chỉ để có một thứ tự tổng nhất quán với nhân quả.

Q3
Cho hai vector clock [3, 1, 4][3, 2, 4] (ba node A, B, C). Quan hệ của chúng là gì, ô nào quyết định?

So từng ô: A 3 <= 3, B 1 <= 2, C 4 <= 4. Mọi ô của vector đầu nhỏ hơn hoặc bằng vector sau, và có ít nhất một ô nhỏ thật sự (ô B: 1 vượt lên 2). Vậy vector đầu → vector sau (xảy ra trước).

Ô quyết định là ô B: nó là ô duy nhất khác nhau, và vì nó tăng (1 lên 2) trong khi không ô nào của vector đầu lớn hơn vector sau, quan hệ là nhân quả một chiều chứ không song song. Trực giác: sự kiện sau đã thấy thêm một sự kiện của node B so với sự kiện trước.

Q4
Vì sao việc phân biệt 'hai ghi song song' với 'hai ghi có quan hệ nhân quả' lại quan trọng khi giải xung đột replication?

Hai ghi có quan hệ nhân quả (ghi sau đã thấy ghi trước) thì cái sau đè cái trước là hợp lệ — người ghi sau biết trạng thái cũ và cố ý cập nhật. Nhưng hai ghi song song là hai cập nhật độc lập, không cái nào thấy cái kia: đây là xung đột thật, và vứt một cái đi sẽ mất dữ liệu người dùng.

Nếu không phân biệt được (như LWW-timestamp hay Lamport clock), hệ sẽ mù quáng giữ một ghi và vứt cái kia kể cả khi chúng song song — mất dữ liệu âm thầm. Vector clock cho biết cặp nào song song để hệ gộp (merge/CRDT) hoặc để người dùng giải, thay vì đoán.

Q5
Vector clock mạnh hơn Lamport clock nhưng không phải lúc nào cũng nên dùng. Cái giá của nó là gì, và khi nào Lamport clock đã đủ?

Cái giá là bộ nhớ và quản lý: vector dài bằng số node, phải đính kèm theo message và lưu cùng mỗi phiên bản dữ liệu. Với hệ nhiều node và node thêm/bớt liên tục, các ô vector phình to và việc dọn ô của node đã rời hệ trở nên phức tạp.

Lamport clock đã đủ khi bạn chỉ cần một total order nhất quán với nhân quả mà không cần phát hiện song song — ví dụ gán số thứ tự toàn hệ cho log/sự kiện để sắp xếp trình bày, hoặc làm khoá tăng dần. Ở đó một con số nhỏ gọn thắng, và việc không phân biệt được song song không gây hại.

Bài tiếp theo: Partial failure & fencing token — chống split-brain

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