本文通过一封 618 前的 R2M(公司内部缓存组件,可以认为等同于 Redis) 告警,由浅入深的分析了该告警的直接原因与根本原因,并根据原因提出相应的解决方法,希望能够给大家在排查类似问题时提供相应的思路。

一、问题排查

1.1 邮件告警

正值 618 值班前夕,某天收到了邮件告警,告警内容如下:

您好,R2M 监控报警,请您及时追踪一下! 报警信息:告警 ID:6825899, 应用:zr_credit_portal, 负责人:zhangsan, 告警类型:内存使用率, 时间:2023-06-15 16:00:04。实例:(10.0.0.0:5011-slave), 当前:9212MB 超过警戒值:8748MB 实例最大内存:10800 MB,内存使用率:85 % ;实例:(10.0.0.0:5023-master), 当前:9087MB 超过警戒值:8748MB 实例最大内存:10800 MB,内存使用率:84 % ;实例:(10.0.0.0:5017-master), 当前:9214MB 超过警戒值:8748MB 实例最大内存:10800 MB,内存使用率:85 % ;

大概内容是说,R2M 集群使用率已经达到 85%,需要紧急处理下。

我们的缓存集群配置如下,总共 32400MB 容量,三主三从,每个主节点 10800M 容量,目前使用最高的已经达到 9087M。R2M 使用集群模式进行部署。

image-20230713164727082

首先的思路就是使用大 key 统计,查看是哪些缓存占用了容量。因为大 key 统计是从节点进行扫描,所以不用担心会影响线上主流程。

image-20230713170916595

1.2 代码分析

大 key 主要分为两类,一类是 xxx_data,一类是 xxx_interfacecode_01,按照此规律去代码中寻找存放 key 的地方

String dataKey = task.getTaskNo() + "_data";
cacheClusterClient.setex(dataKey.getBytes(), EXPIRATION, DataUtil.objectToByte(paramList));

key = task.getTaskNo() + "_" + item.getInterfaceCode() + "_" + partCount;
cacheClusterClient.setex(key.getBytes(), EXPIRATION,DataUtil.objectToByte(dataList));


找到了代码位置后,分析其业务流程:

伏羲运营后台插入数据优化-第 3 页.drawio

1.3 告警原因

综合上图分析,此次占用率过高的原因可以分为直接原因与根本原因:

1.3.1 直接原因

查看运营后台确实发现有用户在此前三天创建了大量的跑批任务,导致缓存中样本与结果数量增加,从而导致缓存使用率过高。

1.3.2 根本原因

分析代码后,根据上文描述缓存中主要有两块数据:样本与结果

至此,已经分析出了本次缓存使用率过高的原因(其实还没有,直接原因只分析出了表象,直接原因的 “根本原因” 还未有结论)。

二、问题解决

上文分析了本次告警的排查过程,以下是如何解决问题,也是分为如何解决直接原因与解决根本原因。

2.1 直接原因

2.1.1 原因分析

正值 618 前夕,最好不考虑操作会对系统产生的影响,因此只能先考虑让对应的用户暂时停止创建跑批,以免继续占用内存导致影响线上业务。

此时观察监控图又发现:

image.png

用户是从三天前就开始创建跑批任务的(对应缓存开始增长的时间点),但是缓存的有效期只有一天,按道理来说从第二天开始每天的缓存都应该下降不少才对(因为前一天的已经过期了),为什么看监控图这三天的缓存使用率近乎直线上升呢?

此处可以思考 30s,与 Redis 特性有关。

根据之前刚系统的学完 Redis 的相关特性,关注到此问题点后,开始思考有没有可能是虽然我们设置了超时时间是一天,但是实际上数据并没有被物理删除呢(Redis 的缓存淘汰策略)?

随后查看 R2M 相关文档:

image-20230907145129672

其中:

如果带有生存时间的键非常多的话, 那么在键的生存时间变为0, 直到键真正被删除这中间, 可能会有一段比较显著的时间间隔。

这不就是我们的特性吗,从刚刚的我们搜索大 key 的图中可以看到,我们有很多带超时的 key 并且 size 都很大,很有可能虽然已经超时了(即 TTL 变为 0)但该数据并没有访问,并且由于 R2M 渐进式删除,某一个 Key 可能会在超时后很久才会被真正的物理删除

至此,直接原因的根本原因已经找到了。

2.1.2 解决

那么如何解决呢?根据一个 Key 过期时被物理删除的两种策略:

注意:

Redis 使用以下两种方式删除过期的键

首先通过访问的形式去删除数据肯定是愚蠢且没必要的(都能访问并且知道要过期了不如直接删除),那么可以选择提高渐进的查找速率。从而将那些超时的数据物理删除

于是我们联系了 R2M 对应运维:

image.png

根据上述聊天记录可知,确实有参数可以调整渐进式物理删除的频率,而我们的缓存集群则之前因为不知名原因(项目团队做过更换)被调整为了 10,大约降低了六倍,此结果也符合我们的预期,从侧面印证了我们的猜想是正确的。

当时处于 618 前夕,我们没有并没有修改该参数,在 618 之后,我们随即提了工单修改该参数,将该参数从 10 提高到 80:

image-20230713183643785

审批通过之后,我们观察 r2m 的下降速率:

image-20230713183917668

可以看到,在 6.20 号我们调整了参数后,在没有大批量数据添加后,r2m 使用率的下降速率明显变快

至此,缓存使用率告警的直接原因已经解决完毕,真正的原因就是有大量的 key 过期后并没有被删除,观察后续缓存使用率都没有太高。此外,即时有大量的跑批任务,如果不是在同一天内直接添加,一般不会造成使用率过高的问题。

2.2 根本原因

上文在调整参数后,基本可以满足用户的日常业务需求,但是如果用户确实有一天之内有大量跑批任务的需求,那么此方案仍不能解决根本问题,还会造成使用率过高有可能影响线上业务的风险。

那么要从根本上解决此问题,就需要对跑批流程进行优化,按照 1.2 中流程示意以及原因分析:

  1. 样本就已经完全没有必要存储在缓存中,所以在代码中直接采用参数传递的方式传入给下一步流程。

  2. 结果分片肯定是有意义的,原因上文中也提到了,但是 redis 缓存的空间(即内存)是比较宝贵的,而 oss 的空间成本(硬盘)则是比较廉价的,并且考虑本身业务就是离线业务,时效性以及查询速率并不是最关键的因素,因此综合考虑将跑批的结果分片数据存储至 oss

  3. 在跑批流程结束后,主动删除 oss 中的结果分片数据,避免数据无用后仍占用存储空间。此外在 oss 端设置 7 天自动删除,防止系统原因异常导致数据未删除而永久存在

综上,结合以上的优化思想,重新设计的流程图如下:

伏羲运营后台插入数据优化-第 4 页.drawio

至此,即时后续用户数据量再大,也无论是分一天创建还是多天创建,都不会导致缓存使用率告警而有可能带来的线上业务问题。

2.3 优化率

上线完成后的缓存使用情况:

image-20230911155108232

可以看到几乎是断崖式下降。

缓存优化率:

改造前

image-20230911155042608

改造后

image-20230911155540703

(8.35-0.17)/8.35≈97.96%

三、总结

本文主要通过一封告警邮件,由浅入深的将系统中存在的缓存问题与流程优化。在解决完实际问题后,我们应该都会有一些心得与总结,从而下次自己在开发过程中避免再犯这样的问题,也能够自己对自己再做一次总结与归档。要做到能够知其然更要知其所以然。以下总结是自己的由浅入深的一点点心得。

3.1 不同中间件应该负责不同的事

“韩信点兵,多多益善”,一名好的将军就是能将不同的士兵分配不同的职责,从而让士兵能够在自己擅长的领域内各尽其职。对我们研发来说,选择不同的中间件完成不同的功能则是能够反应我们研发的技术水平。

像本文来说,可供存储中间数据的有好多中间件,除了 Redis、Oss,还有 Mysql、Hive、ES、CK 等,我们需要根据不同的业务需求选择不同的中间件完成对应的功能。本案例中很明显数据的特性为大量的、不要求速度的,而 Redis 的存储特性为少量的、快速的,很明显这两个是背道而驰的需求与业务,因此我们在选用时应该选择正确的中间件。

3.2 学习技术细节有没有用

其实之前也有学了很多的技术、框架的实现细节,但是绝大多数都是学完就学完了,并没有太多实践的环节。而这次案例分析正好处于之前刚刚学完 Redis 的相关细节,没隔多久就能够应用到本次的实践环节,算是理论与实践结合。此外这次案例也能够很大程度上提升自己的学习兴趣。

作者:京东科技 韩国凯

来源:京东云开发者社区 转载请注明来源


↙↙↙阅读原文可查看相关链接,并与作者交流