一次 redis 幽灵内存的分析
一、背景业务使用的 Redis 由主从切换到了 Cluster 后Redis 出现了诡异的内存上涨。二、排查路径1.查看 info查看 info 观测内存使用就是很高且碎片率很低,而且 key 的数量很少info memory2.查看真实数据大小用 rob 等工具分析真实大小详见文档发现真实数据只有不到 1M3.解析内存真实使用情况用 gdb 分析 内存真实使用情况注意在低峰期分析3.1 编写 gdb 脚本cat/tmp/sndbuf.gdbEOF set pagination off set print elements 0 set confirm off set $t server.cluster-nodes set $tab $t-ht[0].table set $size $t-ht[0].size set $i 0 set $total_alloc (unsigned long)0 set $total_len (unsigned long)0 set $link_count 0 printf \n Cluster Link sndbuf \n printf %-42s %-22s %-6s %-14s %-14s\n, NODE_NAME, IP:CPORT, TYPE, SDS_LEN, SDS_ALLOC while $i $size set $e $tab[$i] while $e ! 0 set $n (clusterNode*)$e-v.val if $n-link ! 0 if $n-link-sndbuf ! 0 set $s (unsigned char*)$n-link-sndbuf set $flags *($s - 1) set $type $flags 7 set $len (unsigned long)0 set $alloc (unsigned long)0 if $type 0 set $len $flags 3 set $alloc $len end if $type 1 set $len *(unsigned char*)($s - 3) set $alloc *(unsigned char*)($s - 2) end if $type 2 set $len *(unsigned short*)($s - 5) set $alloc *(unsigned short*)($s - 3) end if $type 3 set $len *(unsigned int*)($s - 9) set $alloc *(unsigned int*)($s - 5) end if $type 4 set $len *(unsigned long*)($s - 17) set $alloc *(unsigned long*)($s - 9) end printf %-42s %s:%-6d type%d %-14lu %-14lu\n, $n-name, $n-ip, $n-cport, (int)$type, (unsigned long)$len, (unsigned long)$alloc set $total_alloc $total_alloc $alloc set $total_len $total_len $len set $link_count $link_count 1 end end set $e $e-next end set $i $i 1 end printf \n Summary \n printf Links: %d\n, $link_count printf Total LEN: %lu bytes (%.2f MB)\n, $total_len, ((double)$total_len) / 1048576.0 printf Total ALLOC: %lu bytes (%.2f MB)\n, $total_alloc, ((double)$total_alloc) / 1048576.0 detach quit EOF3.2 执行脚本分析# 639587 为 redis 进程号sudotimeout3gdb-p639587-batch-x/tmp/sndbuf.gdb213.3 结果分析LEN 0所有链接当前没有积压待发送数据发送通道是通的网络 / 对端消费正常。ALLOC 406MB但缓冲区曾经膨胀过并且没有被回收这是 Redis Cluster 的一个典型内存隐性膨胀问题SDS 容量只增不减Redis 的 sds 在 sdsclear() / 数据发送完后只会把 len 置 0不会释放已分配的 buf。所以一旦某瞬间堆积过大内存就被永久占用直到链接重建。[New LWP637685][New LWP637686][New LWP637687][New LWP637688][Thread debugging using libthread_db enabled]Usinghostlibthread_db library/lib/x86_64-linux-gnu/libthread_db.so.1.0x00007fc5395b3e16inepoll_wait()from /lib/x86_64-linux-gnu/libc.so.6Cluster Link sndbufNODE_NAME IP:CPORT TYPE SDS_LEN SDS_ALLOC 77ed8a2e0c39d2f2a304e1ad216b56e3e0664a3b192.168.1.1:16379 type3041943030ee29bbe0d05ccd9a64215631ee140ea27921c81b192.168.1.2:16379 type306291446347b2126df4bde7c5b1ebb10ff43a51d09535733192.168.1.3:16379 type30167772150a4980d05cd232dadab82e25e7429c2f50c433ab9192.168.1.4:16379 type3041943030394723213b2d62e0b6276dfbf4d5247b80d289b0192.168.1.5:16379 type30167772150SummaryLinks:5Total LEN:0bytes(0.00MB)Total ALLOC:425721806bytes(406.00MB)[Inferior1(process637684)detached]3.4 详细原因排查那么为什么会有这种现象呢我们分析监控发现每次内存增长的时候都有一个 eval 的慢日志然后我们通过慢日志发现如下命令单条 publish 的 payload 接近 4.5MB因为 Redis 7 以下是全量广播所以会出现这个放大的问题。│ 一次业务 EVAL 调用的放大链 │ │ │ │ 客户端发出:4.5MB │ │ ↓ Lua 内2次 publish │ │ Redis 处理:9MB │ │ ↓ cluster bus 广播到5个节点 │ │ 集群内传输:45MB │ │ ↓ 其中真有订阅者的可能只有1~2 个节点 │ │ 有效流量: ≤9MB │ │ 浪费流量: ≥36MB(80%!)│ └─────────────────────────────────────────────────────┘命令4)1)eval2)for i 2, #ARGV do redis.call(\publish\, ARGV[i], ARGV[1]) end3)04){\src\:\******\,\seq\:5,\v\:0,\create\:{\type\:\******,\data\:{\tabs\:[\0\,\...(29959420morebytes) 5) ****** 6) *******三、解决方案1.解决方案升级到 Redis 7.2.9因为 Redis 7 只广播到对应节点且有 sds 回收机制所以升级到 7 后内存不再有上升的情况2.遇到的问题第二天中午时出现业务告警显示 command 命令不支持这是因为 command 返回的数据格式有差异低版本的 lettuce 处理不了这个问题而 command 命令只有重连的时候会出现所以第一天没有发现3.如何解决和避免解决业务 lettuce 版本由 5.1.8 升级 5.3.7解决问题能升级 6.2 以上更好不过需要注意 sring boot 的版本避免在测试的时候业务应该把 DBA 提供的访问 Redis 的服务代码用 AI 扫描下这样就能避免不适配的命令