Menu
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Đang tải...

Bài 22 — CAP Theorem — Distributed Systems Trade-off

Architecture Decision-Making Bài 22/60

Mở đầu — vì sao bài này quan trọng

Hãy tưởng tượng bạn đang là kiến trúc sư trưởng cho một sàn thương mại điện tử tại Việt Nam. Sếp bước vào phòng và hỏi một câu tưởng chừng đơn giản: "Tại sao hệ thống của mình không thể vừa luôn luôn nhanh, luôn luôn đúng dữ liệu, lại vừa không bao giờ ngừng hoạt động?". Nếu bạn không có một khung tư duy vững, bạn sẽ ấp úng — hoặc tệ hơn, hứa hẹn những điều bất khả thi.

CAP Theorem chính là câu trả lời khoa học cho câu hỏi đó. Đây là một trong những định lý nền tảng nhất của hệ phân tán (distributed systems), và nó không phải là kiến thức hàn lâm khô khan. Mỗi khi bạn quyết định dùng database nào, đặt server ở mấy vùng, hay xử lý ra sao khi mạng giữa hai trung tâm dữ liệu bị đứt — bạn đang trực tiếp đối mặt với CAP.

Điều khiến CAP quan trọng với vai trò của một người ra quyết định kiến trúc (đúng tinh thần khóa học này) là: nó buộc bạn phải chọn. Không có kiến trúc "hoàn hảo", chỉ có kiến trúc phù hợp với bài toán. Người kỹ sư giỏi biết viết code. Người kiến trúc sư giỏi biết đánh đổi có ý thức và biết giải thích lựa chọn đó cho cả đội ngũ kỹ thuật lẫn ban lãnh đạo. Bài này trang bị cho bạn đúng ngôn ngữ và tư duy để làm điều đó.

Trong phạm vi bài này, chúng ta chỉ tập trung vào bản thân định lý CAP: ba thuộc tính, ý nghĩa thực sự của việc "chọn 2 trong 3", và cách áp dụng nó vào quyết định thực tế. Phần mở rộng PACELC (khi mạng bình thường thì đánh đổi gì) sẽ được bàn riêng ở bài kế tiếp, còn các mô hình nhất quán chi tiết (strong, eventual, causal) sẽ có bài của nó.

Khái niệm cốt lõi

Nguồn gốc: Eric Brewer và cú va chạm với thực tế

CAP Theorem được Eric Brewer — giáo sư tại UC Berkeley, sau này là VP Infrastructure của Google — trình bày lần đầu năm 2000 trong một bài keynote. Đến năm 2002, hai nhà nghiên cứu Seth Gilbert và Nancy Lynch tại MIT chứng minh nó một cách hình thức. Từ đó CAP trở thành kim chỉ nam cho cả một thế hệ database phân tán.

Phát biểu gốc nghe rất gọn: Một hệ phân tán chỉ có thể đảm bảo đồng thời 2 trong 3 thuộc tính sau: Consistency, Availability, và Partition Tolerance.

Nghe thì đơn giản, nhưng chính sự đơn giản này đã khiến rất nhiều người hiểu sai. Chúng ta hãy mổ xẻ từng chữ.

Ba thuộc tính C — A — P

Consistency (C) — Tính nhất quán. Mọi node trong hệ thống đều nhìn thấy cùng một dữ liệu tại cùng một thời điểm. Nói cách khác, sau khi một thao tác ghi (write) hoàn tất, bất kỳ thao tác đọc (read) nào sau đó — dù đọc từ node nào — cũng phải trả về giá trị mới nhất. Đây thực chất là khái niệm "linearizability": hệ thống hành xử như thể chỉ có một bản sao dữ liệu duy nhất. Lưu ý: chữ C trong CAP khác với chữ C trong ACID của database truyền thống; ở đây C nói về sự đồng bộ giữa các bản sao.

Availability (A) — Tính sẵn sàng. Mọi request gửi tới một node đang hoạt động (non-failing) đều nhận được phản hồi hợp lệ — không lỗi, không treo vô thời hạn. Lưu ý quan trọng: "phản hồi" ở đây không đảm bảo đó là dữ liệu mới nhất. Hệ thống chỉ hứa rằng nó sẽ trả lời bạn một cái gì đó, chứ không hứa cái đó luôn đúng tuyệt đối.

