线上问题排查实例分析|关于 Redis 内存泄漏

Redis 作为高性能的 key-value 内存型数据库,普遍使用在对性能要求较高的系统中,同时也是滴滴内部的内存使用大户。本文从 KV 团队对线上 Redis 内存泄漏定位的时间线维度,简要介绍 Linux 上内存泄漏的问题定位思路和工具。

16:30 问题暴露

业务反馈缩容后内存使用率90%告警,和预期不符合,key 只有1万个,使用大 key 诊断,没有超过512字节以上的大 key。

16:40 确认内存泄漏

发现该系统中有部分实例内存明显偏高达到300~800MB,正常实例只有10MB左右,版本号为4ce35dea,在9月份时已经有发现49bdcd0b这个较老版本有内存泄漏情况发生,现象看起来一样,说明内存泄漏问题一直存在,未被修复,于是开始排查该问题。

17:30 开始排查社区版本

排查问题先易后难,先排除是不是社区的版本Bug问题:

  • 不需要从最新修复一直倒叙确认到3系列的 commit 提交,因为如果是严重的内存泄漏,3系列的旧版本也一定会有 backport 修复记录。

查看3.2.8的commit记录,只有一次内存泄漏相关提交:Memory leak in clusterRedirectBlockedClientIfNeeded.

本次提交只修复了在 cluster 出现 key 重定向错误时对 block client 处理时对一个指针的泄漏,不可能出现如此大的泄漏量。3.2.8的社区版已上线数年,但在社区内未搜索到相关内存泄漏问题,因此推测是我们的某些定制功能开发引入的 Bug。

18:10 整理监控和日志

整理当前已知监控和日志信息,分析问题的表面原因和发生时间

1、监控信息

odin 监控只能看到最近两个月的内存使用曲线,从监控上可以得到三点信息:

  • 两个月前已经发生内存泄漏

  • 内存泄漏不是持续发生的,是由于某次事件触发的

  • 内存泄漏量大,主实例使用内存800MB,从实例使用内存10MB

21c89a9b5aa7a9dda8229161a4fcaea6.png

2、日志信息

排查发生内存泄漏的容器日志:       

36a6a8d5612614e074cccaef63fa96f0.png

07161cf1671c59ba5012d8eb4adafdc7.png

Redis 在10月11日被创建后,只有在20日出现有大量日志,之后无日志,日志有以下内容:

  • Redis 横向扩容 slot 迁移

  • 主从切换

  • AOF 重写

  • 搜索该系统的历史短信告警,在10月11日11:33分出现三次内存使用率达到100%的告警,因此可以推测出现 key 淘汰

Manager平台操作信息:

71e708e93d03f80747d14a6a1d8d80fc.png

  • 垂直扩容

  • 横向扩容

  • Redis 重启

综合 Redis 的日志和平台日志信息,虽然未能直接发现问题原因,可以确定内存泄漏发生在10月20日11:30左右,由以下单个事件或者混合触发的:

  • 主从切换

  • key 迁移

  • key 驱除

18:00 打印内存 dump 信息

在实例上使用 GDB  把泄漏实例的所有内存 dump 出来,初步发现内存上有很多 key(647w个),不属于本节点,info 里数据库只有1.6W个 key,  怀疑是slot 迁移有问题。

803061b9a2d6c26ab1d6f227d73e86e1.png

18:30 第一次 diff 代码

由于3.2.8自研版本有两个重大修改:

  1. slot 的所属 key 集合记录,把跳跃表改为了4.0以后的基数树结构,从社区的 unstable 分支 backport 下来的;

  2. 支持多活

由于出问题的系统没有使用多活功能,且恰巧事发时有 slot 迁移,因此重点怀疑 slot 迁移中 rax 树相关操作有内存泄漏,首先查看了相关代码,有几个疑似的地方,但都排除掉了。

20:30 尝试使用工具定位

  1. memory doctor

    Redis4 引入的内存诊断命令,3系列未实现

  2. 3.2.8版本使用 jemalloc-4.0.3作为内存分配器,尝试使用 jeprof 工具分析内存使用情况,发现 jemalloc 编译时需要提前添加--enable-prof编译选项,此路不通

  3. 使用 perf 抓取 brk 系统调用,未发现异常(实际上最近两个月也未发生泄漏)

  4. valgrind 作为最后手段,不确定是否可以复现

22:00 组内沟通进展

和组内同学沟通下午的调查情况,仍然怀疑 rax 泄漏,其次多活或者 failover 混合动作触发的 case 导致泄漏。

第二天10:00 重新整理思路

