[Series ioredis - Bài 4] Kiến Trúc Phân Tán: Sống Sót Qua Giông Bão Với Redis Sentinel & Cluster
Chào các bạn! Trong 3 bài viết trước, chúng ta đã code và thao tác trên một server Redis đơn lẻ (Standalone). Nhưng thực tế phũ phàng là: Server nào rồi cũng có lúc "đột quỵ" do lỗi phần cứng, đứt cáp, hoặc tràn RAM.
Thử tưởng tượng bạn đang vận hành một hệ thống giao dịch cực kỳ nghiêm ngặt, nơi mà mọi dữ liệu phiên bản (session) hay bộ đệm (cache) đều nằm trên một con Redis duy nhất. Nếu con Redis đó "tắt thở", toàn bộ hệ thống sẽ tê liệt (Single Point of Failure).
Để giải quyết bài toán tính sẵn sàng cao (High Availability - HA) và mở rộng (Scaling), Redis cung cấp hai giải pháp: Sentinel và Cluster. Điểm tuyệt vời nhất là ioredis sinh ra để làm việc với các kiến trúc này một cách mượt mà đến khó tin.
1. Redis Sentinel: Vệ sĩ tự động chuyển đổi dự phòng (Failover)
Cơ chế hoạt động: Trong mô hình này, chúng ta có một cụm gồm 1 Master (để Đọc/Ghi) và nhiều Slaves/Replicas (chỉ để Đọc, đồng bộ dữ liệu từ Master). Cùng với đó là các tiến trình Sentinel đóng vai trò "người canh gác". Nếu Sentinel phát hiện Master bị chết, chúng sẽ bầu chọn một Slave lên làm Master mới.
ioredis xử lý Sentinel như thế nào? Thay vì kết nối trực tiếp vào Master, ioredis sẽ kết nối với các Sentinel. Sentinel sẽ báo cho ứng dụng Node.js biết ai đang là Master hiện tại. Nếu Failover xảy ra, ioredis tự động nhận cấu hình mới và kết nối lại hoàn toàn trong suốt (transparent) với người viết code.
const Redis = require("ioredis");
// Khởi tạo kết nối thông qua Sentinel
const redis = new Redis({
sentinels: [
{ host: "10.0.0.1", port: 26379 },
{ host: "10.0.0.2", port: 26379 },
{ host: "10.0.0.3", port: 26379 },
],
name: "mymaster", // Tên của cụm Master đã được config trong Sentinel
// Các option bổ sung
sentinelPassword: "super-secret-password",
});
redis.on("ready", () => {
console.log("Đã kết nối thành công tới Master thông qua Sentinel!");
});
async function runHA() {
// Bạn vẫn dùng các lệnh bình thường, ioredis tự lo việc routing
await redis.set("system:status", "stable");
}
2. Redis Cluster: Chia để trị (Sharding & Failover)
Sentinel giải quyết được tính sẵn sàng, nhưng không giải quyết được vấn đề "nghẽn cổ chai" về dung lượng. Nếu bạn có 100GB dữ liệu, bạn cần một con RAM 100GB.
Redis Cluster mang đến khái niệm Sharding (phân mảnh dữ liệu). Thay vì nhồi tất cả vào 1 Master, Cluster chia nhỏ dữ liệu ra và lưu rải rác trên nhiều Master Node khác nhau (ví dụ 3 Master, mỗi con chứa ~33GB). Cấu trúc này rất quen thuộc nếu bạn đã từng tìm hiểu về kiến trúc chia cắt dữ liệu của các hệ quản trị cơ sở dữ liệu phân tán.
Khi dùng ioredis với Cluster, thư viện sẽ tự động tính toán (dựa trên thuật toán CRC16) xem cái key bạn đang cần nó nằm ở Node nào để gửi request thẳng đến đó.
const Redis = require("ioredis");
// Khởi tạo một Cluster Client
const cluster = new Redis.Cluster([
{ host: "127.0.0.1", port: 7000 },
{ host: "127.0.0.1", port: 7001 },
{ host: "127.0.0.1", port: 7002 },
]);
// Thao tác y hệt như bản Standalone
async function testCluster() {
await cluster.set("user:101", "Alice");
const val = await cluster.get("user:101");
console.log("Giá trị lấy từ Cluster:", val);
}
3. Cạm bẫy cực kỳ nguy hiểm khi dùng Cluster: Phân mảnh Key
Khi chuyển từ Standalone sang Cluster, nhiều đoạn code dùng Pipeline hoặc thao tác nhiều key cùng lúc (như MGET, MSET) sẽ bị văng lỗi rầm rầm: CROSSSLOT Keys in request don't hash to the same slot.
Lý do: Trong Cluster, các key khác nhau có thể nằm ở các Node khác nhau. Redis không cho phép bạn dùng một lệnh gom để thao tác trên nhiều Node cùng lúc.
Cách giải quyết: Hash Tags {} Nếu bạn muốn nhóm các key lại để chúng chắc chắn rơi vào cùng một Node, hãy bọc phần định danh chung bằng ngoặc nhọn {}. ioredis (và Redis) sẽ chỉ dùng phần trong ngoặc nhọn để tính toán vị trí.
// Bị lỗi vì 2 key này có thể nằm ở 2 Node khác nhau
// await cluster.mset("station:1:status", "ok", "station:2:status", "error");
// Chạy thành công vì cả 2 key đều có Hash Tag {station}
// Chúng sẽ được phân phối vào cùng một mảnh (slot)
await cluster.mset("{station}:1:status", "ok", "{station}:2:status", "error");
Tổng kết Bài 4
Tính sẵn sàng cao là yêu cầu bắt buộc của mọi hệ thống Backend Production. Qua bài này, hy vọng các bạn đã thấy được sức mạnh của ioredis trong việc trừu tượng hóa (abstract) sự phức tạp của hạ tầng mạng. Dù bạn đang cắm vào một cụm Sentinel hay một Cluster hàng chục Node, code của bạn ở tầng Application gần như không phải thay đổi quá nhiều.
Ở bài tiếp theo - cũng có thể là bài cuối cùng của series, chúng ta sẽ tìm hiểu về một thứ vũ khí tối thượng để giải quyết các bài toán "Race Condition" (cạnh tranh dữ liệu) khi nhiều người cùng thao tác một lúc: Distributed Lock và Lua Scripts trong ioredis.
Hẹn gặp lại các bạn!
All rights reserved