Redis 分片集群这个东西,我第一次搭的时候踩的坑比前面学 Sentinel 加起来还多。标题里写的是“从入门到精通”系列的第十五篇、分布式缓存第三篇,实际上到了分片集群这一层,已经不再是“配个主从、挂个哨兵”能糊弄过去的阶段了——它解决的是单机内存上限和写并发上限这两个硬约束。单节点 Redis 再强,内存也就那么点,QPS 也就那么多,业务量一上来,垂直扩容的成本曲线陡得吓人。分片集群(Redis Cluster)就是把这个天花板拆掉:把 16384 个哈希槽分散到多个主节点上,每个主节点再挂从节点做高可用,数据分片、故障转移、请求路由全在集群内部完成。这篇我打算按自己实际搭一遍的流程来讲,从槽位原理、节点规划、集群组建、扩缩容到故障排查,把能直接抄作业的命令和那些文档里不写的坑都摊开说。适合已经会单机 Redis、懂主从和哨兵、准备上生产级分片集群的兄弟参考,纯新手建议先把前面几篇的基础打牢再看。
1. 先搞清楚分片集群到底在解决什么问题
1.1 单机与主从架构的天花板在哪
很多人对 Redis 扩容的第一反应是“加内存、换更好的机器”,这叫垂直扩容。单机内存从 16G 加到 64G,确实能撑一阵,但有两个绕不过去的坎。第一是内存上限:单实例内存越大,RDB 持久化时的 fork 开销和全量同步的耗时越夸张,一次 fork 拷贝页表在几十 G 的实例上能卡住主线程几百毫秒,业务侧就是一波超时告警。第二是写并发上限:Redis 单线程处理命令,QPS 再优化也就十万量级,写密集场景下主从架构里所有写都压在一个主节点上,从节点只分担读,写这一侧根本没有横向扩展的余地。
主从加哨兵解决的是高可用问题——主挂了从能顶上,但它不解决容量和写吞吐问题。哨兵集群里数据始终是一份完整的副本,每个节点都存全量数据,你加再多从节点,写的还是那一个主。所以当业务的数据量超过单机内存、或者写 QPS 超过单主处理能力时,主从哨兵这套就顶不住了,必须上分片。
1.2 分片集群的核心思路:把数据切开
分片集群的本质就一句话:把整个数据集按规则切成 N 份,每份放在不同的主节点上,每个主节点负责自己那部分数据的读写。这样内存总量是所有主节点之和,写并发也是所有主节点之和,横向扩展的能力就出来了。
Redis Cluster 采用的切分规则是哈希槽(hash slot),总共固定 16384 个槽。为什么是 16384 而不是 65536?这个数字不是随便定的:集群内部用 bitmap 记录每个节点负责哪些槽,16384 个槽的 bitmap 是 2KB,节点间心跳包要携带这个信息,65536 就是 8KB,心跳开销会明显变大;同时集群规模一般不会超过 1000 个节点,16384 个槽足够均匀分配,再多的槽在心跳和迁移时都是负担。这个取舍在官方设计文档里有说明,实际部署时记住结论就行:槽数是固定的,节点数是可变的,扩缩容本质是挪槽。
每个 key 落到哪个槽,算法是CRC16(key) mod 16384。注意这里有个细节:如果 key 里带{},只对花括号内的内容做 CRC16,这叫hash tag,用来强制把相关 key 落到同一个槽,后面讲批量操作和事务时会用到。
1.3 集群模式与代理模式怎么选
上分片有两条主流路线:客户端分片/代理分片和Redis Cluster 原生集群。代理模式(比如各类 proxy 中间件)是在客户端和 Redis 之间加一层,由代理负责路由,客户端当单机用;原生 Cluster 是把路由逻辑下放到客户端,客户端 SDK 自己维护槽位映射表,直连对应节点。
| 对比维度 | 原生 Redis Cluster | 代理模式 |
|---|---|---|
| 路由位置 | 客户端 SDK | 中间代理层 |
| 额外组件 | 无 | 需要部署维护代理集群 |
| 运维复杂度 | 中,扩缩容需 reshard | 高,代理本身也要高可用 |
| 客户端兼容 | 需支持 Cluster 协议的 SDK | 普通客户端即可 |
| 跨槽操作 | 受限,需 hash tag | 代理可做一定聚合 |
| 故障转移 | 集群内自动完成 | 依赖代理感知与后端哨兵 |
我选原生 Cluster 的理由很直接:少一层组件就少一个故障点和一份运维成本,而且主流语言的客户端对 Cluster 协议支持已经很成熟。代理模式适合那些客户端技术栈老旧、改不动 SDK 的场景,或者需要做跨槽聚合查询的特殊业务。绝大多数新项目,直接上原生 Cluster 是更省心的选择。
2. 搭建前的规划:节点、端口与槽位分配
2.1 最小生产可用拓扑怎么定
Redis Cluster 要求至少 3 个主节点才能形成多数派(majority),因为故障转移需要超过半数主节点投票同意。3 主 3 从是最小的高可用拓扑,6 个实例。但我要提醒一句:3 主 3 从只是“能跑”,生产环境建议6 主 6 从起步,原因有两个。一是 3 主时任意一个主挂掉,它负责的约 1/3 槽位在故障转移完成前不可用,转移窗口内业务会有明显抖动;主节点多一些,单点故障影响面就小。二是 6 主能把 16384 个槽分得更细,单节点内存压力更均匀。
节点规划我一般按这个原则:主节点数 = 预估总内存 / 单节点安全内存上限。单节点内存我建议控制在 10G 到 16G 之间,超过 20G 就要警惕 fork 和同步开销。假设业务预估需要 80G 内存,那就 6 到 8 个主节点,每个主节点配一个从节点,总共 12 到 16 个实例。从节点不参与写,但故障转移时要顶上来,所以配置要和主节点一致。
2.2 端口与目录的规范约定
集群模式下每个实例要开两个端口:一个是客户端访问的业务端口(比如 7000),另一个是业务端口 + 10000 的集群总线端口(7000 对应 17000),用于节点间 gossip 通信、故障检测和配置传播。这个总线端口必须开放,很多人搭集群失败就是防火墙只放了 7000 没放 17000,节点之间握手一直失败。
目录结构我习惯这样组织,方便批量管理:
/data/redis-cluster/ ├── 7000/ │ ├── redis.conf │ ├── data/ │ └── logs/ ├── 7001/ │ └── ... └── 7005/每个实例独立目录、独立配置文件、独立数据目录,好处是扩缩容和故障隔离时不会互相干扰,删一个节点直接删目录就行,不会误伤别的实例。
2.3 关键配置项逐条说明
集群模式的配置文件有几个必改项,我列出来并解释为什么:
# 业务端口 port 7000 # 集群总线端口,必须显式指定,默认是 port+10000 cluster-port 17000 # 开启集群模式,这是总开关 cluster-enabled yes # 集群配置文件,由 Redis 自己维护,不要手动改 cluster-config-file nodes-7000.conf # 节点间通信超时,单位毫秒,超过这个时间认为节点失联 cluster-node-timeout 15000 # 允许从节点在故障转移时参与数据迁移 replica-read-only yes # 持久化,集群模式建议开 AOF appendonly yes appendfilename "appendonly.aof" # 绑定地址,集群内网通信建议绑内网 IP bind 0.0.0.0 # 保护模式关闭(内网环境) protected-mode no # 后台运行 daemonize yes # 日志与数据目录 logfile "/data/redis-cluster/7000/logs/redis.log" dir "/data/redis-cluster/7000/data"cluster-node-timeout这个值要重点说。设太小,网络抖动就触发故障转移,容易误判;设太大,真挂了要等很久才转移。15000 毫秒(15 秒)是官方推荐的经验值,实际要根据内网 RTT 调整,内网稳定的话 10000 到 15000 都行。另外cluster-config-file千万别手动编辑,那是 Redis 自己维护的节点状态文件,手改会导致集群元数据错乱。
注意:
cluster-enabled yes一旦开启,这个实例就不能用普通的SELECT切库了,集群模式只支持 db0。如果老业务依赖多库,迁移前要先改造。
3. 一步步把集群拉起来
3.1 批量生成配置与启动实例
手工一个个写配置太蠢,我一般用脚本批量生成。假设要搭 6 个实例(7000 到 7005),用一段 shell 就能搞定:
for port in $(seq 7000 7005); do mkdir -p /data/redis-cluster/${port}/{data,logs} cat > /data/redis-cluster/${port}/redis.conf <<EOF port ${port} cluster-port $((port + 10000)) cluster-enabled yes cluster-config-file nodes-${port}.conf cluster-node-timeout 15000 appendonly yes daemonize yes protected-mode no bind 0.0.0.0 logfile "/data/redis-cluster/${port}/logs/redis.log" dir "/data/redis-cluster/${port}/data" EOF redis-server /data/redis-cluster/${port}/redis.conf done启动后每个实例都是孤立的单节点,cluster-enabled是开了,但还没组成集群,此时用redis-cli -p 7000 cluster info看,cluster_state是fail,这是正常的,别慌。
3.2 用 cluster create 组建集群
组建集群用redis-cli --cluster create,把 6 个节点地址列出来,加--cluster-replicas 1表示每个主节点配一个从节点:
redis-cli --cluster create \ 192.168.1.10:7000 192.168.1.10:7001 192.168.1.10:7002 \ 192.168.1.10:7003 192.168.1.10:7004 192.168.1.10:7005 \ --cluster-replicas 1执行后会打印一份槽位分配方案让你确认,输入yes回车。这里有个关键点:前三个地址会被当作主节点候选,后三个作为从节点候选,Redis 会自动把从节点和主节点配对。如果你对主从配对关系有要求(比如希望从节点和主节点不在同一台物理机),要么调整地址顺序,要么组建后用cluster replicate手动指定。
组建成功的标志是cluster info里cluster_state:ok,并且cluster_known_nodes等于 6。这时候用cluster nodes能看到每个节点的角色、负责的槽位范围、主从关系。槽位分配默认是均分的,6 个节点 3 主,每个主大约 5461 个槽。
3.3 验证集群是否真的可用
组建完别急着上业务,先做几组验证。第一组是槽位覆盖检查:
redis-cli --cluster check 192.168.1.10:7000这个命令会检查所有槽是否被完整覆盖、有没有槽冲突、主从关系是否正常。输出里[OK] All 16384 slots covered才算过关。
第二组是读写路由验证。用-c参数启动客户端,让它自动跟随重定向:
redis-cli -c -p 7000 127.0.0.1:7000> set user:1001 "alice" -> Redirected to slot [12345] located at 192.168.1.10:7002 OK 127.0.0.1:7000> get user:1001 -> Redirected to slot [12345] located at 192.168.1.10:7002 "alice"看到Redirected说明客户端连的节点不负责这个 key 的槽,集群返回了 MOVED 重定向,客户端自动转到了正确节点。这是集群正常工作的表现,不是错误。如果没加-c,客户端不会自动跳转,会直接报 MOVED 错误,这是新手最容易懵的地方。
第三组是故障转移演练。手动kill掉一个主节点进程,观察它的从节点是否在cluster-node-timeout之后被提升为主。用cluster nodes反复看,从节点的角色会从slave变成master,并且接管原主节点的槽位。这一步一定要在测试环境做一遍,生产上真出事时心里才有底。
4. 槽位迁移与扩缩容实操
4.1 扩容:加节点并重新分片
业务涨了要加主节点,流程分三步:启动新实例、加入集群、迁移槽位。假设要加一个 7006 作为新主节点:
# 第一步:按前面的配置模板启动 7006 实例 redis-server /data/redis-cluster/7006/redis.conf # 第二步:把新节点加入集群(作为主节点加入,暂时不分配槽) redis-cli --cluster add-node 192.168.1.10:7006 192.168.1.10:7000加入后 7006 是个空主节点,槽位数为 0。第三步迁移槽位:
redis-cli --cluster reshard 192.168.1.10:7000 \ --cluster-from all \ --cluster-to <7006的node-id> \ --cluster-slots 2000 \ --cluster-yes--cluster-from all表示从所有现有主节点各匀一部分槽给新节点,--cluster-slots 2000是迁移总量。迁移过程中,槽位里的 key 会逐个从源节点搬到目标节点,迁移期间集群仍然可用,但涉及正在迁移的槽的请求可能会收到 ASK 重定向,客户端要能正确处理。reshard 是渐进式的,不会一次性锁住整个集群,这点比停机迁移友好得多。
4.2 缩容:下线节点前先清空槽位
下线一个主节点不能直接kill,得先把它负责的槽迁走,否则那些槽就没人管了,集群会进入 fail 状态。步骤是先把槽迁到其他主节点,再删节点:
# 把 7006 的槽全部迁回其他主节点 redis-cli --cluster reshard 192.168.1.10:7000 \ --cluster-from <7006的node-id> \ --cluster-to <目标主节点node-id> \ --cluster-slots 2000 \ --cluster-yes # 槽迁空后,删除节点 redis-cli --cluster del-node 192.168.1.10:7000 <7006的node-id>如果 7006 上还挂着从节点,要先删从节点再删主节点,或者先把从节点cluster replicate到别的主节点下。删节点前用cluster nodes确认它的slots字段为空,这是硬性检查,别偷懒。
4.3 迁移过程中的客户端行为
槽迁移时,源节点对属于该槽但已经迁走的 key 返回ASK 重定向,对还没迁走的 key 正常处理;目标节点对迁入中的槽,如果 key 已存在就正常返回,不存在则返回 ASK 让客户端去源节点找。这套机制叫ASKING,客户端 SDK 内部处理,业务代码无感知。但如果你用的是自己封装的简易客户端,必须实现 MOVED 和 ASK 两种重定向的处理逻辑,否则迁移期间会大量报错。
实操心得:大规模 reshard 建议放在业务低峰期做,虽然集群可用,但迁移会占用网络带宽和 CPU,对延迟敏感的业务还是有影响。迁移速率可以用
--cluster-pipeline参数控制批量大小,默认 10,网络好的话调到 100 能明显加快。
5. 集群内部原理:gossip、故障转移与一致性
5.1 节点间如何感知彼此
集群里没有中心节点,所有节点通过gossip 协议互相通信。每个节点周期性(默认每秒)向随机几个节点发 PING,收到 PONG 后交换彼此掌握的节点状态和槽位信息。信息像谣言一样在集群里扩散,最终每个节点都有一份完整的集群视图。这也是为什么cluster-config-file那么重要——它持久化的就是这份视图,节点重启后靠它快速恢复对集群的认知。
gossip 的好处是去中心化、容错强,坏处是状态收敛有延迟。节点规模越大,收敛越慢,所以集群规模不建议超过 1000 个节点。日常运维中如果发现某个节点状态更新不及时,先查网络和cluster-node-timeout,别急着怀疑集群坏了。
5.2 故障转移的完整流程
一个主节点被判定为FAIL需要两个条件:主观下线(PFAIL)和客观下线(FAIL)。某个节点发现另一个节点超过cluster-node-timeout没响应,标记为 PFAIL,这是主观判断;当超过半数主节点都报告 PFAIL,才升级为 FAIL,这是客观判断。这个设计防止了单个节点的误判导致不必要的转移。
升级为 FAIL 后,该主节点的从节点发起选举:从节点向其他主节点拉票,获得超过半数主节点投票的从节点晋升为新主,接管原主节点的槽位,并广播新的配置。原主节点恢复后,会以从节点身份重新加入,自动同步新主的数据。整个过程无需人工介入,这也是原生 Cluster 相比手工主从切换的最大优势。
5.3 集群模式下的数据一致性边界
要清醒认识到:Redis Cluster不是强一致的。主节点写入成功即返回客户端,此时数据可能还没同步到从节点,如果主节点在同步完成前挂掉,这部分数据就丢了。这是异步复制的固有特性,和主从架构一样。另外网络分区时,少数派那一侧的主节点在cluster-node-timeout内仍可能接受写入,分区恢复后这些写入会被丢弃。
所以对一致性要求高的场景,要么业务层做补偿和对账,要么用WAIT命令强制等待 N 个副本确认(会牺牲延迟)。我的经验是:缓存场景接受最终一致,关键数据落库为准,Redis 只做加速层,这样设计最稳。
6. 常见问题排查速查
6.1 集群组建失败的典型原因
| 现象 | 可能原因 | 排查与解决 |
|---|---|---|
cluster create卡住或报连接失败 | 总线端口 17000+ 未开放 | 检查防火墙和安全组,业务端口和总线端口都要放行 |
cluster_state:fail且槽未全覆盖 | 部分节点未加入或槽分配失败 | cluster check看哪些槽缺失,重新 reshard 补齐 |
| 节点反复 PFAIL/FAIL | cluster-node-timeout过小或网络抖动 | 适当调大超时,排查内网丢包 |
客户端报CLUSTERDOWN | 多数派主节点失联 | 检查是否超过半数主节点挂了,恢复节点 |
MOVED错误刷屏 | 客户端未开启集群模式 | SDK 开启 cluster 支持,或命令行加-c |
6.2 跨槽操作的坑
集群模式下,涉及多个 key 的命令(MGET、MSET、事务、Lua 脚本)要求所有 key 在同一个槽,否则报CROSSSLOT错误。解决办法是用hash tag把相关 key 绑到同一槽:
# 这两个 key 因为 {} 内都是 user:1001,会落到同一个槽 set {user:1001}:name "alice" set {user:1001}:age 30 # 这样 MGET 就不会跨槽 mget {user:1001}:name {user:1001}:agehash tag 是把双刃剑:用得好能解决批量操作问题,用过头会导致数据倾斜——大量 key 挤到同一个槽、同一个节点,分片就失去意义了。我的原则是只在确实需要原子操作的关联 key 上用,且 tag 的粒度要控制好,别拿一个大业务 ID 当 tag 把所有数据绑死。
6.3 内存与槽位不均衡
有时候槽位是均分的,但各节点内存差异很大,这叫数据倾斜。原因通常是某些 key 特别大(大 key)或者 hash tag 设计不当。排查用redis-cli --cluster check看各节点 key 数量,再用--bigkeys扫大 key。治理手段:拆分大 key、调整 hash tag、必要时手动 reshard 把热点槽迁到独立节点。这个问题在业务增长期很常见,建议上线前就做好 key 设计规范。
7. 生产环境我踩过的几个坑
第一个坑是从节点只读配置。集群模式下从节点默认replica-read-only yes,但如果你想让从节点分担读流量,客户端得支持读从节点的路由策略(READONLY命令),否则所有读还是打到主节点。很多 SDK 默认只读主,配置里要显式打开从节点读,并且接受从节点数据可能有微小延迟。
第二个坑是集群重启顺序。整集群停机维护后重启,如果先起的主节点数量不足半数,集群会一直处于 fail 状态,起够了才恢复。所以批量重启时要注意,别一台台慢慢起导致长时间不可用,可以并行启动所有节点。另外nodes-*.conf文件要保留好,删了它节点会以全新身份加入,集群元数据就乱了。
第三个坑是客户端连接池与拓扑刷新。集群拓扑变化(扩缩容、故障转移)后,客户端需要刷新槽位映射。好的 SDK 会自动处理 MOVED 并刷新,但有些老版本只在收到 MOVED 时才刷新单个槽,拓扑大变动时会有一段时间的抖动。上线前确认 SDK 版本,必要时配置定时刷新拓扑。
最后分享一个排查小技巧:集群出问题时,先
cluster info看整体状态,再cluster nodes看每个节点的角色和槽位,最后cluster check做全面体检。这三板斧下来,八成问题能定位。剩下两成,多半是网络和配置的锅,抓包和看日志比瞎猜快得多。
这套集群我在几个中等规模的项目里跑过,稳定运行一年多,扩过一次容、演练过两次故障转移,整体符合预期。分片集群不是银弹,它把运维复杂度换来了扩展能力,值不值取决于业务体量。数据量小、写并发不高的场景,主从加哨兵足够,别为了“先进”硬上集群,多出来的槽位迁移和跨槽限制都是实打实的成本。