Partition Tolerance (P) — Chịu lỗi phân mảnh mạng. Hệ thống vẫn tiếp tục vận hành ngay cả khi mạng giữa các node bị đứt gãy, khiến chúng không liên lạc được với nhau. "Partition" nghĩa là mạng bị chia thành các nhóm cô lập — node ở nhóm A không nói chuyện được với node ở nhóm B, nhưng không bên nào biết chắc bên kia còn sống hay đã chết.

Hiểu đúng: đây không phải "chọn 2 trong 3" một cách tự do

Đây là điểm mà 90% tài liệu giải thích sai, và bạn cần khắc cốt ghi tâm. Trong thực tế của bất kỳ hệ phân tán nào chạy trên mạng thật, P (Partition Tolerance) không phải là một lựa chọn — nó là điều bắt buộc.

Tại sao? Vì mạng luôn có thể lỗi. Cáp quang bị đứt, switch hỏng, một vùng AWS bị mất kết nối, độ trễ tăng đột biến khiến node tưởng nhau đã chết. Bạn không thể chọn "không chịu partition" bằng cách... cầu nguyện mạng không bao giờ đứt. Nếu bạn xây hệ thống chạy trên nhiều máy nối với nhau qua mạng, partition chắc chắn sẽ xảy ra — chỉ là khi nào thôi.

Vậy nên câu chuyện thực sự của CAP không phải "chọn 2 trong 3", mà là: khi partition xảy ra (P là bắt buộc), bạn buộc phải hy sinh một trong hai — C hoặc A. Lựa chọn thật sự của bạn thu về còn hai kịch bản:

  • CP (Consistency + Partition Tolerance): Khi mạng đứt, hệ thống thà từ chối trả lời (hy sinh Availability) còn hơn trả về dữ liệu có nguy cơ sai. Nó nói "Tôi không chắc dữ liệu đúng, nên tôi tạm ngừng phục vụ".
  • AP (Availability + Partition Tolerance): Khi mạng đứt, hệ thống vẫn trả lời (giữ Availability) nhưng chấp nhận rằng dữ liệu có thể chưa được đồng bộ, tức không nhất quán tạm thời (hy sinh Consistency).
Còn khi mạng bình thường (không có partition)? Lúc đó bạn hoàn toàn có thể có cả C lẫn A. CAP chỉ "buộc chọn" trong đúng khoảnh khắc partition xảy ra. Đây là lý do nhiều chuyên gia nói CAP nên được hiểu là "trong điều kiện partition, chọn C hay A".

Một minh họa trực giác

Hình dung hai chi nhánh ngân hàng ở Hà Nội và TP.HCM, mỗi nơi giữ một bản sao số dư tài khoản của bạn. Bình thường hai chi nhánh đồng bộ với nhau qua đường truyền. Một ngày, đường truyền đứt.

Bây giờ bạn tới chi nhánh Hà Nội rút tiền. Hệ thống có hai lựa chọn:

  • Cách CP: "Tôi không liên lạc được với TP.HCM để xác nhận số dư mới nhất, nên xin lỗi, tôi tạm khóa giao dịch." — An toàn tuyệt đối về dữ liệu, nhưng bạn không rút được tiền (mất availability).
  • Cách AP: "Cứ cho anh rút theo số dư tôi đang có ở Hà Nội." — Bạn rút được ngay, nhưng nếu cùng lúc ở TP.HCM có người cũng rút từ tài khoản đó, hệ thống có thể bị âm quỹ khi mạng nối lại (mất consistency).
Không có đáp án "đúng" tuyệt đối. Đáp án phụ thuộc vào việc bạn sợ điều gì hơn.

Tình huống thực tế

Ví dụ 1 — Sàn thương mại điện tử ngày Flash Sale (chọn AP)

Hãy đặt tình huống tại một sàn TMĐT lớn ở Đông Nam Á, giả định tên là "ShopFast", trong ngày sale 12/12. Lượng truy cập đạt đỉnh 400.000 người dùng đồng thời. Giỏ hàng, danh sách sản phẩm, và số lượt xem sản phẩm được lưu trên một database phân tán kiểu AP (ví dụ Cassandra hoặc DynamoDB).

