Data Engineering 6 — Hệ phân tán

13 thg 7, 2026 3 lượt xem
#data-engineering
#cap-theorem
#replication
#distributed-systems

Vì sao cần hệ phân tán?

Một hệ thống dữ liệu hiện đại hiếm khi chạy trên một máy duy nhất. Khi dữ liệu lớn lên đến hàng terabyte, lượng truy vấn tăng lên hàng triệu mỗi giây, hay yêu cầu phải luôn sẵn sàng kể cả khi một máy chủ chết, ta buộc phải phân tán công việc ra nhiều máy. Có ba động cơ chính.

Mở rộng (scalability). Một máy có giới hạn vật lý về CPU, RAM và đĩa. Khi đạt trần, ta có hai lựa chọn: scale up (mua máy mạnh hơn — đắt, có trần) hay scale out (thêm nhiều máy thường — rẻ hơn theo đơn vị, gần như không trần). Hệ phân tán cho phép scale out: thêm máy là thêm năng lực.

Sẵn sàng cao (high availability). Nếu chỉ có một máy, máy đó chết thì cả dịch vụ chết. Khi nhân bản dữ liệu trên nhiều máy ở nhiều vị trí, một máy hỏng vẫn còn máy khác phục vụ. Đây là điều kiện sống còn cho dịch vụ chạy 24/7.

Chịu lỗi (fault tolerance). Trong một cụm hàng nghìn máy, hỏng hóc là chuyện bình thường, không phải ngoại lệ. Đĩa hỏng, mạng đứt, nguồn điện chập chờn xảy ra mỗi ngày. Hệ phân tán được thiết kế để tiếp tục hoạt động đúng dù một phần hệ thống đang gặp sự cố.

Ngoài ra còn có độ trễ (latency): đặt dữ liệu gần người dùng (ở nhiều vùng địa lý) giúp giảm thời gian phản hồi.

Những thách thức cốt lõi

Phân tán không miễn phí. Khi tách một hệ thống thành nhiều máy trao đổi qua mạng, ta đối mặt với những vấn đề không tồn tại trên một máy.

Mạng không tin cậy

Trên một máy, gọi hàm luôn trả về (trừ khi crash). Qua mạng, một yêu cầu có thể: tới đích nhưng phản hồi bị mất, bị trễ tùy ý, hay không bao giờ tới. Quan trọng nhất: khi không nhận được trả lời, bạn không thể phân biệt giữa "máy kia đã chết", "máy kia đang xử lý chậm", hay "phản hồi đang trên đường về". Mọi giao thức phân tán phải sống chung với sự mơ hồ này, thường bằng timeout — nhưng timeout chỉ là phỏng đoán, không phải sự thật.

Đồng hồ lệch (clock skew)

Mỗi máy có đồng hồ riêng và chúng không bao giờ khớp tuyệt đối. Dù có NTP đồng bộ, vẫn còn sai lệch hàng mili giây đến vài giây. Hệ quả: bạn không thể tin cậy dùng "thời điểm" (timestamp) để quyết định sự kiện nào xảy ra trước. Hai ghi đồng thời ở hai máy có thể có timestamp cho thấy thứ tự sai so với thực tế. Vì vậy các hệ phân tán nghiêm túc dùng logical clock (như Lamport clock, vector clock) thay vì đồng hồ vật lý để suy luận thứ tự nhân quả.

Lỗi cục bộ (partial failure)

Đây là điểm gây đau đầu nhất. Trên một máy, hoặc tất cả chạy, hoặc tất cả dừng. Trong hệ phân tán, một phần đang chạy tốt trong khi một phần đã chết — và phần còn sống không chắc chắn biết phần kia đã chết hay chưa. Phải thiết kế để hệ thống vẫn đúng đắn giữa trạng thái nửa vời này.

Định lý CAP

