解决 Redis Cluster 扩容故障
双11啦,为了给商品详细redis进行扩容,扩容动作就放在了今天晚上进行,很不巧,今天晚上是个多事之秋;
做了次数据恢复,做了次集群迁移,在迁移的时候还踩了个坑!
集群中有个节点挂掉了,并且报错信息如下:
------ STACK TRACE ------
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | EIP: /usr/local/bin/redis-server 0.0 . 0.0 : 6380 [cluster](migrateCloseSocket+ 0x52 )[ 0x4644f2 ] Backtrace: /usr/local/bin/redis-server 0.0 . 0.0 : 6380 [cluster](logStackTrace+ 0x3c )[ 0x45bd5c ] /usr/local/bin/redis-server 0.0 . 0.0 : 6380 [cluster](sigsegvHandler+ 0xa1 )[ 0x45cc41 ] /lib64/libpthread.so. 0 [ 0x336b60f710 ] /usr/local/bin/redis-server 0.0 . 0.0 : 6380 [cluster](migrateCloseSocket+ 0x52 )[ 0x4644f2 ] /usr/local/bin/redis-server 0.0 . 0.0 : 6380 [cluster](migrateCommand+ 0x7cd )[ 0x46744d ] /usr/local/bin/redis-server 0.0 . 0.0 : 6380 [cluster](call+ 0x72 )[ 0x424192 ] /usr/local/bin/redis-server 0.0 . 0.0 : 6380 [cluster](processCommand+ 0x365 )[ 0x428d75 ] /usr/local/bin/redis-server 0.0 . 0.0 : 6380 [cluster](processInputBuffer+ 0x109 )[ 0x435089 ] /usr/local/bin/redis-server 0.0 . 0.0 : 6380 [cluster](aeProcessEvents+ 0x13d )[ 0x41f86d ] /usr/local/bin/redis-server 0.0 . 0.0 : 6380 [cluster](aeMain+ 0x2b )[ 0x41fb6b ] /usr/local/bin/redis-server 0.0 . 0.0 : 6380 [cluster](main+ 0x370 )[ 0x427220 ] /lib64/libc.so. 6 (__libc_start_main+ 0xfd )[ 0x336b21ed1d ] /usr/local/bin/redis-server 0.0 . 0.0 : 6380 [cluster][ 0x41d039 ] |
挂掉了之后, 我们用redis_tribe 这个脚本进行对我们的redis 集群进行状态检查,发现有个槽很久都处于import状态和migrate状态之间。
[WARNING] Node 10.112.142.21:7210 has slots in importing state (45).
[WARNING] Node 10.112.142.20:6380 has slots in migrating state (45).
之后我们用 fix 对这个集群进行修复,然后整个集群才ok了。
以下是我们开始尝试着用 rebalance ,让redis 自己来帮我们调整整个集群的solt 分配情况:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | [root@GZ-JSQ-JP-REDIS-CLUSTER-142-21 ~] #<strong> /usr/local/bin/redis-trib.rb rebalance 10.112.142.21:7211</strong> >>> Performing Cluster Check (using node 10.112.142.21:7211) [OK] All nodes agree about slots configuration. >>> Check for open slots... >>> Check slots coverage... [OK] All 16384 slots covered. >>> Rebalancing across 7 nodes. Total weight = 7 Moving 892 slots from 10.112.142.20:6380 to 10.112.142.21:7210 [ERR] Calling MIGRATE: ERR Target instance replied with error: <strong>BUSYKEY Target key name already exists.< /strong > >>> Check for open slots... [WARNING] Node 10.112.142.20:6380 has slots in migrating state (45). <strong>[WARNING] The following slots are open : 45 >>> Fixing open slot 45< /strong > *** Found keys about slot 45 in node 10.112.142.21:7210! Set as migrating in : 10.112.142.20:6380 Set as importing in : 10.112.142.21:7210 Moving slot 45 from 10.112.142.20:6380 to 10.112.142.21:7210: *** Target key exists. Replacing it for FIX. /usr/local/lib/ruby/gems/2 .3.0 /gems/redis-3 .3.1 /lib/redis/client .rb:121: in `call': MOVED 45 10.112.142.21:6380 (Redis::CommandError) <strong>from /usr/local/bin/redis-trib .rb:942: in `rescue in move_slot' < /strong >from /usr/local/bin/redis-trib .rb:937: in `move_slot' from /usr/local/bin/redis-trib .rb:607: in `fix_open_slot' from /usr/local/bin/redis-trib .rb:422: in `block in check_open_slots' from /usr/local/bin/redis-trib .rb:422: in `each' from /usr/local/bin/redis-trib .rb:422: in `check_open_slots' from /usr/local/bin/redis-trib .rb:360: in `check_cluster' from /usr/local/bin/redis-trib .rb:1140: in `fix_cluster_cmd' from /usr/local/bin/redis-trib .rb:1696: in `<main>' |
那么很明显,这样是行不通的,报了一些奇奇怪怪的问题,说是我们的key 已经存在了,那么开到这里是不是我们的的集群里边有脏数据了呢?于是我们把这两个节点的所有key 拿出来对比了一番,发现并没有重复的key出现,也就是没有胀数据啦,那怎么办呢?我们集群是一定要扩容的,不然双11肯定是抗不住的。
ok, 还好这个工具提供了另外一种人工迁移solt 的方式【reshard】,知道了这个方式后,我们很愉快的迁移了大部分节点, 但是在迁移第45个solt 的时候又出问题了。出的问题就和上边的类似。
看样子问题是出现在这第45 号solt 身上,如果我们不解决这个问题,这个节点上的负载就会很大,双11 就可能会成为瓶颈。我们看一下这个reshard 都做了哪些动作:
通过redis-trib.rb 源码可以看到:
cluster setslot imporing
cluster setslot migrating
cluster getkeysinslot
migrate
setslot node
我们进行重新分片的时候我们会进行这几个操作。那么从上边重新分片失败的情况看,有可能是由于在迁移过程中超时导致的,或者说某个很大的key堵塞了redis导致的。
OK,我们先猜想到这里, 那我们接下来开始验证我们的猜想,看看第45 号solt 是不是有比较大的solt。
首先利用上边给出的信息,我们看一下这个solt 里边都有哪些key:
10.205.142.21:6380> CLUSTER GETKEYSINSLOT 45 100
1) "JIUKUIYOU_COM_GetCouponInfo_3479num"
2) "com.juanpi.api.user_hbase_type"
3) "t2526767"
4) "t2593793"
............
然后呢,我们看一下每一个key序列化后都占了多大的空间:
10.205.142.21:6380> DEBUG OBJECT com.juanpi.api.user_hbase_type
Value at:0x7f973b7226d0 refcount:1 encoding:hashtable serializedlength:489435339 lru:1802371 lru_seconds_idle:3013 【466MB!!!!】
(7.11s)
到此,我们看到了一个蛮大的key,看一下量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | 10.205.142.21:6380> <strong>HLEN com.juanpi.api.user_hbase_type< /strong > -> Redirected to slot [45] located at 10.205.142.20:6380 (integer) <strong>6589164< /strong > 10.205.142.20:6380> <strong>HSCAN com.juanpi.api.user_hbase_type 0< /strong > 1) "3670016" 2) 1) "6581cc3950e071873763e4b016b66914" 2) "{\"type\":\"A1\",\"time\":1434625093}" 3) "4baca6b94be68d704f348ee0a3e45915" 4) "{\"type\":{\"A\":\"A3\",\"C\":\"C1\"},\"time\":1442105611}" 5) "b83ce222b54890bf4de03cfad2362e9e" 6) "{\"type\":\"A1\",\"time\":1434672804}" 7) "821d1f1a27c63d6d40bc1d969bcec5f6" 8) "{\"type\":{\"A\":\"A6\",\"C\":\"C3\"},\"time\":1435705301}" 9) "cbce79067ad2773b87360fb91b9a325c" 10) "{\"type\":\"A3\",\"time\":1433925484}" 11) "8eef2bc24fd819687e017f7bd1ad8e1c" 12) "{\"type\":{\"A\":\"A6\",\"C\":\"C2\"},\"time\":1435543716}" 13) "d4112308e47066f2e3d35dbcf96ba092" 14) "{\"type\":{\"A\":\"A1\",\"C\":\"C4\"},\"time\":1435141321}" 15) "2ca72205e82f5d9188cc9c56285ea161" 16) "{\"type\":{\"A\":\"A2\",\"C\":\"C3\"},\"time\":1434946176}" 17) "b259ce800a0112dbd316f54aae7679a6" 18) "{\"type\":{\"A\":\"A6\",\"C\":\"\"},\"time\":1441892174}" 19) "039b9011d1470791143563a2660d8dc2" 20) "{\"type\":{\"A\":\"A6\",\"C\":\"C3\"},\"time\":1435442319}" 21) "82def11dfe206501074eb200558fb8a5" 22) "{\"type\":\"C2\",\"time\":1434466702}" |
到此,问题我们基本锁定就是这个solt里边有个非正常大小的key了,那么到底是不是这个key导致的呢?如果是我们又改如何验证呢?
首先我们想到的就是能不能跳过这个solt 的迁移,或者说迁移指定的solt呢?
那么对于迁移指定的solt,对于原始的这个工具里边是没有支持的,而如果要实现的话,我们需要手动拆解这几个步骤,自己实现逻辑
那么跳过这个solt 呢?看起来稍微的比较容易实现一点,那么我们就修改一下redis-trib.rb 源码好了:
首先我们找到函数入口: def reshard_cluster_cmd(argv,opt) 【大约在1200行左右】
然后找到这句话:print "Do you want to proceed with the proposed reshard plan (yes/no)? "
在它下边几行添加个逻辑:判读当前solt是否是 45 solt,如果是则跳过,如果不是则迁移
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | if !opt[ 'yes' ] print "Do you want to proceed with the proposed reshard plan (yes/no)? " yesno = STDIN .gets.chop exit( 1 ) if (yesno != "yes" ) end reshard_table. each {|e| xputs "------------------------> #{e[:slot]}" case e[ :slot ] when 45 puts "sb 45" else move_slot(e[ :source ],target,e[ :slot ], :dots => true , :pipeline =>opt[ 'pipeline' ]) END <br> END <br> END |
Ok,我们就来试一把吧,看看跳过这个solt之后, 我们重新分片是否成功!那么结果符合我们的猜测,就是由于这个solt 里边有个超大的key导致的。事后通过更业务方商量,这个key是个无效的key,可以删掉的!呵呵
最后,问题到此已经顺利解决了。
总结一下:
有时候报错的信息不一定能够准确的反应问题所在,我们需要清理在报错期间我们执行了什么操作,这个操作的具体步骤有哪些,涉及道德这个系统的原理又是怎么样的。通过一步步的推理、猜测、验证最终得到问题的解答。
上边那种粗暴的方法来修改源码其实还有没有考虑到的地方,
比如有些除了这个solt之外又没有其它solt 有类似这个大的key呢?
是不是应该在迁移solt前,对整个solt的key大小进行一次扫描,检查呢?
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· DeepSeek如何颠覆传统软件测试?测试工程师会被淘汰吗?