Giới thiệu về hệ thống phân tán
Xem thửYêu cầu thanh toán đã gửi đi. Rồi sao nữa?
order-service của gdev-shop gửi yêu cầu thu tiền đến payment-service. Sau 3 giây vẫn không có phản hồi. Câu hỏi: tiền đã bị trừ chưa?
Có ít nhất ba khả năng, và order-service không có cách nào phân biệt chúng chỉ từ chỗ nó đứng:
- Yêu cầu không bao giờ đến
payment-service(mạng mất gói). Tiền chưa trừ. payment-servicenhận được, xử lý xong, nhưng phản hồi bị mất trên đường về. Tiền đã trừ.payment-serviceđang rất chậm và sẽ trả lời sau 10 giây. Tiền sắp trừ.
Nếu order-service gửi lại yêu cầu, ở khả năng 2 và 3, khách bị trừ tiền hai lần. Nếu không gửi lại, ở khả năng 1, khách không bao giờ được thanh toán. Đây là tình huống cơ bản của hệ thống phân tán, và là điều làm nó khó hơn rất nhiều so với một chương trình chạy trên một máy.
Hệ thống phân tán là gì?
Hệ thống phân tán (distributed system) là một tập các máy tính độc lập, nối với nhau bằng mạng, phối hợp để hoạt động như một hệ thống thống nhất trước mắt người dùng. Bất kỳ hệ thống nào có nhiều tiến trình trên nhiều máy giao tiếp qua mạng đều là hệ thống phân tán: microservices, database có replica, cụm cache, hàng đợi thông điệp.
Vì sao người ta xây hệ thống phân tán? Vì một máy không đủ:
- Mở rộng: một máy có giới hạn về CPU, bộ nhớ, đĩa, kết nối.
- Sẵn sàng và chịu lỗi: một máy hỏng là mất hết, nhiều máy cho dự phòng.
- Độ trễ địa lý: đặt máy gần người dùng ở nhiều vùng.
- Tổ chức: nhiều nhóm sở hữu các phần độc lập.
Nhưng việc có nhiều máy giao tiếp qua mạng tạo ra một lớp vấn đề hoàn toàn mới mà chương trình trên một máy không có.
Điều gì khác biệt so với một máy?
Trên một máy, một lời gọi hàm hoặc thành công, hoặc thất bại rõ ràng, bộ nhớ chung, một đồng hồ. Trong hệ phân tán:
- Mạng không đáng tin: gói tin có thể mất, trễ, trùng, đến sai thứ tự.
- Lỗi một phần (partial failure): một phần hệ thống hỏng trong khi phần còn lại vẫn chạy, và bạn không chắc phần nào hỏng.
- Không có bộ nhớ chung: mỗi máy chỉ biết trạng thái của riêng nó và những gì nó nhận được qua mạng.
- Không có đồng hồ chung đáng tin: đồng hồ các máy lệch nhau và không thể dùng để sắp xếp chính xác các sự kiện.
- Không có góc nhìn toàn cục tức thì: không ai nhìn thấy "trạng thái của cả hệ thống ngay lúc này".
Ba khả năng sau một lời gọi không có phản hồi
Đang tải sơ đồ…
Đang tải Animation…
Đang tải Animation…
Cách đọc sơ đồ
order-service gửi yêu cầu rồi chờ. Không có phản hồi. Ba ghi chú cuối là ba "thế giới" không thể phân biệt từ phía order-service:
- A (yêu cầu mất):
payment-servicekhông bao giờ biết có yêu cầu. - B (phản hồi mất):
payment-serviceđã thu tiền nhưng câu trả lời không về được. - C (chậm):
payment-servicevẫn đang làm.
Từ phía order-service, cả ba trông giống hệt nhau: im lặng. Đây là bản chất của sự bất định trong hệ phân tán: không thể phân biệt một thành phần chết với thành phần chậm hay mạng mất gói. Timeout chỉ là một phỏng đoán ("quá 3 giây thì coi như lỗi"), có thể sai.
Ba hệ quả thiết kế rút ra, đã gặp rải rác ở các lesson trước:
- Mọi lời gọi qua mạng phải có timeout, vì không có nó, trường hợp C làm treo mãi.
- Thao tác phải idempotent hoặc dùng idempotency key, để gửi lại an toàn dù không biết yêu cầu trước đã đến hay chưa (giải quyết cả A, B và C).
- Cần cách hỏi lại trạng thái (endpoint truy vấn kết quả theo mã giao dịch) để biết sự thật sau khi im lặng.
Tám ngộ nhận kinh điển của tính toán phân tán
Người thiết kế hệ phân tán nhiều lần phạm cùng những giả định sai. Danh sách quen thuộc (do các kỹ sư tại Sun Microsystems tổng hợp) nêu những điều không đúng:
| Giả định sai | Thực tế |
|---|---|
| Mạng đáng tin cậy | Gói tin mất, trễ, trùng, sai thứ tự |
| Độ trễ bằng không | Mỗi lời gọi mạng tốn thời gian, và biến thiên |
| Băng thông vô hạn | Có giới hạn, và dùng chung |
| Mạng an toàn | Dữ liệu có thể bị nghe lén hoặc sửa |
| Cấu trúc mạng không đổi | Máy thêm, bớt, địa chỉ đổi |
| Chỉ có một quản trị viên | Nhiều tổ chức và chính sách khác nhau |
| Chi phí vận chuyển bằng không | Tốn tiền và công sức (băng thông, mã hóa, tuần tự hóa) |
| Mạng đồng nhất | Nhiều loại thiết bị, giao thức, phiên bản |
Bạn không cần nhớ thuộc lòng danh sách, nhưng cần nhớ tinh thần: mỗi khi viết một lời gọi qua mạng, hãy tự hỏi nó có thể sai theo những cách nào.
Các loại lỗi cần phân biệt
- Lỗi dừng (crash): một node ngừng hoạt động, và có thể khởi động lại. Loại lỗi dễ xử lý nhất.
- Lỗi mạng (network failure): mất kết nối giữa các node, có thể chia hệ thống thành các nhóm không liên lạc được (network partition, đã gặp ở CAP).
- Lỗi chậm (slowness): node không chết nhưng phản hồi rất chậm. Thường tệ hơn lỗi dừng, vì khó phát hiện và làm tắc nghẽn lan rộng.
- Lỗi bất thường (Byzantine): một node hoạt động sai và có thể nói dối, gửi thông tin mâu thuẫn. Hiếm trong hệ thống nội bộ đáng tin, quan trọng trong hệ thống như blockchain. Ở mức Advanced.
Phần lớn hệ thống doanh nghiệp thông thường giả định crash và mạng lỗi, không giả định lỗi bất thường.
Đồng hồ và thứ tự: một vấn đề tế nhị
Giả sử hai máy A và B cùng ghi một bản ghi với dấu thời gian lấy từ đồng hồ riêng. Đồng hồ máy B chạy nhanh hơn 2 giây. Bản ghi B ghi sau nhưng có dấu thời gian sớm hơn hoặc muộn hơn thực tế, và nếu dùng dấu thời gian để quyết định "ghi sau thắng" (last write wins), một thay đổi có thể bị mất do đồng hồ lệch. Nguyên tắc: không dùng đồng hồ vật lý để sắp xếp chính xác các sự kiện giữa các máy.
Cách tiếp cận thay thế (nhập môn):
- Số phiên bản và số thứ tự do một nơi cấp (như offset trong một partition): thứ tự do một nguồn duy nhất quyết định.
- Đồng hồ logic: đếm "chuyện xảy ra trước chuyện" thay vì dùng giờ thật. Mỗi sự kiện mang một số đếm và các máy cập nhật số đếm khi trao đổi tin nhắn.
- Chấp nhận thứ tự một phần khi nghiệp vụ cho phép.
Các kỹ thuật nền tảng đã gặp
Nhìn lại, nhiều mẫu bạn đã học thực ra là câu trả lời cho các vấn đề của hệ phân tán:
| Vấn đề phân tán | Kỹ thuật đã học |
|---|---|
| Không biết yêu cầu đến chưa | Idempotency key, retry có backoff |
| Một phụ thuộc chậm kéo cả hệ thống | Timeout, circuit breaker, bulkhead |
| Giao dịch qua nhiều service | Saga, bù trừ, outbox |
| Dữ liệu nhiều bản sao lệch nhau | Replication, mô hình nhất quán, CAP |
| Phối hợp nhiều node đồng ý một giá trị | Đồng thuận (bài tiếp theo) |
| Không biết request đi đâu | Distributed tracing (bài sau) |
| Mạng không an toàn | TLS, mTLS |
Khi nào nên tránh phân tán?
Vì cái giá của phân tán lớn, nguyên tắc cơ bản: đừng phân tán nếu chưa cần. Một monolith trên một máy mạnh kèm database có replica thường đủ cho rất nhiều hệ thống. Chỉ phân tán khi áp lực mở rộng, sẵn sàng, địa lý hoặc tổ chức thật sự biện minh cho chi phí về độ phức tạp, gỡ lỗi, và vận hành.
Những hiểu lầm phổ biến
- "Mạng nội bộ đủ tin cậy để coi như không lỗi." Ngay cả trong một trung tâm dữ liệu, gói tin vẫn mất, thiết bị vẫn hỏng, độ trễ vẫn biến thiên.
- "Timeout cho biết chắc yêu cầu thất bại." Nó chỉ cho biết chưa nhận được phản hồi, yêu cầu có thể đã được xử lý.
- "Dùng dấu thời gian để sắp xếp sự kiện giữa các máy." Đồng hồ lệch có thể làm đảo thứ tự và mất dữ liệu.
- "Hệ phân tán chỉ là nhiều chương trình nhỏ." Nó là một lớp vấn đề mới: lỗi một phần, bất định, đồng thuận.
Must Know, Good to Know, Advanced
- Must Know: hệ phân tán là gì và vì sao xây; mạng không đáng tin; lỗi một phần; không phân biệt được chết, chậm và mất gói nên timeout là phỏng đoán; hệ quả: timeout, idempotency, cách hỏi lại trạng thái.
- Good to Know: tám ngộ nhận; phân loại lỗi (crash, mạng, chậm); đồng hồ lệch và đồng hồ logic; đừng phân tán nếu chưa cần.
- Advanced: lỗi Byzantine, mô hình đồng bộ và bất đồng bộ, định lý FLP. Chỉ cần biết rằng có nền tảng lý thuyết giải thích giới hạn của bài toán.
Tự kiểm tra
- Gửi yêu cầu thu tiền, 3 giây không có phản hồi. Nêu ba khả năng và vì sao không phân biệt được chúng.
- Vì sao dùng dấu thời gian từ đồng hồ từng máy để quyết định "ghi sau thắng" có thể làm mất dữ liệu?
- Nêu ba kỹ thuật đã học giúp chịu được sự bất định "không biết yêu cầu đến chưa".
Đáp án gợi ý
- Ba khả năng: yêu cầu chưa đến server (mất gói), server đã xử lý và thu tiền nhưng phản hồi bị mất, hoặc server đang chậm và sẽ xử lý xong sau. Không phân biệt được vì từ phía client chỉ quan sát thấy "không có phản hồi" trong cả ba trường hợp. Timeout chỉ là phỏng đoán, nên không được thử lại thu tiền mà không có idempotency.
- Đồng hồ các máy lệch nhau (clock skew), có thể vài trăm mili giây hoặc hơn. Nếu "ghi sau thắng" dựa trên dấu thời gian, một ghi xảy ra sau thật nhưng từ máy có đồng hồ chậm sẽ bị coi là cũ và bị bỏ, làm mất dữ liệu. Cần số phiên bản, đồng hồ logic hoặc dùng nguồn sự thật duy nhất.
- Ba kỹ thuật: timeout kèm retry có giới hạn và backoff, idempotency key để thử lại an toàn, và endpoint hỏi lại trạng thái (client hỏi "giao dịch X đã xong chưa?" thay vì đoán). Ngoài ra có outbox/inbox và saga cho luồng nhiều bước.
Nếu chỉ nhớ 3 điều
- Hệ phân tán là nhiều máy phối hợp qua mạng; mạng mất, trễ, trùng, sai thứ tự và có lỗi một phần.
- Không thể phân biệt chết, chậm và mất gói: timeout chỉ là phỏng đoán; vì vậy cần timeout, idempotency và cách hỏi lại trạng thái.
- Đừng tin đồng hồ để sắp xếp sự kiện giữa các máy, và đừng phân tán nếu chưa cần.