ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Redis集群模式全解析:从原理到搭建踩坑指南

Redis集群模式全解析:从原理到搭建踩坑指南 熟悉Redis的朋友应该都有同感单机版用起来确实爽性能高、API简单但流量一上来、数据一多单机的那点内存和单线程吞吐就成了墙。Redis集群模式正是用来撞开这堵墙的官方方案它不仅解决了容量和吞吐的横向扩展问题还顺手把高可用做了进去。这篇就把集群模式从原理到搭建到踩坑完整捋一遍适合正准备把Redis从主从升级到集群的团队也适合那些想把集群原理理清楚再去应付面试的人。1. 先理清问题集群模式到底在解决什么1.1 单机Redis的容量与吞吐天花板很多人刚接触Redis时会被它的高性能震撼单实例轻松跑到十万级QPS读写延迟基本在亚毫秒级别。但这里有个容易被忽略的现实再强的单机也是有上限的。内存方面一台物理机不可能无限插内存就算你用大内存机器RDB全量持久化时fork子进程的开销、内存碎片率、网络带宽占用都会成倍放大。CPU方面Redis 6.x之前的单线程模型让单个实例只能用一个核心就算后面的多线程IO把网络读写分摊出去真正的命令执行依然是串行的。我见过不少团队踩过类似的坑业务增长后Redis内存占用直逼30GBRDB持久化一次要好几秒期间主线程阻塞导致业务超时或者QPS冲到一定程度后单个实例的CPU率先满了连扩容都不知道怎么扩只能把热点key拆到多个独立Redis里然后自己维护一堆路由规则痛苦得很。所以当你发现单机Redis快撑不住的时候本质上是在面对三个问题数据容量不够、吞吐能力不够、某个节点挂了影响面太大。集群模式就是冲着这三个问题去的。1.2 主从复制和哨兵模式的不可扩展之困在聊集群之前得先说说为什么很多团队明明已经用了主从复制和哨兵模式最后还是得迁移到集群。主从复制解决了数据备份和读扩展的问题主节点负责写从节点分担读流量。但写入能力始终只有一个主节点在扛内存总量也还是那一份。你加再多的从节点也只是把读分散了写瓶颈和数据容量瓶颈没有丝毫缓解。哨兵模式则是在主从复制基础之上加了监控和自动故障转移。它解决了高可用问题主节点挂了哨兵可以自动把从节点晋升为主节点。可它还是一种“一个主节点对外服务”的架构同样存在写入能力单点的问题。这两种模式还有个共同的麻烦当数据量超过单机内存你是没有办法自动把数据分到多台机器上的。Redis Cluster用一句话概括就是数据自动分片每个节点只存一部分数据同时内置了类似哨兵的高可用能力。不是主从和哨兵不好而是当数据规模和写入规模上来之后它们不具备平滑扩展的基因。1.3 集群模式的核心承诺分片、高可用、去中心化集群模式的设计目标很明确官方叫法Redis Cluster核心就三件事。第一数据按槽位自动分片。整个集群把 key 空间分成 16384 个槽每个节点负责一部分槽写入哪个 key就根据 key 算出一个槽然后路由到对应节点。你不需要像以前那样自己维护key和节点的映射关系加节点、减节点时集群会把槽连同数据自动迁移一部分过去。第二内置高可用。每个分片可以配一个或多个从节点主节点挂了从节点自动升级。整个集群不需要哨兵在外面盯着节点之间自己通过内部通信协商出谁来接管。第三去中心化。集群里的每个节点都保存了全集群的槽位分布信息和节点状态信息客户端连上任意一个节点都能拿到完整路由表不存在一个“总指挥”节点。这个设计带来的最大好处就是没有单点坏处则是节点间的状态同步得靠一套内部通信机制来保证也就是后面要讲的Gossip协议。2. 集群模式核心机制拆解2.1 16384个槽位为什么是这个数Redis Cluster把key空间分成16384个槽每个key通过CRC16算法算出一个0到16383之间的数字这个数字就是它归属的槽。CRC16是一个16位的校验算法理论上能产生65536个不同的值但Redis只用了其中16384个。为什么不是65536原因有两方面一方面16384个槽对大多数规模集群已经足够精细比如你有100个主节点均摊下来每个节点负责约164个槽另一方面集群节点之间通信时每个节点需要把槽位信息广播给其他节点如果槽位数太多心跳包就要携带更大的位图网络开销会明显增加。16384个槽用2KB的位图就能表示而65536个槽需要8KB集群规模越大这个差距在频繁心跳通信时就越明显。槽位分配是一个只有运维人员能控制的事情。初始创建集群时可以平均分配也可以手动指定某个节点多分一些槽后者适合节点硬件配置不均的场景。不过大多数情况下直接用自动分配就够了。关于“为什么有的key在同一个集群里却没法在同一个事务里操作”关键也在这个槽机制上。后面第5章会详细展开。2.2 Gossip协议节点之间怎么“通风报信”集群里的节点没有中央协调者那它们怎么知道谁在线、谁挂了、槽位在哪呢靠的是节点之间持续的Gossip通信。Gossip协议你可以把它理解成办公室里的八卦传播一个人知道的消息会传给身边的几个人这几个人再分别传给他们的朋友很快全公司都知道了。Redis集群里每个节点每隔一段固定时间默认是每隔100ms就会随机挑几个其他节点发PING消息对方收到后回PONG消息。PING/PONG消息里不光有发送节点的状态还会捎带一部分它从别人那里听来的节点信息。这种传播方式的优点是状态信息最终会收敛到全集群一致而且不需要专门维护一个中心注册表缺点是消息传播有延迟某个节点的状态变化不会瞬间被所有人知道。所以Redis集群在故障检测上用了两阶段的判定机制先标记疑似下线PFAIL再经过一段时间和一番确认之后才把节点标记为真正下线FAIL并且这个判定会通过Gossip传遍集群。值得一提的还有集群总线Cluster Bus节点之间除了对外服务的16379端口端口上是637910000还会额外占用一个端口专门跑Gossip等内部通信。日常运维时如果发现Redis端口不通记得检查这个内部端口有没有被防火墙拦掉。2.3 故障转移从PFAIL到FAIL再到从节点上位故障转移的过程分三步疑似下线、确认下线、从节点竞选。当节点A发现节点B连续一段时间默认node-timeout通常是15秒联系不上它不会立刻认定B挂了而是先把B标记为PFAIL。随后通过Gossip把这个信息传播出去其他节点也会对B做同样的探测。如果集群里超过一半的主节点都认为B是PFAILB就会被确认为FAIL状态这个信息会被广播到整个集群。确认FAIL之后B的从节点就开始争取上位。这里有个微妙的设计不是所有从节点都能马上升级而是只有一个从节点能胜出。选举机制用的是类似RAFT的过半原则从节点会向所有主节点发起投票请求主节点在一个配置纪元内只能投一票得票超过一半的从节点胜出。胜出后这个从节点会把自己的配置纪元加一然后开始“篡位”把持有的槽位信息接管到自己名下并广播给全集群。客户端后续访问这些槽的请求就会被路由到新主节点上。整个过程里最幸运的是数据在从节点上本来就是热备份切换时不需要像主从复制那样全量同步所以故障转移时间很大程度上取决于选举和心跳探测的速度通常几秒到十几秒级别。2.4 哨兵模式 vs 集群模式一张表看清区别很多人会把哨兵和集群搞混其实它们解决的是不同维度的问题。我常用Excel打过一个比方哨兵更像是给某个业务配了备用人员人挂了能替补但活儿还是那一个人干集群则是把一个业务拆给多个小组干每组还有自己的备用人员。对比维度哨兵模式集群模式数据存储全部数据在每个主节点各存一份数据按槽分散在不同节点写入扩展不可扩展始终只有主节点可写可扩展整体写入能力随节点增加自动分片无需要应用自己处理内置key哈希到槽自动路由高可用哨兵本身也是一组独立进程负责监控与切换节点间自行检测与选举不依赖外置组件多key操作不受限于slot同一节点可直接操作多个key只有带相同Hash Tag的key才能保证在同一个槽运维复杂度相对简单适合中小规模相对高需要关注槽迁移与集群网络适用场景读多写少、数据总量可控数据规模大、写入量大、需要水平扩展一句话总结如果你的Redis总内存不超过单机范围优先用主从加哨兵一旦数据量和写入量单机扛不住集群模式几乎是唯一平滑的官方路线。3. 从零搭建一套三主三从集群实操篇3.1 准备6个Redis实例每个节点该配什么这里我用Docker来演示因为Docker方式最省事生产环境如果用物理机或云主机配置思路完全相同只是少了容器这层包装。先拉镜像这里用Redis 7.x作为示例docker pull redis:7然后创建集群专用的Docker网络方便容器间按主机名互通docker network create redis-cluster-net接着准备6个节点我规划了三主三从。每个节点用一个独立的容器端口从7000映射到7005。核心配置项有这几个cluster-enabled yes、cluster-config-file、appendonly yes。cluster-enabled一定要开否则节点之间根本不参与集群通信。以节点7000为例一条命令搞定docker run -d --name redis-7000 \ --network redis-cluster-net \ -p 7000:6379 \ -v /data/redis-cluster/7000:/data \ redis:7 \ redis-server --port 6379 \ --cluster-enabled yes \ --cluster-config-file nodes-7000.conf \ --cluster-node-timeout 5000 \ --appendonly yes这里注意几件事cluster-node-timeout设成5000也就是5秒测试环境方便观察故障转移生产环境建议10秒以上太短的超时容易在网络抖动时误判节点下线。appendonly yes开启AOF持久化。集群模式虽然内置复制但持久化依然要配置不然节点重启后数据全丢从节点复制也会出问题。cluster-config-file是节点自己维护的集群状态文件里面记录了节点的ID、槽位归属等信息由Redis自动读写千万别手动改。6个节点创建完成后逐一启动看到日志里输出“Cluster state changed”之类的不重要现在它们还只是6个独立的“孤岛”还没组成集群。3.2 用redis-cli --cluster create快速完成集群初始化Redis 5.0以后官方推荐使用redis-cli自带的集群管理命令不再需要单独的redis-trib.rb脚本这是个很大的进步因为之前那套Ruby脚本还得单独装Ruby环境很多团队就是卡在这一步放弃了集群。在任意一个Redis容器里执行创建命令docker exec -it redis-7000 redis-cli --cluster create \ 172.20.0.2:6379 172.20.0.3:6379 172.20.0.4:6379 \ 172.20.0.5:6379 172.20.0.6:6379 172.20.0.7:6379 \ --cluster-replicas 1命令后面的6个IP:PORT就是所有节点地址。--cluster-replicas 1表示每个主节点配1个从节点。Redis会自动把这6个节点分成三主三从并打印出一段分配方案让你确认大概长这样 Performing hash slots allocation on 6 nodes... Master[0] - Slots 0 - 5460 Master[1] - Slots 5461 - 10922 Master[2] - Slots 10923 - 16383 Adding replica 172.20.0.5:6379 to 172.20.0.2:6379 ...看到这段信息后输入yes确认它会自动把槽位分好、把主从关系建立好最后输出集群已经创建成功。这里有个实践心得IP地址在生产环境最好用固定的内网IP或域名千万不要用容器的动态IP一旦容器重建IP变了整个集群配置就得重新折腾非常痛苦。用redis-cli --cluster check检查一下redis-cli --cluster check 172.20.0.2:6379正常情况下会输出每个节点的角色、槽位范围、从节点列表最后一行显示All 16384 slots covered这表示所有槽都已分配完毕集群可用。3.3 验证集群读写数据到底落在了哪个节点集群创建好后连上任意节点读写试试。这里要注意直接执行redis-cli不带-c参数是连不上集群的因为数据可能落在了其他节点上。redis-cli -c -p 7000-c是cluster模式客户端会自动处理节点的重定向逻辑。写入一个key看看127.0.0.1:7000 set user:1001 zhangsan - Redirected to slot [5474] located at 172.20.0.4:6397 OK这个输出就很有意思了。user:1001这个key通过CRC16算出来的槽是5474不属于7000这个节点于是客户端被重定向到了负责5474槽的节点。redis-cli加的-c参数让它自动跟随重定向所以看起来像是没感知。但如果我不加-c直接setRedis会返回一个MOVED错误告诉你该去哪个节点访问。这一步其实就是集群工作的最直观展示数据不在某个固定的主节点上而是按照槽位散落在各个节点。实际业务里客户端SDK会在本地缓存槽位和节点的映射关系直接计算出某个key该去哪个节点不会每次都靠重定向。3.4 模拟主节点宕机故障转移怎么发生集群高可用最精彩的演示就是杀掉一个主节点。假设我们先把集群信息捋清楚找到某个主节点比方说172.20.0.3这个节点的ID。直接在容器层面把它停掉docker stop redis-7001停掉之后你观察一下日志或者等十几秒再查集群状态。执行redis-cli --cluster check 172.20.0.2:6379你会发现原来属于172.20.0.3的槽位现在挂到了它的从节点名下并且从节点的角色已经变成了master。整个过程没有人工介入全是靠Gossip探测和选举完成的。这里我想强调一个生产上的细节故障转移期间原本路由到宕机节点的请求会短暂失败返回CLUSTERDOWN或者连接错误持续时间大概在node-timeout加上选举时间这么长。所以上游业务一定要配好重试机制不然一次主节点切换就会造成一批报错这在线上会非常明显。4. 集群模式最容易踩的坑与排查记录4.1 MOVED和ASK客户端为什么老报错“MOVED”和“ASK”是集群模式下最常见的两条报错信息很多第一次接触集群的人看到后第一反应是“是不是集群坏了”其实都是正常的路由逻辑。MOVED表示这个key对应的槽位已经固定由某个节点负责客户端你找错门了应该转到指定节点访问。这种情况一般发生在客户端初始化的时候或者槽位分配有变化但客户端缓存了旧路由。遇到MOVED正确的处理是更新本地缓存并重发请求。ASK和MOVED长得像但含义完全不同。ASK发生在槽位迁移过程中这个槽正在从节点A迁往节点Bkey可能还在A上也可能已经被搬到B上了。当客户端访问一个迁移中的槽时节点如果认为key已经不在自己这里就会返回ASK错误并给出目标节点地址。客户端需要先发一个ASKING命令然后再发真正的请求。你现在可以把MOVED理解成“这户人家永久搬家了你记一下新地址”把ASK理解成“这户人家正在搬东西一部分在这边一部分在那边你去那边先亮个通行证”。生产环境的客户端SDK基本都把这些逻辑封装好了但我们自己写脚本用裸redis-cli时还是要靠-c参数或者手动处理。4.2 扩容、缩容时的槽位迁移不能乱来集群一个非常实用的特性是动态扩容。想把新节点加进去分两步第一步把新节点加入集群第二步把一部分槽分配给它。但很多人会忽略第二步结果新节点一直是空转的数据没有分布过去就以为扩容成功了。正确的流程是这样的先启动一个新Redis实例然后用ADD-NODE把它加进集群redis-cli --cluster add-node 新节点IP:PORT 任意已有节点IP:PORT这时候新节点状态是handshake continue到最后它还没有任何槽位。接下来要把一些槽迁移过去可以用redis-cli --cluster reshard 任意已有节点IP:PORT它会问你打算迁移多少个槽、迁移到什么节点ID、从哪些源节点迁出。按照提示填完数据就开始搬了。整个迁移过程是逐槽进行的每个槽的迁移会把槽内所有key逐个挪过去期间这个槽对外表现为迁移中状态客户端处理ASK逻辑就行。生产上我一般选择凌晨流量低峰期操作并且一次迁移不要太多槽控制每个槽的key总量否则很容易出现超时。缩容也容易踩坑。直接从一个节点上remove-node是行不通的如果它上面还有槽位集群会拒绝移除。必须先把他负责的槽迁移到其他节点然后再把节点摘除。另外如果这个节点还挂着从节点也要先处理从节点关系或者把从节点一并删掉。4.3 网络分区与脑裂集群也有脆弱时刻很多人觉得集群有了自动故障转移就万事大吉这是不对的。集群在网络分区时同样会出现脑裂风险。举个典型场景某个主节点和它的从节点分别处于两个网络分区里主节点所在的区域和集群其他主节点通信中断但和它自己的从节点可能还能通信。如果大多数主节点都认为这个主节点挂了那么它的从节点成功上位成为新主。可如果此时旧主节点还没死透网络又恢复了两个节点都认为自己是主就出现了脑裂。Redis靠两个方面来控制这个问题一是选举时要求从节点得到超过半数主节点的投票确保只有“大多数人认可”的从节点才能上位二是客户端写入时如果主节点发现自己已经联系不上集群里的大多数节点它会在一定时间内拒绝写入防止数据在分区期间被写入到即将被废弃的旧主上。即便如此脑裂依然会带来两种可能的后果一是有段时间数据不一致两个节点都接受了写入二是当旧主恢复并发现自己的槽位归属已被撤销后它会把自己切换成新主的从节点然后清空与主节点不一致的数据也就是强制丢弃脑裂期间写入的那些数据。这种后果是设计使然所以对于要求不丢数据的业务需要在上层做补偿或校验。4.4 持久化、缓存穿透和性能取舍一起捋清楚集群模式本身不会自动帮你做好持久化策略每个节点依然要单独配置RDB和AOF。我的建议是AOF至少开启everysec这是性能和安全的折中RDB可以按天做一次全量备份用来冷备和快速重建节点。集群下缓存穿透的问题会更麻烦一点。因为数据分散了一次穿透查询会打到某一个分片节点上如果某个热点key被恶意构造它只会打爆一个节点而其他节点正常表现就是集群里某台Redis CPU飙高其他节点没事。我的处理思路是布隆过滤器前置拦截加上对单个key的短时间空值缓存。缓存击穿也一样热点key过期的一瞬间大量请求打到数据库集群模式下热点key还是要靠单节点的逻辑解决比如用互斥锁重建缓存。性能取舍上集群不等于无限扩展。槽位迁移期间会占带宽和CPU多个key的批量操作受到槽位限制全局扫描类命令比如KEYS、SCAN虽然可以用SCAN慢慢遍历但跨节点汇总数据要依赖客户端自己做。所以设计之初就要接受这些约束不要用集群思维解决所有缓存问题。5. 从使用到落地客户端选型与key设计约束5.1 Smart Client和Proxy两种接入方式的体验差异集群模式下的客户端主要分两类。一类是Smart Client比如Jedis配ShardedJedisPool或JedisCluster、Lettuce、Redisson。这类客户端会在本地维护槽位和节点的映射表直接计算出key应该打到哪台节点上性能最好因为没有任何中间层。缺点是客户端要处理节点变更时的路由表更新好在主流SDK都做好了。另一类是Proxy模式比如Codis和Redis Cluster自带的集群代理。客户端只连一个代理地址由代理去做路由转发。它对业务的侵入最小但多了一层网络转发延迟略高而且代理本身要保证高可用。我个人在项目里优先选择Lettuce或Redisson这类Smart Client。原因很简单性能损耗最小而且它们对集群模式的支持非常成熟。需要说明的是不同语言的SDK对槽位缓存的更新时机不一样有些SDK在遇到MOVED错误时才刷新路由表有些会额外监听集群配置变更选型时要确认这一点不然节点变更后可能有一段时间报错。5.2 多key操作限制与Hash Tag的正确用法集群模式一个无法回避的限制是多个key只有在属于同一个槽时才能放进同一个事务、同一个Lua脚本或者用MSET、MGET一次操作。如果两个key不在同一个槽这些操作会直接报CROSSSLOT错误。怎么让多个key落在同一个槽答案是Hash Tag。Redis计算key槽位的时候如果key里包含花括号{}那么只对花括号内部的内容计算CRC16。例如user:{1001}:profile和user:{1001}:orders它们的Hash Tag都是1001所以会落入同一个槽。使用Hash Tag的核心原则是只有确实需要一起操作的key才加相同的Tag不要为了让所有key都聚集强行设计成同一个Tag那样会严重破坏数据分布的均衡性。我看过有团队为了让一段Lua脚本跑通把所有key的Tag都写成同一个固定字符串结果整个集群的写入全部集中在一两个节点上其他节点全是空的集群直接被写成了“单机”。合理的设计思路是业务维度的聚合要尽量小比如订单详情、订单列表、订单状态这些强相关的key用同一个订单号做Tag跨订单的统计类操作直接放弃集群内的原子性通过业务补偿或其他存储解决。5.3 分布式锁在集群模式下的正确打开方式单机Redis实现分布式锁很简单SET NX EX一条命令就搞定了比如set lock:order:1001 1 nx ex 30。但在集群模式下经典的单key分布式锁会暴露出一个问题如果锁所在的节点发生故障转移锁数据可能已经没了但业务还在跑。Redis官方给出的一个标准答案是RedLock算法在主节点上获取锁时同时向大多数Redis节点尝试加锁只有当超过半数的节点加锁成功并且消耗时间在锁有效期内才认定加锁成功释放时则向所有节点广播释放。现实中很多团队直接用Redisson它内置了RedLock的分布式锁实现对使用者透明。我自己的实践是普通业务用单节点锁加一个业务自身唯一ID判断就够了只有对绝对安全性要求极高的跨节点事务才考虑RedLock因为RedLock本身的复杂度、时钟依赖和性能开销都不小。另外要注意不管用哪种锁加锁时必须绑定一个唯一的请求标识这样释放锁的时候才能确认是自己加的锁避免误删别人的锁。集群模式还有一类常见问题一个大事务想拆成多个小事务或者某个key的value特别大导致槽位迁移很慢这些都属于使用层面需要提前设计的约束别等上了集群再被各种报错追着跑。先把这些边界想清楚集群其实没有想象中那么难伺候。我自己搭过几次Redis集群最深刻的体会就是cluster相关的坑大部分不是原理有多深而是很多人没认真看日志、没搞懂MOVED和ASK的重定向、没提前设计好Hash Tag。你把这些基础机制吃透了再上手集群会非常顺。日常维护时建议多利用redis-cli --cluster check做巡检关注槽位迁移是否完成、主从节点数量是否正常这些东西在异常出现前就会给你信号。
返回列表