CAP là khung tư duy nền tảng nhất khi nói về hệ phân tán lưu trữ dữ liệu. Ba chữ cái:

  • C — Consistency (nhất quán): mọi lần đọc nhận được dữ liệu mới nhất đã ghi, hoặc nhận lỗi. Tức là mọi node nhìn thấy cùng một dữ liệu tại cùng một thời điểm. (Lưu ý: đây là consistency theo nghĩa linearizability, khác với chữ C trong ACID.)
  • A — Availability (sẵn sàng): mọi yêu cầu tới một node còn sống đều nhận được phản hồi (không lỗi), dù không đảm bảo đó là dữ liệu mới nhất.
  • P — Partition tolerance (chịu phân mảnh mạng): hệ thống tiếp tục hoạt động dù các thông điệp giữa các node bị mất hoặc trễ — tức là mạng bị "chia" thành các nhóm không liên lạc được.

Định lý CAP nói rằng: khi xảy ra phân mảnh mạng (P), bạn chỉ có thể chọn một trong hai — C hoặc A, không thể cả hai.

Điểm hay bị hiểu sai: trong thực tế, mạng sẽ có lúc phân mảnh (cáp đứt, switch lỗi, vùng địa lý mất kết nối). Phân mảnh không phải lựa chọn — nó là sự thật bạn phải gánh. Nên P gần như bắt buộc, và lựa chọn thực sự chỉ là CP hay AP khi phân mảnh xảy ra.

CP vs AP — ví dụ trực giác

Hình dung một cụm có hai bản sao dữ liệu ở hai trung tâm, và đường mạng giữa chúng đứt. Một khách hàng kết nối tới bản sao bên A và ghi giá trị mới. Một khách hàng khác kết nối tới bản sao bên B và đọc.

  • Hệ CP chọn nhất quán: bản sao bên B nhận ra nó không thể xác nhận mình có dữ liệu mới nhất (vì mất liên lạc với A), nên nó từ chối phục vụ (trả lỗi). Không có ai đọc được dữ liệu cũ — nhưng phải hy sinh tính sẵn sàng.
  • Hệ AP chọn sẵn sàng: bản sao bên B vẫn trả lời bằng dữ liệu cũ nó đang có. Khách hàng luôn nhận được phản hồi — nhưng có thể là dữ liệu lỗi thời. Sau khi mạng hồi phục, hai bên hòa giải (reconcile) để trở về nhất quán.
Tiêu chíCP (Consistency + Partition)AP (Availability + Partition)
Khi phân mảnh, ưu tiênĐúng đắn dữ liệuLuôn phản hồi
Hành vi node bị cô lậpTừ chối / chờ (trả lỗi)Vẫn phục vụ dữ liệu cũ
Rủi ro cho ứng dụngTạm thời không khả dụngĐọc/ghi dữ liệu lỗi thời
Phù hợp vớiGiao dịch tài chính, tồn kho, khóaGiỏ hàng, like, đếm view, cache
Ví dụ hệ thốngZooKeeper, etcd, HBase, Spanner*Cassandra, DynamoDB, Riak

(*Spanner đạt tính nhất quán mạnh nhờ đồng hồ nguyên tử TrueTime, nhưng vẫn ưu tiên C khi phân mảnh.)

Thực tế phức tạp hơn nhị phân CP/AP: nhiều hệ cho phép điều chỉnh theo từng truy vấn (tunable consistency), ví dụ Cassandra cho bạn chọn mức cho mỗi lần đọc/ghi.

Các mức nhất quán

"Nhất quán" không phải bật/tắt mà là một phổ.

Strong consistency (nhất quán mạnh)

Sau khi một ghi hoàn tất, mọi lần đọc tiếp theo (từ bất kỳ node nào) đều thấy giá trị mới. Đây là mô hình dễ lập trình nhất — hệ thống "cư xử như một máy duy nhất" — nhưng đắt: cần phối hợp giữa các node trước khi trả lời, làm tăng độ trễ và giảm sẵn sàng.

Eventual consistency (nhất quán cuối cùng)

Nếu ngừng ghi, sau một khoảng thời gian mọi bản sao sẽ hội tụ về cùng giá trị. Trong lúc chuyển tiếp, các lần đọc có thể thấy dữ liệu cũ. Đổi lại: độ trễ thấp, sẵn sàng cao. Phù hợp khi việc thấy dữ liệu cũ vài giây là chấp nhận được (đếm lượt thích, gợi ý sản phẩm).

Causal consistency (nhất quán nhân quả)

