【Redis从入门到精通】第47篇:Redis Cluster——官方分布式集群方案全解析
上一篇【第46篇】Sentinel选主——Raft算法的精妙应用下一篇【第48篇】哈希槽——Redis Cluster的数据分片机制主从复制Sentinel解决了高可用问题但有一个天花板单机容量。一个Redis实例能存多少数据取决于机器的内存上限。当数据量超过单机容量时就需要把数据分散到多个节点上——这就是Redis Cluster要做的事。一、Redis Cluster的设计目标Redis Cluster从设计之初就明确了三大目标┌────────────────────────────────────────────────────┐ │ Redis Cluster 三大设计目标 │ ├──────────────┬──────────────┬──────────────────────┤ │ 线性扩展 │ 高可用 │ 数据分片 │ │ Scale Out │ High Avail │ Data Sharding │ │ │ │ │ │ 水平扩展节点 │ 自动故障转移 │ 数据自动分布到 │ │ 容量随节点数 │ 无需Sentinel │ 多个节点 │ │ 线性增长 │ 内置选举机制 │ 16384个哈希槽 │ └──────────────┴──────────────┴──────────────────────┘与主从Sentinel方案的对比特性主从SentinelRedis Cluster数据分布所有节点存全量数据数据分散到多节点容量上限单机内存上限随节点数线性扩展高可用依赖Sentinel内置故障转移客户端任意节点读写需要集群感知客户端运维复杂度中等较高那什么时候用主从Sentinel什么时候用Cluster呢一个简单的判断标准数据量能装在一台机器里选主从Sentinel数据量一台机器装不下选Cluster。对于99%的创业公司一台64GB/128GB的Redis足够支撑到C轮所以别过早优化。但当你的key数量超过千万级别或者单机内存快撑满了就是上Cluster的时候了。另外Redis Cluster有个重要的舍与得舍弃多键操作的事务性MULTI/EXEC跨slot操作不保证原子性舍弃Lua脚本的跨节点执行脚本中使用的key必须落在同一个slot获得线性扩展能力加节点就能加容量理论上可以扩展到1000个节点这些限制是由数据分片的架构决定的——一条命令涉及的数据如果分散在多个节点上分布式事务的代价太高了。所以设计key时要考虑亲和性把需要一起操作的key放到同一个slot。二、集群节点的本质Redis Cluster中的每个节点本质上就是一个运行在集群模式下的Redis实例。# 启用集群模式的配置redis.confcluster-enabledyescluster-config-file nodes-6379.conf# 集群配置文件自动生成cluster-node-timeout15000# 节点超时时间毫秒关键点集群模式下每个实例有两个端口客户端端口如6379和集群通信端口客户端端口10000即16379集群通信端口用于节点间的Gossip协议通信节点间不通过Sentinel而是直接通信Node A (6379/16379) Node B (6380/16380) │ │ │ 客户端连接 6379 │ 客户端连接 6380 │ │ │ ── Gossip (16379) ────→16380 ──→ │ ←── Gossip (16380) ←───16379 ←──踩坑提示确保防火墙开放了集群通信端口客户端端口10000。很多人只开放了客户端端口导致节点间无法通信集群无法组建。同时也要开放客户端端口100001集群总线端口虽然目前Redis只用10000端口。三、clusterNode结构体每个集群节点在内存中都对应一个clusterNode结构体structclusterNode{// 基本标识mstime_tctime;/* 节点创建时间 */charname[CLUSTER_NAMELEN];/* 节点名称40位十六进制 */intflags;/* 节点状态标志MASTER/SLAVE/PFAIL/FAIL等 */uint64_tconfigEpoch;/* 配置纪元 */char*slot_info_str;/* 槽位信息字符串 */// 地址信息charip[NET_IP_STR_LEN];/* IP地址 */intport;/* 客户端端口 */intcport;/* 集群通信端口 */// 连接和状态link*link;/* 与该节点的连接 */mstime_tping_sent;/* 上次发送PING的时间 */mstime_tpong_received;/* 上次收到PONG的时间 */mstime_tfail_time;/* 被标记FAIL的时间 */// 槽位相关unsignedcharslots[CLUSTER_SLOTS/8];/* 槽位位图该节点负责哪些槽 */intnumslots;/* 负责的槽位数 */intnumslaves;/* 从节点数量 */// 主从关系clusterNode*slaveof;/* 如果是从节点指向主节点 */list*slaves;/* 如果是主节点从节点列表 */// 故障检测list*fail_reports;/* 其他节点报告该节点故障的列表 */};// 关键标志位说明// CLUSTER_NODE_MASTER 1 // 主节点// CLUSTER_NODE_SLAVE 2 // 从节点// CLUSTER_NODE_PFAIL 4 // 可能故障主观// CLUSTER_NODE_FAIL 8 // 确认故障客观// CLUSTER_NODE_MYSELF 16 // 当前节点自身// CLUSTER_NODE_HANDSHAKE 32 // 正在握手未完成// CLUSTER_NODE_NOADDR 64 // 地址未知这几个标志位构成了节点状态机。一个节点的生命周期是这样的通过MEET进入HANDSHAKE状态 → 握手完成后清除HANDSHAKE标志 → 如果心跳超时进入PFAIL → 如果超过半数主节点也认为它PFAIL进入FAIL → 如果是从节点且有足够的从库晋升票数可以晋升为MASTER。slots字段是一个位图unsigned char [CLUSTER_SLOTS/8]即2048字节用于表示该节点负责哪些槽。检查一个槽是否属于某节点的操作就是简单查位图——O(1)时间。## 四、clusterState结构体 每个节点还维护一个全局的 clusterState记录整个集群的视角 c typedef struct clusterState { clusterNode *myself; /* 当前节点自身 */ uint64_t currentEpoch; /* 当前集群纪元 */ int state; /* 集群状态CLUSTER_OK/CLUSTER_FAIL */ int size; /* 至少负责一个槽的主节点数 */ dict *nodes; /* 所有节点的字典name→clusterNode */ dict *nodes_black_list; /* 黑名单节点 */ /* 槽位映射 */ clusterNode *slots[CLUSTER_SLOTS]; /* 槽→节点映射表16384个元素*/ clusterNode *migrating_slots_to[CLUSTER_SLOTS]; /* 正在迁出的槽 */ clusterNode *importing_slots_from[CLUSTER_SLOTS]; /* 正在迁入的槽 */ /* 故障转移 */ mstime_t failover_auth_time; /* 下次选举时间 */ int failover_auth_count; /* 收到的票数 */ int failover_auth_sent; /* 是否已发起选举 */ int failover_auth_rank; /* 从库排名 */ } clusterState;五、CLUSTER MEET命令让两个节点握手要让两个节点加入同一个集群需要在其中一个节点上执行CLUSTER MEET# 在节点A上执行让A和B握手redis-cli-p6379CLUSTER MEET192.168.1.1026380握手过程Node A (6379) Node B (6380) │ │ │ ──── MEET ────────────────→ │ │ │ │ ←─── PONG ───────────────── │ │ │ │ ──── PING ────────────────→ │ │ │ │ ←─── PONG ───────────────── │ │ │ │ 握手完成A和B互知对方存在 │握手完成后两个节点会通过Gossip协议逐渐感知到集群中的其他节点。踩坑提示不需要对所有节点对都执行CLUSTER MEET。只需要让每个节点与集群中任意一个节点握手Gossip协议会自动传播节点信息。通常的做法是选一个种子节点让其他所有节点与它握手。六、Gossip协议集群信息的传播者Gossip协议是Redis Cluster节点间通信的核心机制。它的思想很简单每个节点定期把自己的信息告诉随机几个邻居邻居再告诉它的邻居最终所有节点都知道了全集群的信息。┌─────────────────────────────────────────────┐ │ Gossip 协议工作原理 │ │ │ │ A ──→ B 每隔一段时间 │ │ ↕ ╲ ╱ ↕ 每个节点随机选择 │ │ C ──→ D 几个邻居发送消息 │ │ ↕ ╲↕ │ │ E ──→ F 信息像病毒一样扩散 │ │ │ │ 最终所有节点都拥有完整的集群视图 │ └─────────────────────────────────────────────┘集群消息类型消息类型说明MEET加入集群请求握手PING心跳检测 信息交换PONG回复MEET/PINGPUBLISH集群内的发布/订阅FAIL标记某个节点为FAILUPDATE通知其他节点更新槽位映射Gossip消息的数据包结构┌──────────────────────────────────────┐ │ Gossip 消息头 │ ├──────────────────────────────────────┤ │ type: MEET/PING/PONG/... │ │ sender: 当前节点的信息 │ │ - name (40位ID) │ │ - epoch │ │ - flags (MASTER/SLAVE/...) │ │ - 负责的槽位 │ │ - IP和端口 │ ├──────────────────────────────────────┤ │ Gossip 消息体 │ ├──────────────────────────────────────┤ │ 随机选择的几个其他节点的摘要信息 │ │ - node1: name, IP, port, epoch │ │ - node2: name, IP, port, epoch │ │ - node3: ... │ └──────────────────────────────────────┘每秒每个节点会向随机几个节点发送PING消息消息中附带其他节点的摘要。收到PING后回复PONG并更新自己对集群的认知。Gossip的发送时机与反熵机制Gossip的核心是反熵Anti-Entropy——每个节点通过定期和邻居交换信息来消除信息差。具体规则每隔 cluster-node-timeout/2 毫秒 ┌──────────────────────────────────┐ │ 从所有已知节点中随机选取 N/21 个 │ │ 优先选择 │ │ 1. 上次通信时间最久的节点 │ │ 2. PFAIL状态的节点 │ │ 3. 随机节点包含已通信过的 │ └──────────────────────────────────┘默认cluster-node-timeout为15000ms所以每个节点大约每7.5秒向五六個节点发送一次PING。注意PING消息不仅用于心跳检测还携带了槽位分配信息和随机选取的其他节点的摘要是一石二鸟的设计。收到PING消息后接收方会做三件事更新发送方信息更新该节点的IP、端口、epoch、槽位分配处理消息中附带的节点摘要如果发现了自己不知道的节点标记为HANDSHAKE状态并主动联系回复PONG把自己的信息和随机选取的邻居摘要带回去这就是为什么只需要CLUSTER MEET连接一个种子节点整个集群的节点就能互相感知——信息通过Gossip自动扩散。七、CLUSTER INFO命令输出解读127.0.0.1:6379CLUSTER INFO cluster_state:ok# 集群状态ok表示所有槽都有节点负责cluster_slots_assigned:16384# 已分配的槽位数cluster_slots_ok:16384# 正常的槽位数cluster_slots_pfail:0# 可能故障的槽位数cluster_slots_fail:0# 故障的槽位数cluster_known_nodes:6# 已知节点数cluster_size:3# 主节点数cluster_current_epoch:6# 当前集群纪元cluster_my_epoch:1# 当前节点的纪元cluster_stats_messages_sent:12345# 发送的消息数cluster_stats_messages_received:12340# 接收的消息数八、CLUSTER NODES命令127.0.0.1:6379CLUSTER NODES a1b2c3...192.168.1.101:637916379 myself,master -016400000001connected0-5460 d4e5f6...192.168.1.102:638016380 master -016400000012connected5461-10922 g7h8i9...192.168.1.103:638116381 master -016400000023connected10923-16383 j0k1l2...192.168.1.104:638216382 slave a1b2c3...016400000031connected m3n4o5...192.168.1.105:638316383 slave d4e5f6...016400000042connected p6q7r8...192.168.1.106:638416384 slave g7h8i9...016400000053connected输出格式解析node-id ip:portcport flags master-id ping-sent pong-recv epoch link-state slots 字段说明: node-id: 节点ID40位十六进制 ip:portcport: 客户端端口集群通信端口 flags: myself,master,slave,pfail,fail,handshake,noaddr,noflags master-id: 如果是从节点指向主节点ID主节点为- slots: 负责的槽位范围九、集群最小部署要求3主3从Redis Cluster要求至少3个主节点为什么故障转移需要多数投票节点标记FAIL需要超过半数主节点同意类似Sentinel的ODOWN3个主节点是最小多数派2个主节点中1个挂了只剩1个无法形成多数派每个主节点至少1个从节点主节点挂了从节点可以晋升最小集群拓扑3主3从: ┌──────────┐ ┌──────────┐ ┌──────────┐ │ Master A │ │ Master B │ │ Master C │ │ 0-5460 │ │ 5461-10922│ │ 10923-16383│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │ │ ┌────┴─────┐ ┌────┴─────┐ ┌────┴─────┐ │ Slave A │ │ Slave B │ │ Slave C │ └──────────┘ └──────────┘ └──────────┘踩坑提示生产环境建议至少3主3从共6个节点。如果预算有限可以使用3个节点各运行2个Redis实例一个主一个从但要确保主从不在同一台机器上。十、集群模式下的客户端redis-cli -c 模式# 普通模式不自动重定向redis-cli-p6379GET key# 如果key不在当前节点返回 (error) MOVED 3999 192.168.1.102:6380# 集群模式自动重定向redis-cli-c-p6379GET key# 自动重定向到正确的节点并返回结果智能客户端主流客户端库都支持集群模式它们会缓存槽位映射关系// Jedis 集群模式SetHostAndPortnodesnewHashSet();nodes.add(newHostAndPort(192.168.1.101,6379));nodes.add(newHostAndPort(192.168.1.102,6380));nodes.add(newHostAndPort(192.168.1.103,6381));JedisClusterjedisClusternewJedisCluster(nodes);jedisCluster.set(key,value);// 自动路由到正确节点Docker/K8s中部署的注意事项┌──────────────────────────────────────────────────┐ │ Docker/K8s 部署 Redis Cluster 注意事项 │ ├──────────────────────────────────────────────────┤ │ │ │ 1. 使用 host 网络模式 │ │ 容器内看到的IP和外部访问的IP一致 │ │ 否则MEET握手可能失败 │ │ │ │ 2. 或使用 cluster-announce-ip │ │ 告诉其他节点用哪个IP连接自己 │ │ CONFIG SET cluster-announce-ip 192.168.1.101 │ │ │ │ 3. 或使用 cluster-announce-port / bus-port │ │ 指定对外暴露的端口 │ │ cluster-announce-port 6379 │ │ cluster-announce-bus-port 16379 │ │ │ │ 4. K8s中避免用ClusterIP │ │ 使用 Headless Service StatefulSet │ │ 每个Pod有稳定的网络标识 │ │ │ └──────────────────────────────────────────────────┘踩坑提示Docker的NAT网络是Redis Cluster的天敌。容器内部的IP如172.17.0.2和宿主机IP不同节点间通信会失败。必须使用--nethost或配置cluster-announce-ip。一个可用的 Docker 部署流程多机部署时建议使用network_mode: host模式。以下是一个6节点集群的典型部署流程# 每台机器上启动Redis假设6台机器端口均为6379redis-server--port6379--cluster-enabledyes\--cluster-config-file nodes-6379.conf\--cluster-node-timeout5000\--cluster-announce-ip本机IP\--cluster-announce-port6379\--cluster-announce-bus-port16379\--appendonlyyes--daemonizeyes# 在任意一台机器上通过redis-cli创建集群redis-cli--clustercreate\192.168.1.101:6379\192.168.1.102:6379\192.168.1.103:6379\192.168.1.104:6379\192.168.1.105:6379\192.168.1.106:6379\--cluster-replicas1--cluster-replicas 1告诉工具这6个节点自动分配成3主3从。前3个成为主节点后3个成为对应主节点的从节点。十一、CLUSTER RESET节点的格式化按钮最后介绍一个收拾残局的神器——CLUSTER RESET# 软重置保留当前纪元信息清空集群拓扑CLUSTER RESET# 硬重置完全清空连纪元一起重置CLUSTER RESET HARD使用场景集群配置出现混乱节点信息不一致想把一个节点从旧集群迁移到新集群节点之间的握手卡住了比如IP地址变了先 RESET 再 MEET等于重新让节点单身后再相亲。踩坑提示CLUSTER RESET不会删除数据如果节点上已有数据这些数据仍然存在。需要手动执行FLUSHALL或FLUSHDB清理否则旧数据与新的槽位分配不匹配会导致数据访问异常——你的key明明在节点上但客户端根据新的槽位映射去了别的节点找。总结Redis Cluster通过Gossip协议实现节点间的自动发现和信息传播通过CLUSTER MEET命令加入集群通过16384个哈希槽实现数据分片。3主3从是最小部署要求客户端需要支持集群模式才能正确路由请求。在容器化部署时务必注意网络配置确保节点间能正常通信。下一篇我们将深入Redis Cluster最核心的数据分片机制——16384个哈希槽的设计哲学。上一篇【第46篇】Sentinel选主——Raft算法的精妙应用下一篇【第48篇】哈希槽——Redis Cluster的数据分片机制