使用 hexdump 观察昨天的内存 dump 文件,发现泄漏内存为 SDS 字符串数据类型,且连续分布。

1552109436f54ecc7c44bfc325e97df7.png

每隔4、5行都会出现OO TT SS等字符,对应 SDS 类型的 sdshdr 结构体。      

5868a6662eb195e238affee34eca7fab.png

每个泄漏的 key 字符串大约在80字节左右,因此使用时 sdshdr8(为了节约内存,sds 的 header 有五种 sdshdr5,sdshdr8、sdshdr16、sdshdr32、sdshdr64,其中8指的是长度小于1<<8的字符串使用的 sdshdr)。    

a0b9ea6cd80567ed1d54daee801c945a.png

以TT那行为例,结合 SDS 字符串的 new 函数分析,key 字符串长度为84字节等于0x54,结合代码看,sh->len和sh->alloc都是0x54,第三个字节标识 type 类型,sdshdr8 的 type 值刚好是0x1,因此可以确认泄漏的是 sds 类型的 key 值,并且排除 rax 树泄漏的可能,因为内存 dump 和 rax 树的存储结构不符。附典型的 rax 存储结构:   

a5157eef46d9227f7bc3d60c11bbf402.png

14:00 根据dump的分析重新排查代码

排除了 rax 树的泄漏,同时综合 redis 使用 sds key 的情况,此时把怀疑重点放在了 write 等 dict 的释放方法上,以及 rdb 的加载时 key 的临时结构体变量。

此时 diff 代码,不再局限有变更的代码,以功能为粒度进行走读代码,但把重点放在了 failover 时的 flushdb 和 loadRDB 操作上。

17:00 排查slot迁移代码

在上一轮代码走读中,再次排除了 failover,key 淘汰的代码有内存泄漏的可能,因此重新怀疑 slot 迁移中的某些动作导致 key 字面值的内存泄漏,尤其是 slot 清空等操作。

18:30 找到根因

在 slot 迁移过程中,会遍历旧节点中的所有 key,然后把遍历得到的 key 从旧节点迁移到新节点中。

1e4911620568f73809d11ae28795ff1b.png

这个功能在3.2.8代码中没有被改动,但其调用的 getKeysInSlot 函数有了修改。getKeysInSlot 是遍历 rax 树,拿到待迁移 key 列表,对每个 key 从 rax 树中取出完整字符串,来拷贝创建 obj 类型指向 sds 字符串;这些字符串作为数组指针类型返回给了出参 keys,但在上层调用把这些字符串返回给客户端后,没有释放这些字符串,导致了内存泄漏的发生。

原生的3.2.8代码中 getKeysInSlot 函数,由于使用的是跳跃表,该跳跃表中的每个节点都是一个 key 的 obj 类型,因此只需要返回这个 key 的指针即可,无需内存拷贝动作,因此上层调用中也就不需要内存释放动作。这个根因查明,也反过来解释了很多疑问:

  • 为什么刚开始只有老版本才有内存泄漏,新版本未发现。原因是老版本的实例上线时间长,有水平扩容的需求较多,内存泄漏的实例也就较多。

  • 泄漏的内存为什么连续分布?原因是在一次 slot 迁移动作中,这些 key 遍历动作都是连续进行的。

  •  这个系统为什么泄漏比例这么高?原因是该系统中 key 占用的内存比 value值更高,key 通常80字节,而 value 大多是0、1等数值。

20:00 修复动作

相比较根因的查找,修复就简单多了,只需添加一行代码即可。   

cd96b31e0005648bc3d23e7bcecd4a1f.png

后续思考

1、代码 review 需要从功能视角去走读代码,不能只关注 diff 不同。在本次调查中,第一遍走读代码只关注 diff 点,是无法发现问题的。

2、对内存泄漏的排查,在代码设计阶段是避免此类问题的效率最优解,代码 review 阶段比测试阶段代价要小,测试阶段发现要比上线后排查容易得多,越是工程后期修复 bug 越难。具体在该函数设计中,由于内存申请和释放没有内聚性,导致内存泄漏很容易出现,而这个函数在3系列使用跳跃表时是没有问题的,因为不涉及到内存的申请释放。开发和 QA 在测试中引入工具进行功能覆盖测试,动态工具如 valgrind、sanitizers 等,线上工具如memleak、perf等。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/214560.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数据结构:二叉查找树,平衡二叉树AVLTree,红黑树RBTree,平衡多路查找数B-Tree,B+Tree

