
1. 先搞清楚单机扛不住的到底是什么很多人第一次接触 Redis Cluster动机都很朴素——数据量涨了单机内存装不下或者 QPS 涨了一台机器的 CPU 和网卡顶不住了。这个判断没错但不够细。真正决定你要不要上 Cluster 的是三件事里最先撞墙的那一件内存容量、写入吞吐、单线程阻塞。这三件事的解法完全不同搞混了就会花大价钱买来一堆新麻烦。先说内存。Redis 是纯内存数据库一台 64GB 的机器扣除主从复制 buffer、AOF rewrite 时的 fork 写时复制开销、客户端输出缓冲区实际能安全存放的数据量大概只有 60%~70%差不多 40GB 上下。如果你的业务数据三年后预估到 200GB那么你至少需要 5~6 个分片这还没算副本。这个账要先算清楚因为 Cluster 的分片数量在你建集群那天就基本定型了后面每次扩容都是一次带风险的运维动作。再说吞吐。Redis 单线程处理命令一台中等配置的机器跑 8~10 万 QPS 是比较常见的水位再往上就要靠多核绑定多实例或者分片。这里有一个特别容易被忽略的点瓶颈往往不在 Redis 本身而在网卡和客户端连接数。如果每个请求平均 1KB10 万 QPS 就是 100MB/s千兆网卡早就打满了。所以我排查性能问题时的第一件事不是看 Redis 的 INFO而是看sar -n DEV 1里网卡有没有跑满。最后说阻塞。单线程模型下一个大 key 的DEL、一次全量的KEYS *、一个耗时的 Lua 脚本都会把整个实例卡住。这种问题不是靠分片能根治的分片只是把「卡住全部」变成了「卡住一部分」。所以我的态度一直很明确Cluster 解决的是容量和吞吐的横向扩展问题不解决单条慢命令问题。这两类问题要用两套办法。还有一个绕不开的现实从主从复制到哨兵再到 Cluster其实是三种不同的可用性等级。主从只是数据冗余主挂了要人工切哨兵解决了自动切换但数据仍然在一台机器上容量没变Cluster 才同时解决容量和可用性代价是客户端复杂度上升、跨槽操作受限、运维成本变高。我在做技术选型时有个简单判断标准数据量小于单机安全水位、且能接受 30 秒级的故障切换窗口就别碰 Cluster。哨兵足够用运维成本低一个量级。只有当单机放不下或者写吞吐确实顶到天花板时才值得付出 Cluster 的复杂度。提醒一句不要因为「集群听起来更高级」就上 Cluster。我见过不止一个项目数据总共才 8GB却搭了 6 个节点结果每次扩容、每次故障排查都要多花几个小时收益几乎为零。2. 哈希槽Cluster 数据分片的底座设计2.1 为什么是 16384 个槽而不是一致性哈希Redis Cluster 没有用一致性哈希而是用了固定 16384 个哈希槽hash slot的方案。计算方式很直接对 key 做 CRC16 校验然后对 16384 取模得到 0~16383 之间的一个数字这个数字归哪个节点管key 就存在哪个节点。$$\text{slot} \text{CRC16}(key) \bmod 16384$$这个设计的好处在于「映射关系是显式的、可枚举的」。一致性哈希的好处是增删节点时迁移量小但它的虚拟节点环维护复杂而且节点之间的数据分布容易不均匀。哈希槽把「数据位置」变成了一张 16384 项的映射表这张表每个节点都完整持有客户端也可以缓存。扩容时你迁移的是「槽」而不是「key」粒度可控进度可观测出问题能回滚。那 16384 这个数字是怎么来的这是社区里被问得最多的问题之一。核心原因是集群总线的心跳包要携带槽位归属信息。每个节点在 gossip 心跳里用 bitmap 表示自己负责哪些槽16384 个槽就是 16384 bit也就是 2KB。如果改成 65536 个槽这个 bitmap 就变成 8KB心跳包体积翻四倍在几百个节点的大集群里光心跳就能吃掉可观的带宽。另一方面16384 个槽配到几十上百个节点上每个节点也还能分到几百个槽粒度足够细分布足够均匀。所以这是一个「带宽成本」和「分布粒度」之间的折中值不是随便定的。集群规模的上限也来自这里。官方建议 Cluster 的节点数不要超过 1000实践中我更保守主节点控制在 3~30 个之间。原因很实在gossip 协议是 O(N) 的消息扩散节点越多每个节点每秒处理的 ping/pong 消息越多CPU 和带宽占用呈平方级增长。我见过一个跑到 60 多个主节点的集群CLUSTER INFO里cluster_stats_messages_received每秒上万节点本身的业务 QPS 反而不高等于把一半资源喂给了集群自身的通信。2.2 槽的三种状态与数据分布的可控性集群里每个槽只有三种归属状态被某个节点负责、正在迁移中、无人负责。CLUSTER INFO里的cluster_slots_assigned就是已分配槽的数量cluster_slots_ok是正常服务的数量cluster_slots_pfail和cluster_slots_fail是需要重点盯的指标。只要cluster_slots_assigned不等于 16384集群状态就是fail客户端会直接报CLUSTERDOWN Hash slot not served。这是新手最容易踩的第一个坑集群建起来了节点都在但槽没分完。手动控制槽的分配是理解 Cluster 的关键。下面这些命令我在排查和演练时用得最多# 查看集群整体状态与槽分配情况 redis-cli -p 7000 cluster info redis-cli -p 7000 cluster nodes # 查看某个 key 落在哪个槽 redis-cli -p 7000 cluster keyslot user:1001 # 手动把槽指派给节点node-id 从 cluster nodes 里取 redis-cli -p 7000 cluster addslots 0 1 2 3 redis-cli -p 7000 cluster delslots 0 1 2 3 # 让集群自动平衡慎用会在业务高峰期产生迁移流量 redis-cli --cluster rebalance 127.0.0.1:7000 --cluster-use-empty-masterscluster nodes的输出信息量很大值得逐字段拆开看。一行大概是这个格式node-id ip:portcport flags master-id ping-sent pong-recv config-epoch link-state slot slot ...flags里的组合最能反映问题myself,master表示当前节点自己是主节点fail?是 pfail表示疑似下线还需要其他节点投票确认fail是确认下线handshake表示正在握手还没加入集群noaddr表示地址未知。我排查时会先扫一遍有没有fail?和fail再看link-state是不是connected最后核对槽区间有没有重叠或缺漏。槽的迁移过程有一个细节必须理解迁移中的槽旧节点和新节点会同时「认领」。旧节点上还有没迁走的 key新节点上已经有迁过来的 key客户端访问时就会收到ASK重定向。这个机制保证了迁移过程中的可用性但也意味着迁移不是原子操作中途中断会留下一个「半迁移」的槽需要人工介入清理。这一点我在第 6 节会详细展开。3. 手把手搭一套三主三从集群3.1 配置文件里必须写对的几行我用单机多实例的方式演示端口 7000~7005实际生产一般是 6 台独立机器或者 3 台每台两个实例。配置文件里跟集群相关的参数就那么几个但每一个写错都能让你卡半天。# redis-7000.conf 的关键配置 port 7000 daemonize yes pidfile /var/run/redis-7000.pid logfile /var/log/redis/redis-7000.log dir /data/redis/7000 # 集群三件套缺一不可 cluster-enabled yes cluster-config-file nodes-7000.conf cluster-node-timeout 15000 # 网络与地址通告多网卡环境下必配 bind 0.0.0.0 protected-mode no cluster-announce-ip 192.168.10.11 cluster-announce-port 7000 cluster-announce-bus-port 17000 # 持久化按业务需要但 AOF 建议开 appendonly yes appendfsync everysec这里有几个我踩过的坑逐个说。cluster-config-file是节点自己维护的集群状态文件绝对不能手工编辑也绝对不能被多个实例共用。它记录了这个节点视角下的整个集群拓扑、槽归属、epoch 编号。如果两个实例配了同一个文件路径或者这个文件被误删节点重启后会带着过期的拓扑信息回来轻则被集群踢出重则引发槽冲突。我的习惯是把它放在dir目录下文件名带上端口号并且和 RDB/AOF 放在一起统一备份。cluster-node-timeout默认 15000 毫秒很多人出于「快速发现故障」的心理改小到 3000 或 5000这是很危险的做法。这个值同时决定了三件事节点判定下线的等待时间、故障转移的触发时机、网络抖动时的容错窗口。设得太小一次 GC 停顿或者网络瞬断就能让集群误判主节点挂了触发无谓的切换设得太大主节点真挂了要等 15 秒才开始切业务那边早就超时了。我的经验值是同机房 10000~15000跨可用区 20000~30000具体要结合你的网络 P99 延迟来定原则是「远大于正常网络抖动小于业务可接受的中断时长」。cluster-announce-ip这些通告参数在 Docker、NAT、多网卡环境下是救命配置。节点之间是通过CLUSTER MEET交换地址的如果它通告的是容器内的 172.x 地址别的节点就连不上集群会一直卡在handshake状态。我遇到过最折腾的一次是一台机器上有内网和专线两张网卡节点互相通告了错误的地址cluster nodes里一堆noaddr最后就是靠这几个参数钉死的。3.2 建集群、验集群、模拟一次故障六个实例都启动之后用redis-cli --cluster create一条命令建起来。--cluster-replicas 1表示每个主节点配一个从节点总共 6 个节点就是 3 主 3 从。redis-cli --cluster create \ 192.168.10.11:7000 192.168.10.11:7001 192.168.10.11:7002 \ 192.168.10.11:7003 192.168.10.11:7004 192.168.10.11:7005 \ --cluster-replicas 1执行过程中会打印出槽的分配方案3 个主节点分别是 0-5460、5461-10922、10923-16383。这里 16384 除以 3 是 5461.33不能整除所以前两个主各拿 5461 个槽最后一个拿 5462 个差一个槽完全不影响。注意这个 create 命令会先做一次集群检查如果某个节点上已有数据或者cluster-config-file已经存在旧拓扑它会直接拒绝。演练时经常忘了清dir目录导致一直报Node xxx is not empty。建完之后第一件事不是急着压测是验证。# 1. 看整体状态 redis-cli -p 7000 cluster info | grep -E cluster_state|cluster_slots_assigned|cluster_known_nodes|cluster_size # 期望输出 # cluster_state:ok # cluster_slots_assigned:16384 # cluster_known_nodes:6 # cluster_size:3 # 2. 看拓扑确认每个主都有对应的从 redis-cli -p 7000 cluster nodes # 3. 写几个 key 验证路由 redis-cli -c -p 7000 set user:1001 hello redis-cli -c -p 7000 get user:1001注意-c这个参数它代表 follow redirects客户端会自动跟随 MOVED 跳转。不加-c的话一旦 key 不在 7000 这个节点上你会收到(error) MOVED 12345 192.168.10.11:7001。很多人第一次看到这个报错以为是集群坏了其实这是 Cluster 的正常工作方式。然后是节点的下线演练这一步一定要在预发环境做过。手动把某个主节点 kill 掉观察集群多久完成切换# 杀掉一个主节点 redis-cli -p 7001 shutdown nosave # 立刻在另一个节点上反复观察 watch -n 1 redis-cli -p 7000 cluster nodes | grep -E fail|master # 关键日志能看到完整的选举与晋升过程 tail -f /var/log/redis/redis-7002.log | grep -i failover\|election\|promoted正常情况下cluster-node-timeout到期后从节点会发起选举拿到多数主节点的投票后晋升为新主接管原主的槽全程大概 10~20 秒。这个过程中的写请求会失败客户端会收到CLUSTERDOWN或者连接错误。这个中断窗口是 Cluster 固有的任何声称「零中断」的说法都是不准确的。如果真的要做到业务无感需要在客户端侧做重试和降级这部分放到第 4 节讲。演练完记得把挂掉的节点拉起来它会自动以从节点身份重新加入并且开始同步新主的数据。如果发现它没有自动加入检查cluster-config-file里记录的旧拓扑是不是和当前集群冲突了必要时删掉这个文件让它重新握手。4. 客户端路由MOVED、ASK 和 smart client 的门道4.1 两个重定向含义完全不同Cluster 模式下客户端有两条路可走。一条是「笨客户端」随便连一个节点收到重定向再跳转redis-cli -c就是这么干的。另一条是「smart client」启动时执行CLUSTER SLOTS新版用CLUSTER SHARDS把槽位映射全量拉到本地缓存之后请求直接发到正确的节点只有映射变更时才刷新缓存。生产环境毫无疑问要用后者Jedis、Lettuce、redis-py 的 cluster 模式都支持。但即使是 smart client也一定会遇到重定向因为映射缓存总会过期。这里有两个错误码含义完全不同混淆了就会写出错误的客户端逻辑。错误码出现场景客户端应该怎么做是否更新槽缓存MOVED slot ip:port槽的归属已经永久变更比如故障转移、扩容完成重发请求到新地址是ASK slot ip:port槽正在迁移中这个 key 恰好在目标节点上先发ASKING再重发请求否只对本次请求生效CLUSTERDOWN集群状态为 fail槽没分配完或多数主不可达直接失败或按策略重试不适用CROSSSLOT一次操作涉及的多个 key 不在同一个槽改造 key 设计或用 hash tag不适用TRYAGAIN多 key 操作涉及迁移中的槽部分 key 在旧节点短暂等待后重试否ASKING这个命令的作用是告诉目标节点「我知道这个槽还不归你但这个 key 已经迁过来了这次请求你处理一下」。它是一次性的只对紧随其后的那一条命令生效。为什么需要这个机制因为迁移中的槽如果同时被两个节点接受写入会破坏数据一致性。ASKING相当于一次临时的授权。这里有个实操细节如果你手动用redis-cli测试迁移中的槽必须连着发两条命令而且ASKING之后不能有任何其他命令插进来否则又得重新ASKING。我一般直接用-c参数客户端会自动处理。4.2 客户端必须做好的三件事第一刷新策略要正确。Lettuce 默认在收到 MOVED 时会重连并刷新拓扑Jedis 3.x 之后也支持自动刷新。但有些老版本客户端只在启动时拉一次映射主从切换后就一直报错必须重启应用才能恢复。这个问题我遇到过一次现象是故障转移后部分请求持续命中旧主日志里全是READONLY You cant write against a read only replica最后是靠升级客户端版本加显式配置拓扑刷新周期解决的。上线前一定要用CLUSTER FAILOVER手动切一次验证客户端能不能自动恢复。第二超时要分层设置。连接超时、读超时、重试次数、重试间隔这四个参数要一起考虑。集群故障转移的窗口是十几秒如果读超时设成 2 秒、重试 3 次业务侧大概 6 秒后就彻底失败了。合理做法是让重试覆盖住转移窗口比如读超时 1000ms、最多重试 15 次、指数退避同时在业务层做一个「读取降级到本地缓存」的兜底。第三multi-key 操作必须改造。Cluster 下不存在「一个命令原子地操作多个节点」这种事。以下这些操作全部受限MGET、MSET涉及多个 key必须要求同槽否则客户端直接抛CROSSSLOTMULTI/EXEC事务的所有 key 必须同槽Lua 脚本里用到的所有 key 必须同槽而且必须通过KEYS[1]、KEYS[2]声明不能自己在脚本里拼 key 名SUNIONSTORE、ZUNIONSTORE这类跨集合操作要求源和目标都同槽KEYS *、SCAN、FLUSHALL只能作用于单个节点要遍历整个集群得自己循环所有主节点真正能用的技巧是 hash tag。用花括号把 key 的一部分包起来CRC16 只计算花括号里的内容就能把一组 key 强制打到同一个槽# 这三个 key 都会落到同一个槽因为只对 {order:8891} 做 CRC16 order:{order:8891}:info order:{order:8891}:items order:{order:8891}:amount # 验证一下 redis-cli -p 7000 cluster keyslot order:{order:8891}:info redis-cli -p 7000 cluster keyslot order:{order:8891}:itemshash tag 用得爽但要克制。如果某个业务对象的所有 key 都打上同一个 tag你就等于在集群里造了一个「单点热点」这个槽所在节点的内存和 QPS 会远超其他节点集群的意义就没了。我的原则是只有确实需要原子操作的 key 组才用 tag而且 tag 的基数要足够大比如按订单号、按用户 ID 分保证槽的分布依然均匀。5. 故障转移集群自己怎么选新主5.1 从 pfail 到 failover 的完整链路理解故障转移才能理解为什么有时候「明明机器还活着集群却把它踢了」。整个过程分四步。第一步是主观下线pfail。节点 A 在cluster-node-timeout时间内没收到节点 B 的 pong 回复就在自己的视角里把 B 标记为fail?然后在 gossip 心跳里把「我认为 B 挂了」这个消息带上。第二步是客观下线fail。当集群里超过半数的主节点都认为 B 挂了B 才会被标记为fail。注意是主节点的多数从节点的意见不计入。这个设计是为了防止单个节点的网络问题导致误判。第三步是从节点发起选举。B 的从节点发现自己主节点挂了且自己与主节点断连的时间没超过cluster-node-timeout × cluster-replica-validity-factor默认 factor 是 10也就是 150 秒就向所有主节点拉票。主节点在同一个 epoch 内只会投给第一个请求者。第四步是晋升与接管。拿到多数票的从节点晋升为主节点接管原主的全部槽并通过心跳把新拓扑扩散到全网。这里有一个容易被忽略的竞争条件从节点的数据新鲜度。选举时主节点会优先投给复制偏移量replication offset更靠前的从节点。所以如果你有一个主配了三个从其中一个从因为网络问题长期同步滞后它几乎不可能当选。这也意味着从节点的同步状态本身就是可用性的一部分INFO replication里的master_link_status和slave_repl_offset要纳入监控。还有一个参数cluster-migration-barrier默认值是 1。它的作用是「当某个主节点没有任何从节点时允许一个有多个从的主节点分一个从给它」。这个机制叫 replica migration是 Cluster 自愈能力的一部分。如果主节点数量不多但每个都配了从建议保持默认值 1让集群自己平衡。5.2 两个常见误判场景与收敛方法场景一主节点假死被降级。现象是业务侧突然出现大量READONLY写入失败日志里显示某个节点从 master 变成了 replica但机器好好的。原因通常是这个主节点执行了一次长耗时的操作比如大 key 删除、BGREWRITEAOF、SAVE导致它阻塞超过cluster-node-timeout没有及时回复心跳。解决办法不是简单调大 timeout而是根治慢操作用UNLINK替代DEL把 AOF rewrite 放到从节点做关掉定时SAVE改用从节点备份。场景二网络分区导致集群永久 fail。假设集群 3 主 3 从跨两个机房某天机房之间的链路断了一侧有 2 主 1 从另一侧有 1 主 2 从。少数派那一侧的 1 个主节点收不到多数派的心跳会主动拒绝写入cluster-node-timeout之后进入fail状态少数派那侧的从节点也拉不到票无法晋升。这是设计上的正确行为Cluster 用的是多数派原则宁可让少数侧不可写也不允许脑裂。但如果链路恢复后集群仍然是 fail那就需要人工介入了。常用的收敛手段是在故障侧节点上执行CLUSTER RESET SOFT让它清空本地拓扑重新加入硬重置HARD会清空数据只能用在从节点上。我处理这类问题时会先看CLUSTER INFO里的cluster_current_epoch和cluster_my_epoch。如果某个节点的 epoch 远小于集群当前 epoch说明它的拓扑信息是过期的直接重置它比修别的更快。注意生产环境一定要在机房维度上做规划。3 个主节点如果全在同一个机架、同一台交换机下那么这个交换机就是整个集群的单点。合理布局是 3 个主分布在 3 个故障域从节点和主节点交叉放置。6. 在线扩缩容与槽迁移实操6.1 扩容的两种姿势自动 reshard 与手动迁移扩容的完整流程是先加节点空节点槽数为 0再把槽挪过去。加节点有两条命令一条是CLUSTER MEET一条是--cluster add-node后者封装了前者加握手等待。# 新增一个主节点 7006 和一个从节点 7007 redis-cli --cluster add-node 192.168.10.12:7006 192.168.10.11:7000 redis-cli --cluster add-node 192.168.10.12:7007 192.168.10.11:7000 \ --cluster-slave --cluster-master-id 7006的node-id新节点加进来时槽数是 0cluster nodes里它的 flags 是master后面没有任何槽区间。这时候集群状态仍然是 ok 的因为槽确实全被分配了。接下来才是重头戏迁移槽。自动方式用--cluster reshard交互式会问你迁多少个槽、从哪些节点迁、迁给谁。生产环境我更倾向于把参数全写死避免手抖redis-cli --cluster reshard 192.168.10.11:7000 \ --cluster-from 源node-id1,源node-id2,源node-id3 \ --cluster-to 目标node-id \ --cluster-slots 4096 \ --cluster-yes \ --cluster-pipeline 10 \ --cluster-timeout 30000几个参数值得说明。--cluster-pipeline 10表示迁移时一次批量取 10 个 key这个值设大了会占用源节点的输出缓冲区设小了迁移慢10 是我实测比较均衡的值。--cluster-timeout 30000是单个MIGRATE命令的超时如果槽里有大 key这个值要往上调否则会反复超时重试。手动迁移单个槽的流程更能体现原理我建议每个运维都至少手动做过一遍# 假设把 slot 100 从 A(旧节点idaaa) 迁到 B(新节点idbbb) # 1. 在目标节点 B 上标记我要接管这个槽 redis-cli -h B -p 7006 cluster setslot 100 importing aaa # 2. 在源节点 A 上标记我要迁走这个槽 redis-cli -h A -p 7000 cluster setslot 100 migrating bbb # 3. 循环取出这个槽上的 key 并逐个迁移 redis-cli -h A -p 7000 cluster getkeysinslot 100 100 # 拿到 key 列表后批量迁移 redis-cli -h A -p 7000 migrate B_IP 7006 0 5000 KEYS key1 key2 key3 # 4. 确认槽上没 key 了在双方都执行归属确认 redis-cli -h B -p 7006 cluster setslot 100 node bbb redis-cli -h A -p 7000 cluster setslot 100 node bbb第 4 步的SETSLOT ... NODE要在所有相关节点上执行不只是新旧两个节点。因为其他节点的槽映射还需要通过心跳慢慢扩散而这两个节点是立刻变更的。如果只在一个节点上执行会出现「两个节点都认为自己拥有这个槽」的冲突状态触发槽冲突修复机制。MIGRATE这个命令有几个细节目标地址必须写 IP不能写主机名AUTH参数在集群模式下一般不需要因为节点之间用集群总线认证REPLACE参数用于覆盖目标端已有的同名 key正常迁移不应该需要它如果需要了说明数据有脏写要查原因。6.2 迁移过程中最容易翻车的五个点大 key 阻塞。迁移一个 500MB 的 hashMIGRATE会用DUMPRESTORE的方式整块搬运源节点在这个过程中是阻塞的业务会出现秒级抖动。根治办法是提前拆分大 key或者改用UNLINK分批删。我在迁移前一定会跑一遍大 key 扫描redis-cli --bigkeys先粗筛再对可疑的 key 用MEMORY USAGE key精确看。客户端映射过期。迁移中的槽会产生大量ASK重定向如果客户端对ASK处理不正确比如错误地更新了缓存会在迁移完成后继续往旧节点发请求表现为持续的MOVED循环。看到这个现象先检查客户端版本和缓存刷新逻辑别急着怀疑集群。迁移到一半中断。运维脚本 CtrlC 或者网络抖动导致槽处于「importing / migrating」状态但没完成。这时候cluster nodes里能看到这两个节点都标记了这个槽需要手动清理在新节点执行CLUSTER SETSLOT slot STABLE在旧节点执行同样的命令槽会退回给旧节点数据不会丢。slot 相关的拒绝服务。迁移期间如果某个 key 刚被迁走源节点上其实还能通过ASKING访问到一些残留 key。这种状态下多 key 操作会返回TRYAGAIN。客户端要能识别这个错误并做短暂重试不能当成致命错误。忘记迁移从节点。新加的主节点 7006 有了槽但 7007 这个从节点还没跟上这时候 7006 挂了这批槽就会彻底不可用。加从节点的动作必须在迁槽之前完成而且迁完之后要检查INFO replication里 7007 的master_replid是不是和 7006 一致。7. 高频故障与排查实录这一节我把这几年踩过的坑整理成一张速查表后面再挑几个展开讲。报错/现象根因排查命令处理方式CLUSTERDOWN Hash slot not served槽没分配完或多数主节点失联cluster info看cluster_slots_assigned补齐槽分配恢复失联节点CROSSSLOT Keys in request dont hash to the same slot多 key 操作跨槽cluster keyslot key1、cluster keyslot key2用 hash tag 改造 key 设计MOVED循环刷新客户端拓扑缓存更新异常抓包看请求目标节点升级客户端显式配置刷新READONLY You cant write against a read only replica请求打到了从节点或主节点被降级INFO replication看 role检查客户端拓扑或排查假死集群状态长期 fail网络分区后少数派无法收敛cluster nodes看 flags、epochCLUSTER RESET SOFT重置异常节点某个槽数据量暴涨hash tag 使用过度分布不均cluster countkeysinslot逐个对比拆分 tag重新设计 key迁移超时大 key 或网络问题MEMORY USAGE、CLUSTER GETKEYSINSLOT拆大 key调大 timeout写入偶发超时节点阻塞BGREWRITEAOF / 大 key 删除SLOWLOG GET 20、LATENCY DOCTOR用 UNLINKrewrite 放从节点7.1 CLUSTERDOWN 的三类成因第一类是槽没分完。建集群时中途失败或者有人手动执行了CLUSTER DELSLOTS。这种情况最简单把缺的槽补上即可redis-cli --cluster fix 127.0.0.1:7000能自动修复大部分不一致。第二类是多数主节点不可达。比如 3 主里挂了 2 个剩下的 1 个凑不出多数票整个集群进入 fail即使还有部分节点活着也不提供服务。这是多数派协议的代价。应对办法是至少部署 3 个主节点并且监控要能在第一个主挂掉时就报警——因为从 3 个挂到 2 个就已经失去容错能力了好消息是 3 主里挂 1 个仍然可用2 主 2 从的集群里挂 1 个主就只剩 1/2等于半数勉强可用但没有任何冗余了所以 3 主是最低配置。第三类是cluster-require-full-coverage配置的影响。这个参数默认是yes含义是「只要有一个槽不可用整个集群就拒绝服务」。改成no的话只有出问题的槽返回错误其他槽正常服务。这听起来很诱人但我不建议轻易改改成no之后故障变得「安静」了可能有一批业务一直报错而没人发现监控不告警的话问题会拖很久。是否要改取决于你的业务能不能接受「部分 key 不可用但整体不中断」。我的做法是保持默认yes把监控压在cluster_state这个指标上fail立刻告警。补充一个排查技巧redis-cli --cluster check ip:port会一次性把拓扑、槽覆盖、数据一致性全检查一遍输出比cluster nodes直观得多。新接手一个集群先跑这一条心里就有底了。7.2 事务、Lua 和 Pipeline 在集群下的改造事务MULTI/EXEC的所有 key 必须同槽而且客户端必须把事务整体发到同一个节点。Jedis 的 cluster 模式会自动校验不满足就直接抛异常。如果业务上确实需要跨对象的事务只能上分布式锁或者用业务补偿Cluster 本身给不了。Lua 脚本除了 key 必须同槽还有一个隐藏要求——脚本里用到的所有 key 必须通过KEYS数组传入不能靠字符串拼接生成 key。因为 Redis 需要提前解析出所有 key 来判断槽的归属。我见过一个脚本用KEYS[1]..:lock拼了个新 key单机跑得好好的上集群直接报CROSSSLOT。改造办法是把:lock这个 key 也作为参数传进去或者用 hash tag 让两者同槽。Pipeline这个坑最隐蔽。Pipeline 里混合了多个槽的 key 时客户端无法把它们发到同一个节点Jedis 会直接报错Lettuce 则是按节点分组后分别发送——后者看起来能用但批量返回的结果顺序会乱。所以 Pipeline 在集群下要自己按槽分组先算每个 key 的槽按目标节点分桶每个桶单独发一个 Pipeline最后按原顺序合并结果。虽然麻烦但这是唯一正确的做法。# 按目标节点分桶的一个思路示意 from collections import defaultdict def group_by_node(keys, slot_cache, node_of_slot): buckets defaultdict(list) for k in keys: slot slot_cache[k] buckets[node_of_slot[slot]].append(k) return buckets8. 落地选型与几条不写在文档里的经验8.1 Cluster、哨兵、代理方案怎么选把三种方案放在一起对比选择就清晰了。维度主从 哨兵Redis Cluster代理分片方案数据容量上限单机内存横向无限受节点数限制横向扩展客户端复杂度低哨兵地址即可高需支持重定向低当成单机用跨节点多 key 操作支持不支持需同槽支持代理层做归并故障转移时间10~30 秒10~20 秒取决于组件自身运维复杂度低高中多一层要维护数据一致性强单机最终一致异步复制取决于分片策略适用规模数据 50GB数据大、写并发高想屏蔽分片细节我的判断逻辑是如果数据量在单机能放下的范围内永远优先选哨兵能省下大量精力和故障面。如果确实超了才考虑 Cluster。代理方案现在用得少了因为它多一跳网络、多一个要自己维护的组件而且分片逻辑一旦写死扩容比 Cluster 还麻烦。但它的一个优势仍然存在——业务代码完全不用改对于老系统改造是个务实的选择。还有一个常被忽略的问题Cluster 只能使用 db0。SELECT命令在集群模式下直接报错。如果你的应用依赖多 db 做环境隔离比如 db0 生产、db1 测试迁移到 Cluster 时必须改成用 key 前缀隔离。这个改造量往往比想象中大因为前缀会改变 key 的槽位分布可能引发热点。8.2 我的几条实践经验单节点内存别超过 16GB。这不是 Redis 的限制是运维经验的总结。节点越大fork 做持久化时的内存开销越大、全量同步越慢、故障恢复越久。宁可多分几个片也不要造一个 64GB 的巨型节点。主从必须跨故障域。3 主 3 从如果全在一台物理机上那这台机器就是单点。理想布局是 3 个可用区各放 1 主 1 从且主从交叉——A 区的主从在 B 区B 区的主从在 C 区C 区的主从在 A 区。监控这几个指标就够了。cluster_state必须是 ok、cluster_slots_assigned必须 16384、cluster_known_nodes不该无故变化、每个节点的connected_clients、used_memory与maxmemory的比值、master_link_status、keyspace_misses。前两个是生死线后面几个是预警线。定期做故障演练。每季度至少手动 kill 一次主节点验证三件事集群能否自动切换、客户端能否自动恢复、监控能否正确告警。演练要记录时间线我一般会记「kill 时刻 → pfail 时刻 → fail 时刻 → 新主晋升时刻 → 客户端首个成功请求时刻」这条时间线就是你的真实 RTO。备份不要在主节点做。BGSAVE会 forkfork 在高内存占用下可能阻塞几百毫秒足以触发一次误判。把备份任务指向从节点并且在从节点上关掉 AOF如果从节点只做备份用途能减少不少麻烦。版本对齐。集群里所有节点的 Redis 版本必须一致至少大版本一致。混版本最典型的问题是CLUSTER SLOTS和CLUSTER SHARDS的返回格式差异以及某些新命令在旧节点上不存在。滚动升级时先升从节点再CLUSTER FAILOVER切主最后升原来的主——这个顺序可以保证全程有可用副本。最后说一个我自己的体会。Cluster 最难的从来不是搭起来而是在它出问题时能快速判断「这是集群的问题还是业务的问题」。我遇到过好几次业务方说「Redis 集群挂了」一查发现是他们的 key 设计导致某个槽数据量是其他槽的 20 倍那个节点内存打满触发了淘汰业务读到空值就以为集群故障。这种问题用redis-cli --cluster check加cluster countkeysinslot逐个槽比对一眼就能看出分布不均。所以我现在接手任何集群第一件事就是拉一张「每个主节点的槽数、key 数、内存占用」的表把这张表存成基线后面所有的异常判断都拿它做参照。这张表比任何监控大盘都直观。