Giữa cao điểm, một partition xảy ra giữa cụm server ở Singapore và cụm ở Jakarta trong 45 giây. Đội kỹ thuật đã chủ động thiết kế theo AP: hệ thống vẫn cho người dùng thêm sản phẩm vào giỏ, vẫn hiển thị trang sản phẩm, dù con số "đã bán 1.203 sản phẩm" ở hai vùng có thể lệch nhau vài chục đơn vị trong 45 giây đó.

Diễn giải: Với giỏ hàng và lượt xem, việc lệch vài đơn vị trong ít giây hầu như không gây hậu quả nghiêm trọng. Cái giá của việc "ngừng bán" (nếu chọn CP) trong 45 giây cao điểm 12/12 có thể là hàng tỷ đồng doanh thu và trải nghiệm tồi tệ. Sau khi partition kết thúc, các bản sao tự đồng bộ lại (reconcile).

Bài học rút ra: Với dữ liệu mà độ lệch tạm thời chấp nhận được, và nơi mà "downtime = mất tiền trực tiếp", AP thường là lựa chọn khôn ngoan. Ưu tiên giữ hệ thống sống.

Ví dụ 2 — Ví điện tử và bài toán số dư (chọn CP)

Giờ đổi bối cảnh: một ví điện tử phổ biến tại Việt Nam, giả định tên "PayViet", xử lý phần lõi số dư tài khoản. Ở đây, câu chuyện đảo ngược hoàn toàn.

Giả sử tài khoản của bạn còn đúng 500.000đ. Do một partition mạng, giả sử hệ thống chọn AP và cho phép cả hai vùng cùng xử lý giao dịch độc lập. Bạn dùng app thanh toán 500.000đ ở một điểm, đồng thời một giao dịch tự động khác cũng trừ 500.000đ. Cả hai cùng thành công vì hai vùng không thấy nhau. Kết quả: tài khoản âm 500.000đ — hệ thống vừa "tạo ra tiền" từ hư không. Đây gọi là bài toán double-spending.

Vì thế, phần lõi tài chính của PayViet được thiết kế theo hướng CP. Khi không thể xác nhận nhất quán được số dư giữa các node, hệ thống thà từ chối giao dịch và báo "Giao dịch không thực hiện được, vui lòng thử lại" còn hơn là cho phép một giao dịch có nguy cơ sai lệch tiền bạc.

Diễn giải: Với người dùng, việc thấy thông báo lỗi và thử lại sau 5 giây gây khó chịu, nhưng có thể chấp nhận. Việc mất tiền hoặc bị trừ sai thì tuyệt đối không. Ngân hàng Nhà nước và bản thân người dùng sẽ không tha thứ cho một hệ thống "lâu lâu tính sai số dư".

Bài học rút ra: Với dữ liệu mà tính đúng đắn là tối thượng — tiền bạc, tồn kho quyết định bán, đặt vé số ghế giới hạn — hãy chọn CP. Thà từ chối phục vụ tạm thời còn hơn phục vụ sai.

Ví dụ 3 — Cùng một hệ thống, nhiều lựa chọn CAP khác nhau

Đây là insight quan trọng nhất mà nhiều kỹ sư mới bỏ lỡ: CAP không áp dụng cho toàn bộ hệ thống như một khối, mà áp dụng cho từng loại dữ liệu/dịch vụ.

Quay lại sàn ShopFast. Trong cùng một nền tảng, họ ra quyết định khác nhau cho từng phần:

  • Catalog sản phẩm, đánh giá, lượt xem: AP. Chấp nhận lệch tạm thời để luôn hiển thị nhanh.
  • Tồn kho khi đặt hàng (phần trừ kho cuối cùng): CP. Không được bán quá số hàng thực có, nếu không sẽ phải hủy đơn và mất uy tín.
  • Thanh toán và ví: CP nghiêm ngặt nhất.
  • Wishlist, lịch sử xem gần đây: AP thoải mái, thậm chí mất vài mục cũng chẳng sao.
Diễn giải: Một kiến trúc sư trưởng thành không hỏi "hệ thống của tôi là CP hay AP?", mà hỏi "dịch vụ này, dữ liệu này nên là CP hay AP?".

