1. 为什么要在虚拟机上搭建 Redis Cluster
1.1 从单机 Redis 到集群的必然演进
很多同学刚开始接触 Redis 的时候,都是在本地或者一台测试机上装一个单实例,跑跑缓存、做做队列,感觉已经够用了。但一旦业务量上来,单实例的问题就会集中爆发:内存受限于单机物理上限、QPS 顶到 CPU 瓶颈、主节点一挂整个缓存层直接雪崩。这时候你自然会想到 Redis 官方提供的分布式方案——Redis Cluster。
Redis Cluster 是 Redis 3.0 之后官方推出的分布式集群方案,核心能力有三个:数据分片、高可用和去中心化。它把整个键空间划分成 16384 个哈希槽(slot),每个主节点负责一部分槽位,客户端根据 key 计算 CRC16 再对 16384 取模,就能定位到具体节点。同时每个主节点可以挂若干从节点做副本,主挂了从顶上,保证可用性。节点之间通过 Gossip 协议互相通信,没有中心节点,任意节点都能响应客户端请求并转发。
那为什么要在虚拟机里搭,而不是直接上云或者用 Docker?这个问题我被问过很多次。原因其实很实在:第一,虚拟机最接近真实的生产环境,你能完整地控制网络、端口、内存、磁盘,能模拟出节点宕机、网络分区这些真实故障;第二,成本低,一台物理机开六台虚拟机就能跑一个三主三从的完整集群,比买六台云主机便宜太多;第三,可反复折腾,快照一打,搞坏了直接回滚,学习成本几乎为零。Docker 虽然轻量,但网络模型和真实服务器差异较大,排查问题时容易混淆是容器网络的问题还是 Redis 本身的问题。
1.2 三主三从的最小生产级拓扑
Redis Cluster 官方要求至少 3 个主节点,这是因为集群的故障转移需要多数派投票,3 个主节点能容忍 1 个挂掉。如果只有 2 个主节点,挂一个就凑不齐多数派,集群直接不可用。所以最小可用拓扑是 3 主,加上每个主配 1 个从,就是经典的3 主 3 从共 6 个节点。
这个拓扑的意义在于:任意一个主节点宕机,它的从节点会被提升为新主,集群继续对外服务,槽位覆盖不缺失。如果只搭 3 个主不搭从,那挂一个主,它负责的那 5461 个槽就没人管了,整个集群进入 fail 状态,客户端写入直接报CLUSTERDOWN。
在虚拟机里,我一般这样规划:6 台虚拟机,每台 2 核 4G 内存,系统用常见的 Linux 发行版,网卡桥接模式让它们在同一网段互通。IP 规划成 192.168.100.11 到 192.168.100.16,端口统一用 6379,这样最直观。当然如果你机器资源紧张,也可以一台虚拟机跑多个实例,用不同端口区分,但那样故障隔离性差,学习阶段不推荐。
提示:虚拟机内存别给太小,Redis 默认会 fork 子进程做 RDB 持久化,内存不足时 fork 会失败甚至触发 OOM。4G 是比较舒服的起步配置。
1.3 这套方案适合谁参考
如果你正在准备面试被问到 Redis 集群、想自己动手验证分片和故障转移的原理、或者公司项目要上 Redis Cluster 但你没在生产环境练过手,那这套虚拟机方案就是为你准备的。它不需要你有多深的运维功底,只要会基本的 Linux 命令、能看懂配置文件,跟着走一遍就能跑起来。后面我还会讲怎么模拟节点宕机、怎么观察槽位迁移、怎么排查集群起不来的常见坑,这些都是文档里不会细说但实际一定会遇到的东西。
2. 搭建前的环境准备与核心参数规划
2.1 虚拟机与网络的基础配置
先把 6 台虚拟机准备好,系统装完后有几件事必须做。第一是关闭防火墙或者放行端口,Redis Cluster 不光要用 6379 这个客户端端口,还需要一个集群总线端口,默认是客户端端口加 10000,也就是 16379。节点之间靠这个端口做 Gossip 通信、故障检测、配置传播。很多人集群建不起来就是只开了 6379 没开 16379。
第二是设置主机名和 hosts 解析。虽然用 IP 也能跑,但集群节点握手后会把 IP 写进配置,一旦 IP 变了集群就乱。我习惯给每台机器设个主机名,比如 node1 到 node6,然后在每台的/etc/hosts里把 6 个名字和 IP 对应上。这样配置文件里写主机名,可读性好,迁移也方便。
第三是时间同步。集群节点之间判断故障是靠时间戳和心跳的,如果各机器时间差太大,会出现误判节点下线的情况。用系统自带的时间同步服务保证 6 台机器时间一致就行。
第四是调整系统参数。Redis 在高并发下需要大量的文件描述符和网络连接,默认的 1024 往往不够。需要改/etc/security/limits.conf,把nofile提到 65535。另外如果开启了 RDB 持久化,建议把vm.overcommit_memory设为 1,避免 fork 时内存分配失败。
2.2 Redis 版本选择与安装方式
版本上我建议用Redis 6.x 或 7.x 的稳定版。5.0 之前集群功能虽然能用,但运维工具和稳定性差一些;6.0 引入了多线程 IO 和 ACL,7.0 在集群和持久化上又有优化。学习阶段用 6.2 或 7.2 都很稳。
安装方式有两种:包管理器直接装,或者源码编译。包管理器省事,但版本可能偏旧,而且配置文件路径和源码安装不一样。我推荐源码编译,因为你能完全控制安装路径、配置文件位置,也方便后续升级。步骤就是下载源码包、解压、make编译、make install安装。编译前确保装了 gcc 和 make。编译完成后,redis-server、redis-cli、redis-trib(新版本已集成到 redis-cli 里)这些工具就都有了。
这里有个细节:Redis 6 之后集群管理命令已经合并进redis-cli,用redis-cli --cluster就能创建和管理集群,不再需要单独的redis-trib.rb脚本,也就省去了装 Ruby 环境的麻烦。这是个很大的便利,老教程里还在讲装 Ruby 的可以直接跳过。
2.3 关键配置项逐个拆解
每个节点的redis.conf里有几个参数是集群专属的,必须配对,配错一个集群就起不来。我逐个说清楚它们的作用和取值逻辑。
cluster-enabled yes:这是总开关,不开这个 Redis 就是普通单机模式,不会参与集群。默认是注释掉的,必须手动打开。
cluster-config-file nodes.conf:这是节点自己维护的集群状态文件,记录着本节点认识的其它节点、槽位分配、主从关系等。这个文件不要手动改,Redis 自己会写。每次节点重启会读它恢复集群状态。如果这个文件丢了,节点会以全新身份加入,可能导致集群混乱。
cluster-node-timeout 15000:节点超时时间,单位毫秒。意思是如果一个节点 15 秒没响应心跳,就认为它可能挂了。这个值不能太小,太小会因网络抖动误判;也不能太大,太大故障转移慢。15000 是官方推荐值,内网环境够用。如果虚拟机性能差、网络延迟高,可以适当调到 20000。
cluster-require-full-coverage no:这个参数很关键。默认是 yes,意思是只要有任何一个槽位没有节点负责,整个集群就拒绝所有请求。生产环境一般设成 no,这样即使某个主节点挂了还没完成转移,其它槽位的数据依然可读写,只是挂掉那部分不可用。学习阶段建议设 no,方便你观察部分可用状态。
appendonly yes:集群模式下强烈建议开启 AOF 持久化。因为集群节点重启后要从持久化文件恢复数据,RDB 可能丢较多数据。AOF 配合appendfsync everysec在性能和安全性之间比较平衡。
bind 和 protected-mode:虚拟机之间要互通,bind要写成0.0.0.0或者具体的内网 IP,protected-mode设成 no,否则外部连不上。但注意这只在内网环境这么做,公网环境绝不能这么配。
port 和 cluster-announce 系列:如果节点在 NAT 后面或者端口做了映射,需要用cluster-announce-ip和cluster-announce-port告诉其它节点自己的真实地址。纯内网直连的话不用配。
把这些参数整理成一张表,方便你对照检查:
| 配置项 | 推荐值 | 作用说明 |
|---|---|---|
| cluster-enabled | yes | 开启集群模式的总开关 |
| cluster-config-file | nodes.conf | 节点集群状态持久化文件 |
| cluster-node-timeout | 15000 | 判定节点下线的超时毫秒数 |
| cluster-require-full-coverage | no | 部分槽位不可用时是否拒绝全部请求 |
| appendonly | yes | 开启 AOF 持久化 |
| appendfsync | everysec | AOF 刷盘策略,每秒一次 |
| bind | 0.0.0.0 | 监听所有网卡(仅内网) |
| protected-mode | no | 关闭保护模式(仅内网) |
| daemonize | yes | 后台运行 |
3. 六节点集群的完整搭建实操
3.1 批量生成节点配置目录
6 台机器如果一台台手动配,容易配错还费时间。我的做法是写一个简单的 shell 脚本,在每台机器上执行,自动创建目录结构、生成配置文件。目录结构我习惯这样:/data/redis/6379/放配置和数据,/data/redis/logs/放日志,/data/redis/pid/放 pid 文件。这样每个实例的数据、日志、配置都隔离,排查问题清晰。
配置文件的生成可以用模板加变量替换。比如把redis.conf做成模板,里面的端口、绑定 IP、节点超时这些用占位符,脚本读取本机 IP 后替换生成最终配置。这样 6 台机器跑同一个脚本,生成的配置只有 IP 不同,减少人为失误。
脚本里还要做几件事:创建数据目录、设置目录权限(Redis 进程用户要有读写权限)、把vm.overcommit_memory和文件描述符限制写进系统配置。这些一次性做完,后面启动就顺了。
注意:如果你是在一台机器上跑 6 个实例(用不同端口),那
cluster-config-file和appendonly文件必须放在各自独立的目录,否则会互相覆盖,集群直接崩。
3.2 启动所有节点并验证单机状态
配置生成后,逐个启动节点。启动命令是redis-server /data/redis/6379/redis.conf。启动后先用ps确认进程在,再用redis-cli -h 本机IP -p 6379 ping看是否返回 PONG。这一步是确认每个节点作为单机是活的。
然后重点看集群状态:redis-cli -h 本机IP -p 6379 cluster info。刚启动还没组集群时,cluster_state应该是fail,cluster_known_nodes是 1,cluster_size是 0。这是正常的,因为还没握手。如果这里cluster_enabled显示 0,说明配置文件里cluster-enabled没生效,回去检查配置。
再看cluster nodes,应该只有自己一个节点,角色是 master,没有槽位。如果这一步就有异常,比如端口没监听、日志报错,先解决单机问题再往下走,不要急着组集群。我见过太多人跳过这步,结果组集群时各种超时,回头排查发现是某个节点根本没起来。
日志文件这时候要重点看,路径在配置里指定的logfile。正常启动日志会显示Ready to accept connections,集群模式下还会显示Node configuration loaded或者Cluster enabled。有报错就按报错信息查,常见的是端口占用、权限不足、配置文件语法错误。
3.3 用 redis-cli 创建三主三从集群
所有节点都确认单机正常后,就可以创建集群了。在 Redis 6 以上版本,一条命令搞定:
redis-cli --cluster create \ 192.168.100.11:6379 \ 192.168.100.12:6379 \ 192.168.100.13:6379 \ 192.168.100.14:6379 \ 192.168.100.15:6379 \ 192.168.100.16:6379 \ --cluster-replicas 1这里--cluster-replicas 1的意思是每个主节点配 1 个从节点。命令执行后,redis-cli 会自动计算槽位分配方案,把 16384 个槽平均分给前 3 个节点作为主,后 3 个作为从跟随对应的主。它会打印出一个分配方案让你确认,输入yes回车。
分配逻辑是这样的:前 3 个 IP 被选为主节点,分别负责 0-5460、5461-10922、10923-16383 三段槽位。后 3 个节点作为从节点,随机或者按顺序跟随主节点。这个分配不是固定的,redis-cli 会根据节点顺序和内部算法决定,但保证每个主分到的槽数尽量均衡。
执行过程中你会看到类似>>> Performing hash slots allocation on 6 nodes...的输出,然后是M: xxx和S: xxx标记主从。最后显示[OK] All 16384 slots covered就说明集群创建成功了。如果卡在Waiting for the cluster to join很久,多半是总线端口 16379 没通,回去检查防火墙。
3.4 集群健康状态检查清单
集群建好后,别急着用,先做一轮完整体检。我整理了一个检查清单,按顺序过一遍:
第一,redis-cli -c -h 任意节点IP -p 6379 cluster info,看cluster_state是不是ok,cluster_slots_assigned是不是 16384,cluster_known_nodes是不是 6,cluster_size是不是 3。这四个数字对了,集群基本健康。
第二,cluster nodes看每个节点的角色、槽位、主从关系。主节点后面跟着它的从节点 ID,从节点会显示slaveof哪个主。确认 3 主 3 从关系正确,没有节点处于fail或handshake状态。
第三,做一次实际读写。用redis-cli -c的集群模式连上去,set foo bar然后get foo。注意-c参数不能少,它让客户端自动跟随重定向。如果你 set 一个 key 返回MOVED错误,说明客户端没开集群模式,加上-c就好。
第四,验证分片。故意 set 几个不同的 key,观察它们被分配到不同节点。你可以用cluster keyslot key名命令算出某个 key 属于哪个槽,再对照槽位分配表看落在哪个节点。这一步能直观理解分片原理。
第五,检查持久化文件是否生成。去每个节点的数据目录看有没有appendonly.aof和nodes.conf。这两个文件存在说明持久化和集群状态都在正常写入。
4. 故障模拟与集群行为观察
4.1 手动下线一个主节点看故障转移
集群搭好了不模拟故障,等于白搭。最经典的实验就是手动干掉一个主节点,观察从节点自动顶替的过程。先找到某个主节点的进程,直接kill掉,模拟宕机。
然后在另一个节点上持续执行cluster nodes,你会看到这样的变化:刚开始那个主节点状态变成fail?,表示疑似下线;过了cluster-node-timeout时间后变成fail,确认下线;接着它的从节点开始竞选,日志里出现Starting a failover election;竞选成功后从节点变成 master,接管原来的槽位,状态恢复ok。整个过程通常在 15 到 30 秒内完成。
这个实验有几个观察点值得注意。第一,故障转移期间,被挂主节点负责的那部分槽位不可用,客户端访问这些 key 会报CLUSTERDOWN或者超时,但其它槽位正常。这就是为什么前面建议cluster-require-full-coverage设 no。第二,从节点提升后,原来的主节点如果重新启动,它会以从节点身份加入,跟随新的主,不会抢回主位。这是 Redis Cluster 的设计,避免脑裂。第三,整个转移过程不需要人工干预,完全自动。
4.2 模拟网络分区与脑裂防护
比宕机更复杂的是网络分区。你可以用防火墙规则把某个主节点和集群其它节点隔离开,模拟它"失联"但进程还活着。这时候集群多数派会认为它下线,触发故障转移,从节点提升。而那个被隔离的主节点自己还认为自己是主,继续接受它那侧客户端的写入,这就形成了脑裂。
Redis Cluster 用cluster-node-timeout和多数派投票来缓解这个问题。被隔离的节点因为收不到多数派的心跳确认,无法完成故障转移投票,最终会把自己降级。当网络恢复后,它会以从节点身份重新加入,并且它的数据会被新主的数据覆盖(因为新主在隔离期间可能已经写入了新数据)。所以集群模式下,数据一致性是靠主从复制和故障转移保证的,但极端分区下仍可能丢少量数据,这是 CAP 权衡的结果,要心里有数。
做这个实验时,恢复网络后一定要检查cluster nodes,确认被隔离节点已经正确降级并同步。如果它还固执地认为自己是主,可能需要手动cluster reset再重新加入。这个坑我在测试环境踩过,原因是隔离时间太长超过了配置的容忍度,节点状态错乱。
4.3 槽位迁移与重新分片实操
集群运行中难免要扩缩容,比如加一个新节点进来分担压力,或者下线一个节点把它的槽位挪走。这就是重新分片(resharding)。Redis 提供了redis-cli --cluster reshard命令来做这件事。
假设要把某个主节点的 1000 个槽迁移到新节点。命令会问你迁移多少个槽、迁到哪个节点、从哪些节点迁。你可以指定从某一个源迁,也可以从所有主节点平均迁。执行过程中,槽位是一个个迁移的,每个槽迁移时,源节点会把该槽的 key 逐个MIGRATE到目标节点,迁移完成后更新集群配置。这个过程是在线的,不影响其它槽的读写,但迁移中的槽如果有请求进来,客户端会收到ASK重定向,需要客户端支持。
迁移完成后,用cluster nodes确认槽位归属已经更新,cluster info确认cluster_slots_assigned还是 16384。这里有个经验:迁移大批量槽时,最好在业务低峰期做,因为MIGRATE是同步阻塞的,key 多的时候会卡住源节点。可以分批迁,每次几百个槽,观察负载再继续。
5. 常见问题排查与避坑经验
5.1 集群创建失败的典型原因速查
集群创建阶段失败是最常见的,我把踩过的坑整理成一张速查表:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| Waiting for the cluster to join 卡住 | 总线端口 16379 未放行 | telnet 目标IP 16379 测试连通性 |
| Node is not empty 报错 | 节点数据目录有旧集群数据 | 删除 nodes.conf 和 aof/rdb 后重启 |
| cluster_state 一直 fail | 槽位未全覆盖或节点未握手 | cluster info 看 slots_assigned 是否 16384 |
| 节点显示 handshake 状态 | 节点间网络不通或端口错 | 检查 bind、防火墙、总线端口 |
| MOVED 重定向频繁 | 客户端未开集群模式 | redis-cli 加 -c 参数 |
| 从节点不跟随主 | 复制关系未建立 | cluster nodes 看 slaveof 是否正确 |
其中"Node is not empty"这个坑特别常见。如果你之前在这台机器上跑过集群,数据目录里残留了nodes.conf,新集群创建时节点会带着旧身份加入,导致冲突。解决办法就是创建前清空所有节点的数据目录,确保是干净状态。我现在的习惯是每次重建集群前,先跑一个清理脚本,把所有节点的nodes.conf、appendonly.aof、dump.rdb全删掉,再启动。
5.2 运行期的性能与稳定性问题
集群跑起来后,问题往往出在性能和稳定性上。第一个常见问题是内存碎片。Redis 频繁增删 key 后,内存碎片率会升高,used_memory_rss远大于used_memory。这时候可以开启activedefrag yes让 Redis 自动整理碎片,但会消耗 CPU。或者干脆在低峰期重启节点,重启后内存会重新整理。
第二个问题是大 key 和热 key。集群分片是按 key 分的,但如果某个 key 特别大(比如一个 list 存了几十万条),迁移和访问都会很慢。热 key 则是某个 key 被高频访问,压力全集中在一个节点上。解决办法是拆分大 key,热 key 做本地缓存或者加随机后缀分散。这些在单机时可能不明显,集群下会被放大。
第三个问题是主从复制延迟。如果主节点写入量大,从节点同步跟不上,故障转移时可能丢较多数据。可以监控master_repl_offset和从节点的slave_repl_offset差值,差值持续增大说明复制有压力。优化方向是减少大 key 写入、提高网络带宽、或者调整repl-backlog-size。
5.3 我踩过的三个真实坑
第一个坑是配置文件里的 bind 写成了 127.0.0.1。单机测试时这么写没问题,但集群节点之间要互相通信,绑到回环地址后其它节点根本连不上,创建集群时一直卡在 join。改成内网 IP 或 0.0.0.0 后立刻就好了。这个坑隐蔽在于单机 ping 是通的,容易误判。
第二个坑是虚拟机内存给太小导致 fork 失败。有次给每台机器只分了 1G 内存,Redis 数据量涨到几百兆后,做 RDB 持久化时 fork 子进程失败,日志报Can't save in background: fork: Cannot allocate memory。后来把vm.overcommit_memory设为 1,内存加到 4G 才稳定。教训是集群节点内存要留足余量,至少是数据量的两倍以上。
第三个坑是时间不同步导致误判下线。有台虚拟机的时间比其它机器慢了几分钟,结果集群频繁认为它心跳超时,反复触发故障转移,日志里全是节点上下线记录。用时间同步服务校准后问题消失。这个坑最难查,因为 Redis 本身不报时间错误,只能通过观察故障转移频率来发现。
6. 集群运维的日常操作与扩展思路
6.1 日常巡检该看哪些指标
集群上线后,日常巡检主要盯几个指标。cluster info里的cluster_state必须是 ok,cluster_slots_assigned必须是 16384,这两个是红线。cluster nodes里不能有节点长期处于 fail 或 handshake 状态。每个节点的used_memory和maxmemory比值要关注,接近 1 就要考虑扩容或清理。connected_clients突增可能是有客户端连接泄漏。instantaneous_ops_per_sec反映实时 QPS,结合keyspace_hits和keyspace_misses算命中率,命中率骤降要查是不是缓存穿透。
我习惯写一个巡检脚本,定时抓这些指标输出成表格,一眼就能看出异常。比一个个节点手动敲命令高效得多,也避免遗漏。
6.2 扩容缩容的正确姿势
扩容就是加新节点然后迁移槽位。新节点先以空集群模式启动,用redis-cli --cluster add-node把它加入集群,此时它是主节点但没有槽。然后用reshard从现有主节点迁一部分槽给它。如果要给它配从,再用add-node --cluster-slave加从节点。
缩容则相反,先把要下线节点的槽全部迁走,确认它没有槽了,再用del-node把它移除。注意移除前一定要确认槽已迁空,否则集群会缺槽导致 fail。从节点下线相对简单,直接del-node即可,不影响槽位。
整个过程的核心原则是:任何时候都要保证 16384 个槽全覆盖。迁移过程中槽是逐个转移的,中间状态也是全覆盖的,所以在线扩容是安全的。但操作要慢,一步步确认,别图快。
6.3 后续可以深入的方向
这套虚拟机集群跑通后,你可以往几个方向深入。一是客户端接入,用 Jedis、Lettuce 或者各语言的 Redis 客户端连集群,理解客户端是怎么做槽位缓存和重定向的。二是监控告警,接入 Prometheus 加 Grafana,把集群指标可视化,设置阈值告警。三是压测,用 redis-benchmark 或者自定义压测工具,测出集群的吞吐上限和瓶颈点。四是持久化与备份策略,研究 AOF 重写、RDB 快照、主从备份的组合方案。
我个人觉得最有价值的是把故障模拟做透。生产环境的故障千奇百怪,你在虚拟机里能模拟的越多,真出问题时就越从容。比如模拟磁盘满、模拟 CPU 打满、模拟网络延迟抖动,观察集群在这些极端情况下的表现,比看十篇文档都管用。这套环境搭一次,能反复用很久,性价比极高。