Nằm giữa hai mức trên: đảm bảo các thao tác có quan hệ nhân quả được mọi node nhìn thấy theo đúng thứ tự, còn các thao tác độc lập thì không bắt buộc. Ví dụ: nếu bạn bình luận sau khi đọc một bài đăng, thì không ai được thấy bình luận của bạn mà chưa thấy bài đăng. Đây thường là mức nhất quán mạnh nhất có thể đạt được mà vẫn giữ tính sẵn sàng khi phân mảnh.

Partitioning / Sharding

Khi một bộ dữ liệu quá lớn để nằm trên một máy, ta chia (partition / shard) nó ra nhiều máy. Mỗi máy giữ một tập con. Mục tiêu: phân tải đều và cho phép xử lý song song. Câu hỏi mấu chốt: chia theo cái gì?

Hash partitioning

Áp một hàm băm lên khóa (ví dụ hash(user_id) % N) để quyết định bản ghi thuộc shard nào. Ưu điểm: phân bố đều, tránh điểm nóng. Nhược điểm: mất tính cục bộ của thứ tự — truy vấn theo khoảng (range query) phải hỏi mọi shard. Thêm/bớt máy gây xáo trộn lớn, nên thực tế dùng consistent hashing để khi thêm node chỉ một phần nhỏ dữ liệu phải di chuyển.

Range partitioning

Chia theo khoảng giá trị khóa (ví dụ A–H ở shard 1, I–P ở shard 2...). Ưu điểm: range query rất hiệu quả vì các khóa gần nhau ở cùng shard. Nhược điểm: dễ tạo hot partition nếu dữ liệu hay truy cập tập trung vào một khoảng (ví dụ chia theo ngày, mọi ghi mới đều dồn vào shard "hôm nay").

Hot partition

Là tình huống một shard nhận tải nặng bất cân xứng — biến lợi ích phân tán thành nút thắt cổ chai. Nguyên nhân thường là chọn khóa kém (khóa có phân bố lệch, hoặc một giá trị "siêu phổ biến" như một người nổi tiếng có hàng triệu follower). Cách giảm thiểu: chọn khóa có entropy cao, thêm hậu tố ngẫu nhiên vào khóa nóng để rải tải, hoặc tách riêng các thực thể đặc biệt.

Replication (nhân bản)

Partitioning trả lời "chia dữ liệu ra sao"; replication trả lời "giữ bao nhiêu bản sao của mỗi mảnh, để chịu lỗi và phục vụ đọc". Mô hình phổ biến nhất là leader–follower (còn gọi master–slave / primary–replica).

Mọi ghi đi qua leader; leader ghi vào log rồi truyền (replicate) thay đổi xuống các follower. Đọc có thể phục vụ từ leader hoặc bất kỳ follower nào.

Đồng bộ vs bất đồng bộ

  • Replication đồng bộ (synchronous): leader chờ follower xác nhận đã nhận trước khi báo "ghi thành công" cho client. Ưu điểm: nếu leader chết, follower chắc chắn có dữ liệu mới nhất — không mất dữ liệu. Nhược điểm: chậm, và nếu follower treo thì ghi bị nghẽn.
  • Replication bất đồng bộ (asynchronous): leader báo thành công ngay sau khi tự ghi, rồi mới đẩy xuống follower sau. Ưu điểm: nhanh, không bị follower kéo lùi. Nhược điểm: nếu leader chết trước khi follower kịp nhận, dữ liệu đó mất.

Nhiều hệ chọn đường giữa — semi-synchronous: chỉ cần một follower xác nhận đồng bộ, phần còn lại bất đồng bộ.

Đọc từ replica và độ trễ

Cho client đọc từ follower giúp chia tải đọc rất tốt. Nhưng do replication có độ trễ, follower có thể tụt sau leader — gọi là replication lag. Hệ quả khó chịu: người dùng ghi một bình luận (vào leader), rồi tải lại trang (đọc từ follower chưa kịp cập nhật) và không thấy bình luận của chính mình. Đây là vi phạm read-your-writes consistency. Cách xử lý: đọc dữ liệu của chính mình từ leader, hoặc theo dõi vị trí log đã ghi và chỉ đọc từ follower đã bắt kịp.

Đồng thuận (consensus): Raft và Paxos

