资讯详情

Redis集群架构核心机制详解:分片、故障转移与客户端路由实践

📅 2026/10/21 3:23:59 | 华诺云谱 👁 阅读
Redis集群架构核心机制详解:分片、故障转移与客户端路由实践
提到分布式 Redis 集群架构很多人的第一反应是把几台 Redis 节点拼在一起、让它们互相认识。这个直觉对了一半集群确实需要多台机器但真正的技术难点从来不在“数量”而在数据怎么分片、节点之间怎么通信、主库挂了之后怎么完成切换、客户端面对一堆节点时该怎么路由。我在几个真实项目里从单机 Redis 一步步迁到集群踩过不少坑也花了很长时间才把整套机制串起来这篇文章就把我总结出来的经验和判断依据完整写一遍适合正在做容量规划、准备从单机或主从架构迁移的团队参考。先提醒一句这篇文章不是“照着敲命令就能完成”的速成手册我更想把集群为什么这样设计、每一步背后在防什么坑讲清楚。你理解了原因再去敲命令会顺手很多排障时也知道下手的方向。1. 单机 Redis 撑不住的真实原因内存、线程与可用性三条线同时亮红灯1.1 容量瓶颈数据量超过物理内存之后一切策略都失效单机 Redis 最朴素的问题就是内存有限。比如一台 64GB 的机器你不可能把 100GB 的热数据塞进去常规做法是加大内存、把冷数据淘汰掉或者干脆只缓存一部分。但只要业务增长超过规划问题就会接踵而至。我自己经历过一个很典型的场景某个订单系统把用户维度的数据全部缓存进 Redis量级从几千万 key 涨到上亿之后单机内存报警紧接着就是 RDB 持久化时间变长。生成的快照文件十几 GB保存期间 fork 子进程做内存拷贝主进程写入时还会触发写时复制内存峰值一度逼近物理极限。那次事故的直接诱因是 Linux 内存不足触发了 OOM KillerRedis 主进程被系统干掉。这里要补充一个容易忽略的原理Redis 单个 key 的内存开销不只是 value 的大小还包括 dict 条目、过期时间、指针等元数据。上亿 key 时光 key 本身的元数据可能就占掉几个 GB。你用redis-cli --bigkeys扫出来的是大 value如果大量 key 都很小、数量特别多真正吃内存的反而是这些“看不见的壳”。容量规划绝不能只算 value 总量必须留出 1.5 到 2 倍的余量。1.2 单线程模型CPU 资源卡在命令执行队列上Redis 的主处理模型是单线程事件循环这也是它高性能的基石但同样是瓶颈来源。单线程意味着同一时刻只有一条命令在执行所有命令都串行通过一个队列。CPU 从两核升到十六核只要瓶颈在主线程性能不会有本质提升。读多写少的业务还能通过主从复制分担读流量但写密集场景就尴尬了。举个例子某个运营系统每秒要写入几十万条计数数据单实例 CPU 直接跑满所有客户端请求延迟从 1ms 飙到 200ms。我当时做了几次压测单实例在纯写入场景下大约能扛住 8 万到 10 万 QPS再往上延迟曲线就陡增。你当然可以继续压榨单机性能但一条命令的耗时一旦因为慢查询变长后面所有命令都会被堵住这种连锁效应在单机上没法隔离。1.3 可用性缺口主从复制能解决一部分但替代不了数据分片很多人会先想到主从复制一个主节点多个从节点读写分离主挂了从顶上。这个方案确实解决了读扩展和故障转移的一部分问题但主从模式的核心局限在于——所有节点保存的是同一份全量数据。这意味着两个比较大的问题第一内存总量被单机上限锁死从节点再多也不能分担写容量第二全量复制带来的网络和磁盘开销会随着数据量增长变得非常可怕主节点生成 RDB 快照的瞬间从节点同步时间可能长达几分钟甚至更久。三种常见方案的对比如下方案数据是否分片自动故障转移写容量扩展适用阶段主从复制不分片需要额外部署哨兵不支持读多写少、数据量可控哨兵 主从不分片支持不支持高可用要求高、数据量可控Redis Cluster分片内置支持支持数据量大、需要横向扩容我见过不少团队在主从架构上硬撑直到单机内存逼近上限才开始考虑 Cluster。从运维角度看主从架构本身没错但如果你已经预见到半年内数据量会翻倍早一点切换到 Cluster 的成本要远低于后期在线迁移。2. 16384 个槽位怎么算出来分片机制与 CRC16 的设计取舍2.1 槽位映射把整个 key 空间切成固定格子Redis Cluster 没有用一致性哈希环而是把整个 key 空间固定划分为 16384 个槽位。每个 key 计算出一个整数槽位集群再把这 16384 个槽位按需分配给各个主节点。你可以在任意一个节点上执行CLUSTER KEYSLOT user:1001得到它对应的槽位编号。这里有个公式slot CRC16(key) % 16384。如果你用了哈希标签公式会变成对标签内的内容计算后面第五节详细讲。为什么是 16384 而不是 65536 或者更多我在实际运维中体会到的一个重要原因是槽位信息需要在节点之间通过心跳消息传播。每个节点要保存整个集群的槽位分配位图16384 个槽位对应 2KB 的位图这个数据会放进心跳包同步给所有节点。如果槽位数量扩大到 65536位图就是 8KB心跳包变大网络开销和序列化成本都会增加。16384 是在节点规模、消息体大小和分配粒度之间做出的平衡官方给出的合理节点上限是 1000 个主节点对绝大多数业务来说完全够用。2.2 固定槽位和一致性哈希的根本区别数据归属是否明确一致性哈希的核心思路是把 key 也映射到一个环上节点变动时只有环上部分 key 需要迁移。它听着很美好但有一个让运维头疼的问题数据归属边界模糊。当节点列表变化时你需要额外维护一个虚拟节点和路由表才能让客户端知道“到底该访问谁”。Redis Cluster 的固定槽位方案则不同每个槽位在任意时刻都有一个明确的归属节点。你通过CLUSTER SLOTS可以拿到全局槽位到节点的映射表客户端缓存这张表之后本地就能算出某个 key 该访问哪个节点完全不需要做一次额外的路由查询。槽位方案的另一个优势是迁移可控。迁移其实就是把一部分槽位从源节点移动到目标节点每次都明确知道“哪些槽在迁移、迁移进度多少”。反观一致性哈希虽然减少了迁移范围但迁移结束后需要额外检查一堆 key 是否落在正确节点排障时要更仔细。2.3 MOVED 和 ASK客户端和服务端之间怎么兜底集群模式下客户端算出槽位后直接访问对应节点大多数情况一次命中。但槽位迁移期间客户端缓存的映射表可能已经过期它访问的节点已经不是该槽位的持有者。这时候服务端会返回两类错误很多人把这两者搞混MOVED表示该槽位已经永久归属另一个节点客户端必须更新本地缓存以后都访问新节点。ASK表示槽位正在迁移过程中目标节点告诉你“数据可能正在迁过来”你需要先发一个ASKING命令再执行实际操作。两者区别很微妙但对正确性很重要。我的踩坑经历是早期有个内部工具直接对迁移中的节点批量发命令遇到 MOVED 不更新缓存遇到 ASK 不执行 ASKING 而直接拒绝导致迁移期间部分更新失败。后来我在客户端实现里明确区分了“硬重定向”和“软重定向”两种处理迁移期间的成功率才稳定下来。3. 从零搭建三主三从集群配置文件、节点握手与槽位迁移实操3.1 最容易被忽略的配置项cluster-enabled 只是第一步搭建集群之前你首先要改 Redis 配置文件。最低限度的集群配置一般包括下面几行cluster-enabled yes cluster-config-file nodes-6379.conf cluster-node-timeout 15000 appendonly yes第一行启用集群模式第二行指定节点持久化集群状态的文件第三行是节点间通信的超时阈值第四行是建议开启 AOF 而不是默认的 RDB 快照。原因很简单集群环境下节点宕机后需要快速重启并恢复数据RDB 快照可能丢失最后一次持久化之后的数据AOF 的丢失窗口更小。这里我要特别提醒一个不少人踩过的坑cluster-node-timeout不是越大越好。它既影响故障检测的速度也影响从节点能否快速发起选举。你把超时设为 60 秒主节点断了之后整个集群可能要等 60 秒甚至更久才能完成故障转移对可用性要求高的业务来说这个时间不可接受。我通常先设 10 到 15 秒根据实际网络抖动情况再调整。3.2 用命令完成初始化节点是如何“互相认识”的准备好六个节点的配置文件启动它们之后用集群管理命令完成初始化。这里以三主三从为例redis-cli --cluster create \ 10.0.0.11:6379 10.0.0.12:6379 10.0.0.13:6379 \ 10.0.0.14:6379 10.0.0.15:6379 10.0.0.16:6379 \ --cluster-replicas 1--cluster-replicas 1表示每个主节点配一个从节点。执行这条命令后工具会自动分配槽位前三个节点成为主节点后三个节点按照一定规则成为它们的从节点。整个过程会自动完成节点间握手、槽位分配和主从关系绑定。完成后一定要做三件事验证redis-cli -h 10.0.0.11 -p 6379 cluster info redis-cli -h 10.0.0.11 -p 6379 cluster nodes redis-cli -h 10.0.0.11 -p 6379 cluster slotscluster info里重点看cluster_state:okcluster nodes里检查每个从节点是否绑定了正确的主节点cluster slots里确认 16384 个槽位全部有主节点负责。我一直强调一个原则没做这三步验证就不算集群真正建好。3.3 在线扩容加节点、迁移槽位、观察数据分布集群上线之后数据量增长到某个阈值你可能需要加节点。先说一个独立于集群架构的重要经验在动手之前先检查一下集群里是否有大 key尤其是 value 达到几十 MB 甚至上百 MB 的 key。槽位迁移时大 key 的迁移是原子的整个 key 一次性搬过去迁移期间所有访问都会被阻塞时间可能长达几秒。我处理过一个 200MB 的 value迁移一次直接让两个节点卡了 8 秒页面上所有依赖 Redis 的接口一起超时。扩容的标准流程是先加入空节点redis-cli --cluster add-node 10.0.0.17:6379 10.0.0.11:6379注意第二个参数必须是集群中某个已存在的节点新节点通过它完成握手并获取集群信息。然后执行槽位迁移redis-cli --cluster reshard 10.0.0.11:6379 \ --cluster-from 源节点ID \ --cluster-to 新节点ID \ --cluster-slots 1000迁移过程会分批搬运 key每批数量可以调整默认是一次处理一定数量的 key。如果你希望迁移过程更平滑可以用更小的批次。迁移期间源节点和目标节点可能同时拥有同一个槽位的数据客户端请求会通过 ASK 重定向到目标节点所以业务一般无感。迁移完成后看一遍cluster nodes确认新节点的槽位数量符合预期然后观察内存分布和请求延迟。如果新节点内存上涨明显低于预期说明迁移的槽位里大部分 key 是空的或者说冷数据不多这就要再检查下槽位分布策略。4. 主节点挂掉后的真实选举链路PFAIL、FAIL 与从节点晋升4.1 集群总线节点之间并不靠客户端通信感知故障Redis Cluster 的每个节点除了对外提供服务端口还会额外占用一个端口用于集群内部通信这个端口通常是服务端口加 10000。比如服务端口是 6379集群总线端口就是 16379。节点之间通过这个总线持续交换 PING、PONG 消息消息里携带槽位信息、节点状态、配置版本等。很多新手会想当然地以为主节点故障要靠 Sentinel 或某些外部组件来感知其实不是。集群内部的每个节点都在互相监控所有节点共享集群状态。你执行CLUSTER INFO看到的cluster_state就是所有节点通过总线通信后汇总得到的结果。4.2 从 PFAIL 到 FAIL一个节点什么时候算真正“死掉”这里有一个关键机制节点不会因为一次 PING 没回应就直接判定对方故障。所谓PFAIL表示“疑似故障”是单个节点对某个节点的初步判断。当一个主节点被超过一半的主节点都标记为 PFAIL 之后它才会被确认为FAIL这个状态会广播到整个集群。我一开始不太理解为什么要设计两个层级后来在真实的网络抖动场景里才体会到。如果某个节点因为 GC 停顿或者网络瞬时偶发丢包整个集群就立刻把它踢掉反而会引发频繁而无效的故障转移对系统伤害更大。PFAIL 机制实际上是一个“少数服从多数”的缓冲层过滤掉瞬时抖动。从节点开始抢主位置时选举过程可以简单描述为从节点发现主节点被标记为 FAIL。它向所有主节点发送请求投票的消息。主节点确认自己有投票权且还没有投过票就投给它。获得超过半数主节点票数的从节点成为新主。这里面还有一个看不见的竞争条件多个从节点同时发起竞选时谁优先Redis 的规则是复制偏移量越大数据越新从节点就越有优势。所以在发起竞选投票前从节点会等待一个随机延迟复制偏移量大的从节点更早发起投票请求从而提高拿到选票的概率。4.3 脑裂与部分数据丢失不可回避的代价集群给了你高可用但它是通过牺牲部分一致性换来的。假设一个主节点和它的从节点被网络分区隔开从节点那边看不到主节点于是从节点发起选举成为新主。此时旧主仍然存活客户端可能继续往旧主写入数据。分区恢复后旧主尝试连接新主却发现自己的数据版本落后它只能丢弃掉自己多写入的数据重新从新主做全量同步。这意味着分区期间写入旧主的数据会丢。分布式系统里这叫脑裂窗口Redis Cluster 无法完全消除。你能做的是尽量缩小窗口比如适当降低cluster-node-timeout让故障转移更快同时通过客户端层面的重试和幂等逻辑来兜底。另一个相关参数是cluster-require-full-coverage默认值是 yes表示只要有任何一个槽位没有可用主节点整个集群就拒绝写入。这个设计是为了防止客户端写入了一个没有备份的槽位之后数据彻底消失但也意味着当某个节点完全失联时集群会“集体罢工”。如果你的业务可以接受部分不可用希望尽量多地提供服务可以把它改成 no。5. 客户端接入集群时必须注意的规则路由、多键命令与 hash tag5.1 Smart Client 的本地缓存与刷新机制普通单机 Redis 的客户端只需要配置一个连接地址但集群环境下的客户端必须知道每个槽位对应哪个节点才可能把请求精确路由到正确的目标。所谓 Smart Client就是在本地缓存一份槽位与节点的映射表并且能根据服务端返回的 MOVED 指令主动刷新缓存。你要注意的点是你的客户端是否真的支持集群模式还是仅仅把多个节点地址混在一起做负载均衡我排查过一起诡异故障某个内部服务用的客户端不支持集群协议它只是随机挑一个节点连接结果每次请求要么命中、要么收到 MOVED 报错。表面上看是“能连上”实际上整个逻辑是错的。主流语言的官方或社区客户端基本都支持 Cluster你只需要在配置文件里把所有节点列表都填上。初次连接时客户端会查询一次CLUSTER SLOTS建立映射表之后本地直连。如果节点扩缩容导致映射变化客户端通过 MOVED 指令重新感知。5.2 多键操作、事务和 Lua 脚本跨槽操作是硬边界这是很多业务迁到集群时踩得最深的一个坑。单机 Redis 上你可以放心执行MSET、MGET、SUNIONSTORE这些命令同时操作多个 key。但在集群模式下如果这些 key 分散在不同槽位命令就执行不了。解决办法是使用哈希标签它允许你通过花括号强制指定某个子串参与哈希计算user:{1001}:name user:{1001}:age user:{1001}:city这三个 key 都会命中user:{1001}对应的槽位。注意花括号里只能有一个子串如果一个 key 里出现多个花括号比如user:{1001}:{1001}不同版本的策略不一致我自己的一条经验是永远只写一组花括号避免产生歧义。事务和 Lua 脚本也有同样的限制。MULTI/EXEC只能在单个槽位内执行Lua 脚本里的所有 key 必须落在同一个槽位。这里的原理不难理解Redis Cluster 的原子性边界就是单个槽位跨槽位的原子操作在架构上无法保证。5.3 SCAN 和读取从节点把“全量遍历”难度升级了在单机 Redis 上执行KEYS或SCAN就是对一个实例做遍历。集群环境下每个 key 分散在 16384 个槽位里你必须把所有主节点遍历一遍再把结果聚合起来。KEYS在生产环境本来就不推荐到了集群里更应该禁用改用SCAN配合批量获取。这里有一个真实的坑用SCAN遍历集群时如果你对每个节点用相同的游标参数可能会漏掉部分 key。因为 SCAN 的游标是节点内部的哈希表位置不同的节点各自维护各自的游标你必须在客户端维护一个“每个节点独立的游标集合”。不少工具没有处理好这一层结果扫描出来的 key 总数明显小于实际数量数据统计完全失真。如果你打算把读请求分担到从节点千万不要在同一个命令里混用主从节点。集群模式下每个节点都有明确的主从身份从节点可以接受读请求但它的槽位归属关系和主节点一致。客户端必须知道从节点对应的槽位映射否则一个读请求可能被路由到没有对应槽位数据的从节点上。6. 集群稳定运行期的监控重点与调优参数6.1 集群状态指标眼睛盯住五个关键值集群装好之后日常监控比“临时排障”更重要。我建议重点关注以下指标这些指标都能通过CLUSTER INFO拿到或通过客户端采集指标正常值参考异常含义cluster_stateok有槽位没有主节点时变成 failcluster_slots_assigned16384小于 16384 说明槽位没有分配完全cluster_slots_ok接近 16384大量下降说明节点通信异常cluster_known_nodes符合实际节点数异常增加可能是有重复节点注册cluster_size主节点数量反映分片规模多节点之间如果出现部分节点不可达集群可能处于降级状态。你需要尽早知道集群的“健康分数”而不是等某个业务接口超时了才去排查。6.2 慢查询与大 Key 排查集群里更容易被成倍放大慢查询在单机上只影响一个实例在集群里则可能让整个分片都变慢进而拖垮依赖同一个槽位的所有请求。生产环境我建议开启慢日志slowlog-log-slower-than 10000 slowlog-max-len 128阈值的单位是微秒10000 表示记录执行时间超过 10ms 的命令。之后再通过SLOWLOG GET定期收集慢命令。要特别留意的是DEL、KEYS、SORT这类时间复杂度高的命令。请一定注意删除大 Key 用DEL在单机上也可能阻塞事件循环几百毫秒在集群里对某个分片的影响同样存在。建议用UNLINK替换DEL它是一个异步删除命令占用主线程的时间很短。关于大 Key我常用的检查手段是redis-cli --bigkeys扫描它能筛出每个分片里最大的若干 key。另外DEBUG OBJECT可以查看单个 key 的内部编码和序列化长度适合精确定位。不要相信“我从来没设过大 value”线上很多大 key 是程序 bug 导致某个 hash 无限增长扫一遍很快就有发现。6.3 内存淘汰策略和集群热 Key最容易忽略的两个问题maxmemory-policy这个参数在单机上很多人已经配好了但集群环境下它的影响范围是每个分片。如果你用的是allkeys-lru当某个分片内存不足时它只会淘汰自己槽位上的 key不会管其他分片。也就是说即使整个集群内存还有富余某个数据分布不均的节点也可能提前触发淘汰导致大量热 key 被淘汰引发缓存穿透。热 Key 问题到了集群里更有意思同一个 key 永远落在同一个槽位意味着它只能被一个主节点承载。某个商品的秒杀流量如果集中在几个 key 上那么这几个 key 对应的分片就会成为热点其余分片的 CPU 和内存可能都很闲。加节点也解决不了这个问题因为 key 的哈希关系不会因为你增加节点而改变。应对思路常见的有两种。一是把热 key 的访问分散到多个“影子 key”上比如把一个 key 复制成product:123:0、product:123:1写多份读的时候随机选一个。二是在业务侧加一层本地缓存把高频读取在应用内存里消化掉。两种方案都有额外的一致性成本没有银弹但我更推荐先用监控把热点量化出来再决定方案而不是凭感觉猜。另外如果你的业务有明确的热点窗口可以临时调整lfu-log-factor相关参数让 LFU 淘汰策略更精准。redis-cli --hotkeys命令是一个很好的自检工具但它需要启用 LFU 淘汰策略默认的 noeviction 模式下会用不了。6.4 几个容易被遗忘的调优点第一client-output-buffer-limit。主从复制时如果从节点同步慢主节点会积压输出缓冲区内存压力陡增。我给主节点配置一个合理的输出缓冲上限避免一个拖后腿的从节点把主节点拖垮。第二repl-backlog-size。它决定主节点保留多少复制增量如果设得太小从节点短暂断开后重连时你会发现它只能做全量同步全量同步的代价在集群环境下非常高。建议根据写入量和断线时长估算一般来说 64MB 起步写频繁的业务可以更大。第三注意跨机房部署的问题。如果你把集群节点分散在两个机房网络抖动会非常容易触发 PFAIL、FAIL。跨机房场景下你更要谨慎设置cluster-node-timeout不要为了“尽快切换”把阈值调得太低否则一次正常的链路抖动就会引发大规模故障转移。结尾一点个人习惯最后分享一个我自己的习惯每次线上集群变更之前我会先把cluster nodes的输出、内存用量和慢日志保存一份作为基线变更之后再做一次对比。这个习惯帮我快速定位过不少问题比如明明只迁移了 1000 个槽位为什么某分片内存上涨幅度远大于预期拿出基线一看原来是迁移命令把大量冷数据也带了过来。这类问题光靠直觉很难发现数据和变更记录的对比是最好用的排查手段。分布式 Redis 集群架构本身并不玄妙关键在于把分片、通信、故障转移和客户端路由这四件事理解透。希望这篇文章能帮你少走一点弯路尤其是那些从单机时代带过来的思维惯性——它往往是迁移过程中最容易绊倒人的地方。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