二叉查找树 二叉树具有以下性质&#xff1a;左子树的键值小于根的键值&#xff0c;右子树的键值大于根的键值。 对该二叉树的节点进行查找发现深度为1的节点的查找次数为1&#xff0c;深度为2的查找次数为2&#xff0c;深度为n的节点的查找次数为n&#xff0c;因此其平均查找次…

探索移动端可能性:Capacitor5.5.1和vue2在Android studio中精细融合

介绍&#xff1a; 移动应用开发是日益复杂的任务&#xff0c;本文将带领您深入探索如何无缝集成Capacitor5.5.1、Vue2和Android Studio&#xff0c;以加速您的开发流程Capacitor 是一个用于构建跨平台移动应用程序的开源框架。Vue 是一个流行的 JavaScript 框架&#xff0c;用…

无需外接显示器,直接使用windows安装树莓派系统并可远程桌面登录

准备工作: 1.安装树莓派官方烧录工具 raspberry pi imager 2.下载树莓派系统镜像(也可选择在线下载安装) 打开imager工具&#xff0c;选择需要安装包树莓派版本 点击"NEXT"&#xff0c;在弹出的选项中选择编辑设置。 设置登录名和密码&#xff0c;已经所连接的wif…

华大基因助力乌兹别克斯坦精准医学发展,共筑健康丝绸之路

今年上半年&#xff0c;中国与中亚五国元首齐聚陕西西安&#xff0c;举办中国&#xff0d;中亚峰会。过去的20年里&#xff0c;中国已经成为中亚国家的主要投资来源国&#xff0c;总投资额接近400亿美元。乌兹别克斯坦是中国&#xff0d;中亚合作机制的重要参与者&#xff0c;乌…

【100个Cocos实例】东皇太一的技能环绕效果

引言 Cocos中物体围绕物体做圆周运动。 不管是2D还是3D游戏&#xff0c;旋转是游戏中常见的操作之一&#xff0c;它可以用来改变游戏对象的方向、角度或者位置&#xff0c;从而创造出更加生动和有趣的游戏体验。 本文将介绍一下如何实现王者荣耀中东皇太一的技能环绕效果。 …

6S精益管理必备装备降低物料损耗

在工厂生产环境中&#xff0c;设备管理是确保生产效率和质量的关键因素之一。6S管理方法是一种源自日本的管理体系&#xff0c;旨在通过整顿、整理、清扫、清洁、素养、遵守六个步骤&#xff0c;实现工作环境的优化和管理的高效。 仓库管理中&#xff0c;库存损耗一直是企业面…

大模型生态新篇章:以AI Agent为引,助企业创新应用落地

文 | 智能相对论 作者 | 沈浪 以聊天机器人、虚拟助手、智能客服等为代表的对话式人工智能 (Conversational AI Agents ) 在具体服务场景中的应用已经十分普遍。今年以来&#xff0c;随着大模型技术的爆发与加持&#xff0c;对话式AI被市场赋予了更高的期望。 “所有行业都值…

Linux应用开发基础知识——I2C应用编程(十三)

一、无需编写驱动程序即可访问 I2C 设备 APP 访问硬件肯定是需要驱动程序的&#xff0c;对于 I2C 设备&#xff0c;内核提供了驱动程序 drivers/i2c/i2c-dev.c&#xff0c;通过它可以直接使用下面的 I2C 控制器驱动程序来访问 I2C 设备。 i2c-tools 是一套好用的工具&#xff0…

有没有免费可用的亚马逊两步验证器软件?

两步验证&#xff08;Two-Step Verification&#xff09;是一种可以增加账号安全性的有效方式&#xff0c;启用两步验证后&#xff0c;即使你的密码被泄露也不影响&#xff0c;因为除了输入密码外&#xff0c;用户还需要提供二步验证码&#xff0c;通常是通过手机短信、身份验证…

C/C++ 开发SCM服务管理组件

SCM&#xff08;Service Control Manager&#xff09;服务管理器是 Windows 操作系统中的一个关键组件&#xff0c;负责管理系统服务的启动、停止和配置。服务是一种在后台运行的应用程序&#xff0c;可以在系统启动时自动启动&#xff0c;也可以由用户或其他应用程序手动启动。…

免杀原理(php)

免杀原理 0x01 前言 何为免杀&#xff0c;免杀就是一种逃脱杀毒软件查杀的方法&#xff0c;免杀的目的就是绕过“墙”&#xff0c;去执行危险的操作。那么如何绕过这堵“墙”&#xff0c;就是免杀的本质。有句俗话说得好“知己知彼&#xff0c;百战不殆”&#xff0c;想要用好…