记一次redis stream数据类型内存不释放问题

https://www.cnblogs.com/nanxiang/p/15268124.html
之前简单写过一次关于 redis 数据库steam数据类型介绍,该类型类似于 kafka消息队列,生产者将消息写入steam类型的key,消费者消费数据。
 

1、背景

今天线上 Redis 集群DB-1节点数据库内存告警,因为是在阿里云上,所以很方便的使用了内存分析功能,发现了占用内存过大的 key 都是 stream 类型,怀疑是消息没有及时清导致的,开始着手排查。
【 中间有个小插曲,内存分析的并不准确,而且差距很大,我们单节点 redis 一共4G,分析后单个 KEY 占用内存10G左右,和阿里云沟通反馈是官方 redis 计算偏差较大,在后期版中会进行merge。https://github.com/redis/redis/pull/10152

 

2、排查过程

2.1 想确定元素长度

127.0.0.1:6379[2]> xlen my.stream
(integer) 0
127.0.0.1:6379[2]> xrange my.stream - + count 3
(empty list or set)
127.0.0.1:6379[2]> 

首先使用了xlen命令查看队列长度,发现为0,又使用xrange命令查看详细内容,发现都是空的,这个非常奇怪。

因为采用了阿里云redis集群版,和官方集群不太一样,以为命令下发到了非DB-1节点上导致返回结果为空,后经确认,不会出现这个问题。
使用阿里云redis集群版自研命令,imonitor 看下该节点都有哪些请求。
【链接:https://help.aliyun.com/document_detail/145969.html】
发现有xadd命令,还有xdel命令,和业务侧确认,应用程序消费数据后,会调用xdel stream xxxxx-0去删除消息,以达到释放内存的目的。
参考https://wiki.shileizcc.com/confluence/display/RED/Redis+Stream# 文章,xdel命令删除,并不会释放内存,只会打上删除记。
 

2.2 计划删除队列中的消息,释放内存

先将备份下载到备份服务器,启动 Redis 数据库,计划使用xtrim 命令删除 n 个元素,只保留最近的 x 个,为了防止数据库一次删除元素过多发生性能问题,只删除 1000 个元素,即保留 stream 里更多的消息。
https://redis.io/commands/xtrim/ 从官方文档看,删除代价是O(N),即一次删除的越多,代价越大`。

127.0.0.1:6379[2]> xtrim my.stream maxlen 8000000
(integer) 0
127.0.0.1:6379[2]> xtrim my.stream maxlen 7000000
(integer) 0
127.0.0.1:6379[2]> xtrim my.stream maxlen 10000
(integer) 0

所有命令几乎是在一瞬间完成的,快的似乎没有做任何删除操作,观察了一会 info memory命令结果中 used_memory 值没有任何变化。
突然想到业务侧只是消费数据,删除数据,并没有向服务器返回过 ack 操作,根据文档中的提示,如果忘记做 ack,那么 PEL 占用内存就会放大,因为在消费者结构中维护着一个 PEL 列表,如果服务端收到了客户端的 ack ,也就是确认客户端一定消费到了这条记录。
 

#查看steam下消费组,能显示组名称,及conumser个数
127.0.0.1:6379[2]> xinfo groups  my.stream
1) 1) "name"
   2) "my.default.group"
   3) "consumers"
   4) (integer) 1

#查看到这个消费者有 8959646 条消息没有 ack 过。在线上这个值一直在增长
127.0.0.1:6379[2]> XINFO CONSUMERS  my.stream my.default.group
1) 1) "name"
   2) "my.default.consumer"
   3) "pending"
   4) (integer) 8959646
   5) "idle"
   6) (integer) 757

 
从网上没有更多相关资料,怀疑 Redis 为消费者维护了 PEL 列表,如果客户端没有为这条消息返回过 ack ,xtrim 之类的命令即不能从内存释放掉它(目前也不太确定,xtrim对xdel删除的消息是否有效)。
于是写了一个python脚本,准备将没有 ack 过的消息,都 ack 一遍。
 
xpending 命令

#xpending 命令可以查看没有收到过 ack 消息的列表
127.0.0.1:6379[2]> xpending  my.stream my.default.group - + 3
1) 1) "1636438062753-0"
   2) "my.default.consumer"
   3) (integer) 14178417705
   4) (integer) 1
2) 1) "1636438063496-0"
   2) "my.default.consumer"
   3) (integer) 14178416963
   4) (integer) 1
3) 1) "1636438064575-0"
   2) "my.default.consumer"
   3) (integer) 14178415884
   4) (integer) 1

 
Python 脚本

import redis
r = redis.Redis(host='172.16.1.10', port=6379, decode_responses=True,db=2,password='123')
glo_count = 1
while True:
    resulst=r.xpending_range('my.stream','my.default.group','-','+',1000)
    for x in resulst:
        r.xack('my.stream','my.default.group',x['message_id'])

 
继续执行info memory命令,查看内存使用量,一直在一点点的减少。
线上的Redis 数据库也在使用这个办法释放内存,和开发二次沟通,消费消息后一定要 ack 一次, 同时定期xtrim 清除队列消息或在 xadd 命令的同时,加上maxlen x 参数,控制一下队列长度。
 
 

3 总结

a. xdel 命令并不会真正删除消息,只是打上删记,内存不会释放。参考文档的内容和官网说法有些出入【https://redis.io/commands/xdel/】,在我实际测试时,xdel 命令删除后,内存是立即释放的,也可能是测试的数据量比较小。
b. xtrim 命令,xadd xxx maxlen n 命令,在当前队列有显式的有消息时,执行后会有直接的效果。xrange之类命令也不显示已经删除的消息。
c. 如果消息没有 ack 过,会导致 PEL 列表不断增涨,占用大量内存,此次问题就是这个原因,有两个 stream ,每个stream 未ack过的消息有900万左右,redis 为了维护PEL列表占了大多内存。需要对消息进行确认,PEL 列表变短,内存才会释放。

 
 

说几个相关命令:
xinfo stream my.stream  --显示stream 相关信息,长度,有几个消费组等
xinfo groups my.stream   --- 显示stream 有几个消费组,组名称
XINFO CONSUMERS my.stream my.default.group  --后边两个参数是key名称和消费组名称,显示消费者相关信息(名称,多少条消息没有ack,空闲多长ms)
xpending my.stream my.default.group - + 3   --查看没有ack消息的列表

posted on 2022-04-22 20:51  柴米油盐酱醋  阅读(3931)  评论(1编辑  收藏  举报

导航