Không phải lỗi. Đây là sự đánh đổi bạn cần hiểu.
Cập nhật ảnh đại diện, tải lại trang trên điện thoại, và trong giây lát ảnh cũ vẫn hiện ra. Đó có phải là lỗi không? Nhiều người thường nghĩ vậy: “Tôi vừa lưu thay đổi, sao mọi thứ chưa hiển thị ngay?”
Đó không phải lỗi. Đó là eventual consistency, và nếu bạn xây dựng các hệ thống phân tán, bạn đang dựa vào nó liên tục, dù bạn đã thiết kế cho điều đó hay chưa.
Mục lục
Cam kết chúng ta muốn, và cái giá của nó
Cam kết mọi người mong muốn rất đơn giản: bạn cập nhật thứ gì đó, và mỗi dịch khu vực, mỗi bộ nhớ đệm đều thấy nó ngay lập tức. Một phiên bản duy nhất của sự thật, luôn luôn. Đó là strong consistency, và nó là một điều hoàn toàn hợp lý để mong muốn.
Nó cũng đắt đỏ. Đảm bảo rằng mọi node đồng ý trước khi bạn coi một lệnh ghi hoàn thành có nghĩa là sự phối hợp. Và sự phối hợp qua mạng, đặc biệt qua các khu vực, có nghĩa là chờ đợi các chuyến đi vòng lặp mà bạn không kiểm soát được. Làm điều đó trên mỗi lệnh ghi, với quy mô lớn, độ trễ của hệ thống bạn bây giờ bị ràng buộc bởi node chậm nhất và nhảy mạng tồi tệ nhất của bạn.
Vì vậy, hầu hết các hệ thống phân tán không làm như vậy. Họ chấp nhận một cam kết yếu hơn để đổi lấy tốc độ và khả dụng, và gọi đó là eventual consistency.
Eventual Consistency thực sự có nghĩa gì
Đây là cơ chế: một lệnh ghi đến một node (hoặc một tập con nhỏ các node). Các node khác biết được sau đó, thông qua sao chép, gossip, hoặc một cơ chế lan truyền khác, không phải là một phần của lệnh ghi ban đầu. Nếu không có lệnh ghi mới nào được thực thi, mọi node sớm muộn sẽ hội tụ về cùng một giá trị.
Từ “sớm muộn” ở đây đóng vai trò rất quan trọng, và đáng để nói rõ về nó. Đó là một cam kết về kết quả, không phải về thời gian. Đủ thời gian và không có lệnh ghi mới nào, tất cả các bản sao sẽ đồng ý. Trong thực tế, “sớm muộn” thường là phần nghìn giây, đôi khi vài giây, và chỉ thỉnh thoảng mới là điều bạn để ý. Nhưng bản thân cam kết không nói gì về như thế nào là nhanh. Nó chỉ nói sớm muộn, đó là một cam kết về tính sống, không phải về thời gian.
Sự đánh đổi CAP
Đây là nơi định lý CAP xuất hiện. Nó không trừu tượng như âm thanh khi bạn kết nối nó với một quyết định bạn đang thực hiện. CAP nói rằng một hệ thống phân tán chỉ có thể đảm bảo hai trong ba thuộc tính này tại một thời điểm, và trong thực tế, khả năng chịu lỗi phân mảnh (partition tolerance) là bắt buộc với bất kỳ hệ thống nào trải rộng hơn một node:

Điều đó để lại một sự lựa chọn thực sự chỉ khi phân mảnh thực sự xảy ra: duy trì tính nhất quán, nơi mọi node nhìn thấy cùng một dữ liệu, hoặc duy trì tính khả dụng, nơi mỗi yêu cầu đều nhận được phản hồi, ngay cả khi đó không phải là dữ liệu mới nhất. Bạn không thể có cả hai hoàn toàn trong thời gian phân mảnh đó.
Eric Brewer, người đề xuất CAP vào năm 2000, đã sửa đổi khung của mình vào năm 2012 chính vì lý do này. Khả năng chịu lỗi phân mảnh không bao giờ là nút bạn có thể tắt cho một hệ thống có hơn một node, vì vậy đối xử với nó như một lựa chọn thứ ba ngang hàng là gây hiểu lầm. Quyết định duy nhất quan trọng là điều gì xảy ra với tính nhất quán và khả dụng khi phân mảnh thực sự ập đến.
Các hệ thống thực tế công bố bên nào của quyết định đó họ chọn. Các hệ thống CP (tính nhất quán + khả năng chịu lỗi phân mảnh), như MongoDB trong cấu hình mặc định hoặc HBase, chọn tính nhất quán: thay vì phục vụ bạn một cái gì đó sai, họ muốn từ chối yêu cầu. Các hệ thống AP (khả dụng + khả năng chịu lỗi phân mảnh), như Cassandra hoặc DynamoDB, chọn khả dụng: họ sẽ phục vụ bạn dữ liệu có thể chậm vài lệnh ghi hơn là không gì cả, và dựa vào eventual consistency để thu hẹp khoảng cách sau đó. Các hệ thống CA (tính nhất quán + khả dụng), như một cài đặt PostgreSQL hoặc MySQL truyền thống chạy ở một nơi, được giữ cả hai, nhưng chỉ vì họ không bao giờ được yêu cầu tồn tại qua phân mảnh giữa các node. Trải rộng cùng cơ sở dữ liệu đó qua hai trung tâm dữ liệu, và CA lặng lẽ ngừng có trên bàn.
Eventual consistency là sự lựa chọn có chủ ý của khả dụng hơn tính nhất quán mạnh. Bạn đang nói: tôi muốn hệ thống của mình tiếp tục phản hồi, ngay cả với dữ liệu hơi lỗi ở đâu đó, hơn là từ chối trả lời. Nó không nên phải chờ đợi cho đến khi mọi người đồng ý. Với nhiều hệ thống, đó chính xác là quyết định đúng. Đáng để gọi đó là một quyết định, không phải thứ gì đó chỉ tình cờ xảy ra với bạn.
Nơi bạn đã sử dụng nó
Điều này không phải mới lạ. Bạn có thể giả định eventual consistency là thứ mà chỉ các chuyên gia hệ thống phân tán mới xử lý. Hãy xem xét bao nhiêu cơ sở hạ tầng hàng ngày của bạn đã hoạt động theo cách này.
DNS là ví dụ lâu đời nhất: cập nhật một bản ghi và nó có thể mất vài phút để lan truyền đến mọi nơi, không phải phần nghìn giây. CDN lưu nội dung tại các node biên và mất thời gian để vô hiệu hóa nó sau thay đổi. Nguồn cấp dữ liệu mạng xã hội (lượt thích, bình luận, lượt xem) lan truyền bất đồng bộ. Không ai mong đợi số lượt thích được cập nhật ở mọi nơi cùng một lúc. Và nếu bạn chạy thiết lập cơ sở dữ liệu chính/bản sao, các bản sao đọc của bạn, về mặt cấu tạo, là eventual consistency với bản chính. Luôn có độ trễ, ngay cả khi nó thường nhỏ đến mức không ai để ý.
Bạn đã dựa vào eventual consistency nhiều năm rồi. Câu hỏi duy nhất là liệu bạn có đang thiết kế cho nó có chủ đích hay không.
Điều này có ý nghĩa gì trong thực tế
Thiết kế cho eventual consistency có chủ đích trông khác với việc chịu đựng nó một cách tình cờ. Một vài điều có thể giúp:
- Hiển thị những gì người dùng gửi, không phải những gì đã được xác nhận. Đây là giao diện lạc quan (optimistic UI). Cập nhật màn hình ngay sau hành động. Đừng chờ đợi chuyến đi vòng lặp đến mỗi bản sao trước khi hiển thị thay đổi.
- Các lần thử lại nên là idempotent. Nếu một thao tác có thể đã thành công một phần, thử lại nó một lần nữa cần phải an toàn, không phải cộng dồn.
- Đánh dấu phiên bản dữ liệu. ETags, số thứ tự, hoặc vector clocks cho phép bạn phát hiện xung đột thay vì lặng lẽ mất một lệnh ghi cho lệnh ghi khác.
- Xung đột cần có thể giải quyết được, không chỉ phát hiện được. “Lệnh ghi cuối cùng thắng” là mặc định dễ dàng, nhưng nó không phải lúc nào cũng đúng. Biết khi nào nó chấp nhận được và khi nào không.
Không điều nào trong số này phải là tất cả hoặc không gì cả ở cấp độ hệ thống. Hầu hết các cơ sở dữ liệu phân tán cho phép bạn điều chỉnh tính nhất quán theo từng thao tác thay vì theo triển khai. Cassandra cho phép bạn yêu cầu đọc QUORUM trên số dư tài khoản và đọc ONE trên luồng hoạt động, trong cùng một ứng dụng. DynamoDB mặc định là đọc eventual consistency nhưng cung cấp cho bạn đọc tính nhất quán mạnh theo yêu cầu, với chi phí đọc gấp đôi roughly. Các thiết lập writeConcern và readPreference của MongoDB thực hiện cùng công việc. Chọn cam kết cho từng truy vấn, không phải cho toàn bộ cơ sở dữ liệu.
Cũng nên biết các mô hình lỗi cụ thể cần chú ý. Chúng luôn là cùng một handful:
- Đọc lệnh ghi của riêng bạn và nhận lại giá trị cũ (bạn vừa lưu điều này, sao nó không hiển thị?).
- Tổng hợp lỗi thời, nơi số lượng hoặc tổng落后 phía sau các bản ghi mà nó đang tóm tắt.
- Hai người dùng cập nhật cùng một bản ghi đồng thời, với một người lặng lẽ ghi đè lên người kia.
- Các hành động downstream (email, thông báo) được kích hoạt dựa trên dữ liệu đã lỗi thời vào thời điểm chúng chạy.
Không có lỗi nào trong số này là lỗi mới lạ. Chúng là hậu quả có thể dự đoán được của eventual consistency gặp phải mã được viết như thể nó là tính nhất quán mạnh.
Không phải mọi phần của hệ thống bạn nên chấp nhận sự đánh đổi eventual consistency. Các giao dịch tài chính (ghi có và ghi nợ) cần là nguyên tử. Đặt chỗ tồn kho cần tính nhất quán mạnh, nếu không hai khách hàng có thể đều “thành công” claimed item cuối cùng trong kho. Token xác thực bị thu hồi cần vô hiệu ngay lập tức, không phải eventually. Eventual consistency là mặc định hợp lý cho nhiều phần hệ thống của bạn. Nó không phải quy tắc phổ quát. Coi nó như vậy, và bạn sẽ kết thúc việc gỡ lỗi một mục được bán gấp đôi lúc 2 giờ sáng.
Kết luận
Eventual consistency đánh đổi “luôn đúng” lấy “luôn khả dụng”. Đối với hầu hết những gì bạn xây dựng, đó là sự đánh đổi đúng. Sai lầm không phải chọn nó. Sai lầm là không biết bạn đã chọn nó.
Những lỗi eventual consistency tồi tệ nhất tôi thấy không đến từ sự đánh đổi này. Chúng đến từ việc không ai nhận ra họ đã thực hiện một sự đánh đổi, cho đến khi một người dùng nhận thấy thay đổi của họ đã biến mất.
P.S: Cho tôi biết nếu tôi bỏ sót điều gì, liên hệ tôi trên Twitter/X hoặc LinkedIn, và theo dõi ở đó nếu bạn muốn thêm nội dung tương tự. Hãy trò chuyện.
eventual-consistency cap-theorem distributed-systems data-consistency
Chia sẻ bài viết:
Thích bài viết này?
Tôi viết về .NET, messaging, và distributed systems hầu hết mỗi tuần – những phần không xuất hiện trong bài LinkedIn.

Xin chào! Chào mừng đến với blog của tôi
Tham gia bản tin của tôi
Distributed systems, .NET, và các công cụ đang định hình lại cách chúng ta xây dựng chúng – những sự đánh đổi không xuất hiện trong tài liệu.
Hãy kết nối

Công việc mới nhất của tôi
Xem trên Dometrain
Xem trên Dometrain- From Zero to Hero: gRPC in .NET course
- Web Api for the absolute beginner book
REST APIs in Production: Pagination, HATEOAS, and the Patterns That Actually Matter (Part 3 of 3)
GitHub Dev.to LinkedIn Twitter Bluesky
Tất cả nội dung © Irina Scurtu



