0

0

记一次 android 线上 oom 问题

雪夜

雪夜

发布时间:2025-07-10 12:01:04

|

983人浏览过

|

来源于php中文网

原创

背景

公司的主打产品是一款跨平台的 app,我的部门负责为其提供底层的数据传输 sdk,我负责的是 android 端的 sdk 开发。

SDK 并不直接加载在 App 主进程中,而是隔离在一个单独的进程中,两个进程通过 TCP 连接进行通信。这种做法的目的是减少因 SDK 崩溃导致主进程崩溃,为用户带来更好的体验。

记一次 android 线上 oom 问题如图所示,SDK 主要实现于 service.so 中,被 Work 进程加载,kernel.so 通过 JNI 嵌入在 App 主进程中,前者作为侦听端,后者为连接端。

然而,这种方式存在一个问题:当侦听端口被占用时,两个进程无法建立通信,导致数据无法传输。为了解决这个问题,我们计划使用本地套接字(Unix Domain Socket)替代 TCP 套接字,因为前者不依赖端口号,只依赖文件路径,而 Android 的私有存储可以有效防止文件冲突。

这个替换过程不能一蹴而就,因为 App 进程加载的 SO 文件与 Work 进程加载的可能不是同一个版本。考虑到向后兼容,新的 service 版本需要同时侦听 TCP 和本地两个通道,新的 kernel 版本也需要同时连接这两个通道,哪个先连接上就使用哪个。

开发完成的自测阶段一切正常,验证了以下组合:

连接端 侦听端 结果
TCP 本地, TCP TCP 成功
本地 本地, TCP 本地成功
本地, TCP TCP TCP 成功
本地, TCP 本地, TCP 本地, TCP 均成功,一般本地抢先

结果符合预期,提测阶段也顺利通过,于是通过版本灰度,逐渐替换线上的旧版本,各个灰度阶段观察正常,最后正式全量发布。

问题发生

全量发布两天后,正式将特性分支合并入 master,结果合并后不到 30 分钟,QA 反馈主端 OOM(内存溢出)崩溃异常升高,需要回滚版本验证。

了解情况后,发现主端的全部版本崩溃率确实从 0.01% 升高到了 0.05%~0.07% 的水平,且大量新增的崩溃类型堆栈显示 OOM 信息。最关键的是,崩溃升高的趋势与 SDK 灰度的节奏完全吻合,而在这期间主端没有发布新的版本,于是只能回滚 SDK 版本尝试。

糟糕的是刚刚合并的代码,使用 revert 回滚提交的几个 commit 又出现了一大堆冲突提示。在解决冲突的过程中,QA 等不及了,建议从之前合并的位置直接拉分支打版本,一顿操作猛如虎,很快就打好了回滚版本,当天就通过了测试小流量。

第二天一看,崩溃率果然应声下降,于是 QA 开启全量修复。同时研究了一个短平快的 master 回滚方案:新建一个目录,克隆并 checkout 到合并前的代码,将 .git 目录删除后用这个目录覆盖旧的工作目录,最后将所有 modified 的文件作为新版本直接提交。这样做的好处是可以得到与合并前完全相同的代码,防止手工处理冲突引入新的变更。

问题分析

随着回滚版本的放量,主端 OOM 崩溃逐渐回归正常,进一步坐实了新版本存在问题。OOM 问题非常不好排查,原因是崩溃时的堆栈与引入 bug 的地方已经相差了十万八千里,不能直接定位问题点。

好在这个版本之前做过一次小流量,看当时的崩溃率没有明显升高。在准备全量前,合并了 master 上的最新修改、iOS 平台的一些代码等,因此重点排查两个版本的差异部分,应该就可以定位引入问题的点。

走查了一遍,没有发现明显的内存泄漏代码:

  • master 是稳定版本,不存在内存泄漏;
  • iOS 平台代码通过宏定义作了隔离,对 Android 没有影响;
  • 只有一个地方非常可疑——这是一个日志上报操作,只在特定场景下发生,日志上报时并不是直接上报到服务器,而是放入一个队列,再由专门的线程负责上传。一次上报并不会占用太多内存,但关键是一旦进入这个特定场景,日志就会一直产生,而主端会在传输数据的过程中频繁调用这个接口,导致大量的日志进入队列,特别是当用户处于非 WiFi 环境下,日志上报会被关闭来节省流量,进一步加剧了队列积压,最终导致队列疯狂增长耗尽内存……

知道了原因,改起来就简单了,加一个 bool 标记,上报过后设置这个标记下次就不再上报了,因为这类日志有一条用来排查问题就足够了。

问题定位修复版都打好准备送测了,老大的一句话提醒了我——最好能在本地复现一下。于是基于有问题的版本,稍加修改让它一启动就不停上报日志,关闭 WiFi 打开 4G,用这个版本在测试机上跑了一整天,进程居然没崩溃!

