Chuyển tới nội dung chính

Bố trí replica & Quorum

Quy tắc: độ bền dữ liệu không phải là số bản sao bạn đã cấu hình — mà là số bản sao phải xác nhận một lần ghi, chỉ tính trên những failure domain không hỏng cùng lúc với nhau. Replication factor 3 mà chỉ cần một xác nhận, hoặc cả ba bản sao nằm chung một rack, thực chất là replication factor 1 với chi phí gấp ba.

Thuộc nhóm storage: storage engine · index & query plan · replication & sharding · bố trí replica & quorum (trang này) · session consistency · connection pool & độ trễ

Mục lục​

Anti-pattern​

Một cấu hình đọc lên nghe như "ba bản sao, an toàn":

# Kafka topic
replication.factor=3
min.insync.replicas=1 # broker default
# producer
acks=1 # "faster"
// Cassandra keyspace RF=3, application code
session.execute(debit_stmt.setConsistencyLevel(ONE)) // "lowest latency"
# Kubernetes StatefulSet, replicas: 3, no affinity rules
replicas: 3

Mỗi mảnh đều là giá trị mặc định hoặc một tùy chọn có trong tài liệu. Ghép lại, chúng có nghĩa là: một lần ghi được báo thành công ngay khi một bản sao nhận được nó, và ba bản sao có thể nằm chung một node, một rack hoặc một zone. Hệ thống mang chi phí của RF=3 nhưng chỉ chịu lỗi được như RF=1.

Vì sao môi trường dev che giấu lỗi này​

Ở local chỉ có một node, hoặc ba container trên cùng một laptop, và không có gì hỏng độc lập với nhau. Mọi lần ghi đều tới mọi bản sao trong vòng một mili giây, nên ONE và QUORUM trả về kết quả giống hệt nhau và acks=1 không làm mất gì. Benchmark còn thưởng cho thiết lập yếu: càng ít xác nhận thì độ trễ càng thấp và throughput càng cao, nên chính cấu hình làm mất dữ liệu lại thắng trong bài load test.

Sự khác biệt chỉ lộ ra khi một bản sao bị tụt lại hoặc biến mất đúng lúc quan trọng — một node chết giữa lúc đã xác nhận và lúc replicate, một zone hỏng cả khối, một replica đang lag đúng lúc có lần đọc rơi vào nó. Không điều nào trong số đó xảy ra trong bộ test, trừ khi có người cố ý gây ra.

Ba con số, không phải một​

Con sốCâu hỏi nó trả lờiTên thường gặp
N — số bản saoRốt cuộc có bao nhiêu bản sao tồn tại?replication.factor, RF của keyspace, số replica
W — số xác nhận ghiBao nhiêu bản sao phải giữ lần ghi trước khi client được báo "xong"?acks + min.insync.replicas, consistency level khi ghi, synchronous_standby_names
R — số bên tham gia khi đọcBao nhiêu bản sao được hỏi trước khi một lần đọc trả về?consistency level khi đọc; 1 với mọi lần đọc từ một replica

N là con số mọi người cấu hình và nói tới. W quyết định độ bền — một lần ghi đã xác nhận sống sót qua việc mất tối đa W − 1 bản sao. W và R cùng nhau quyết định lần đọc có mới hay không. Một lần review chỉ kiểm tra N là đã kiểm tra con số ít quan trọng nhất.

Các failure mode​

1. Xác nhận trên một bản sao. Với W = 1, client được báo lần ghi thành công trong khi nó chỉ tồn tại trên một máy. Nếu máy đó chết trước khi replicate, failover sẽ promote một bản sao chưa từng thấy lần ghi đó. Với một lần nạp tiền, đó là khách hàng đã bị ngân hàng trừ tiền nhưng ví vẫn hiện số dư cũ. Không có gì "bị hỏng"; hệ thống làm đúng những gì W = 1 cho phép.

2. acks=all mà thực ra là một. Trong Kafka, acks=all chờ mọi replica đang đồng bộ. Khi các follower tụt lại, tập in-sync co lại — có thể chỉ còn mỗi leader — và lúc đó acks=all được thỏa mãn bởi một bản sao. Chỉ min.insync.replicas=2 (với RF=3) mới khiến broker từ chối những lần ghi mà nó không thể đặt lên hai bản sao. Mặc định của broker là 1, nên acks=all với cấu hình mặc định âm thầm tụt thành acks=1 đúng vào lúc cluster đang có vấn đề.

