尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Redis Cluster哈希槽深度拆解:从16384个槽到MOVED/ASK迁移机制

Redis Cluster哈希槽深度拆解:从16384个槽到MOVED/ASK迁移机制 最近准备跳槽的朋友应该都有同感十家做大数据的公司里至少有八家面试官会往Redis Cluster上问。而哈希槽这个东西几乎是绕不开的坎。市面上讲Redis哈希槽的文章不少但大多停留在16384个槽、CRC16取模这种表面解释真到面试官追问一句为什么是16384不是65536迁移过程中客户端到底怎么找到数据很多人就卡壳了。这篇文章不打算复述文档而是站在面试和实操两个角度把哈希槽从设计动机到迁移细节完整拆一遍。想搞懂Redis分片原理的、准备大厂面试的、以及正在维护集群的都可以参考。1. 哈希槽到底要解决什么问题1.1 从数据分片说起Redis作为缓存/存储中间件用久了一定会撞上单机瓶颈内存不够了、写并发上不去了、单节点故障影响面太大。这时候最朴素的思路就是多搞几台机器把数据拆开放也就是分片。分片方案看起来简单核心问题只有一个一个key到底该放到哪台机器上。最古老的做法是取模hash(key) % NN是节点数。这个方案在节点稳定的时候很好用简直简单到不需要思考。但只要集群里加一台机器或者挂一台机器N变了几乎所有的key映射关系都会变缓存会瞬间大面积失效。流量直接打到数据库上这在生产环境叫做缓存雪崩能把下游数据库打挂。后来大家用一致性哈希把key哈希到一个环上每个节点负责环上的一段区间。节点变化时只影响相邻区间确实解决了取模法全量失效的问题。但一致性哈希有个老毛病数据分布不均匀。明明有10台机器可能有个节点分到的数据特别多那个节点就是热点。引入虚拟节点可以缓解但虚拟节点的数量、环上的位置都需要调优运维成本上来了分布效果却依然取决于运气。更麻烦的是一致性哈希对数据迁移的掌控力很差你很难说清楚某个节点到底要迁走哪些数据、迁走多少。哈希槽的思路跟它们完全不在一个维度先固定地把整个keyspace切成16384个槽然后把这些槽分配到各个节点上。key先通过哈希映射到槽槽再映射到节点。节点变化时只需要迁移涉及的那部分槽影响范围精确可控。这就是哈希槽第一个厉害的地方把数据分布和节点管理解耦了。1.2 两层映射到底解决什么很多人第一次看哈希槽会觉得奇怪为什么要多一层槽的映射直接让key映射到节点不好吗这里的关键在于key到节点的映射如果是一步到位的那节点一变化所有key的映射关系全部要重算这就是取模法的死穴。哈希槽引入中间层之后key永远只映射到一个0到16383的整数这个计算跟集群的拓扑没有任何关系而槽到节点的分配关系是可以在运行时动态调整的。用银行柜台来类比16384个柜台窗口是固定的柜员节点可以增加或减少窗口和柜员的对应关系随时可以调。办业务的客户只认窗口号不认柜员。柜员走了只需要把他的窗口重新分配给另一个柜员客户体验完全不受影响。从实操角度看这个设计带来的一个直接好处是客户端的路由逻辑非常清晰。请求一个key的时候两步走——先用CLUSTER KEYSLOT算出槽号这一步本地就能算纯函数再查一下这个槽当前归属哪个节点这一步需要集群的槽映射信息两步一拼就知道该访问谁了。就算槽映射变化客户端也只需要更新槽到节点的对应表而key到槽的计算永远不需要变。2. 从CRC16到16384两个关键数字的由来2.1 一个key是怎么变成槽号的Redis计算key属于哪个槽用的是CLUSTER KEYSLOT命令。集群模式下每个key先经过CRC16算法计算出一个16位的校验值然后对16384取模。因为16384恰好是2的14次方所以代码里直接用位运算CRC16(key) 16383效率比取模更高。# 在任意集群节点上执行 127.0.0.1:6379 CLUSTER KEYSLOT user:1001 (integer) 10820这里有个非常实用、面试也常考的小机制hash tag哈希标签。如果key里包含{...}这样的花括号结构CRC16只计算花括号里的部分。比如user:{1001}:profile和order:{1001}:list这两个key看起来完全不一样但因为hash tag部分都是1001它们的槽号必然相同。127.0.0.1:6379 CLUSTER KEYSLOT user:{1001}:profile (integer) 5798 127.0.0.1:6379 CLUSTER KEYSLOT order:{1001}:list (integer) 5798这个机制的设计初衷是为了解决多key操作的跨节点问题。Redis Cluster模式下像MSET、MGET、Pipeline这类涉及多个key的命令如果key不在同一个槽里会直接报CROSSSLOT错误。通过hash tag把相关的key强制放进同一个槽就能在集群模式下继续使用多key操作。但hash tag也是个陷阱如果业务里所有key都共用同一个标签比如{user}:xxx那大量key会全部挤进少数几个槽里数据分布直接歪掉热点问题比不用hash tag还严重。所以hash tag一定要用在确实需要共槽的key上而且标签的取值要有足够的离散度。2.2 为什么偏偏是16384不是更大或更小这个问题几乎是Redis面试必考答得好不好直接体现你对集群协议的理解深度。先说结论16384是心跳包网络开销和集群规模上限之间的一个平衡点。Redis Cluster的节点之间用Gossip协议通信每个节点会定期向其他节点发送PING/PONG消息消息里要带上本节点视角下的槽分配信息。为了压缩体积槽位置信息是用bitmap表示的16384个槽就是16384个二进制位换算下来是2KB。如果Redis把槽数设计成655362的16次方那每个心跳包光是槽位信息就要占8KB。8KB和2KB看起来差距不大但在集群环境里这个成本要乘以节点数量。假设有100个节点每个节点每秒钟要把心跳发给其他所有节点那多出来的6KB会在整个集群的网络里放大100倍。节点越多这个开销越夸张。Redis官方给过参考集群节点数一般不会超过1000个16384个槽在1000个节点的规模下完全够用。反过来说如果节点数太少比如3个节点分16384个槽每个节点分到的槽数量依然很充足分布不会出现明显倾斜。这个数字是经过了实际规模推演的。另外还有一个容易被忽略的细节CRC16算法本身生成的是16位输出取值范围0到65535。如果把槽数设为65536理论上可以直接拿CRC16的结果当槽号不需要取模。但就像前面说的心跳包体积翻4倍这个代价太大了。设为16384用 16383做掩码运算既保留了CRC16的分布均匀性又控制了网络开销这是工程权衡的典型例子。3. 槽的分配、MOVED重定向与在线迁移3.1 把16384个槽分给节点自动与手动搭建Redis Cluster的时候槽的初始分配有两种方式。最常用的是自动分配用官方提供的集群管理工具redis-cli --cluster create \ 192.168.1.10:6379 192.168.1.11:6379 192.168.1.12:6379 \ 192.168.1.13:6379 192.168.1.14:6379 192.168.1.15:6379 \ --cluster-replicas 1这个命令会自动把16384个槽平均分配给三个主节点然后给每个主节点挂一个从节点。命令执行完可以用CLUSTER SLOTS查看槽的归属情况127.0.0.1:6379 CLUSTER SLOTS 1) 1) (integer) 0 2) (integer) 5460 3) 1) 192.168.1.10 2) (integer) 6379 ...输出结果里每一行是一个槽区间包含起始槽、结束槽、以及负责这个区间的主节点地址。这个命令也是客户端获取槽映射关系的主要途径。手动分配用的命令是CLUSTER ADDSLOTS和CLUSTER DELSLOTS这种方式更底层需要自己精确控制每个节点负责哪些槽。生产环境一般用不上除非你有非常特殊的场景比如想给某台高配置机器多分配一些槽或者做小范围的槽调整。手动分配的坑在于分配完之后如果节点重启槽信息是从配置和持久化文件里恢复的如果分配记录没写对容易出问题。3.2 MOVED与ASK两种重定向的底层逻辑Redis Cluster的客户端和普通Redis客户端不一样它内部会维护一张槽-节点的映射表。正常情况下客户端算完槽号后直接去找对应节点一步到位。但集群的槽映射不是一成不变的节点增减、槽迁移都会让它失效。这时候就轮到MOVED和ASK这两个重定向机制出场了。MOVED的含义是这个槽已经永久性地归别人管了。假设客户端算出一个key在槽100它根据缓存的映射去找节点A但槽100实际上已经被迁移到了节点B。节点A收到请求后发现自己的槽列表里没有100就会返回一个MOVED错误告诉客户端槽100现在在节点B地址是xxx你以后直接找它。客户端收到MOVED后要做两件事把这次的请求重新发到节点B同时更新本地缓存把槽100的归属记成B。如果客户端不更新缓存下次请求还是会打到A反复触发重定向性能损耗就大了。ASK则完全是另一种场景它发生在槽迁移过程中。迁移过程中源节点A里的槽100正在逐渐把数据搬给目标节点B但还没搬完。这时候客户端带着key来问A要数据A发现这个key已经搬到B那边了就会返回ASK错误同时附带一个临时IP端口告诉客户端你要的key现在在B你跟着这个地址去拿一次。注意这里的区别ASK只对当前这一次请求生效客户端不能因为收到ASK就更新本地缓存因为迁移还没完成下次这个key可能又换位置了。用一个生活化的类比MOVED是你朋友搬家了以后都去新地址找他ASK是你朋友正在搬家今天去新地址能拿到人但地址还没定下次再问。这个区别是面试里最容易踩的坑。3.3 槽迁移的完整过程扩容不只是一条命令在集群里执行redis-cli --cluster reshardRedis会问你迁多少个槽、从哪些源节点迁、目标节点是谁。回答完之后迁移就开始在后台运行了。但这条命令背后发生的事情远不止把key从A搬到B这么简单。迁移的最小单位是槽但槽只是一个逻辑概念真正要搬的是槽里的所有key。源节点A会把目标槽的状态标记为migrating目标节点B把槽的状态标记为importing。然后A开始逐批地把这个槽里的key发给B每一批通过MIGRATE命令传送默认一次最多迁移100个key直到这个槽在A上完全为空A才会把槽的归属从自己的槽列表里摘掉整个迁移才算完成。迁移期间客户端的老映射缓存依然指向A。如果请求的key还没搬走A直接正常返回数据如果key已经搬走了A返回ASK把客户端引导到B。这就实现了在线迁移扩容全程不需要停机业务流量不断。实际操作中真正让人头疼的问题出在迁移速度上。如果槽里有超大key比如几MB甚至几十MB的字符串或者一个巨大的Hash结构MIGRATE命令执行期间会在源节点上阻塞其他命令因为Redis是单线程模型。这种阻塞会导致客户端请求排队表现出来就是热词里那个经典的报错redis command timed out。我在一次扩容时就遇到过一个写满了几百万字段的Hash key迁移它的时候整个节点卡了几秒钟线上告警直接拉满。后面梳理了这个大key并拆成多个小key才把迁移时间压下来。4. 面试问答的踩分点这样回答差别很大4.1 高频追问与答题要点面试官问哈希槽基本会围绕下面这几个问题层层推进。我把关键踩分点列出来供大家对照检查自己的答案深度。问题一为什么Redis Cluster不用一致性哈希初级回答因为哈希槽数据更均匀。这个答案只能算及格。加分回答要落到两个层面一是哈希槽的分布是确定性算法加固定槽位不依赖环上的随机落点CRC16的均匀性经过验证分布更可控二是哈希槽支持精细化的槽迁移运维层面可以精确控制哪部分数据搬走、搬到哪而一致性哈希虽然节点变化影响范围小但定位到具体哪些数据需要迁移非常模糊虚拟节点也只是让分布更均匀并没有改善迁移的可控性。问题二为什么槽数是16384这块在前面已经拆过一遍面试时把三个要点答全就够16384是2的14次方可以用位掩码计算槽号提升效率心跳包用bitmap表示槽信息只需2KB换成65536要8KB节点多了网络开销被放大不划算Redis集群节点规模在1000以下16384槽数在这个规模下有足够的分布精度。如果还能补一句槽数跟CRC16的16位输出范围没有强绑定是工程权衡的结果说明你确实理解了这个设计的取舍。问题三MOVED和ASK的区别区分核心在于永久还是临时。MOVED表示槽归属已永久变更触发客户端更新槽映射缓存ASK表示迁移过程中的临时访问权限只对当前请求生效客户端不得更新缓存。如果面试官再深挖一句为什么要有这种区分回答是迁移过程是渐进的如果客户端在迁移完成前就更新了槽映射迁移完成后映射又变导致缓存频繁抖动所以用ASK这种一次性重定向来做过渡。问题四哈希槽能让数据绝对均匀吗不能。哈希槽只在算法层面保证key的分布均匀业务层面依然可能出现热点。一种情况是key的字典序相近导致CRC16结果集中但这种情况非常少见更常见的是hash tag使用不当大量key落到同一个槽还有就是单纯的少数热点key被高频访问这时候不管key分布在哪个槽负载压力都会落在对应节点上。面试时主动讲出这些边界情况比单纯背结论更有说服力。4.2 结合业务场景的实战题很多面试官喜欢在原理题之后跟一个业务题看你能不能把原理落到场景里。我遇到的比较典型的几道例子一你的分布式锁的key在集群里是怎么分布的锁的key比如lock:order:1001、lock:order:1002是天然分散的不同订单的锁key经过CRC16计算后会落到不同的槽分布在集群各个节点上。这对分布式锁的实现是好事锁请求的压力被分散到多个节点不容易出现单节点瓶颈。但如果你的锁key全都用了同一个hash tag比如lock:{order}后面接不同后缀那这些锁key全部会落到同一个槽、同一个节点锁的并发压力就全压在一台机器上了。例子二业务需要一次性操作多个key但集群报CROSSSLOT错误怎么解决答案是hash tag。把需要共槽的key设计成统一格式比如order:{1001}:info和order:{1001}:items让它们共享同一个hash tag。但要注意hash tag的取值必须是业务天然分散的字段比如订单号、用户ID不能是所有key共用的常量。否则就是给自己埋雷数据倾斜比跨槽错误还难处理。例子三集群扩容后热点数据还在老节点怎么办哈希槽解决了数据分布均匀的问题但解决不了访问热度的不均衡。这种情况只能把热点key拆细。比如原来一个key是一整个用户的购物车可以拆成cart:{uid}:base、cart:{uid}:items多个key通过hash tag保证共槽同时把value拆分降低单key热度。或者引入本地缓存层把热点读操作挡在Redis之前。这些都属于缓存治理的常见手段跟哈希槽配合使用才能妥善处理好热点。5. 实操记录本地搭建3主3从集群验证哈希槽5.1 Docker环境快速起集群理论讲再多不如亲手跑一个集群。本地起Redis Cluster最省事的方式是用Docker Compose。下面是三主三从不带密码的最小配置version: 3 services: redis-6379: image: redis:7.0 container_name: redis-6379 command: redis-server --port 6379 --cluster-enabled yes --cluster-config-file nodes.conf --appendonly yes networks: redis-cluster-net: ipv4_address: 172.28.0.2 redis-6380: image: redis:7.0 container_name: redis-6380 command: redis-server --port 6380 --cluster-enabled yes --cluster-config-file nodes.conf --appendonly yes networks: redis-cluster-net: ipv4_address: 172.28.0.3 redis-6381: image: redis:7.0 container_name: redis-6381 command: redis-server --port 6381 --cluster-enabled yes --cluster-config-file nodes.conf --appendonly yes networks: redis-cluster-net: ipv4_address: 172.28.0.4 redis-6382: image: redis:7.0 container_name: redis-6382 command: redis-server --port 6382 --cluster-enabled yes --cluster-config-file nodes.conf --appendonly yes networks: redis-cluster-net: ipv4_address: 172.28.0.5 redis-6383: image: redis:7.0 container_name: redis-6383 command: redis-server --port 6383 --cluster-enabled yes --cluster-config-file nodes.conf --appendonly yes networks: redis-cluster-net: ipv4_address: 172.28.0.6 redis-6384: image: redis:7.0 container_name: redis-6384 command: redis-server --port 6384 --cluster-enabled yes --cluster-config-file nodes.conf --appendonly yes networks: redis-cluster-net: ipv4_address: 172.28.0.7 networks: redis-cluster-net: driver: bridge ipam: config: - subnet: 172.28.0.0/24容器起来之后进入任意一个容器执行集群创建命令。注意要用容器的IP而不是localhost因为Redis Cluster的节点间通信需要互相能访问到对方的IP。# 进入容器 docker exec -it redis-6379 bash # 创建集群--cluster-replicas 1 表示每个主节点配一个从节点 redis-cli --cluster create \ 172.28.0.2:6379 172.28.0.3:6380 172.28.0.4:6381 \ 172.28.0.5:6382 172.28.0.6:6383 172.28.0.7:6384 \ --cluster-replicas 1创建过程中会打印出一份分配计划显示每个主节点分到多少槽、从节点挂在哪个主节点下面确认无误后输入yes执行。这里建议大家留意一下输出信息里的槽区间分配比如第一个主节点分到0-5460第二个主节点分到5461-10922第三个主节点分到10923-16383这就是哈希槽在做初始分配。5.2 四个关键场景验证集群起来之后先用CLUSTER INFO确认集群状态是ok然后依次验证几个关键行为。验证一通过CLUSTER KEYSLOT验证key分布# 连上任意节点 redis-cli -h 172.28.0.2 -p 6379 172.28.0.2:6379 CLUSTER KEYSLOT user:1001 (integer) 8485 172.28.0.2:6379 CLUSTER KEYSLOT user:1002 (integer) 7166 172.28.0.2:6379 CLUSTER KEYSLOT user:1003 (integer) 1585三个user key得到的槽号完全不同说明在正常无hash tag的情况下CRC16算法会让key尽量均匀地分散到不同的槽。这对理解为什么哈希槽能做到均匀分布有非常直观的体感。验证二触发MOVED重定向直接往172.28.0.2这个节点写一个不属于它的key。先算一下槽号# 在 172.28.0.2 上执行 172.28.0.2:6379 CLUSTER KEYSLOT app:book:1 (integer) 12158 # 反查这个槽在谁那里 172.28.0.2:6379 CLUSTER SLOTS如果12158这个槽不在172.28.0.2手里执行172.28.0.2:6379 SET app:book:1 hello (error) MOVED 12158 172.28.0.4:6381这个报错就是重定向机制在起作用。如果使用官方redis-cli的-c参数客户端会自动跟随重定向你不会看到这个错误但在很多客户端工具和自研访问层里这个错误需要显式捕获处理。所以触发一次MOVED对理解客户端路由非常有帮助。验证三hash tag让不同key落在同一槽172.28.0.2:6379 CLUSTER KEYSLOT order:{9527}:info (integer) 7531 172.28.0.2:6379 CLUSTER KEYSLOT order:{9527}:log (integer) 7531 172.28.0.2:6379 CLUSTER KEYSLOT order:{9528}:info (integer) 11960同一个订单号9527下的info和log两个key槽号相同而另一个订单号9528则落到了不同槽。这验证了hash tag只会对花括号里的内容做哈希也意味着你确实可以通过hash tag把需要共槽的key安排在一起。验证四用可视化客户端查看槽分配本地装一个Another Redis Desktop Manager热词里搜得到的那个连接任意一个集群节点的IP和端口它通常会认出集群模式并展示节点列表和槽分布情况。在界面上可以看到16384个槽被切成了三段分别归属三个主节点每个主节点下面挂一个从节点。在生产环境排查数据分布不均的问题时这种可视化视图比命令行直观很多。5.3 迁移演示与日志排障接下来做一次真实迁移把槽从172.28.0.3:6380迁一部分到172.28.0.4:6381。# 在集群任意节点上执行 redis-cli --cluster reshard 172.28.0.2:6379按提示输入要迁的槽数量比如100个输入目标节点ID这里填172.28.0.4:6381的node ID通过CLUSTER NODES可以查到然后输入源节点ID填172.28.0.3:6380的node ID最后输入done确认。执行后Redis会显示迁移进度。迁移期间如果持续对涉及迁移槽的key做读写可以在客户端观察到ASK重定向。这时候如果想知道迁移有没有问题看节点的Redis日志是关键docker logs redis-6380 --tail 200日志里如果出现大key迁移耗时过长的记录或者大量MIGRATE命令导致的慢日志就要考虑拆key了。热词里那个redis command timed out的报错我在生产环境排查的经验是先用CLUSTER SLOTS确认槽映射是否在频繁变化再检查慢日志里是不是有MIGRATE或大key操作把事件循环卡住最后看客户端连接池的连接数是不是被打满了。这个排查路径在集群运维里很通用记下来能省很多时间。6. 避坑清单与最后的一点建议我把这几年的集群维护经验和面试陪跑经历整理成一份避坑清单每条都是踩过或者见过别人踩的坑问题场景典型表现处理建议hash tag滥用大量key集中在一个槽、一个节点内存和流量双热点只在需要共槽的场景使用hash tag标签值必须有足够离散度大key迁移迁移期间节点阻塞客户端大面积超时提前用MEMORY USAGE命令排查大key拆key后再迁移客户端路由不更新收到MOVED但不更新缓存流量反复打回老节点确认客户端版本支持集群模式检查连接池配置集群节点少但槽多单节点分到的槽范围大热点集中明显适当增加节点数让每个节点分到的槽更细迁移期间不做流量控制网络带宽打满正常业务响应变慢低峰期执行迁移或使用限速参数控制MIGRATE速率说到最后我个人的体会是哈希槽这个知识点真正的分水岭不在于知不知道16384而在于能不能把为什么有槽、槽怎么分、槽怎么迁、客户端怎么应对这条链路完整地讲清楚。面试官问这个东西背后考的是你对分布式系统数据分片模型的理解Redis哈希槽只是这个模型的具象化载体。准备面试的时候如果把文档背熟但讲不出设计动机很容易被后续的追问打穿。反过来动手搭过集群、亲自触发过MOVED、被大key迁移坑过这些经验会让你在回答时带上一种非常自然的笃定感这就是面试官想看到的东西。希望这篇拆解能帮你把哈希槽的拼图拼完整。
返回列表