Mở đầu — vì sao bài này quan trọng
Ở bài trước, bạn đã làm quen với CAP Theorem — cái khung tư duy kinh điển buộc mọi hệ phân tán phải chọn giữa Consistency (nhất quán) và Availability (khả dụng) khi mạng bị phân mảnh (Partition). CAP là điểm khởi đầu tuyệt vời, nhưng nếu bạn dừng ở đó khi ra quyết định kiến trúc, bạn sẽ rơi vào một cái bẫy rất phổ biến: bạn chỉ nghĩ về hệ thống của mình trong lúc "cháy nhà" — lúc network partition xảy ra — mà quên mất rằng 99,9% thời gian còn lại hệ thống chạy hoàn toàn bình thường.
Và đây mới là điều đau đầu: ngay cả khi mạng khỏe mạnh, không có partition nào cả, hệ phân tán của bạn vẫn phải chọn giữa hai thứ — tốc độ trả lời (Latency) và mức độ nhất quán của dữ liệu (Consistency). CAP hoàn toàn im lặng về trade-off này. Đó chính là lỗ hổng mà PACELC — do giáo sư Daniel Abadi đề xuất năm 2010 — sinh ra để lấp đầy.
Với vai trò là người ra quyết định kiến trúc, hiểu PACELC giúp bạn trả lời một câu hỏi mà sếp, khách hàng hoặc chính đội của bạn chắc chắn sẽ hỏi: "Tại sao hệ thống đọc dữ liệu chậm thế?" hoặc "Tại sao user vừa cập nhật xong mà refresh lại thấy dữ liệu cũ?". Đằng sau những câu hỏi tưởng chừng đơn giản đó là một trade-off kiến trúc mà PACELC mô tả cực kỳ gọn gàng. Bài này sẽ giúp bạn nắm chắc khung tư duy đó và biết cách áp dụng nó vào việc chọn database, thiết kế replication, và giải thích quyết định của mình một cách thuyết phục.
Khái niệm cốt lõi
PACELC là gì
PACELC là một chuỗi chữ cái viết tắt, đọc như một câu điều kiện hai vế:
> Partition thì chọn giữa Availability và Consistency; Else (bình thường) thì chọn giữa Latency và Consistency.
Viết đầy đủ ra sẽ dễ nhớ hơn:
- P (Partition) — Khi mạng bị phân mảnh, các node không liên lạc được với nhau...
- A / C — ...bạn phải chọn: ưu tiên Availability (vẫn trả lời request, chấp nhận dữ liệu có thể không đồng bộ) hay ưu tiên Consistency (từ chối request để giữ dữ liệu luôn đúng). Đây chính là phần CAP đã mô tả.
- E (Else) — Ngược lại, khi mạng bình thường, không có partition...
- L / C — ...bạn vẫn phải chọn: ưu tiên Latency (trả lời nhanh, chấp nhận đọc từ replica có thể hơi cũ) hay ưu tiên Consistency (đợi đồng bộ đủ số node rồi mới trả lời, chậm hơn nhưng luôn đúng).
Tại sao vế "Else" mới là phần đáng giá
Hãy nghĩ về xác suất thực tế. Trong một cụm database chạy trên hạ tầng đám mây tốt (AWS, GCP, hoặc datacenter của một nhà cung cấp như Viettel IDC), network partition nghiêm trọng có thể chỉ xảy ra vài lần một năm, mỗi lần vài giây đến vài phút. Nghĩa là hơn 99,9% thời gian hệ thống ở trạng thái "Else" — bình thường.
Điều đó có nghĩa: trade-off Latency-vs-Consistency ảnh hưởng đến trải nghiệm người dùng của bạn thường xuyên hơn rất nhiều so với trade-off Availability-vs-Consistency. CAP chỉ mô tả cái phần 0,1% hiếm hoi. PACELC buộc bạn phải suy nghĩ nghiêm túc về cái phần 99,9% mà bạn sống chung mỗi ngày.
Nguồn gốc của trade-off L/C này rất trực quan: để đảm bảo consistency mạnh (strong consistency) trong hệ phân tán, một thao tác ghi phải được xác nhận bởi nhiều bản sao (replica) trước khi báo thành công — thường theo quy tắc quorum. Càng đợi nhiều node xác nhận thì càng chậm (latency cao). Nếu bạn chấp nhận trả lời ngay khi chỉ một node ghi xong (rồi lan truyền dần sang các node khác — eventual consistency), bạn nhanh hơn nhưng dữ liệu có thể tạm thời không đồng bộ.
Bốn phân loại hệ thống theo PACELC
Ghép hai vế lại, mỗi hệ phân tán rơi vào một trong bốn kiểu. Ký hiệu chuẩn viết là P[A/C]/E[L/C], gọn hơn thường viết như dưới:
- PA/EL — Khi partition thì ưu tiên Availability; bình thường thì ưu tiên Latency. Đây là nhóm "luôn ưu tiên tốc độ và khả dụng, hy sinh nhất quán". Ví dụ điển hình: Cassandra, DynamoDB (ở cấu hình mặc định), Riak.
- PC/EC — Khi partition thì ưu tiên Consistency; bình thường cũng ưu tiên Consistency. Nhóm "luôn ưu tiên đúng, chấp nhận chậm hơn hoặc từ chối request khi cần". Ví dụ: VoltDB, HBase, các hệ dùng consensus như Google Spanner (Spanner rất đặc biệt, sẽ nói kỹ ở phần ví dụ).
- PA/EC — Khi partition thì chấp nhận mất nhất quán để giữ khả dụng, nhưng lúc bình thường lại ưu tiên nhất quán. Nhóm này hiếm hơn nhưng tồn tại, ví dụ một số cấu hình của MongoDB.
- PC/EL — Khi partition thì ưu tiên nhất quán (từ chối request), nhưng lúc bình thường lại ưu tiên tốc độ. Nhóm này về lý thuyết có nhưng ít gặp trong thực tế; một số cấu hình tuning của MongoDB/Cosmos DB có thể rơi vào đây.
Một điểm tinh tế: đây không phải lựa chọn nhị phân cứng
PACELC mô tả thiên hướng thiết kế mặc định của một hệ thống, nhưng nhiều database hiện đại cho phép bạn tinh chỉnh mức consistency ở từng request. Cassandra chẳng hạn, mặc định nghiêng PA/EL, nhưng bạn có thể đặt consistency level là QUORUM hoặc ALL cho những truy vấn quan trọng để đẩy nó về phía consistency. Vậy nên khi phân loại một hệ theo PACELC, hãy hiểu đó là "cấu hình mặc định / triết lý thiết kế", chứ không phải một cái nhãn bất biến.
Tình huống thực tế
Ví dụ 1 — Sàn thương mại điện tử Việt Nam trong ngày sale 12/12
Hãy tưởng tượng một sàn TMĐT như Tiki hoặc một startup thương mại điện tử Việt Nam đang chuẩn bị cho đợt flash sale 12/12. Hai loại dữ liệu, hai quyết định PACELC hoàn toàn khác nhau:
Giỏ hàng và lượt xem sản phẩm — Đội kỹ thuật chọn lưu trên một hệ PA/EL (giả sử DynamoDB). Lý do: trong ngày sale, hệ thống nhận hàng chục nghìn request mỗi giây. Nếu một user ở Đà Nẵng thêm sản phẩm vào giỏ, họ cần phản hồi trong vài chục mili-giây — latency thấp là ưu tiên số một. Nếu số lượt xem sản phẩm hiển thị là 10.502 trong khi con số thật là 10.548 (do replica hơi trễ), chẳng ai chết cả. Bình thường ưu tiên Latency (EL), và nếu có partition thì thà cho user tiếp tục mua hàng còn hơn chặn họ lại (PA).
Số dư ví và trừ tồn kho — Đội chọn một hệ PC/EC (giả sử một SQL cluster như PostgreSQL với đồng bộ replica, hoặc CockroachDB). Lý do: nếu chỉ còn 1 chiếc iPhone trong kho mà hệ thống vì đọc dữ liệu cũ nên bán cho 3 người, hậu quả là hủy đơn, khách giận, mất uy tín. Ở đây, thà chậm vài chục mili-giây hoặc thậm chí từ chối giao dịch khi mạng trục trặc, còn hơn bán oversell. Consistency là bắt buộc, cả lúc bình thường lẫn lúc partition.
Bài học: Không có câu trả lời PACELC "đúng" cho cả một hệ thống. Cùng một sản phẩm, bạn phân loại theo từng luồng dữ liệu. Đây là lý do kiến trúc hiện đại thường dùng nhiều loại lưu trữ (polyglot persistence) thay vì một database duy nhất.
Ví dụ 2 — Google Spanner và cái giá của "đúng bằng mọi giá"
Google Spanner là ví dụ kinh điển của một hệ PC/EC — nó chọn Consistency cả khi partition lẫn khi bình thường. Điều đáng học ở đây là cái giá mà Google phải trả để đạt được điều đó.
Spanner cung cấp strong consistency trên phạm vi toàn cầu, xuyên nhiều datacenter ở nhiều châu lục. Để làm được, mỗi giao dịch ghi phải đạt đồng thuận qua giao thức Paxos giữa các replica ở các vùng địa lý khác nhau. Google thậm chí phải trang bị đồng hồ nguyên tử và GPS trong datacenter (công nghệ TrueTime) để đồng bộ thời gian cực chính xác.
Kết quả về mặt PACELC vế "E": vì luôn ưu tiên consistency, một giao dịch ghi trong Spanner có thể mất hàng chục mili-giây do phải chờ đồng thuận xuyên vùng — chậm hơn đáng kể so với một hệ PA/EL đọc/ghi cục bộ. Google chấp nhận latency cao hơn này một cách có chủ đích, vì các hệ thống như quảng cáo và thanh toán của họ đơn giản là không được phép sai lệch dữ liệu.
Bài học: Consistency mạnh không miễn phí. Khi bạn chọn nhóm EC, bạn đang ký một tấm séc trả bằng latency. Nếu ứng dụng của bạn không thực sự cần độ chính xác kiểu tài chính, việc chọn EC có thể là "over-engineering" — bạn trả tiền cho thứ mình không dùng đến.
Ví dụ 3 — Fintech Đông Nam Á và bài học từ eventual consistency
Một ví dụ giả định hợp lý nhưng rất sát thực tế: một công ty ví điện tử ở Đông Nam Á (kiểu MoMo, GrabPay, hoặc GoPay) ban đầu chọn Cassandra — một hệ PA/EL — cho toàn bộ hệ thống vì họ ấn tượng với khả năng chịu tải khổng lồ và độ khả dụng cao của nó.
Mọi thứ chạy tốt cho đến khi họ gặp một sự cố: một user nạp 500.000đ vào ví, rồi ngay lập tức chuyển tiền cho bạn. Do đọc từ một replica chưa kịp cập nhật (đặc trưng của EL — ưu tiên latency, chấp nhận eventual consistency), hệ thống đôi lúc "nhìn thấy" số dư cũ và cho phép chi tiêu vượt số dư thật, hoặc từ chối oan một giao dịch hợp lệ. Trong tài chính, cả hai đều là thảm họa.
Cách xử lý đúng không phải là vứt bỏ Cassandra, mà là áp dụng PACELC theo từng ngữ cảnh: giữ Cassandra cho lịch sử giao dịch, log, thông báo (những thứ chịu được eventual consistency), nhưng đặt consistency level QUORUM cho các thao tác đọc/ghi số dư, hoặc chuyển hẳn phần sổ cái số dư sang một hệ EC. Họ chủ động đẩy một số truy vấn từ EL về EC ngay trong cùng một database.
Bài học: Đừng để một cái nhãn PACELC lừa bạn nghĩ rằng nó áp dụng cho mọi truy vấn. Với các database cho phép tinh chỉnh (Cassandra, MongoDB, Cosmos DB), bạn là người quyết định điểm cân bằng ở từng thao tác. PACELC cho bạn từ vựng để nói rõ quyết định đó.
Hướng dẫn từng bước
Đây là quy trình bạn có thể dùng để áp dụng PACELC vào một quyết định kiến trúc cụ thể — ví dụ khi viết ADR chọn database (nhớ lại Bài 4 và Bài 5).
Bước 1 — Xác định luồng dữ liệu, không phải "cả hệ thống". Liệt kê các loại dữ liệu chính: dữ liệu tài chính, dữ liệu phiên đăng nhập, feed hoạt động, cache, log... Mỗi loại là một ứng viên cho một quyết định PACELC riêng.
Bước 2 — Với mỗi luồng, hỏi vế E trước (vì nó xảy ra 99,9% thời gian). Câu hỏi: "Nếu user đọc phải dữ liệu cũ vài trăm mili-giây, hậu quả là gì?". Nếu hậu quả nghiêm trọng (sai số dư, oversell) → nghiêng EC. Nếu hậu quả không đáng kể (lượt like hiển thị hơi lệch) → nghiêng EL.
Bước 3 — Sau đó hỏi vế P. Câu hỏi: "Khi mạng phân mảnh, tôi muốn hệ thống từ chối phục vụ để giữ đúng (PC) hay vẫn phục vụ dù có thể sai (PA)?". Với thanh toán thường là PC; với giỏ hàng, feed, thường là PA.
Bước 4 — Ghép lại thành một trong bốn nhãn (PA/EL, PC/EC, PA/EC, PC/EL) cho từng luồng.
Bước 5 — Đối chiếu nhãn mong muốn với thiên hướng của các database ứng viên. Nếu bạn cần PC/EC, đừng chọn Cassandra làm mặc định. Nếu bạn cần PA/EL cho một feed, một SQL cluster đồng bộ chặt sẽ vừa chậm vừa lãng phí.
Bước 6 — Ghi lại lý do vào ADR. Viết rõ: "Chọn database X vì luồng dữ liệu này cần đặc tính PA/EL — ưu tiên latency dưới 50ms và luôn khả dụng khi flash sale, chấp nhận eventual consistency vì độ lệch dữ liệu không gây thiệt hại nghiệp vụ." Câu này giúp người sau hiểu quyết định mà không phải đoán.
Lỗi thường gặp & mẹo
Lỗi 1 — Coi PACELC là nhãn cho cả hệ thống. Như ba ví dụ trên đã cho thấy, PACELC áp dụng theo từng luồng dữ liệu. Một câu như "hệ thống của tôi là PA/EL" thường là dấu hiệu bạn chưa suy nghĩ đủ sâu.
Lỗi 2 — Quên mất vế E, chỉ nghĩ CAP. Đây là lỗi phổ biến nhất và cũng là lý do PACELC ra đời. Nhiều kỹ sư chọn database chỉ dựa trên "chịu được partition không", rồi ngạc nhiên khi ứng dụng chậm hoặc dữ liệu lệch trong lúc mạng hoàn toàn bình thường.
Lỗi 3 — Nghĩ rằng chọn Consistency (EC) luôn an toàn hơn nên cứ chọn cho chắc. Không. EC trả giá bằng latency và đôi khi bằng cả chi phí hạ tầng (như câu chuyện TrueTime của Spanner). Chọn EC cho một luồng dữ liệu không cần nó là over-engineering — bạn làm user chờ lâu hơn mà chẳng đổi lại giá trị gì.
Lỗi 4 — Nhầm "availability" (vế P) với "latency" (vế E). Hai thứ này khác nhau. Availability là "có trả lời hay không". Latency là "trả lời nhanh hay chậm". Một hệ có thể luôn khả dụng (high availability) nhưng vẫn chậm, và ngược lại.
Mẹo 1 — Học thuộc câu điều kiện. Khi bí, nhẩm lại: "Partition → A hay C; Else → L hay C". Cái vế thứ hai là thứ CAP không có.
Mẹo 2 — Dùng PACELC như từ vựng chung trong review kiến trúc. Khi cả đội nói cùng một ngôn ngữ ("luồng này nên PA/EL"), tranh luận về database trở nên nhanh và rõ ràng hơn nhiều.
Mẹo 3 — Kiểm tra cấu hình thực tế, đừng tin nhãn mặc định mù quáng. Ví dụ MongoDB có thể là EC hay EL tùy readConcern/writeConcern bạn đặt. Luôn xác nhận cấu hình đang chạy, không chỉ tên sản phẩm.
Bài tập thực hành
Bài 1 — Phân loại. Với mỗi hệ thống sau, hãy xác định nhãn PACELC mong muốn (PA/EL, PC/EC, PA/EC hoặc PC/EL) và viết một câu giải thích:
- (a) Bảng xếp hạng người chơi trong một game mobile Việt Nam.
- (b) Sổ cái giao dịch của một ngân hàng số.
- (c) Hệ thống đếm lượt xem video trên một nền tảng như một "YouTube Việt".
- (d) Hệ thống đặt vé máy bay (chống trùng ghế).
Bài 3 — Viết mini-ADR. Chọn một luồng dữ liệu trong một dự án bạn từng làm (hoặc tưởng tượng). Viết một đoạn ADR ngắn gồm: (1) luồng dữ liệu là gì, (2) nhãn PACELC mong muốn, (3) hậu quả nếu đọc phải dữ liệu cũ, (4) database bạn chọn và vì sao nó khớp với nhãn đó.
Bài 4 — So sánh. Trong 2-3 câu, hãy giải thích cho một người mới: PACELC bổ sung điều gì mà CAP không có, và tại sao điều bổ sung đó lại quan trọng hơn trong thực tế vận hành hằng ngày.
Tóm tắt
PACELC, do Daniel Abadi đề xuất năm 2010, là bản mở rộng của CAP Theorem, sinh ra để lấp lỗ hổng lớn nhất của CAP: CAP chỉ mô tả hành vi hệ thống khi có partition, còn phần lớn thời gian hệ thống lại chạy bình thường. Câu điều kiện cốt lõi cần nhớ là: khi Partition thì chọn giữa Availability và Consistency; Else (bình thường) thì chọn giữa Latency và Consistency.
Vế "Else" mới là phần đáng giá, vì trade-off Latency-vs-Consistency ảnh hưởng đến trải nghiệm người dùng thường xuyên hơn nhiều so với trade-off lúc cháy nhà. Mọi hệ phân tán rơi vào một trong bốn nhóm, phổ biến nhất là PA/EL (nhanh và khả dụng — Cassandra, DynamoDB) và PC/EC (đúng bằng mọi giá — Spanner, VoltDB).
Bài học lớn nhất từ ba ví dụ — sàn TMĐT ngày sale, Google Spanner, và ví điện tử Đông Nam Á — là: PACELC áp dụng theo từng luồng dữ liệu, không phải cả hệ thống, và consistency mạnh không bao giờ miễn phí. Là người ra quyết định kiến trúc, hãy dùng PACELC làm từ vựng chung để phân tích từng luồng, hỏi vế E trước rồi vế P, đối chiếu với thiên hướng của database, và ghi lại lý do vào ADR. Ở bài tiếp theo, chúng ta sẽ đi sâu hơn vào chính các Consistency Models — Strong, Eventual, Causal — những thứ nằm ngay trong chữ "C" mà PACELC vừa nhắc tới.