Bối cảnh & Lý do: Mở rộng quy mô vượt giới hạn Single-Node Redis
Sáu tháng trước, dịch vụ thông báo của chúng tôi bắt đầu bị rớt tin nhắn trong các đợt flash sale. Lượng RAM tiêu thụ trên instance Redis standalone 32 GB đã vượt ngưỡng 28 GB (87%), trong khi độ trễ p99 tăng vọt từ 1,2 ms lên 140 ms trong những đợt bùng nổ tác vụ ghi. Các cụm PostgreSQL và MongoDB vẫn xử lý việc lưu trữ bền vững mà không gặp sự cố, nhưng việc đẩy tới 48.000 tác vụ ghi in-memory mỗi giây vào một tiến trình Redis duy nhất đã chạm tới giới hạn phần cứng.
Redis Sentinel tự động hóa quá trình failover, nhưng mọi lệnh ghi vẫn phải dồn về một node primary duy nhất. Khi bạn vượt quá trần CPU hoặc bộ nhớ của một máy, sharding theo chiều ngang là điều bắt buộc. Redis Cluster giải quyết vấn đề này bằng cách phân vùng keyspace trên 16.384 hash slot cố định. Hệ thống ánh xạ từng key tới một slot chính xác bằng công thức CRC16(key) mod 16384, phân bổ các thao tác đọc và ghi trên nhiều node master độc lập.
Tính sẵn sàng cao (High Availability) đạt được bằng cách ghép nối mỗi master với ít nhất một replica chuyên dụng. Khi một master ngừng phản hồi ping, các master còn lại sẽ phát hiện lỗi heartbeat thông qua cluster bus. Chúng tổ chức một cuộc bầu chọn bằng cơ chế đồng thuận gossip và thăng cấp một replica dự phòng lên mà không cần sự can thiệp thủ công của con người. Mô phỏng kiến trúc sáu node này trong Docker cho phép bạn kiểm thử cơ chế failover, slot migration và client redirect ngay trên máy local trước khi đưa lên các cluster staging.
Cài đặt: Mô hình Cluster dạng Container
Một Redis Cluster có khả năng chịu lỗi cần tối thiểu ba node master. Số lượng lẻ này đảm bảo đạt đa số biểu quyết (quorum) khi xảy ra phân vùng mạng (network partition) và ngăn chặn tình trạng split-brain. Thêm một replica cho mỗi master sẽ nâng tổng số node triển khai lên sáu. Mặc dù các cluster production thường chạy trên các host độc lập, một mạng bridge Docker tùy chỉnh sẽ mang lại khả năng cấp phát IP cục bộ cô lập và dễ dự đoán.
Thiết lập thư mục dự án và chuẩn bị các volume dữ liệu riêng biệt cho cả sáu node:
mkdir -p redis-cluster && cd redis-cluster
mkdir -p data/node-{1..6}
Lưu cấu hình compose sau vào tệp docker-compose.yml:
version: '3.8'
networks:
redis-net:
driver: bridge
ipam:
config:
- subnet: 172.28.0.0/16
services:
redis-node-1:
image: redis:7.2-alpine
container_name: redis-node-1
command: ["redis-server", "--port", "7001", "--cluster-enabled", "yes", "--cluster-config-file", "nodes.conf", "--cluster-node-timeout", "5000", "--appendonly", "yes", "--protected-mode", "no"]
volumes:
- ./data/node-1:/data
networks:
redis-net:
ipv4_address: 172.28.0.11
ports:
- "7001:7001"
- "17001:17001"
redis-node-2:
image: redis:7.2-alpine
container_name: redis-node-2
command: ["redis-server", "--port", "7002", "--cluster-enabled", "yes", "--cluster-config-file", "nodes.conf", "--cluster-node-timeout", "5000", "--appendonly", "yes", "--protected-mode", "no"]
volumes:
- ./data/node-2:/data
networks:
redis-net:
ipv4_address: 172.28.0.12
ports:
- "7002:7002"
- "17002:17002"
redis-node-3:
image: redis:7.2-alpine
container_name: redis-node-3
command: ["redis-server", "--port", "7003", "--cluster-enabled", "yes", "--cluster-config-file", "nodes.conf", "--cluster-node-timeout", "5000", "--appendonly", "yes", "--protected-mode", "no"]
volumes:
- ./data/node-3:/data
networks:
redis-net:
ipv4_address: 172.28.0.13
ports:
- "7003:7003"
- "17003:17003"
redis-node-4:
image: redis:7.2-alpine
container_name: redis-node-4
command: ["redis-server", "--port", "7004", "--cluster-enabled", "yes", "--cluster-config-file", "nodes.conf", "--cluster-node-timeout", "5000", "--appendonly", "yes", "--protected-mode", "no"]
volumes:
- ./data/node-4:/data
networks:
redis-net:
ipv4_address: 172.28.0.14
ports:
- "7004:7004"
- "17004:17004"
redis-node-5:
image: redis:7.2-alpine
container_name: redis-node-5
command: ["redis-server", "--port", "7005", "--cluster-enabled", "yes", "--cluster-config-file", "nodes.conf", "--cluster-node-timeout", "5000", "--appendonly", "yes", "--protected-mode", "no"]
volumes:
- ./data/node-5:/data
networks:
redis-net:
ipv4_address: 172.28.0.15
ports:
- "7005:7005"
- "17005:17005"
redis-node-6:
image: redis:7.2-alpine
container_name: redis-node-6
command: ["redis-server", "--port", "7006", "--cluster-enabled", "yes", "--cluster-config-file", "nodes.conf", "--cluster-node-timeout", "5000", "--appendonly", "yes", "--protected-mode", "no"]
volumes:
- ./data/node-6:/data
networks:
redis-net:
ipv4_address: 172.28.0.16
ports:
- "7006:7006"
- "17006:17006"
Lưu ý phần ánh xạ cổng kép cho mỗi container. Redis lắng nghe traffic ứng dụng trên các cổng 7001-7006, nhưng nó cũng phân bổ các cổng giao tiếp cluster bus trên dải 17001-17006 (cổng client + 10.000). Cluster bus truyền tải các gói tin trao đổi nhị phân qua gossip, ping heartbeat và các gói xác thực failover. Nếu tường lửa bên ngoài hoặc mạng container chặn các cổng cao này, các node của bạn sẽ không thể tạo thành quorum.
Khởi chạy cả sáu container ở chế độ chạy ngầm:
docker compose up -d
Cấu hình: Khởi tạo Slot và Cấu trúc liên kết mạng
Việc khởi động các container sẽ chạy sáu instance Redis riêng lẻ ở chế độ cluster. Tuy nhiên, chúng không tự động phát hiện ra nhau. Bạn phải liên kết chúng vào một cấu trúc liên kết (topology) đang hoạt động và phân bổ các hash slot bằng lệnh redis-cli --cluster create:
docker exec -it redis-node-1 redis-cli --cluster create \
172.28.0.11:7001 172.28.0.12:7002 172.28.0.13:7003 \
172.28.0.14:7004 172.28.0.15:7005 172.28.0.16:7006 \
--cluster-replicas 1
Tham số --cluster-replicas 1 chỉ định ba địa chỉ IP đầu tiên làm master chính và gán một replica cho mỗi master. CLI sẽ tạo một phương án phân bổ slot cân bằng trên toàn bộ 16.384 slot khả dụng:
- Node 1 (172.28.0.11:7001): Slot 0 – 5460 (5.461 slot)
- Node 2 (172.28.0.12:7002): Slot 5461 – 10922 (5.462 slot)
- Node 3 (172.28.0.13:7003): Slot 10923 – 16383 (5.461 slot)
Nhập yes khi được nhắc. Redis sẽ thiết lập epoch, liên kết các replica và trả về [OK] All 16384 slots covered.
Xác thực Mạng hạ tầng và Cấu hình
Xung đột CIDR xảy ra thường xuyên khi bạn quản lý nhiều mạng Docker tùy chỉnh trên máy trạm của lập trình viên. Trước khi gán dải mạng 172.28.0.0/16, hãy xác minh rằng dải này không xung đột với các route VPN của doanh nghiệp hoặc các dải pool bridge mặc định của Docker. Bạn có thể tính toán các dải địa chỉ và subnet mask khả dụng bằng công cụ Subnet Calculator trên ToolCraft. Công cụ này xử lý toàn bộ ở phía client trên trình duyệt của bạn, giúp giữ kín kế hoạch hạ tầng nội bộ khỏi các máy chủ bên thứ ba.
Khi chuyển đổi các định nghĩa Docker Compose sang Kubernetes ConfigMaps hoặc Helm values, hãy đảm bảo không có lỗi cú pháp nào lọt vào các manifest triển khai. Bạn có thể chuyển đổi qua lại giữa các định dạng cấu trúc bằng YAML to JSON Converter hoặc kiểm tra tính hợp lệ của các tham số phức tạp bằng JSON Formatter & Validator.
Xác minh & Giám sát: Định tuyến Slot và Kiểm thử Failover
Xác minh xem cấu trúc liên kết của bạn đã hội tụ chuẩn xác chưa bằng cách truy vấn bất kỳ node nào với redis-cli. Luôn thêm cờ -c để kích hoạt cơ chế cluster routing:
docker exec -it redis-node-1 redis-cli -p 7001 -c cluster nodes
Đầu ra console in ra ID node dạng thập lục phân 40 ký tự, vai trò hiện tại (master hoặc slave), trạng thái ping/pong và dải slot do từng master phục vụ.
Kiểm thử cơ chế tự động chuyển hướng Slot
Các client Redis tiêu chuẩn sẽ ném ra lỗi MOVED khi một lệnh nhắm vào một key được lưu trữ trên một node khác. Thêm -c sẽ yêu cầu client bắt tín hiệu đó và tự động đi theo chuyển hướng. Hãy mở một phiên trên Node 1 và ghi hai key khác nhau:
docker exec -it redis-node-1 redis-cli -p 7001 -c
127.0.0.1:7001> SET user:alpha "session_payload_1"
-> Redirected to slot [14674] located at 172.28.0.13:7003
OK
172.28.0.13:7003> SET user:beta "session_payload_2"
-> Redirected to slot [9382] located at 172.28.0.12:7002
OK
172.28.0.12:7002> GET user:alpha
-> Redirected to slot [14674] located at 172.28.0.13:7003
"session_payload_1"
Quan sát đầu ra của client. Lệnh ghi user:alpha được ánh xạ tới slot 14674 trên Node 3, trong khi user:beta rơi vào slot 9382 trên Node 2. Client nhảy qua lại giữa các node master trong chưa đầy một phần nghìn giây, trừu tượng hóa hoàn toàn cấu trúc phân vùng đối với phía gọi.
Mô phỏng sự cố Master và Node Failover
Tuyên bố về tính sẵn sàng cao chẳng có ý nghĩa gì cho đến khi bạn thử tắt một node primary đang hoạt động. Hãy dừng redis-node-1, node hiện đang giữ các slot từ 0 đến 5.460:
docker stop redis-node-1
Theo dõi quá trình bầu cử trong log của replica được gán cho nó (thường là redis-node-4 hoặc redis-node-5, tùy theo phân bổ lúc tạo cluster):
docker logs -f redis-node-4
Trong vòng 5 giây (khớp với giá trị cluster-node-timeout của chúng ta), replica sẽ phát hiện master bị mất kết nối và tiếp quản dải slot:
* Đánh dấu node 172.28.0.11:7001 gặp sự cố (FAIL).
* Bắt đầu cuộc bầu cử failover cho epoch 2.
* Thắng cuộc bầu cử failover: Tôi là master mới.
* Epoch cấu hình được đặt thành 2.
* Xóa trạng thái lỗi của bản thân và phục vụ các slot 0 - 5460.
Truy vấn trạng thái cluster từ Node 2 còn sống để kiểm tra cấu trúc liên kết mới:
docker exec -it redis-node-2 redis-cli -p 7002 -c cluster nodes
Node 4 giờ đây hiển thị cờ master, phục vụ các slot 0–5460 mà không bị mất dữ liệu. Khởi động lại container đã dừng bằng lệnh docker start redis-node-1. Khi hoạt động trở lại, Node 1 nhận ra rằng Node 4 đã tiếp quản các hash slot của mình và tự động hạ cấp xuống thành replica một cách an toàn.
Kinh nghiệm Production sau 6 tháng vận hành
Việc vận hành một sharded cluster dưới tải cao đã làm sáng tỏ nhiều chi tiết vận hành then chốt:
- Bắt buộc định tuyến Hash Slot cho các lệnh Multi-Key: Các thao tác nhiều key như
MGET, transaction dạng pipeline và script Lua sẽ thất bại với lỗiCROSSSLOTnếu các key mục tiêu nằm trên các master khác nhau. Hãy đảm bảo tính cục bộ của slot bằng cách bọc tiền tố chung trong dấu ngoặc nhọn:{tenant:101}:profilevà{tenant:101}:orderssẽ chỉ băm chuỗi bên trong{...}, giúp gắn kết chúng vào cùng một master. - Cân chỉnh
cluster-node-timeoutthực tế: Giá trị timeout mặc định là 15.000 ms khiến ứng dụng phải chờ tới 15 giây trước khi failover hoàn tất. Chúng tôi đã hạ con số này xuống 5.000 ms cho môi trường staging. Tuy nhiên, tránh đặt dưới 2.000 ms trên production, vì những đợt tạm dừng thu gom rác (garbage collection) thông thường của Java hoặc Node.js có thể gây ra hiện tượng dương tính giả trong tích tắc và kích hoạt các đợt failover không cần thiết. - Sử dụng Driver có hỗ trợ Cluster: Luôn dùng các thư viện có khả năng duy trì slot map trong bộ nhớ, chẳng hạn như Lettuce (Java),
redis-pyvớiRedisCluster(Python) hoặcioredis(Node.js). Các driver không lưu cache cấu trúc liên kết cluster sẽ phải thực hiện thêm một round-trip mạng để xử lýMOVEDtrên từng truy vấn không khớp vị trí, ngay lập tức làm tăng gấp đôi độ trễ round-trip của bạn.

