一、背景
业务使用的 Redis 由主从切换到了 Cluster 后,Redis 出现了诡异的内存上涨。
二、排查路径
1.查看 info
查看 info 观测内存使用就是很高且碎片率很低,而且 key 的数量很少
info memory2.查看真实数据大小
用 rob 等工具分析真实大小,详见文档,发现真实数据只有不到 1M
3.解析内存真实使用情况
用 gdb 分析 内存真实使用情况,注意在低峰期分析
3.1 编写 gdb 脚本
cat>/tmp/sndbuf.gdb<<'EOF' 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.gdb2>&13.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.6===Cluster Link sndbuf===NODE_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 type30167772150===Summary===Links: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)"eval"2)"for i = 2, #ARGV do redis.call(\"publish\", ARGV[i], ARGV[1]) end"3)"0"4)"{\"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 扫描下,这样就能避免不适配的命令