OOM排查之路:一次曲折的线上故障复盘

简介: dqwdqwdqwdqwd

我们的服务整合了Paimon数据湖与RocksDB,通过SDK负责数据的查询与写入。近期,该系统在线上环境连续发生了三次内存溢出(OOM)故障。排查过程颇为曲折,笔者与团队成员尝试了多种方法,走了不少弯路,最终成功定位到问题根源并将其妥善解决。

本文旨在将这段“曲折”的排查经历抽丝剥茧,分享我们是如何一步步逼近真相并最终解决问题的,希望能为使用相似技术栈的朋友带来一些启发。

一、问题的发现&解决

1.1 第一次OOM

现象

某天早上,收到了服务的线上告警,发现大批量RPC请求都失败了,登录相关的服务平台才发现,所有对外的RPC服务,全部都下线了。

根据故障排查的经验,此时应该先对服务进行止血,并且保留现场用于排查问题,于是在重启一台机器后,观察另一台机器的监控指标,在监控指标中,我们注意到了一个异常现象。

Java线程数量如上图所示,Runnable的线程数量在某个时间点突增(上图只截取了一部分时间的监控,实际上线程数量会在一些固定时间点突增)。

排查

这些固定时间,自然成为了我们首先怀疑的方向,这些固定时间,均是在整点附近,而我们的服务是在整点,通过定时任务调度SDK向Paimon表中写入数据。

我们询问了提供相关SDK的团队同学,一起排查后发现,我们所建的Paimon表是依赖公司内部其他中间建表的,在没有指定bucket数量时,会默认100个bucket,而SDK会在每张表的每一个bucket在写的时候都会开一个线程,最终就会有 表数量 x 100 个线程在跑,这个数量也符合我们观察到的Java线程数。

解决

后续和相关同学沟通后,决定减少bucket数量,根据查阅相关资料,Paimon表的bucket数量应该参考以下设置:

  • 数据量较小的场景(OLTP场景)
  • 设置较小的bucket数量,一般在4-16之间,较少的bucket数量也可以对查询效率有一些提升。
  • 海量数据(高并发流式写入场景)
  • 设置64、128、256个bucket

总体来说可以参考以下公式,设置为2的次方个bucket

bucket数量 ≈ (预计的最大写入并行度) * N (其中 N 通常取 1 到 4)

在调整bucket数量后,修复上线,线程数降到了理想范围,解决了线程数量突增的问题。

1.2 第二次OOM

现象

上次OOM问题解决后,我们加强了服务的相关告警。然而时隔20多天后,线上服务又告警了,现象依旧是所有对外的RPC服务全部下线了。

登录监控平台查看相关JVM信息,Java线程数一切正常,但是内存占用率已经到了95%+。

登录机器,使用如下命令,发现Java进程被Kill了。

dmesg | grep -i "killed process"

将监控平台对内存利用率的查询时间周期拉长后发现,自从上次重启之后,内存利用率一直在缓慢上升。

因为内存利用率是缓慢上升,而非突增,只能随着时间的推移,不断地排查内存泄漏的原因,于是我们开启了一段为期半个月的内存泄漏排查旅程。

排查

堆内排查

首先,我们对JVM内存相关的监控指标进行了排查,观察是否由堆内存泄漏导致的OOM。

监控突变如上所示,从图中可以看到,“已使用堆内存”呈现周期性的波动,基本可以确认是正常的GC导致的波动,并且机器的内存是8G,堆内存最大也不过4G。而老年代的内存占用量也在0左右,并未出现波动。

基于以上分析,我们可以明确排除因Java对象持续堆积而导致的堆内存泄漏。故障的根源必定在于堆外内存。

堆外排查

线程数量分析

对Java线程数量进行分析,可以看到在上次调整bucket数量之后,线程数量十分稳定,可以排除Java线程数量增长导致的OOM。

DirectMemory和JNIMemory

使用集团内部的MAT文件,分析了Dump文件,发现堆外内存中都是java.nio.DirectByteBuffer。

这个类是NIO的类,阅读相关文章后,找到相关资料,其中提到集团内部的RPC框架使用Netty,可能会申请堆外内存,且无法监控到,慢慢导致能存利用率上升。

使用Arthas 的 memory 命令分析了系统的内存分布,结果如下:

可以看到,direct占了312M,而其他应用的内存分布如下:

direct只有8M,这两者相差较多。

继续分析Netty占用,结果如下:

将所有的netty占用加起来,确实占用了300M。但300M也远远不会让我们的应用OOM,显然这不是系统OOM根因。

使用NMT工具排查,先记录了baseline,在一天过后执行了一次diff。

可以看到,committed一天不过增长了57M,这也和内存利用率的上涨对应不上。

async-profiler,抓取了一段时间系统运行堆栈的内存分布火焰图,来观察哪些类的上涨比较多,当内存的RES上涨100m后,产出了火焰图,发现火焰图中记录的总共只有4M,这和RES上涨差的也很多。