于是不得不评估一下日志上报的泄漏规模,按一条日志最大 300 字节、主端 2 次/秒的调用频率计算,一天占用内存为 300 2 3600 * 24 = 51840000 B。

佐罗电子商务系统改进版
佐罗电子商务系统改进版

主页面上引用了三个页面也说不过去呀。本次主要是把数据库合并了一下,至于功能,没有加什么新的东西,还是那些:在线订购、帐单查询(添加了一个打印的连接)、特价商品列表、热买商品列表、留言本(许多朋友说以前的那个有问题,现在换成枫叶阁女士留言本,挺不错的)、新闻、完善的管理

下载

与同事一起研究这个问题后,我又提出了一个疑点:如果是因为日志泄漏导致的 OOM,那应该是 Work 进程崩溃,而不是出现大量的 App 进程崩溃。如果是因为内存耗尽导致系统上所有进程崩溃,那也至少是崩溃率一起升高,而不像现在只有 App 进程崩溃率升高,所以越看越不像是这个原因导致的。

问题根因

正当排查方向一片迷茫的时候,同事的一句话提醒了我——如果能抓到崩溃现场的日志就好办了。可是怎么抓呢?崩溃平台记录的是崩溃时间和 CUID,后者用于标识一次唯一的崩溃事件;日志抓取需要时间范围和用户 UID,而崩溃平台并不提供 UID。

这时同事神秘兮兮地祭出了一条链接,点开一看:ID-Mapping,可以将各种系统的 ID 进行批量转换,其中就包括 CUID 向 UID 的转换,好家伙,这不就是我想要的?老同事真的浑身都是宝,摸着他们过河错不了~

大部分 UID 没有捞取到日志,只有两个用户有日志。内容非常多但都是重复的,看起来 Work 进程没有启动,导致连接端一直在进行重连。在连接后期都发现了这样的日志:

2021-10-30T20:55:19.84255454 [b61e7920] {netio} LocalHandler::post_connect: local endpoint failed with system:24, fatal error
2021-10-30T20:55:19.84408116 [b61e7920] {netio} kernel_message_transmit:handle_io: pipeerror|system:24 type=1|channel=1
2021-10-30T20:55:19.84480116 [b61e7920] {netio} kernel_message_transmit:handle_io: pipeerror|system:24 type=1|channel=2
2021-10-30T20:55:31.05991064 [b61e7920] {netio} kernel_service_interface:on_ready_timeout: restart! running=1, channel=0

查了下系统错误码:

#define EMFILE      24  /* Too many open files */

这种错误一般是打开的句柄超过 Linux 进程的最大打开文件句柄数(一般是 1024),这个值对于服务器程序来说一般是不够用的,需要通过系统设置来拉高上限。但对于 App 进程是足够了,怎么会超限呢?难道是出现了句柄泄漏。于是马上去走查了连接关闭的代码:

if channel='local' then
   close local_channel
else if channel='tcp' then
   close tcp_channel
else
   nothing
   channel = 'none'

这里使用了伪代码来说明大意,其中 channel 标记当前使用的连接方式,初始时设置为 none,连接时两种方式同时发送异步连接请求,先收到应答的连接将设置对应的 channel 值并关闭另一种连接通道,连接建立成功后 channel 必为两种方式之一(local | tcp)。

上面推演的是正常的场景,当 Work 进程没有启动而导致两个通道都无法完成连接时,channel 将一直保持 none 值直到超时,在连接重启前,会尝试使用上面这段代码清理资源,此时就会命中最后的 else 逻辑——什么也不做——从而导致连接句柄被泄漏。以 10 秒重连、6 秒超时一次计算,每 16 秒就泄漏 2 个句柄,1024 个句柄泄漏光只需要不到 2 小时!

为了验证,专门修改了一版代码,人为制造 Work 进程不启动的场景,果然跑了没多久 App 进程就崩溃重启了。确定了问题根因,再回顾一下现象,之前那几个疑问就能得到解释了:

  • 问题表现为打开文件、创建线程均失败的 OOM 问题,实际是 OOF(Out of FD),句柄泄漏的表现和内存泄漏有相似的地方。
  • 问题存在于 kernel,当 kernel 耗光句柄后对应的 App 进程会因 EMFILE 错误崩溃,Work 进程反而是没什么事,所以表现为 App 进程崩溃率单独升高。
  • 只影响一部分 Work 进程长时间不启动的用户,这部分用户占比较少,所以崩溃率升高有限。
  • 之前小流量的那版也有问题,只是放量较少所以崩溃率升高不明显而已。