Nhiều bài toán phân tán quy về một câu hỏi: làm sao để một nhóm node thống nhất với nhau về một giá trị (ví dụ "ai là leader?", "thứ tự các thao tác là gì?") dù mạng không tin cậy và một số node có thể chết? Đó là bài toán đồng thuận.

Paxos là thuật toán đồng thuận kinh điển (Leslie Lamport), đúng đắn nhưng nổi tiếng khó hiểu và khó cài đặt đúng. Raft ra đời sau với mục tiêu rõ ràng: dễ hiểu hơn mà vẫn tương đương về sức mạnh. Raft chia bài toán thành các phần mạch lạc: bầu leader (leader election), nhân bản log (log replication), và đảm bảo an toàn (safety).

Ý tưởng chung: hệ thống chỉ "chốt" một quyết định khi có đa số (majority/quorum) node đồng ý. Đa số là điều kiện then chốt — vì hai tập đa số trong cùng một cụm luôn giao nhau ít nhất một node, nên không thể có hai quyết định mâu thuẫn được chốt đồng thời. Đây cũng là lý do các cụm đồng thuận (ZooKeeper, etcd) thường có số node lẻ (3, 5, 7): để có ngưỡng đa số rõ ràng và chịu được tối đa số node hỏng.

Idempotency và retry

Vì mạng không tin cậy, client phải thử lại (retry) khi không nhận được phản hồi. Nhưng nhớ lại điều mơ hồ ở trên: bạn không biết yêu cầu trước đó đã được xử lý hay chưa. Nếu nó đã được xử lý mà bạn thử lại, bạn có thể gây ra thao tác trùng — trừ tiền hai lần, tạo hai đơn hàng.

Giải pháp là làm cho thao tác idempotent: thực hiện nhiều lần cho kết quả y như một lần. Ví dụ "đặt số dư = 100" là idempotent (chạy mấy lần cũng vẫn 100), còn "cộng 50 vào số dư" thì không. Khi không thể tự nhiên idempotent, ta gắn idempotency key (mã định danh duy nhất cho mỗi thao tác) để máy chủ nhận ra và bỏ qua các bản trùng. Đây là nền tảng để retry an toàn — và là lý do mọi API thanh toán nghiêm túc đều hỗ trợ idempotency key.

Quorum: trực giác R + W > N

Trong các hệ không có leader (như Dynamo, Cassandra), mỗi mảnh dữ liệu được sao thành N bản. Khi ghi, ta đòi ít nhất W bản xác nhận; khi đọc, ta hỏi ít nhất R bản và lấy giá trị mới nhất trong số đó.

Điều kiện vàng: R + W > N.

Vì sao? Nếu R + W > N thì tập node trả lời cho một lần đọc và tập node đã nhận một lần ghi bắt buộc phải giao nhau ít nhất một node. Node giao đó giữ giá trị mới nhất, nên lần đọc chắc chắn "chạm" được dữ liệu mới — đảm bảo nhất quán mạnh dù không có leader.

Ví dụ với N = 3:

  • W = 2, R = 2 (2 + 2 > 3): cân bằng phổ biến, chịu được 1 node hỏng cho cả đọc lẫn ghi.
  • W = 3, R = 1: ghi chậm/kém chịu lỗi nhưng đọc rất nhanh.
  • W = 1, R = 1 (1 + 1 = 2, không > 3): cực nhanh nhưng không đảm bảo nhất quán — đây là chế độ eventual consistency.

Quorum cho ta một "núm vặn" để đánh đổi giữa nhất quán, độ trễ và khả năng chịu lỗi — ngay trên một cụm.

Liên hệ: vì sao DB phân tán & Kafka thiết kế như vậy

Những nguyên lý trên không trừu tượng — chúng giải thích trực tiếp thiết kế của công cụ ta dùng hàng ngày.

DB phân tán. Cassandra chọn AP và tunable consistency vì nó sinh ra cho các use case quy mô lớn ưu tiên sẵn sàng (logging, time-series), nên dùng hash partitioning + quorum R/W để bạn tự chỉnh đánh đổi. Spanner ngược lại đầu tư vào TrueTime để đạt nhất quán mạnh xuyên vùng địa lý — phục vụ giao dịch tài chính. Cùng một bài toán CAP, hai triết lý khác nhau cho hai loại nhu cầu.

