Hình dung một thư viện lớn tách sách ra ba chi nhánh theo vần chữ cái: A–H ở chi nhánh một, I–P ở chi nhánh hai, còn lại ở chi nhánh ba. Nếu bạn biết trước quy tắc ấy, bạn tới thẳng đúng nơi. Nếu không, bạn tới chi nhánh gần nhất rồi bị bảo "sách này ở chi nhánh kia" và phải đi lại — mất công mỗi lần. Và có một việc bạn không thể làm dù biết luật: mượn một chồng sách nằm rải ba chi nhánh trong một lần ký mượn duy nhất. Redis Cluster chia dữ liệu ra nhiều máy đúng theo mô hình này, và bài này dựng một cụm sáu nút để đo cả cái nó cho lẫn cái nó lấy đi — mà cái nó lấy đi mới là phần đáng nhớ.

Sentinel cho khả năng chịu lỗi nhưng mọi dữ liệu vẫn nằm trên một máy. Cluster chia dữ liệu ra nhiều máy.

16.384 slot chia cho ba nút chính

    0 – 5460    nút 1
 5461 – 10922   nút 2
10923 – 16383   nút 3

slot = CRC16(khoá) mod 16384

Không có bảng tra, không có nút điều phối. Mỗi khách hàng tự tính slot từ tên khoá và biết ngay phải hỏi nút nào. Phân bố thực tế với 20.000 khoá: 6.669 / 6.668 / phần còn lại — đều nhau. CRC16 rải tốt, không cần làm gì thêm.

Khách hàng biết bản đồ hay không: chênh 1,63 lần

Tôi viết hai khách hàng làm cùng một việc.

Khách "ngây thơ" luôn gửi tới nút đầu tiên và chấp nhận bị chuyển hướng:

10.390 ops/s | 13.337 lần MOVED trên 20.000 lệnh (67%)

Khách tự tính CRC16 và gọi thẳng đúng nút:

16.907 ops/s | 0 lần MOVED

Chậm hơn 1,63 lần, và con số 67% chính là tỷ lệ khoá không thuộc nút đầu — 2 trên 3 phân mảnh. Mỗi MOVED là một lượt đi về bị bỏ phí: gửi lệnh, nhận lỗi kèm địa chỉ đúng, gửi lại — đúng cảnh bị đá sang chi nhánh kia. Với cụm 10 nút, tỷ lệ đó là 90%.

Đây là lý do thư viện khách hàng cho Cluster phải nạp và giữ bản đồ slot. Nếu bạn dùng một thư viện không hỗ trợ Cluster và chỉ dựa vào MOVED, bạn đang trả giá này ở mọi lệnh. Bản đồ cũng phải làm mới khi cụm thay đổi: sau một lần chuyển đổi hoặc một lần chuyển slot, bản đồ cũ sai và khách quay lại chế độ chuyển hướng. Thư viện tốt nghe MOVED rồi tải lại CLUSTER SLOTS.

Cái Cluster lấy đi

MGET k:1 k:2 k:3   ->  CROSSSLOT Keys in request don't hash to the same slot
MSET a 1 b 2       ->  CROSSSLOT
EVAL ... 2 k:1 k:2 ->  CROSSSLOT

k:1 ở slot 10166, k:2 ở slot 6101 — hai nút khác nhau, và Redis từ chối thẳng. Điều này áp cho mọi lệnh chạm nhiều khoá: MGET, MSET, SINTER, ZUNIONSTORE, MULTI, và Lua. Tất cả chỉ làm việc trong một slot.

Hệ quả với những gì đã đo ở các phần trước:

  • MGET — thứ nhanh hơn đường ống 45% ở phần 12 — không dùng được qua slot.
  • Đường ống vẫn dùng được, nhưng phải chia theo nút.
  • Lua và MULTI mất phần lớn ích lợi: chúng chỉ nguyên tử được trong một slot.

Đây là cái giá thật của Cluster, và nó không phải hiệu năng mà là khả năng diễn đạt.

Thẻ băm là lối thoát duy nhất

{u1}:ten   ->  slot 4574
{u1}:tuoi  ->  slot 4574

MSET {u1}:ten A {u1}:tuoi 30   ->  OK
MGET {u1}:ten {u1}:tuoi        ->  A 30

Chỉ phần trong {} được băm. Mọi khoá cùng thẻ nằm cùng slot, và mọi lệnh nhiều khoá hoạt động lại bình thường. Nhưng nó phải được thiết kế từ lúc đặt tên khoá. Đổi lược đồ tên khoá trên hệ thống đang chạy là một dự án di trú, không phải một dòng cấu hình.

Và nó có mặt trái: thẻ băm gom dữ liệu vào một slot, nên một thẻ "nóng" tạo ra một nút nóng. Thẻ nên gom đúng những gì cần truy cập cùng nhau — một người dùng, một phiên — không nên gom cả một danh mục.

Khi nào cần Cluster

Từ những gì đo được, ba lý do đứng vững:

  • Dữ liệu không vừa một máy. Đây là lý do chính đáng nhất và cũng dễ nhận ra nhất.
  • Thông lượng ghi vượt một luồng. Phần 2 đo được trần một nhân; Cluster là cách duy nhất vượt qua nó với Redis nguyên bản.
  • Cần chịu lỗi tự động mà không muốn chạy Sentinel riêng. Cluster có sẵn phát hiện lỗi và chuyển đổi.

