Partial Failures & Unreliable Networks — Khi hệ thống phân tán gặp sự cố (DDIA)
Mở đầu
Ảnh: Brett Sayles — Pexels
Nếu bạn đã từng deploy một ứng dụng lên production — không phải localhost, mà là một cluster nhiều máy — chắc bạn cũng từng gặp cảnh: request thỉnh thoảng fail, database timeout, hoặc có node trong cluster tự dưng "mất tích" không rõ lý do.
Cảm giác đó khá khác so với code trên máy local đúng không? Trên local, nếu code chạy được thì nó chạy — nếu lỗi thì crash thẳng. Còn trong môi trường phân tán, mọi thứ không rõ ràng như vậy. Có câu chuyện gì đó đang diễn ra mà bạn không kiểm soát được hết.
Chương 8 của DDIA — "Distributed Systems Troubles" — bắt đầu bằng một nhận xét đơn giản nhưng sâu sắc: trong distributed computing, failure là một nhánh rẽ, không phải là một ngoại lệ hiếm gặp. Và nó bắt đầu bằng việc nói về partial failures và unreliable networks — hai vấn đề căn bản nhất mà bất kỳ ai build distributed systems đều phải đối mặt.
Partial Failures — Lỗi "một phần" khác gì lỗi toàn bộ?
Ảnh: panumas nikhomkhai — Pexels
Trong một chương trình chạy trên một máy đơn lẻ, thiết kế khá đơn giản: hoặc mọi thứ đều ổn, hoặc mọi thứ sập. Nếu một hàm trong chương trình bị lỗi, cả process có thể crash. Nếu RAM bị lỗi, OS treo. Nếu ổ cứng hỏng, toàn bộ dừng lại. Đó là kiểu failure all-or-nothing quen thuộc với mọi lập trình viên.
Nhưng khi bạn có 100 máy (nodes) kết nối qua mạng, failure không còn là all-or-nothing nữa. Một số yêu cầu có thể thành công, số khác thì không. Một số node vẫn chạy, số khác thì chết. Một số kết nối mạng vẫn hoạt động, số khác thì bị gián đoạn. Nói cách khác, hệ thống rơi vào trạng thái partial failure — một phần chết, một phần sống.
Vấn đề là partial failure rất khó debug và khó dự đoán. Bạn không thể đơn giản "reboot lại" và hy vọng mọi thứ ổn, vì bạn không biết chắc chắn phần nào của hệ thống đã bị ảnh hưởng. Nó không phải là một ngăn xếp lỗi trong code — nó là sự không chắc chắn về trạng thái của toàn bộ hệ thống.
Đây là lý do distributed systems cần một cách suy nghĩ khác: thay vì giả định mọi thứ đều đáng tin cậy, bạn phải xây dựng hệ thống với giả định rằng mọi thứ đều có thể hỏng — network, disk, RAM, CPU, clock — và thiết kế để survive khi nó xảy ra.
Unreliable Networks — Mạng không đáng tin cậy như bạn nghĩ
Ảnh: sejio402 — Pexels
Phần lớn distributed systems giao tiếp qua mạng — cụ thể là qua Ethernet, TCP/IP. Và mạng, như bất kỳ ai làm DevOps đều biết, là thứ không đáng tin cậy nhất trong stack hạ tầng.
DDIA liệt kê một số dạng lỗi mạng phổ biến:
- Request bị mất — gói tin không bao giờ tới được server. Có thể do switch lỗi, do routing sai, do load quá cao nên router drop packet.
- Request đang đợi trong queue — gói tin tới được server nhưng server quá tải, request nằm trong hàng đợi và không kịp xử lý.
- Server xử lý xong nhưng response bị mất — request được xử lý thành công, nhưng gói response bị drop trên đường về.
- Server crash — server nhận request, xử lý một phần, rồi crash (hoặc restart hoặc bị kill).
- Delayed packets — gói tin tới chậm do mạng nghẽn, ảnh hưởng đến timeout.
- Duplicated packets — cùng một gói tin tới hai lần (do TCP retransmission).
Điều đáng nói là những lỗi này không phải chuyện hy hữu. Trong bất kỳ datacenter nào, network faults xảy ra hàng ngày. Dây cáp bị đứt, switch bị reboot, router bị misconfigure, DNS bị fail — tất cả đều là chuyện bình thường.
Vấn đề của Timeout — Biết khi nào nên từ bỏ?
Vì mạng không đáng tin cậy, bạn cần timeout. Nhưng timeout thì đặt bao nhiêu là đủ?
- Quá ngắn → false positives: bạn nghĩ server chết nhưng thực ra nó chỉ chậm một chút. Bạn bỏ cuộc quá sớm.
- Quá dài → bạn chờ đợi vô ích trong khi server thực sự đã chết.
Vấn đề triết học ở đây là: bạn không thể phân biệt được giữa "server chết" và "mạng chậm" — ít nhất là qua một network call đơn thuần. Kết quả của một request timeout là sự không chắc chắn: bạn không biết request đã được xử lý hay chưa.
DDIA gọi đây là "the uncertainty principle of distributed systems" — không có cách nào biết chắc chắn điều gì đã xảy ra ở đầu kia của kết nối mạng.
Synchronous vs Asynchronous — Bài học từ mạng viễn thông
Một góc nhìn thú vị trong chương này là so sánh giữa mạng máy tính (packet-switched) và mạng điện thoại (circuit-switched).
Trong mạng điện thoại truyền thống, khi bạn gọi điện, một đường truyền (circuit) được thiết lập và dành riêng cho cuộc gọi đó. Bạn có bandwidth đảm bảo, không cần lo về congestion. Nó hoạt động synchronous — bạn gọi, máy bên kia đổ chuông, có người nhấc máy, và bạn nói chuyện.
Còn mạng máy tính là asynchronous và best-effort: gói tin của bạn được chia nhỏ và gửi đi chung với hàng triệu gói tin khác. Không có đường truyền dành riêng. Nếu một switch bị quá tải, packet của bạn bị drop. Nếu đường truyền bị nghẽn, packet đến chậm.
Ưu điểm của mạng packet-switched là tính linh hoạt và khả năng tận dụng bandwidth — không có đường truyền nào bị "bỏ không" khi đang idle. Nhưng cái giá phải trả là sự không chắc chắn và partial failure. Trong thiết kế distributed systems, bạn cần chấp nhận điều này như một thiết kế constraint, không phải một exception.
Key Takeaways
- Partial failure là bản chất của distributed systems — không phải ngoại lệ. Khác với local computing (all-or-nothing), trong distributed systems một phần hệ thống có thể hỏng trong khi phần còn lại vẫn hoạt động.
- Mạng là thành phần không đáng tin cậy nhất — packet loss, delay, duplication, reordering xảy ra thường xuyên hơn bạn tưởng.
- Timeout không giải quyết được vấn đề không chắc chắn — bạn không thể biết chắc request đã được xử lý hay chưa khi timeout xảy ra.
- Thiết kế distributed system bắt đầu bằng việc chấp nhận sự không đáng tin cậy — không phải loại bỏ failure, mà là survive nó.
📋 Phụ lục thuật ngữ
Partial failure — lỗi một phần, khi một số thành phần trong hệ thống phân tán bị lỗi trong khi các thành phần khác vẫn hoạt động bình thường.
Network partition (netsplit) — tình trạng mạng bị chia cắt, khiến một số node không thể liên lạc được với nhau.
Timeout — cơ chế giới hạn thời gian chờ cho một network request, dùng để phát hiện failure.
Packet-switched network — mạng chuyển mạch gói, nơi dữ liệu được chia thành các gói tin nhỏ và gửi qua nhiều đường khác nhau.
Circuit-switched network — mạng chuyển mạch kênh, nơi một kết nối được thiết lập và dành riêng cho toàn bộ thời gian giao tiếp.
TCP backpressure — cơ chế của TCP để giảm tốc độ gửi khi mạng bị nghẽn.
Flow control — kiểm soát luồng dữ liệu để tránh làm quá tải receiver.
Kết
Chapter 8.1 của DDIA đặt nền móng cho cách suy nghĩ về distributed systems: không phải làm sao để không có failure, mà là làm sao để hệ thống vẫn hoạt động (hoặc ít nhất là fail gracefully) khi failure xảy ra. Partial failure và unreliable networks là hai thực tế mà bạn không thể tránh — chỉ có thể chấp nhận và thiết kế để ứng phó.
Phần tiếp theo của chương 8 sẽ đi sâu vào những kỹ thuật cụ thể: làm sao để phát hiện node chết, làm sao để xây dựng consensus, và làm sao để đảm bảo tính đúng đắn trong môi trường không đáng tin cậy. Đón đọc bài sau nhé.