Kafka. Kafka là minh họa đẹp của mọi khái niệm trên gộp lại. Một topic được chia thành nhiều partition (chính là partitioning — và khóa của message quyết định partition, nên chọn khóa kém sẽ gây hot partition). Mỗi partition có một leader và các follower (replication kiểu leader–follower). Producer ghi vào leader; cấu hình acks chính là núm vặn đồng bộ/bất đồng bộ (acks=all = chờ quorum ISR xác nhận, an toàn nhất). Việc bầu leader cho partition dựa trên đồng thuận (trước đây qua ZooKeeper, nay qua KRaft — chính là Raft). Producer idempotent dùng sequence number để tránh ghi trùng khi retry. Mọi mảnh ghép đều có mặt: partition, replication, leader election, quorum, idempotency.

Hiểu hệ phân tán không phải để học thuộc tên thuật toán, mà để đọc được các đánh đổi ẩn sau mỗi lựa chọn cấu hình — và biết khi nào nên chọn nhất quán, khi nào nên chọn sẵn sàng.

Tóm tắt

  • Hệ phân tán giải quyết mở rộng, sẵn sàng cao và chịu lỗi bằng cách trải công việc trên nhiều máy — đổi lại phải sống chung với mạng không tin cậy, đồng hồ lệch và lỗi cục bộ.
  • Định lý CAP: khi mạng phân mảnh, chỉ chọn được nhất quán (CP) hoặc sẵn sàng (AP). Phân mảnh là điều không tránh khỏi, nên lựa chọn thực sự là CP vs AP.
  • Nhất quán là một phổ: mạnh, nhân quả, đến cuối cùng — đánh đổi giữa độ đúng và độ trễ/sẵn sàng.
  • Partitioning (hash vs range) chia dữ liệu để mở rộng; cảnh giác hot partition. Replication (leader–follower, đồng bộ vs bất đồng bộ) giữ nhiều bản để chịu lỗi và chia tải đọc, với chi phí là replication lag.
  • Đồng thuận (Raft/Paxos) cho phép nhóm node thống nhất nhờ đa số. Idempotency làm retry an toàn. Quorum R + W > N đảm bảo đọc chạm dữ liệu mới mà không cần leader.

Tự kiểm tra

  1. Vì sao trong thực tế P (chịu phân mảnh) gần như là yêu cầu bắt buộc, khiến lựa chọn thật sự chỉ còn là CP hay AP?
  2. Mô tả một tình huống cụ thể mà một hệ AP trả về dữ liệu cũ, và giải thích vì sao điều đó vẫn chấp nhận được với một số ứng dụng.
  3. Vì sao không nên dùng timestamp của đồng hồ vật lý để quyết định thứ tự hai sự kiện ở hai máy khác nhau? Dùng gì thay thế?
  4. Range partitioning theo ngày dễ tạo hot partition như thế nào, và bạn sẽ giảm thiểu ra sao?
  5. Replication bất đồng bộ nhanh hơn nhưng có rủi ro gì khi leader chết? Read-your-writes consistency bị vi phạm trong tình huống nào?
  6. Với N = 3, vì sao cấu hình W = 2, R = 2 đảm bảo nhất quán mạnh còn W = 1, R = 1 thì không?

Đọc tiếp

Data Engineering 7 — Data Lake & Lakehouse

Bài viết liên quan

CSV/JSON/Parquet/Avro, lưu trữ theo hàng vs cột, nén, và OLTP vs OLAP.

13 thg 7, 2026 7

Data Engineering là gì, vai trò trong vòng đời dữ liệu, và bức tranh hệ sinh thái công cụ.

13 thg 7, 2026 4

Vì sao xử lý phân tán, mô hình Spark (RDD/DataFrame), lazy evaluation, shuffle và tối ưu.

13 thg 7, 2026 4

Đảm bảo chất lượng & minh bạch dữ liệu bằng dbt: data tests dựng sẵn (unique, not_null, relationships, accepted_values), singular test, unit test, và tài liệu tự sinh kèm lineage graph.

13 thg 7, 2026 4