Unreliable Clocks & Byzantine Faults — Khi thời gian trong máy tính không còn đáng tin (DDIA)
Mở đầu
Ảnh: AlphaTradeZone — Pexels
Khi xây dựng một hệ thống phân tán, có những thứ tưởng chừng đơn giản nhưng lại gây ra vô số đau đầu — và thời gian là một trong số đó. Trong thế giới thực, ai cũng có đồng hồ và chúng đều chỉ cùng một giờ (hoặc xấp xỉ). Trong distributed systems, mỗi máy có đồng hồ riêng, và việc đồng bộ chúng là một bài toán không hề dễ. Càng khó hơn khi bạn phải đối mặt với những node có thể "nói dối" hoặc gửi thông tin sai lệch — đó là lúc Byzantine faults xuất hiện. Martin Kleppmann dành hẳn một phần quan trọng trong Chapter 8 của DDIA để nói về hai vấn đề này.
Đồng hồ trong máy tính — Hai loại, cả hai đều không hoàn hảo
Ảnh: Erik Mclean — Pexels
Mỗi máy tính đều có ít nhất hai loại đồng hồ: time-of-day clock (hay wall clock) và monotonic clock. Time-of-day clock là thứ bạn nhìn thấy trên góc màn hình — nó cho biết "bây giờ là mấy giờ" theo thời gian thực. Nó được đồng bộ qua NTP (Network Time Protocol) và có thể bị nhảy vọt nếu NTP phát hiện sai lệch lớn. Nguy hiểm hơn, nó có thể quay ngược thời gian nếu đồng hồ địa phương chạy nhanh và NTP quyết định chỉnh lại.
Monotonic clock thì khác — nó chỉ đảm bảo một điều: giá trị luôn tăng dần, không bao giờ giảm. Nó đo khoảng thời gian trôi qua (elapsed time), không phải thời gian tuyệt đối. Bạn dùng nó để đo "request này mất bao lâu" hay "timeout sau 30 giây". Vấn đề là monotonic clock giữa các máy khác nhau là hoàn toàn độc lập — bạn không thể so sánh giá trị monotonic clock của máy A với máy B.
Cả hai loại đều có điểm yếu. Time-of-day clock bị ảnh hưởng bởi NTP, leap seconds, và clock drift (sai số dao động thạch anh). Monotonic clock thì không có ý nghĩa tuyệt đối giữa các máy. Khi bạn viết code distributed, cần phân biệt rõ loại đồng hồ nào phù hợp với tác vụ gì.
Hệ quả của đồng hồ không đáng tin — Từ LWW đến Google Spanner
Clock không đáng tin gây ra những vấn đề rất thực tế. Một ví dụ kinh điển là Last Write Wins (LWW) trong các database phân tán như Cassandra hay Riak. Khi hai node cùng ghi vào cùng một key, node nào có timestamp lớn hơn sẽ thắng. Nhưng nếu đồng hồ của node A chạy nhanh hơn node B vài giây, nó có thể "thắng" một cách bất công — dữ liệu của node B, dù ghi sau về mặt thời gian thực, vẫn bị ghi đè.
Một ví dụ nổi tiếng khác là Google Spanner — hệ thống database phân tán toàn cầu của Google. Spanner giải quyết vấn đề clock không chính xác bằng TrueTime API. Thay vì coi thời gian là một con số chính xác, TrueTime trả về một khoảng [earliest, latest] — nghĩa là "thời gian thật chắc chắn nằm trong khoảng này". Spanner dùng confidence interval này để đảm bảo tính đúng đắn của transaction, với một bộ đồng hồ atomic + GPS đồng bộ riêng trong các data center của Google.
Byzantine Faults — Khi node trong hệ thống "nói dối"
Ảnh: Leonid Altman — Pexels
Nếu unreliable clocks là vấn đề về độ chính xác của thời gian, thì Byzantine faults là vấn đề lớn hơn nhiều: một node trong hệ thống có thể hành xử theo bất kỳ cách nào — kể cả gửi thông tin sai lệch, mâu thuẫn, hoặc phối hợp với các node khác để phá hoại. Đây là bài toán kinh điển có tên Byzantine Generals Problem, được đặt ra bởi Leslie Lamport, Robert Shostak và Marshall Pease năm 1982.
Trong thực tế, hầu hết các hệ thống distributed (crash-fault tolerant) đều giả định rằng node có thể bị crash nhưng không bao giờ cố tình gửi dữ liệu sai. Đây là giả định hợp lý trong môi trường data center nội bộ, nơi bạn kiểm soát phần cứng và phần mềm. Nhưng trong các hệ thống mở như blockchain, môi trường đa tổ chức (multi-org), hoặc các hệ thống hàng không/vũ trụ, Byzantine fault tolerance (BFT) trở nên cần thiết.
Chi phí của BFT rất cao. Các thuật toán BFT (như PBFT — Practical Byzantine Fault Tolerance) yêu cầu 3f + 1 node để chịu được f node Byzantine, và mỗi message phải được xác nhận qua nhiều vòng (rounds). Trong khi crash fault tolerance chỉ cần 2f + 1 node cho f node hỏng.
Tuy nhiên, có những hình thức Byzantine yếu hơn mà bạn có thể áp dụng trong thực tế dễ dàng hơn:
- Checksums — phát hiện dữ liệu bị hỏng (corruption) trong quá trình truyền tải
- Input validation — kiểm tra dữ liệu đầu vào có hợp lệ không
- Signed messages — dùng chữ ký số để đảm bảo message không bị giả mạo
- Audit logs — ghi lại tất cả hành vi để có thể truy xuất sau này
Những cơ chế này không giải quyết được Byzantine fault toàn phần, nhưng chúng tạo ra một lớp phòng thủ quan trọng — đặc biệt khi hệ thống của bạn tương tác với các service bên ngoài hoặc dữ liệu đi qua nhiều tầng xử lý.
Key Takeaways
- Time-of-day clock dùng cho thời gian tuyệt đối, có thể bị nhảy (forward/backward) do NTP — không an toàn khi dùng để đo elapsed time hay timestamp so sánh giữa các máy
- Monotonic clock an toàn để đo elapsed time trên cùng một máy, nhưng không thể so sánh giá trị giữa các node khác nhau
- Google Spanner dùng TrueTime — confidence interval thay vì giá trị thời gian chính xác — để giải quyết vấn đề clock drift toàn cầu
- Byzantine faults xảy ra khi node có thể hành xử tùy tiện (kể cả nói dối), khác với crash fault truyền thống
- BFT đắt hơn crash fault tolerance: cần 3f+1 node, nhiều round xác nhận, và thường không cần thiết trong môi trường data center nội bộ
- Các cơ chế phòng thủ nhẹ (checksum, signature, validation) giúp giảm rủi ro Byzantine trong thực tế
Kết
Unreliable clocks và Byzantine faults là hai trong những vấn đề "âm thầm nhưng nguy hiểm" nhất trong distributed systems. Clock không đáng tin có thể phá hỏng các giả định về thời gian — thứ mà nhiều developer coi là hiển nhiên. Byzantine faults đặt ra câu hỏi sâu hơn: bạn có tin tưởng các node trong hệ thống của mình không? Và nếu không, bạn sẵn sàng trả giá bao nhiêu cho sự an toàn đó?
Hiểu rõ hai vấn đề này giúp bạn thiết kế hệ thống thực tế hơn — biết khi nào cần TrueTime, khi nào chỉ cần monotonic clock, và khi nào nên (hay không nên) lo lắng về Byzantine behavior. Trong bài tiếp theo, chúng ta sẽ tìm hiểu về Failure Detection và cách các hệ thống distributed phát hiện node hỏng.