ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Redis集群模式从原理到实战:分片、槽位与高可用架构全解析

Redis集群模式从原理到实战:分片、槽位与高可用架构全解析 Redis 集群模式这个坑我踩了两年才算彻底爬出来。刚接手公司第一套生产 Redis 时单机 8G 内存、主从 哨兵每天扛着百万级请求倒也能跑。后来业务一扩张缓存数据从几 G 涨到几十 G槽位不够用、写性能瓶颈、大 key 频繁触发内存淘汰那一阵子我几乎是守着监控面板过日子。后来痛下决心上了 Redis 集群模式才真正体会到什么叫分片治百病。这篇文章不聊虚的就直接从我的实际搭建经历出发把 Redis 集群的架构原理、搭建步骤、踩坑记录和运维习惯一次讲清楚。不管你是刚接触 Redis 的新手还是已经在单机下苦苦支撑的开发者这篇文章都能给你一个从 0 到 1 的落地参考。1. Redis 集群模式的本质与适用场景1.1 先搞懂你为什么要用集群很多人一听集群就觉得高深其实说白了Redis 集群模式的核心就是分片。官方叫 Cluster 模式它是把数据自动打散成 16384 个槽位slot然后分散到多个主节点上。每个主节点负责一部分槽位数据一进来Redis 会根据 key 做 CRC16 哈希再对 16384 取模算出这个 key 属于哪个槽位进而找到它应该落在哪个节点上。这个设计解决的最大问题就是单机内存和单点写入的天花板。单机 Redis 哪怕开到 64G实际能存的数据也远小于 64G——因为 Redis 是纯内存操作内存越大持久化 Fork 子进程的时间越长卡顿风险越高。而且单机写能力再强总有一个 CPU 核心数的限制。集群模式则把数据分散到多台机器上每台只用存一部分数据写入和读取天然就是多机并行性能扩展性一下就打开了。不过要注意集群模式不是万能的银弹。它只适合数据量较大、需要水平扩展的场景。如果你数据量不到 10G单机完全够用强行上集群只会增加运维复杂度得不偿失。我这里有个粗略的经验判断单实例内存超过 15~20G 或者写入 QPS 持续超过 5 万且伴随明显延迟抖动才开始认真考虑集群。1.2 集群模式和哨兵模式的边界不少新手会把哨兵模式Sentinel和集群模式搞混。我刚入行时也疑惑哨兵也可以做主从切换、高可用那还要集群干什么后来想通了两者解决的根本不是一个维度的问题。哨兵模式本质上是在主从复制基础上做高可用。它监控主节点的存活状态主挂了自动把某个从节点提升为主。但别忘了整个系统仍然只有一台主节点能写数据总量还是被这台主机的内存上限卡死。哨兵解决的是别让 Redis 单点挂掉的问题不解决数据放不下、写不够快的问题。集群模式则把分片和高可用都做了。集群里每个主节点都可以承担写入数据被自动分散任何一个主节点挂了它旗下的从节点会接管。你在淘宝上买一个商品可能同时有几百个 Redis 节点在并行处理这就是集群的价值。我整理了一张对比表大家可以直接收藏对比项哨兵模式集群模式核心目标主从高可用自动故障转移数据分片 水平扩展 高可用数据存储所有数据在一台主节点上数据分散到多个主节点写入能力受单机限制多主并行线性扩展扩容方式升级硬件垂直扩展增加节点水平分片故障转移哨兵系统选主集群内部自动选主槽位/Key 分布无概念所有 key 随意存按 CRC16 计算槽位自动路由适用场景中小业务单机容量够用追求高可用大数据量、高并发写入、业务持续增长一句话总结哨兵是保证活着集群是有空位给你活。如果公司业务成长期我建议直接一步到位上集群省得后期再折腾一次迁移。2. 集群搭建前的准备与设计2.1 节点规划与端口分配生产环境我建议至少用6 个节点3 主 3 从这也是官方推荐的入门配置。为什么是 3 主 3 从因为一个主节点要对应一个从节点做热备集群里每个主节点挂了都能由从节点顶上。如果只有 3 主没有从哪天一台物理机宕机整个集群的数据就会缺一块严重时甚至CLUSTERDOWN全部拒绝服务。我在公司搭生产集群时用的方案是这样的3 台物理机或者 3 个容器每台机器跑两个 Redis 实例一个主一个从端口分别为 7000 和 7001节点规划如下实例角色端口所在机器master-17000server-aslave-17001server-amaster-27002server-bslave-27003server-bmaster-37004server-cslave-37005server-c这样做的目的是从节点不要跟它的主节点放在同一台物理机或容器里。否则主节点所在的机器宕机从节点也一起挂根本无法完成故障转移。所以实际部署时应打乱主从分布让从节点尽量落在不同的宿主机上这个细节踩过坑的都懂。另外集群模式还需要保证一个前提每个节点之间网络互通端口不能被防火墙挡掉。还有每个节点不仅监听客户端连接端口还会监听一个加 10000 的集群内部通信端口。比如 7000 端口对应的集群总线端口是 17000节点之间靠这个端口互相交换信息。很多人搭建时只开了 7000 端口集群一直报握手失败就是漏了这一点。2.2 关键配置参数逐条解释Redis 集群模式下redis.conf里和集群相关的配置一定不能漏。以下是我在生产环境实际使用的配置模板每一条都有必要说明原因。# 开启集群模式这是最关键的一步 cluster-enabled yes # 集群配置文件名节点自动保存自己的状态信息 cluster-config-file nodes-7000.conf # 节点超时时间单位毫秒 cluster-node-timeout 15000 # 集群是否允许槽位不完整时继续服务 cluster-require-full-coverage no # 开启 AOF 持久化集群模式下我更推荐 AOF appendonly yes # 监听所有网卡否则集群节点之间无法跨机器通信 bind 0.0.0.0 # 后台运行 daemonize yes # 日志文件方便排查 logfile /var/log/redis/redis-7000.log你可能会问cluster-node-timeout 15000是什么意思简单说就是一个节点如果在 15 秒内联系不上另一个节点就认为对方挂了开始触发故障转移。这个值设置太短网络稍有波动就容易误判设置太长主节点真挂了从节点也要等很久才能顶上业务中断时间变长。我实践下来的经验是10~15 秒比较合适。cluster-require-full-coverage这个参数默认值是yes但我在生产环境一定改成no。为什么默认情况下只要集群中有一个槽位没有可用节点服务整个集群就会拒绝所有读写请求返回CLUSTERDOWN。假如你有 3 个主节点其中一台机器彻底宕机且没有从节点能接管那它负责的约 5461 个槽位全部处于不可用状态。此时若cluster-require-full-coverage yes另外两台机器即使完全健康也会停止服务。这对线上业务来说就是一个人感冒全公司放假太恐怖了。改成no后槽位缺失只影响那部分 key其余节点照常工作能保住大部分业务可用。cluster-config-file这个文件也很关键。节点启动后会把自己看到的集群状态节点 ID、槽位分配、主从关系写入这个文件。注意不要手工修改它节点之间会自动同步。我见过有人为了微调槽位手动改了 nodes.conf结果集群脑裂数据全乱最后只能重建。3. 从单机到集群完整搭建与验证3.1 主从复制模式的基础搭建如果你现在还在用单机 Redis可以先从最基础的主从复制模式过渡。虽然这不是集群模式但主从复制是集群里每个分片内部的骨架。集群的每个主节点下面挂一个从节点从节点就是通过主从复制机制实时同步主节点的数据的。搭建主从复制很简单在从节点的配置里加一行replicaof 192.168.8.10 7000意思是让当前节点成为 192.168.8.10:7000 的从节点。启动后从节点会全量同步主节点的数据之后主节点的每次写操作都会以流式协议同步给从节点。这里多说一句我在搭建生产主从时强烈建议给主从都开 AOF 持久化而不是只用 RDB 快照。原因是主从切换时如果从节点的数据落后于主节点RDB 全量同步的代价非常大而 AOF 增量同步的粒度更细丢数据的窗口更小。当然开启 AOF 也有代价就是每秒可能有一次fsync性能会有些损耗。折中方案是设置appendfsync everysec既保证磁盘 IO 不会爆炸又能在极端情况下最多丢 1 秒数据。但主从复制本身解决不了一个问题如果主节点宕机从节点只读不会自动转正成为主节点。这就是哨兵模式要干的活了。不过到了集群模式这个功能被内置了主节点挂了群内会投票选举把从节点晋升为新主不需要外部哨兵介入。3.2 集群创建与槽位分配原理搭建集群最省事的方式就是用 Redis 自带的命令行工具redis-cli --cluster create。假设我有 3 台机器每台上有两个实例总共 6 个节点执行下面这条命令即可一键创建redis-cli --cluster create \ 192.168.8.10:7000 \ 192.168.8.11:7002 \ 192.168.8.12:7004 \ 192.168.8.10:7001 \ 192.168.8.11:7003 \ 192.168.8.12:7005 \ --cluster-replicas 1命令里前三个节点会被设为主节点后三个节点分别作为前三个的从节点--cluster-replicas 1表示每个主节点配 1 个从节点。执行过程中它会打印一个规划表让你确认槽位分配方案输入yes就正式开始分配。创建完成后我们可以用cluster info查看状态redis-cli -p 7000 cluster info正常输出里会有cluster_state:ok和cluster_slots_assigned:16384。看到这个就说明 16384 个槽位已经全部分配完成集群已经处于可用状态。槽位分配的原理其实不复杂。每个 key 通过CRC16(key) % 16384计算出槽位然后根据槽位找到对应的主节点。Redis 官方在创建集群时会把 16384 个槽位分成三等份三个主节点各拿 5461 个左右。随着节点增加或减少可以通过reshard命令动态迁移槽位这个过程在集群扩容时非常重要。有一个很有意思的问题为什么是 16384 而不是 65536这个问题面试也常问。首先CRC16 的进位参数设计时对 256 取模更高效16384 是 2 的 14 次方其次节点之间每秒钟都会同步一次槽位信息槽位越多心跳包体积越大。16384 在性能和扩展性之间刚好是个平衡点官方实测过16384 个槽位的 bitmap 大约 2KB65536 个槽位就要 8KB网络开销多出好几倍完全没必要。3.3 客户端直连与路由验证集群创建完成后直接用普通的redis-cli连上去试一次写操作你会发现一个意外现象redis-cli -p 7000 set name zhangsan这条命令可能会报错MOVED 5798 192.168.8.11:7002然后提示你客户端要使用集群模式。因为普通模式下客户端把请求发给 7000 端口但这个 key 的槽位并不归 7000 管于是 Redis 返回了MOVED重定向信息告诉客户端你应该去找哪个节点。正确姿势是加上-c参数cluster 模式让客户端自动跟随重定向redis-cli -c -p 7000 set name zhangsan-c模式下 Redis 客户端会自动解析MOVED或ASK重定向把请求发到正确的节点。这背后其实反映了集群的一个关键要点集群不是单点连接客户端必须支持集群协议。像 Java 的 Jedis Cluster、Lettuce 集群模式Python 的 redis-py cluster 模式Go 的 go-redis cluster 模式这些客户端库都内置了槽位映射和重定向逻辑。实际生产环境里强烈不建议用redis-cli直接操作集群而是用语言对应的集群客户端。比如 Java 里我一般推荐 Lettuce因为它基于 Netty能够在多个节点之间复用连接池性能很好。Jedis Cluster 虽然成熟但 Jedis 本身是同步的高并发下连接池容易打满除非你做了连接池调优不然并发一上来就会成为瓶颈。验证集群分布是否正确可以用cluster nodes看节点状态redis-cli -p 7000 cluster nodes输出里能看到每个节点的 ID、IP:端口、角色master/slave、负责的槽位范围。正常情况是 3 个 master 各管一大段槽位3 个 slave 通过slaveof对应到对应 master。还有一个我常用的验证方法写一个批量脚本循环向集群写 1000 个 key然后分别统计 7000、7002、7004 三个主节点上的 key 数量。如果数据是均匀分布的每个节点大约有 300 多个 key这就说明集群的分片运转正常。如果出现某个节点一个 key 都没有说明你的业务 key 存在明显的热点前缀下一步就要考虑 hash tag 优化了。4. 集群运维中的高频问题与排查实录4.1 常见问题速查表运维集群一年多我整理了一份高频问题速查表。有些问题刚遇到时一头雾水后来把原理搞懂了就觉得非常简单。报错或现象根因解决思路MOVED XXX ip:portkey 的槽位在其他节点客户端未使用集群模式改用-c或集群客户端CLUSTERDOWN The cluster is down有槽位不可用且cluster-require-full-coverage yes修改配置为no或尽快恢复故障节点ASK xxx ip:port槽位正在迁移过程中正常现象客户端自动重定向即可不要慌写入频繁失败CLUSTERDOWN新主节点被选举出来但槽位信息未完全同步等待节点握手完成检查日志集群创建时报Node X is not empty节点上已有旧数据或旧配置清空该节点的 AOF/RDB 和 nodes.conf重新初始化连接集群偶发超时客户端连接池过小或cluster-node-timeout设置过短调大连接池调整超时时间到 10~15 秒从节点切换后数据不见了一部分从节点复制主节点时有短暂延迟无法完全避免用 AOFeverysec减小丢失窗口4.2 跨槽位操作与 Pipeline 陷阱集群模式下最容易被忽视的坑就是多个 key 的操作不能跨槽位。比如单机 Redis 里你可以随意执行mget key1 key2 key3但在集群模式下如果 key1、key2、key3 的槽位不同这条命令就会直接报错CROSSSLOT Keys in request dont hash to the same slot。同理mset、SUNIONSTORE、RENAME、BLPOP多 key 操作都会受到限制。解决思路是使用 Redis 的hash tag机制。key 里只要包含{}Redis 在计算槽位时就只取花括号里的内容做哈希。比如set user:{10001}:name zhangsan set user:{10001}:age 18 set user:{10001}:address beijing这三个 key 的槽位完全一样因为它们都算{10001}的 hash。这样你就可以对它们执行跨 key 操作了。hash tag 用得好的话还能把某个业务维度相关的数据全部路由到同一个分片减少跨节点访问延迟。另一个高频坑是Pipeline 与集群的配合。很多新手在单机 Redis 上习惯了 Pipeline 批量写入搬到集群后发现客户端直接报错或不支持。原因很简单Pipeline 是建立在一个 TCP 连接上的连续请求流而集群模式下 key 分散在不同节点客户端库必须先把每个 key 的槽位算出来再按节点分组、分别建立 Pipeline 管道。以 Java Lettuce 为例集群模式下 Pipeline 通常需要手动分组。我一般这样做先通过ClusterSlotHashUtil或者客户端内置的getSlotForKey方法把一批 key 按槽位分组再按组分别执行批量操作。比如一次任务要更新 1 万个 redis key我会写一个分段循环一个节点一段数据并发请求实测效率比单机全量 Pipeline 稍稍下降但比逐个 set 快了几十倍。4.3 超时误判与脑裂风险集群模式的高可用依赖于节点之间的心跳检测。节点之间定期发送 PING/PONG 消息如果超过cluster-node-timeout没收到回应就会认为对方失联。这里就有个微妙的场景**一个主节点实际上没挂只是因为 GC 停顿或网络抖动暂时无法响应。**此时从节点发起了故障转移把主节点给替换掉了。等原来的主节点回过神来发现自己已经不是主了集群里出现了双主的短暂脑裂局面。Redis 集群的脑裂处理机制是原主节点发现自己被降级后会尝试恢复主从关系。但如果它在这段时间里继续接受了外部写入这些数据就会因为无法同步到新主而永久丢失。这是我遇到过最麻烦的数据一致性问题到现在也没办法彻底根治只能降低发生概率。我的策略是把cluster-node-timeout调到 15 秒以上避免微小抖动触发切换同时在客户端做重试超时后重新连接尽量缩短原主节点以为自己还是主的窗口。如果你对数据一致性要求极高建议在业务层加一层幂等校验。比如写 Redis 前先记录一条操作日志万一发生脑裂通过日志校对把丢失的数据手动补回去。虽然这很原始但在关键业务里多一道保险总是没错的。5. 集群模式下的分布式锁与缓存治理5.1 分布式锁在集群下的实现技巧单机 Redis 分布式锁比较简单setnx key valueexpire key timeout原子性问题在老版本里需要 Lua 脚本组合新版本直接用一条命令SET key value NX PX 30000即可。但到了集群模式分布式锁有一点微妙如果锁的 key 被路由到主节点 A此时 A 挂了从节点 B 晋升为新主但 A 上的锁数据可能还没来得及同步给 B那么另一个客户端就能在新的主节点上成功加锁破坏了互斥性。这就是著名的 RedLock 算法要解决的问题。RedLock 的思想是同时在 5 个独立的 Redis 节点上加锁只要超过半数成功就算加了锁。这样即使某几个节点同时故障也不会出现两个客户端各拿一把锁的局面。不过我可以坦白说RedLock 在生产环境的使用一直有争议它的实现复杂对时间同步的依赖很高而且 5 个独立节点的运维成本不低。我的实际建议是如果你们用的是集群模式且对分布式锁的可靠性要求不是极端苛刻优先使用客户端库内置的集群锁实现比如 Redisson 的RLock。Redisson 在集群模式下会自动处理槽位和锁续期使用体验远比手写 RedLock 舒服。如果确实需要 RedLock记住必须满足三个条件加锁必须设置过期时间、必须使用独立节点、释放锁时必须校验 value 防止误删别人的锁。我见过一个很典型的误删锁事故一个线程拿到锁后业务处理超时锁自动过期了另一个线程拿到锁写数据前一个线程处理完毕执行del key把后一个线程的锁给删了。解决起来也很简单把 value 设成唯一随机值UUID删除前 Get 一下比对再用 Lua 保证比对和删除的原子性。-- 安全释放锁的 Lua 脚本 if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end5.2 缓存穿透、击穿、雪崩在集群场景的治理集群模式把数据分散到了多个节点但缓存穿透、击穿、雪崩这些问题并不会因为上了集群就自动消失相反它们会在更大规模下被放大。我先说结论集群解决的是性能和容量问题治理策略还是要在应用层做好。缓存穿透是最典型的查询一个不存在的 key每次都打到了数据库数据库压力剧增。集群模式下多个分片都会收到这样的无效请求。常规解法是布隆过滤器把所有存在的业务 ID 提前加载到布隆里查询前先走布隆判断如果布隆说不存在直接返回空。我用过的实现是 Redisson 的 RBloomFilter内存开销很小千万级数据也就占几十 MB集群模式下每个分片放一份就够。缓存击穿是指某个热点 key 在过期瞬间大量请求同时落到数据库。单机时代靠互斥锁重建缓存就能解决集群下要小心锁的粒度。如果热点 key 分散在多个分片你就得对每个分片分别加锁。一个简单的方案是在应用层用一个进程内锁同一个业务进程内同时只允许一个线程去重建缓存其他线程等待。这种方案能挡住应用层 90% 的击穿风险因为重建缓存的速度极快进程内锁通常几毫秒就释放了。缓存雪崩则是大量 key 在同一时间过期导致数据库被一波猛烈的请求打垮。我踩过一次很深的坑当时批量导入用户数据给所有 key 统一设置了 24 小时过期时间第二天凌晨瞬间大量 key 同时过期数据库 CPU 直接被打到 100%。之后我养成了一个习惯过期时间一定要加随机偏移int expireTime 3600 new Random().nextInt(600);让 key 的过期时间在 1~1.2 小时之间波动这样即使有大规模缓存重建请求也会错开访问时间。集群模式下多个分片同时雪崩的概率也大大降低因为数据分散了同一时间过期的 key 也相对分散。5.3 集群下的大 key 治理与持久化策略最后一个必须一提的话题是大 key。单机 Redis 大 key 问题已经很折磨人集群模式下大 key 会让单个分片数据倾斜。比如某个客户的数据量特别大所有 key 都带同样的 hash tag就可能导致一个分片内存膨胀到接近上限而其他分片还很空闲。大 key 的判定标准我一般看两个维度字符串超过 10KB或者集合类超过 5000 个元素。遇到大 key分词条拆分是主流做法。比如一个 hash 里存了 100 万条用户详情可以按用户 ID 分段拆成多个小的 hash或者改成 hash 的 field 拆分到多个 key。这个过程没有统一公式关键看业务怎么读但原则是一致的单个 key 的操作时间必须控制在毫秒级别绝不能出现秒级阻塞。持久化策略这块集群模式下每个节点独立持久化我推荐 AOF 设置everysec。RDB 虽然恢复快但它存在数据丢失窗口而且在大数据量下RDB 触发时 Fork 子进程复制内存页表极端情况下会导致主线程卡顿几十毫秒。AOF 配合everysec则把丢数窗口控制在 1 秒内对大多数业务来说完全可接受。备份恢复也要重新审视。单机 Redis 备份容易直接停服务拷 RDB 文件就行。集群模式备份就比较麻烦你不能只备份一个节点因为每个节点的数据只是分片之一。我目前的备份方案是在业务低峰期用BGSAVE依次触发各个主节点的 RDB 快照然后把所有节点的 dump.rdb 文件统一收集打包。恢复时先按旧集群结构恢复节点再通过redis-cli --cluster fix修复槽位关系不能直接启动一台 Redis 然后导入数据。写在最后的实战感悟我个人在实际运维 Redis 集群的这两年里最大的体会是集群模式不是拿来即用的一锤子买卖它的核心价值在于帮你在数据增长时有一张逃生的底牌。不要等到单机内存告急才去研究集群方案那时候你需要考虑的就是数据迁移、灰度切换、业务缓冲太多细节叠加在一起很容易出问题。我建议任何准备上集群的团队先把集群的槽位机制、故障转移原理、客户端路由方式吃透再在测试环境完整演练一遍崩溃切换最后再动生产。另外一个小技巧日常巡检一定要看cluster_state和节点角色的变化一旦发现从节点连续几天没有同步到最新数据说明主从复制链路可能有问题趁早处理不要坐等主节点宕机。Redis 集群这套东西初学觉得复杂但是当你真正靠它扛住了一次大促流量冲击或者一次数据量翻倍时你就会觉得当初的投入全部值得。
返回列表