最后用pmap命令对比了内存上涨前后的diff,也并未发现异常问题。

解决

尽管我们已将问题初步定位于堆外内存,但由于堆外内存泄漏的成因复杂且监控手段有限,此次排查并未直接定位到根本原因。

最后,我们与JVM专家团队紧密协作,制定了一系列手段来解决内存利用率上涨的问题。

1. 适度调低JVM堆内存上限(-Xmx),将更多物理内存预留给堆外空间使用。

2. 加上-XX:+AlwaysPreTouch参数。

默认情况下,JVM向操作系统申请的堆内存是“懒加载”的,只有在实际使用时才会触发物理内存的分配。这会导致监控到的容器内存曲线随时间推移而“自然”增长,对我们判断是否存在“额外”的内存泄漏造成视觉干扰。启用AlwaysPreTouch能让JVM在启动时就一次性占用所有分配的堆内存。

3. 增加机器内存。

4. 升级RPC框架的 netty共享sar包,减少netty占用。

1.3 第三次OOM


相关文章
|
13天前
|
SQL 存储 关系型数据库
MySQL恢复之Binlog格式详解
本文详解MySQL binlog日志的格式与闪回恢复机制,涵盖误操作数据恢复的核心原理、注意事项及实操步骤。重点解析ROW格式下的各类binlog事件(如Format_desc_event、Query_event、Table_map_event、Write/Update/Delete_rows_event等)的结构与作用,并结合实际场景演示如何通过mysqlbinlog工具解析日志、生成反向SQL实现精准恢复。内容深入浅出,适用于DBA及开发人员提升数据安全保障能力。
100 0
MySQL恢复之Binlog格式详解
|
NoSQL 安全 Java
SpringBoot系列(2)整合MongoDB实现增删改查(完整案例)
自己本科时候一直使用的是Mysql,目前的课题组使用的是MongoDB,因此就花了一部分时间整理了一下,实现springboot与MongoDB的整合,并且实现基本的增删改查操作,从头到尾给出一个完整的案例。
1421 0
SpringBoot系列(2)整合MongoDB实现增删改查(完整案例)
|
14天前
|
存储 安全 Java
Java HashMap 全面解析:原理、用法与实战要点
本文深入解析Java中HashMap的底层原理与使用实践,涵盖其“数组+链表+红黑树”的结构演变、哈希计算、扩容机制及线程安全问题,详解常用方法、性能优化与最佳实践,助力开发者高效掌握这一核心数据结构。
145 10
|
移动开发 网络协议 小程序
基于开源IM即时通讯框架MobileIMSDK:RainbowChat-iOS端v10.0版已发布
RainbowChat是一套基于开源IM即时通讯聊天框架 MobileIMSDK 的产品级移动端IM系统。RainbowChat源于真实运营的产品,解决了大量的屏幕适配、细节优化、机器兼容问题。RainbowChat可能是市面上提供im即时通讯聊天源码的,唯一一款同时支持TCP、UDP两种通信协议的IM产品。与姊妹产品RainbowTalk和RainbowChat-Web 技术同源,历经考验。
257 0
基于开源IM即时通讯框架MobileIMSDK:RainbowChat-iOS端v10.0版已发布
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
852 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
存储 编解码 前端开发
阿里云服务器2核4G、4核8G、8核16G选择经济型、通用算力型和计算型选择参考
如果我们想购买的云服务器配置是2核4G、4核8G、8核16G配置,目前在阿里云的活动中,可选的实例规格除了轻量应用服务器之外,有经济型e、通用算力型u1、计算型c7、计算型c8y等几个实例规格可选,由于不同实例规格的性能和价格及适用场景不同,因此,有的新手用户可能不知道如何选择,本文将讨论在2核4G、4核8G、8核16G配置下,如何选择经济型、通用算力型和计算型实例,以供参考。
|
JSON 网络协议 NoSQL
Linux安装MongoDB数据库,并内网穿透远程连接
Linux安装MongoDB数据库,并内网穿透远程连接
659 0
|
消息中间件 XML JSON
一文就读懂RPC远程调用核心原理
rpc的全称是Remote Procedure Call,即远程过程调用,是分布式系统的常用通信方法。 Remote,简单来说的话就是两个不同的服务之间,两个服务肯定是两个不同的进程。因此,我们就从跨进程进行访问的角度去理解就行了。 Procedure,意思是一串可执行的代码,我们写Java的方法,就是一段课程行的代码。 Call,即调用,调用的就是跨了进程的方法。
719 0
一文就读懂RPC远程调用核心原理
|
XML JSON 开发框架
Asp.Net Core遇到Swagger(三)-Swashbuckle技巧b篇(上)
Asp.Net Core遇到Swagger(三)-Swashbuckle技巧b篇
518 0
|
存储 Java 编译器
【JVM】运行时数据区域
【JVM】运行时数据区域
181 0

热门文章

最新文章