Và lý do không đứng vững: "để mở rộng sau này". Cluster lấy đi lệnh nhiều khoá, Lua qua slot, và giao dịch — ba thứ mà mã của bạn có thể đang dựa vào. Chuyển sang Cluster sau khi đã viết mã dựa vào chúng là viết lại, không phải cấu hình.

Ranh giới thực dụng: một Redis có bản sao phục vụ được rất nhiều. Với 145 GB RAM và 300.000 ops/s trên một nút, phần lớn hệ thống không bao giờ chạm tới ranh giới đó. Đo trước khi chia.

Ba điều nên biết khi vận hành

Sáu nút là tối thiểu thực tế. Ba chính và ba bản sao. Cụm chỉ có nút chính thì mất một nút là mất luôn phần dữ liệu đó, và cụm chuyển sang trạng thái lỗi.

cluster-require-full-coverage mặc định yes: mất một phân mảnh là cả cụm ngừng phục vụ. Đặt no cho phép phần còn lại tiếp tục — hợp với bộ đệm, không hợp với dữ liệu cần đầy đủ.

Chuyển slot là thao tác trực tuyến nhưng không rẻ. redis-cli --cluster reshard di chuyển khoá theo từng lô, và trong lúc đó khách nhận ASK — một dạng chuyển hướng tạm thời. Thư viện phải hiểu cả MOVED lẫn ASK; nhiều thư viện cũ chỉ hiểu cái đầu.

Mẫu số chung: chia mảnh mua dung lượng bằng khả năng diễn đạt

Cái giá thật của Cluster — mất MGET, MULTI, Lua qua slot — là quy luật chung của mọi hệ chia mảnh (sharding/partitioning): chia dữ liệu ra nhiều máy mua thêm dung lượng và thông lượng, nhưng đánh đổi bằng các thao tác xuyên mảnh — join, giao dịch, thao tác nguyên tử nhiều khoá chỉ còn hoạt động trong một mảnh. Và cách duy nhất lấy lại chúng là cùng-định-vị (co-locate) dữ liệu liên quan vào cùng một mảnh bằng cách chọn khoá phân mảnh khéo — một quyết định schema từ đầu, không sửa rẻ về sau.

Trong thế giới Java và hệ luồng, đây đúng là quy tắc co-partition của Kafka: một phép join hai luồng (KStream–KStream join) đòi hai topic phải cùng số partition và cùng khoá phân vùng, để các bản ghi cần ghép nằm cùng một partition; thứ tự cũng chỉ được bảo đảm trong một partition. Muốn ghép được, bạn phải đặt khoá để chúng cùng chỗ — đúng thẻ băm {u1} ở trên.

Trong Go hay bất kỳ ngôn ngữ nào nói chuyện với một kho chia mảnh, client phải tự route theo khoá (đúng việc tự tính CRC16), và mọi thao tác cần nhiều khoá phải bảo đảm chúng cùng mảnh — nếu không, phải kéo về ứng dụng rồi ghép tay, mất tính nguyên tử.

Còn ở cơ sở dữ liệu, đây chính là chuyện của SQL chia mảnh (Citus, Vitess): join xuyên mảnh và giao dịch phân tán hoặc bị cấm hoặc đắt gấp bội, nên Citus có khái niệm distribution column và colocation — đặt các bảng liên quan cùng khoá phân phối để chúng nằm cùng nút và join cục bộ được; DynamoDB/MongoDB cũng sống chết theo việc chọn partition/shard key. Chọn sai khoá phân mảnh là tự khoá mình khỏi các truy vấn quan trọng nhất.

Mẫu số chung: chia mảnh đổi khả năng diễn đạt lấy quy mô — thao tác xuyên mảnh mất đi hoặc hoá đắt, và cách giữ chúng là cùng-định-vị dữ liệu liên quan theo cùng khoá phân mảnh, một quyết định phải làm từ lúc thiết kế tên khoá/lược đồ. Câu hỏi đúng trước khi chia không phải "chia có nhanh hơn không" mà là "những thao tác nào của tôi chạm nhiều thực thể cùng lúc, và tôi đã đặt khoá để chúng rơi vào cùng một mảnh chưa" — vì sau khi mã đã dựa vào join, giao dịch, hay multi-key nguyên tử, chuyển sang chia mảnh là viết lại, không phải cấu hình. Đo trước khi chia.

Muốn tự kiểm cụm có cân bằng không và khách có đang bị chuyển hướng không:

redis-cli --cluster check <mot-nut>:6379
redis-cli -c cluster info | grep -E 'cluster_state|cluster_slots_ok|cluster_known_nodes'

# so so khoa tren tung nut chinh
for n in <ip1> <ip2> <ip3>; do
  echo "$n: $(redis-cli -h $n dbsize)"
done

Số khoá lệch nhau nhiều giữa các nút thường có một nguyên nhân: thẻ băm dùng quá rộng. Một thẻ gom hàng triệu khoá vào một slot làm cả slot đó nằm trên một nút, và không có cách nào chia nhỏ nó ngoài việc đổi tên khoá.

Phần sau: khoá phân tán — đo Redlock và những chỗ nó không bảo đảm điều bạn tưởng.