CAP, PACELC và mô hình nhất quán cho engineer
CAP thật sự nói gì, vì sao PACELC là phiên bản bạn nên dùng, và consistency map sang database .NET ra sao. Bộ từ vựng cho mọi lựa chọn lưu trữ.
Mục lục
- CAP thật sự nói gì - và vì sao luôn bị trích sai?
- PACELC là gì và vì sao là khung tốt hơn?
- Mô hình consistency trong thực tế - và bạn cần cái nào?
- Triển khai strong consistency trên stack .NET ra sao?
- Triển khai eventual consistency an toàn ra sao?
- Lựa chọn consistency tạo ra failure mode nào?
- Khi nào cuộc trò chuyện consistency là phân tâm?
- Đi tiếp đâu từ đây?
Mọi quyết định lưu trữ trong phần còn lại của series đều được tranh luận bằng từ vựng nhất quán: "ở đây dùng Cassandra được vì counter là eventually consistent" hoặc "cái này cần strong consistency, nên giữ trong Postgres". Chương này định nghĩa các từ ấy một lần. Bản ngắn: CAP là ràng buộc ba chiều với một cái tên dở, và PACELC là phiên bản bạn nên trích.
CAP thật sự nói gì - và vì sao luôn bị trích sai?
CAP, do Eric Brewer phát biểu năm 2000, nói một hệ thống phân tán cung cấp được tối đa hai trong:
- Consistency - mọi read thấy write mới nhất.
- Availability - mọi request nhận response không lỗi.
- Partition tolerance - hệ vẫn chạy khi link mạng giữa các node rớt.
Cách trích sai là "chọn hai". Tuyên ngôn thật hẹp hơn: trong lúc partition mạng, bạn phải chọn C hoặc A. Không thể có cả hai vì hai nửa của hệ partition không thể đồng thuận write mới nhất nếu không giao tiếp được.
Khi không có partition - trường hợp phổ biến - bạn có cả ba. Đó là lý do "Postgres là CA" nghe sai nhưng chạy được trong thực tế: Postgres ưu tiên C khi partition, mà partition hiếm. Cách viết tắt gây hiểu nhầm vì hàm ý lựa chọn vĩnh viễn trong khi lựa chọn chỉ kích hoạt lúc có sự cố mạng.
PACELC là gì và vì sao là khung tốt hơn?
PACELC, do Daniel Abadi đề xuất 2010, sửa CAP bằng cách thêm trường hợp phổ biến:
- Partition: chọn A hoặc C.
- Else: chọn Latency hoặc Consistency.
Nửa "else" là trade-off hằng ngày. Cả khi mọi node khoẻ, strong consistency yêu cầu chờ một quorum replica xác nhận write - cộng thêm latency. Eventual consistency trả về ngay và để cluster đuổi kịp ở background.
Nhãn database theo PACELC:
- PostgreSQL với sync standby: PC/EC - luôn nhất quán, write chậm hơn.
- PostgreSQL với async standby: PA/EL - sẵn sàng khi failover, có thể đọc dữ liệu cũ.
- Cassandra (mặc định QUORUM): PA/EL - sẵn sàng, latency thấp, eventually consistent.
- MongoDB (replica set, w=majority): PC/EC mặc định.
- DynamoDB: PA/EL mặc định; PC/EC với strongly-consistent reads.
Nhãn là cấu hình, không phải thương hiệu. Cùng database có thể PC/EC hay PA/EL tuỳ cách đặt replication và read concern.
Mô hình consistency trong thực tế - và bạn cần cái nào?
Năm mô hình thực dụng, từ yếu đến mạnh:
flowchart LR
EV[Eventual<br/>đọc có thể cũ] --> RYW[Read-your-writes<br/>thấy write của chính mình]
RYW --> MR[Monotonic Reads<br/>không lùi thời gian]
MR --> CC[Causal<br/>nguyên nhân trước hệ quả]
CC --> Strong[Strong / Linearizable<br/>thứ tự toàn cục duy nhất]
- Eventual - read có thể cũ nhưng hội tụ. Rẻ nhất. Phù hợp cho view count, like counter, search index.
- Read-your-writes - write của chính bạn thấy được ở read kế tiếp. Phù hợp cho "đăng comment, thấy comment". Thường đạt được bằng session affinity tới primary.
- Monotonic reads - các read liên tiếp không lùi thời gian. Phù hợp cho activity feed.
- Causal - nếu A gây B, mọi quan sát viên thấy A trước B. Phù hợp cho chat, comment có thread.
- Strong (linearizable) - mọi quan sát viên thấy cùng một thứ tự duy nhất. Phù hợp cho tiền, inventory, kiểm tra unique username. Đắt nhất.
Sai lầm là chọn một mô hình cho cả hệ. Một app .NET thật trộn cả chùm: thanh toán strong, feed eventual, chat causal. Kiến trúc đi theo cái trộn đó.
Triển khai strong consistency trên stack .NET ra sao?
Cho phần lớn app .NET, "strong" nghĩa là một Postgres primary + EF Core transaction serializable:
// Strong consistency cho giảm inventory.
// Serializable isolation đảm bảo không hai request cùng pass
// kiểm tra "stock > 0" và giảm xuống dưới không.
await using var tx = await db.Database.BeginTransactionAsync(IsolationLevel.Serializable);
var item = await db.Items
.Where(i => i.Id == itemId)
.FirstAsync();
if (item.Stock <= 0)
{
await tx.RollbackAsync();
return Result.OutOfStock();
}
item.Stock -= 1;
await db.SaveChangesAsync();
await tx.CommitAsync();
Hai thứ cần biết. Một, transaction serializable trong Postgres có
thể fail với serialization conflict (SQLState 40001) - bạn phải
retry. Hai, chi phí xuất hiện dưới contention; nếu 1000 request đồng
thời cùng giảm một row, mỗi lúc chỉ một thành công. Đó chính là
đảm bảo đúng đắn bạn đang mua.
Khi cần strong consistency xuyên service, bạn lên cấp outbox pattern và cuối cùng saga.
Triển khai eventual consistency an toàn ra sao?
Ba quy tắc.
Quy tắc 1: nêu cửa sổ hội tụ. "Eventual" không kèm số là vô dụng. Nói "hội tụ trong 5 giây, p99 30 giây" để team biết kỳ vọng. Cửa sổ đến từ cấu hình replication.
Quy tắc 2: thiết kế thao tác idempotent. Hệ eventually consistent thường retry phía application. Nếu "tăng counter" không idempotent, retry làm phình count. Chương 10 xử lý idempotency key.
Quy tắc 3: đọc từ primary cho read-your-writes. Ngay cả trong hệ eventually consistent, bạn có thể route read của một user về primary trong vài giây sau write. Session affinity ASP.NET Core + header "cửa sổ stickiness" là cách .NET triển khai:
// Sau write, set cookie cho gateway dùng route các read kế tiếp
// của user này tới primary database.
Response.Cookies.Append("db-stickiness", "primary",
new CookieOptions { MaxAge = TimeSpan.FromSeconds(5) });
Cookie được gateway / reverse proxy đọc và lái tải read. Phần lớn user không bao giờ nhận thấy eventual consistency vì họ bị dính vào primary trong lúc write của họ đang lan tới.
Lựa chọn consistency tạo ra failure mode nào?
Mỗi mô hình có lỗi đặc trưng:
- Strong - latency dưới contention; deadlock; trần throughput trên primary.
- Eventual - đọc cũ; lost update nếu bỏ qua idempotency; user bối rối ("tôi vừa đăng, đâu rồi?").
- Causal - phức tạp vector clock; khó debug báo cáo "sai thứ tự".
Chương observability (13) theo dõi các lỗi này: replication lag tính bằng giây cho hệ eventual, transaction deadlock count cho hệ strong, alert vector-clock skew cho hệ causal.
Khi nào cuộc trò chuyện consistency là phân tâm?
Khi traffic thấp. Dưới ~100 RPS, một instance Postgres với replication đồng bộ là strong, nhanh, và đơn giản - không trade-off nào áp dụng. Cả thảo luận CAP/PACELC tồn tại vì ở quy mô lớn bạn không giữ được sự đơn giản đó.
Sai lầm thiết kế khó nhất là với tay sang Cassandra hoặc DynamoDB cho service 50 RPS. Thuế eventual consistency (replication lag, idempotency key, đọc cũ) tốn nhiều code hơn vấn đề throughput không tồn tại. Ở lại Postgres cho đến khi estimate QPS từ chương 2 nói khác đi.
Đi tiếp đâu từ đây?
Bạn đã nắm bộ từ vựng nền tảng - throughput/latency/QPS, ước lượng nhanh, CAP/PACELC. Chương kế tiếp: Redis caching trong .NET, khối xây dựng đầu tiên nằm giữa lưu trữ strong và đọc nhanh. Sau đó các chương building block lắp ghép tự do trên nền tảng này.
Câu hỏi thường gặp
Vì sao CAP cứ bị trích sai?
Postgres là CP hay AP?
Khi nào eventual consistency là câu trả lời đúng?
Triển khai strong consistency trong EF Core ra sao?
[ConcurrencyCheck] và RowVersion của EF Core cho optimistic locking phát hiện lost update. Cho invariant đa dòng, bọc trong IsolationLevel.Serializable. Chi phí hiệu năng có thật - đo trước - nhưng đó chính là 'strong'.