3. RF=2 không có biên dự phòng. Sau khi mất một bản sao, mỗi byte chỉ còn tồn tại một lần cho đến khi re-replication xong — với hàng terabyte ở tốc độ vài trăm MB/s là hàng giờ, trong lúc bản còn lại gánh cả tải của chính nó lẫn lượng đọc phục vụ việc dựng lại. Một sự cố thứ hai trong khoảng thời gian đó là mất dữ liệu. Và với quorum đa số, RF=2 cần cả hai bản sao: chỉ một sự cố là dừng hẳn việc ghi quorum. RF=2 mua chi phí của replication mà không có khả năng chịu lỗi; phép tính giống hệt cái bẫy số node chẵn.

4. Các bản sao hỏng cùng nhau. Ba replica được lập lịch lên cùng một node Kubernetes, một rack sau cùng một top-of-rack switch, hoặc cùng một availability zone. Một sự cố duy nhất xóa sạch mọi bản sao cùng lúc. Đây là kết quả mặc định khi không có quy tắc bố trí tường minh — scheduler tối ưu theo tài nguyên, không phải theo việc trải đều replica của bạn.

5. Sự cố có kế hoạch cộng dồn lên sự cố bất ngờ. Một đợt nâng cấp node cuốn chiếu drain một replica trong khi một replica khác vẫn đang dựng lại sau vụ hỏng đĩa hôm qua. Hai trên ba bản sao ngừng hoạt động mà không ai "gặp sự cố" cả. PodDisruptionBudget và một bước kiểm tra replication khỏe mạnh trước mỗi lần drain là thứ ngăn chuyện này.

6. Các bản sao sao chép trung thành cả dữ liệu hỏng. Replication chép bất cứ thứ gì primary đã ghi, kể cả một lần ghi sai từ controller lỗi hay từ một bug. Càng nhiều bản sao thì lan càng nhanh. Page checksum phát hiện được nó ở tầng storage (PostgreSQL chỉ bật mặc định từ bản 18), và chỉ có backup mới khôi phục được.

W + R > N, và những gì nó không hứa​

Trong các store không có leader (Cassandra, kiểu DynamoDB, Riak), nếu tập ghi và tập đọc buộc phải giao nhau — W + R > N — thì mọi lần đọc đều chạm ít nhất một bản sao có lần ghi đã xác nhận mới nhất, và phiên bản mới nhất thắng. Với N=3, ghi QUORUM và đọc QUORUM (2 + 2 > 3) giao nhau; ONE/ONE (1 + 1 ≤ 3) thì không, và một lần đọc có thể trả về giá trị cũ ngay sau một lần ghi thành công.

Sự giao nhau đó yếu hơn vẻ ngoài. Nó không cho bạn:

  • Thứ tự của các lần ghi đồng thời. "Mới nhất" thường được quyết định bằng timestamp — last write wins — nên clock skew giữa các bên ghi có thể khiến lần ghi sau bị thua. Hai lần trừ tiền đồng thời không được quorum sắp xếp tuần tự; việc đó cần một lần ghi có điều kiện hoặc một lock.
  • Rollback cho những lần ghi thất bại. Một lần ghi tới được một bản sao nhưng chưa đủ W sẽ bị báo là thất bại và không bị hoàn tác. Những lần đọc sau có thể thấy nó hoặc không, tùy chúng chạm vào bản sao nào.
  • Đảm bảo khi có sloppy quorum. Nếu store chấp nhận ghi lên các node thay thế trong lúc sự cố (hinted handoff), W node đã ghi có thể không nằm trong N node mà một lần đọc hỏi tới.

Vậy nên đọc và ghi quorum làm cho việc đọc phải dữ liệu cũ khó xảy ra; chúng không biến một store không có leader thành một sổ cái. Với yêu cầu "số dư không bao giờ được sai", hãy dùng một store có một leader hoặc dựa trên đồng thuận cùng một update có điều kiện, chứ không phải một consistency level.

