一、背景
业务使用的 Redis 由主从切换到了 Cluster 后,Redis 出现了诡异的内存上涨。
二、排查路径
1.查看 info
查看 info 观测内存使用就是很高且碎片率很低,而且 key 的数量很少
bash
info memory
2.查看真实数据大小
用 rob 等工具分析真实大小,详见文档,发现真实数据只有不到 1M
3.解析内存真实使用情况
用 gdb 分析 内存真实使用情况,注意在低峰期分析
3.1 编写 gdb 脚本
bash
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
EOF
3.2 执行脚本分析
bash
# 639587 为 redis 进程号
sudo timeout 3 gdb -p 639587 -batch -x /tmp/sndbuf.gdb 2>&1
3.3 结果分析
LEN = 0:所有链接当前没有积压待发送数据,发送通道是通的,网络 / 对端消费正常。
ALLOC = 406MB:但缓冲区曾经膨胀过,并且没有被回收:
这是 Redis Cluster 的一个典型内存"隐性膨胀"问题:
- SDS 容量只增不减
Redis 的 sds 在 sdsclear() / 数据发送完后,只会把 len 置 0,不会释放已分配的 buf。所以一旦某瞬间堆积过大,内存就被永久占用(直到链接重建)。
bash
[New LWP 637685]
[New LWP 637686]
[New LWP 637687]
[New LWP 637688]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
0x00007fc5395b3e16 in epoll_wait () from /lib/x86_64-linux-gnu/libc.so.6
=== Cluster Link sndbuf ===
NODE_NAME IP:CPORT TYPE SDS_LEN SDS_ALLOC
77ed8a2e0c39d2f2a304e1ad216b56e3e0664a3b 192.168.1.1:16379 type3 0 41943030
ee29bbe0d05ccd9a64215631ee140ea27921c81b 192.168.1.2:16379 type3 0 6291446
347b2126df4bde7c5b1ebb10ff43a51d09535733 192.168.1.3:16379 type3 0 167772150
a4980d05cd232dadab82e25e7429c2f50c433ab9 192.168.1.4:16379 type3 0 41943030
394723213b2d62e0b6276dfbf4d5247b80d289b0 192.168.1.5:16379 type3 0 167772150
=== Summary ===
Links: 5
Total LEN: 0 bytes (0.00 MB)
Total ALLOC: 425721806 bytes (406.00 MB)
[Inferior 1 (process 637684) detached]
3.4 详细原因排查
那么为什么会有这种现象呢,我们分析监控发现每次内存增长的时候都有一个 eval 的慢日志,然后我们通过慢日志发现如下命令
单条 publish 的 payload 接近 4.5MB,因为 Redis 7 以下是全量广播,所以会出现这个放大的问题。
bash
│ 一次业务 EVAL 调用的放大链 │
│ │
│ 客户端发出: 4.5 MB │
│ ↓ Lua 内 2 次 publish │
│ Redis 处理: 9 MB │
│ ↓ cluster bus 广播到 5 个节点 │
│ 集群内传输: 45 MB │
│ ↓ 其中真有订阅者的可能只有 1~2 个节点 │
│ 有效流量: ≤ 9 MB │
│ 浪费流量: ≥ 36 MB (80%!) │
└─────────────────────────────────────────────────────┘
- 命令
bash
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\",\"... (29959420 more bytes)"
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 扫描下,这样就能避免不适配的命令