Bài học rút ra: Đừng ép cả hệ thống vào một lựa chọn CAP duy nhất. Hãy phân rã theo bối cảnh nghiệp vụ, mỗi phần một quyết định riêng.

Hướng dẫn từng bước

Khi đứng trước một quyết định kiến trúc có liên quan tới CAP, đây là quy trình bạn có thể áp dụng:

Bước 1 — Xác định đây có thực sự là hệ phân tán không. Nếu dữ liệu chỉ nằm trên một máy đơn (single node), CAP gần như không áp dụng — bạn không có bản sao để mà không nhất quán. CAP chỉ có ý nghĩa khi có nhiều bản sao dữ liệu trên nhiều node.

Bước 2 — Chấp nhận rằng P là bắt buộc. Đừng phí thời gian mơ về "hệ thống không bao giờ partition". Mặc định P luôn có mặt. Câu hỏi thật sự bắt đầu từ bước 3.

Bước 3 — Phân loại dữ liệu theo mức độ nhạy cảm. Với mỗi loại dữ liệu, tự hỏi: "Nếu hai người dùng nhìn thấy hai giá trị khác nhau trong vài giây, hậu quả là gì?". Nếu hậu quả là "khó chịu nhẹ" thì nghiêng về AP. Nếu hậu quả là "mất tiền, sai pháp lý, bán trùng" thì nghiêng về CP.

Bước 4 — Định lượng cái giá của mỗi bên. Ước lượng: một giây downtime khi chọn CP tốn bao nhiêu tiền/uy tín? Một sự cố sai dữ liệu khi chọn AP tốn bao nhiêu? So sánh hai con số này giúp quyết định trở nên khách quan thay vì cảm tính.

Bước 5 — Chọn công nghệ phù hợp với lựa chọn CAP. Nếu cần CP, hướng tới các hệ như hệ dùng đồng thuận (consensus) kiểu Raft/Paxos — ví dụ etcd, ZooKeeper, hay các database như MongoDB (cấu hình đúng), CockroachDB. Nếu cần AP, hướng tới Cassandra, DynamoDB, Riak với cơ chế eventual consistency.

Bước 6 — Thiết kế hành vi khi partition xảy ra. Với AP: cơ chế đồng bộ lại (reconciliation), giải quyết xung đột (conflict resolution) ra sao? Với CP: thông báo lỗi thân thiện thế nào, cơ chế retry ra sao để người dùng không hoảng?

Bước 7 — Ghi lại quyết định. Đây là lúc bạn viết một ADR (đã học ở các bài đầu khóa) để giải thích: tại sao chọn CP/AP cho phần này, đánh đổi gì, và điều kiện nào sẽ khiến ta xem lại quyết định.

Lỗi thường gặp & mẹo

Lỗi 1 — Hiểu CAP là "chọn tự do 2 trong 3". Như đã nhấn mạnh, trong thực tế P là bắt buộc. Bạn không được chọn CA (bỏ P) cho một hệ phân tán thật. Cái gọi là "CA" chỉ tồn tại trên hệ thống một node hoặc khi mạng được giả định hoàn hảo — điều không có thật.

Lỗi 2 — Nghĩ CP nghĩa là "luôn luôn chậm" và AP nghĩa là "luôn luôn sai". Sai. Khi mạng bình thường, cả hệ CP lẫn AP đều có thể vừa nhanh vừa nhất quán. Sự đánh đổi chỉ lộ ra đúng lúc partition. Đừng bôi đen một bên.

Lỗi 3 — Chọn CAP cho cả hệ thống một cách đồng loạt. Như ví dụ 3, hãy quyết định theo từng dịch vụ, từng loại dữ liệu.

Lỗi 4 — Nhầm "Availability" trong CAP với "uptime 99,99%". Availability trong CAP là một định nghĩa lý thuyết chặt (mọi request tới node sống đều được trả lời). Nó không giống chỉ số SLA uptime mà marketing hay quảng cáo. Một hệ CP có thể có uptime rất cao trong đời thực (vì partition hiếm khi xảy ra), dù về lý thuyết nó "hy sinh A".