Level này thường được đặt theo từng request, và đó là điểm hữu ích: QUORUM cho luồng thanh toán, ONE cho số lượt xem, trong cùng một keyspace. Ở cluster nhiều datacenter, LOCAL_QUORUM giữ độ trễ trong cùng region với cái giá là dữ liệu giữa các region kém mới hơn; xem session consistency.

Bố trí: đếm failure domain, không đếm bản sao​

Với mỗi bản sao, hãy hỏi "sự kiện đơn lẻ nào xóa bản này cùng với một bản khác?" và trải chúng ra cho đến khi câu trả lời là "không có gì khả dĩ":

host → podAntiAffinity (required, not preferred) on kubernetes.io/hostname
zone → topologySpreadConstraints on topology.kubernetes.io/zone
rack → rack-aware placement: Cassandra NetworkTopologyStrategy + snitch, Kafka broker.rack
region → a different problem: latency and failover, not just placement

Anti-affinity dạng preferredDuringScheduling chỉ là một gợi ý mà scheduler bỏ qua khi cluster chật — đúng lúc nó quan trọng nhất. Trên một cluster có ít node hơn số replica, anti-affinity bắt buộc sẽ để một replica ở trạng thái Pending, và đó là kết quả trung thực: bạn không có ba failure domain, và cấu hình nên nói rõ điều đó thay vì giả vờ.

Chọn các con số​

Dữ liệuNWGhi chú
Tiền, đơn hàng, bất cứ thứ gì dùng để ra quyết định3 (hoặc 5) trải qua các zoneđa số (2 trên 3)min.insync.replicas=2, acks=all; đọc từ leader hoặc dùng R với W + R > N
Dữ liệu ứng dụng thông thường3 trải qua các zoneđa sốmức mặc định đáng bảo vệ
Log, metric, cache, projection dựng lại được1–21mất một phần là chấp nhận được theo định nghĩa — hãy ghi rõ thành văn bản

Ý nghĩa của bảng là mỗi dòng là một quyết định tường minh với mức chịu mất dữ liệu được nói rõ — cùng loại quyết định theo từng loại dữ liệu như CAP. Một thiết lập chung cho cả cluster gần như luôn quá yếu với một số dữ liệu hoặc quá đắt với phần còn lại.

Checklist review​

  • Với mỗi store có replication, biết rõ N, W và R — không chỉ N
  • Dữ liệu dùng để ra quyết định (số dư, trạng thái thanh toán) được ghi với W đa số, không bao giờ ONE / acks=1
  • Các Kafka topic mang sự kiện nghiệp vụ đặt min.insync.replicas ≥ 2 với RF=3 và producer dùng acks=all
  • Không store production nào dựa vào RF=2 cho độ bền
  • Những lần đọc phải thấy lần ghi mới nhất hoặc đi tới leader, hoặc thỏa W + R > N
  • Các update đồng thời lên cùng một key được tuần tự hóa bằng ghi có điều kiện, không phải bằng quorum + timestamp
  • Replica được trải ra bằng anti-affinity bắt buộc / topology spread / rack awareness, đã kiểm chứng trên các pod đang chạy
  • Có PodDisruptionBudget để các lần drain có kế hoạch không gỡ quá một replica
  • Biết thời gian re-replication sau khi mất một node, và đã tính đến sự cố thứ hai trong khoảng đó
  • Page hoặc block checksum được bật, và backup tồn tại độc lập với replication

Bản dịch tiếng Việt của bài học cùng tên. Lấy cảm hứng từ series Replication & Scaling phần P14 (Replication Factor) và P15 (Quorum) của TechCraft (collection). Các bài viết cung cấp sự đánh đổi giữa RF=1/2/3, các câu chuyện sự cố về bản sao chung rack và dữ liệu hỏng bị replicate, lập luận giao nhau W + R > N, và lần nạp tiền biến mất khi failover với W = 1. Phần suy giảm min.insync.replicas của Kafka, giới hạn của sự giao nhau quorum (ghi đồng thời, ghi dở không được rollback, sloppy quorum), các quy tắc bố trí trên Kubernetes, trường hợp sự cố có kế hoạch cộng dồn sự cố bất ngờ, và cấu trúc review được viết từ thực tiễn đã được kiểm chứng, không lấy từ các bài viết.