一次 redis 幽灵内存的分析

一、背景

业务使用的 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 的一个典型内存"隐性膨胀"问题:

  1. 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 扫描下,这样就能避免不适配的命令
相关推荐
qq_401700411 小时前
Qt Modbus 工程化封装:超时、重试、取消与资源释放,别让异步请求变成隐患
数据库·qt
ESDWAN1 小时前
SD-WAN 是什么?工作原理、适用场景与选型要点
开发语言·网络·数据库·架构·php
m0_639906941 小时前
MySQL 慢查询优化|explain执行计划详解
数据库·mysql
正儿八经的少年1 小时前
MyBatis 一对一、一对多 SQL
数据库·sql·mybatis
Elastic 中国社区官方博客2 小时前
Elastic 在《Forrester Wave™:2026 年第四季度智能运维平台》报告中被评为强劲表现者
大数据·运维·数据库·elasticsearch·搜索引擎·全文检索
Apache IoTDB2 小时前
从 IoTDB 到 CED-DB:哈尔滨工业大学探索云边端协同时序数据库
数据库·时序数据库·iotdb
闲云野鹤在人间2 小时前
头秃修炼手册:StorageClass+StatefulSet+ETCD 全套渡劫笔记
linux·数据库·笔记·云原生·kubernetes·etcd
Geek-Chow2 小时前
06 · DeepSeek V4.1 Flash 推理端到端走查:一个 Agent 请求穿过 recipe、KV 缓存、Prefill 与 Decode
缓存
前进的李工2 小时前
Agent 学习笔记 04:KV Cache 与上下文缓存
笔记·学习·缓存·agent·注意力机制·kv cache·prompt cache