Mẹo 1 — Dùng câu hỏi vàng: "Khi mạng đứt, tôi thà trả lời sai hay thà không trả lời?". Câu trả lời chính là lựa chọn AP hay CP của bạn.

Mẹo 2 — Đọc kỹ tài liệu database. Nhiều database hiện đại cho phép cấu hình mức nhất quán theo từng truy vấn (tunable consistency). Cassandra cho bạn chọn consistency level cho mỗi lệnh đọc/ghi. Đừng giả định database của bạn cứng nhắc một chế độ.

Mẹo 3 — CAP là điểm khởi đầu, không phải điểm kết thúc. Nó cho bạn khung tư duy thô. Để đi sâu hơn về đánh đổi khi mạng bình thường, bạn sẽ cần PACELC; để chọn đúng mức đồng bộ, bạn sẽ cần hiểu các consistency model. Đó là những bài kế tiếp.

Bài tập thực hành

Bài tập 1 — Phân loại CP/AP. Với mỗi tình huống sau, hãy quyết định nên chọn CP hay AP và viết một câu lý do: a) Hệ thống đếm số lượt "thả tim" trên một bài đăng mạng xã hội. b) Hệ thống đặt vé máy bay chọn ghế cụ thể. c) Bảng xếp hạng người chơi (leaderboard) trong game, cập nhật mỗi phút. d) Hệ thống ghi nợ/có tài khoản chứng khoán khi khớp lệnh.

Bài tập 2 — Kịch bản partition. Bạn vận hành một hệ thống đặt bàn nhà hàng phân tán trên hai vùng. Một partition kéo dài 2 phút xảy ra. Hãy mô tả: (1) chuyện gì xảy ra nếu bạn thiết kế AP, (2) chuyện gì xảy ra nếu CP, (3) với nghiệp vụ đặt bàn, bạn chọn cái nào và vì sao. Chú ý tới rủi ro hai khách đặt trùng một bàn.

Bài tập 3 — Phản biện. Một đồng nghiệp nói: "Hệ thống của mình là CA, vì mình vừa nhất quán vừa sẵn sàng, còn mạng công ty mình rất ổn định nên không lo partition." Hãy viết một đoạn phản biện ngắn (3–5 câu) giải thích tại sao lập luận này có lỗ hổng.

Bài tập 4 — Áp dụng vào dự án của bạn. Chọn một hệ thống bạn đang làm hoặc quen thuộc. Liệt kê 3 loại dữ liệu khác nhau trong đó, và với mỗi loại, quyết định CP hay AP kèm lý do định lượng (cái giá của mỗi bên).

Tóm tắt

CAP Theorem, do Eric Brewer đề xuất năm 2000 và được chứng minh hình thức năm 2002, phát biểu rằng một hệ phân tán chỉ có thể đảm bảo đồng thời 2 trong 3 thuộc tính: Consistency (mọi node thấy cùng dữ liệu), Availability (mọi request được trả lời), và Partition Tolerance (vẫn chạy khi mạng đứt).

Điểm mấu chốt cần nhớ: trong thực tế, Partition Tolerance là bắt buộc vì mạng luôn có thể lỗi. Vì vậy lựa chọn thật sự của bạn thu về việc: khi partition xảy ra, hy sinh Consistency (chọn AP — vẫn trả lời nhưng có thể chưa đồng bộ) hay hy sinh Availability (chọn CP — thà từ chối còn hơn trả lời sai). Khi mạng bình thường, bạn vẫn có thể có cả C lẫn A.

Cách áp dụng khôn ngoan nhất là không hỏi "hệ thống của tôi là CP hay AP" mà hỏi cho từng loại dữ liệu: giỏ hàng và lượt xem nghiêng về AP để luôn sống; số dư ví và tồn kho nghiêng về CP để luôn đúng. Câu hỏi vàng để quyết định: "Khi mạng đứt, tôi thà trả lời sai hay thà không trả lời?".

CAP là nền tảng, nhưng chưa phải toàn bộ bức tranh. Ở bài kế tiếp, chúng ta sẽ mở rộng sang PACELC để xét cả tình huống khi mạng bình thường — nơi bạn còn phải đánh đổi giữa độ trễ (Latency) và tính nhất quán.