问题的修复非常简单,就是在关闭清理资源时,不再根据 channel 判断,直接 close 所有句柄。打好的修复版本在 Work 进程不启动的场景下运行了一天也没有出现崩溃,对外灰度后,观察 App 崩溃率正常,逐步全量覆盖线上版本,最后合并入 master。

结语

复盘整个 OOM 问题产生的过程,为何在灰度阶段没有发现 App 进程崩溃率异常升高呢?原来在看崩溃数据时是过滤了 SDK 版本号的,而实际发生异常升高的版本号却是奇特的 0.0.0.1 版本,因而没有观察到。

为何 OOM 问题会集中在 0.0.0.1 版本中?进一步排查发现并非只有 OOM 崩溃是这样,90% 的崩溃都归类在了这个版本下面,原因竟然是 App 在初始化时没有处理好先后关系,从 SDK 拿版本号时 SDK 还未初始化,所以得到了一个无效的版本值。更严重的是,该问题几乎一直存在,而我们之前过滤版本号的做法几乎可以肯定是不正确的,想到这里不由得背上直冒冷汗!幸好有这次问题的复盘,不然这个问题要继续存在多久还是个未知数~

最后总结一下 OOM 问题的处理方法:

  • 首先不要心慌,特别是在不经求证的情况下靠猜测来定位问题、靠不断发小版本在线上验证问题,这样做一来不严谨,二来效率比较低,最终很可能还会定位不到问题;最好的办法是通过现场日志来定位出错的场景,可以极大的缩小排查范围;
  • OOM 与 OOF 在 Java 崩溃堆栈中有相似的表现,因此遇到这类问题可以多考虑下句柄泄漏的可能性,而不是一味观察内存的分配与释放;
  • 如果认定是内存泄漏,那么从代码层面预估的泄漏规模一定要有符合常识,特别是能制造泄漏场景复现问题。

另外可能还有人对 Work 进程为何没有启动感兴趣,但这就属于另外一个问题了,可以单独写篇文章了。目前仍在排查中,真的是应了那句:生命不息,debug 不止~~

参考[1]. Git 如何优雅地回退代码,用 reset 还是 revert?

相关专题

更多
java
java

Java是一个通用术语,用于表示Java软件及其组件,包括“Java运行时环境 (JRE)”、“Java虚拟机 (JVM)”以及“插件”。php中文网还为大家带了Java相关下载资源、相关课程以及相关文章等内容,供大家免费下载使用。

837

2023.06.15

java正则表达式语法
java正则表达式语法

java正则表达式语法是一种模式匹配工具,它非常有用,可以在处理文本和字符串时快速地查找、替换、验证和提取特定的模式和数据。本专题提供java正则表达式语法的相关文章、下载和专题,供大家免费下载体验。

741

2023.07.05

java自学难吗
java自学难吗

Java自学并不难。Java语言相对于其他一些编程语言而言,有着较为简洁和易读的语法,本专题为大家提供java自学难吗相关的文章,大家可以免费体验。

736

2023.07.31

java配置jdk环境变量
java配置jdk环境变量

Java是一种广泛使用的高级编程语言,用于开发各种类型的应用程序。为了能够在计算机上正确运行和编译Java代码,需要正确配置Java Development Kit(JDK)环境变量。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

397

2023.08.01

java保留两位小数
java保留两位小数

Java是一种广泛应用于编程领域的高级编程语言。在Java中,保留两位小数是指在进行数值计算或输出时,限制小数部分只有两位有效数字,并将多余的位数进行四舍五入或截取。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

399

2023.08.02

java基本数据类型
java基本数据类型

java基本数据类型有:1、byte;2、short;3、int;4、long;5、float;6、double;7、char;8、boolean。本专题为大家提供java基本数据类型的相关的文章、下载、课程内容,供大家免费下载体验。

446

2023.08.02

java有什么用
java有什么用

java可以开发应用程序、移动应用、Web应用、企业级应用、嵌入式系统等方面。本专题为大家提供java有什么用的相关的文章、下载、课程内容,供大家免费下载体验。

430

2023.08.02

java在线网站
java在线网站

Java在线网站是指提供Java编程学习、实践和交流平台的网络服务。近年来,随着Java语言在软件开发领域的广泛应用,越来越多的人对Java编程感兴趣,并希望能够通过在线网站来学习和提高自己的Java编程技能。php中文网给大家带来了相关的视频、教程以及文章,欢迎大家前来学习阅读和下载。

16926

2023.08.03

高德地图升级方法汇总
高德地图升级方法汇总

本专题整合了高德地图升级相关教程,阅读专题下面的文章了解更多详细内容。

72

2026.01.16

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
PostgreSQL 教程
PostgreSQL 教程

共48课时 | 7.4万人学习

Git 教程
Git 教程

共21课时 | 2.8万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号