记一次 Android 周期性句柄泄漏的排查

滴滴国际化外卖 Android 商户端正常迭代版本过程中,新版本发布并且线上稳定一段时间后,突然触发线上 Crash 报警。

97cfe016ee604451f87e7298db909024.png

第一次排查发现是在依赖的底层平台 so 库中崩溃,经过沟通了解到其之前也存在过崩溃问题,所以升级相关底层 so 版本。重新发版后短期没有出现 Crash 大面积上报情况,只有零星上报,但不久后又发生了第二次大面积 Crash 上报。具体信息如下图所示:

09ef9b5b54902d7772da4703f771775a.png

在定位分析问题的过程中收获很多,通过这篇文章分享该 Crash 的排查过程、问题根因以及一些经验总结,希望能为读者在遇到同类型问题时提供一些参考。    

排查流程

Crash 描述

Crash 的量级,从两次高峰期的峰值来看,集中爆发的峰值为每日50次左右,第二次爆发的峰值最高为173次。同时,Crash 和影响用户数量是一比一。

4584850639a2ca2b0f1ef239fc8fbce7.png

两次大面积爆发的问题设备集中在华为的三款机型,运行时间都在14天左右,内存情况正常,线程情况正常,此时还没有相关线索指向句柄,所以这个时候没有关注句柄,如下图:

e794afb2ee3854dff69ec3031ad7d959.png

098dd452d3f401e441b1c6a0e5b1510e.png

定位分析

从整体的 Crash 描述以及 Crash 统计平台的相关数据来分析,每隔14天就大面积爆发一次,可以确定是周期性问题,这种问题的排查难度较高。

根据上报的错误日志,明确其崩溃位置是在底层的 libpush.so 库,同时和其维护的同学沟通后发现依赖的 so 版本出现过问题,所以我们在第一次大面积上报之后,升级了底层 so 库版本。虽然当时增发版本后没有明显的 Crash 上报,但还是存在零星的 Crash 上报,这让我们放松了警惕,未对问题的根因进行定位,才导致了后面更严重的第二次爆发。

第二次爆发后,通过分析 Crash 统计平台上的173次 Crash 日志信息发现,Crash 代码地址都是“000000000007ce08”,如下图,由于静态库中的代码地址都是固定的,所以对底层 so 库进行了代码定位。

ae07428f195951073599066713833185.png

我分析底层 so 库代码习惯是使用 IDA,通过 IDA 定位到的问题代码如下图:

5c8663c090d90188948b20bede43f764.png

找到对应问题代码块,定位到直接原因是 fopen 文件返回空,fwrite 写入数据之前未做空判断。由于 fopen 是调用系统 API,系统 API 出现问题概率极小,所以一定是业务某些异常场景导致打开文件失败。

业务进行了哪种非法调用引发的异常场景?我们需要定位到问题场景的代码执行环境和具体的用户操作路径。此时只有完整复现这个问题,才能找到导致 fopen 失败的根因。

上面我们推测是周期性问题,与业务运营侧同学确认,没有周期性的活动发布,排除客观因素。

从 Crash 相关数据分析,除了定位到 libpush.so 的直接代码位置,没有太好的进展,所以根据对应上报高峰的时间段排查 top5 中的其它新增 Crash,发现其中一个 Crash 从上报时间、运行时间、机型几个纬度与直接 Crash 信息高度一致,大概率是同一个问题导致,查看对应的堆栈信息。

6ceea46965a99709e20ab72cc994ae87.png

综合定位到的底层 so 库的问题代码,分析原因是句柄超限后 fopen 打开失败导致为空,综合 App 长时间运行分析,句柄泄漏问题有14天(左右)的周期性共性条件,同时输出了占比top3问题机型的句柄上限都是1024。我们知道目前国内大多机型的句柄上限是10000+,不过由于我们自己的业务形态是基于定制设备的,定制设备更新换代较慢,机型较老,所以句柄上限较低,最终导致了问题主要集中在业务采购的定制系统设备,非定制的用户设备句柄虽然也会异常增加,但是在一个版本周期内是远远达不到句柄上限的,也就不会出现崩溃问题。

从以上信息推测崩溃问题是句柄泄漏导致超过系统上限,剩下的就是如何复现用户的操作路径和正向代码根因定位了。

问题复现

由于是句柄泄漏问题,输出打开的本地 fd 后,无法直接定位到具体 so。又因为是新版本新增问题,所以通过反向排除法,进行版本 diff,排查更新的代码。而业务代码未涉及句柄操作,故逐个进行依赖 SDK 还原,设备定时输出 fd 数量进行分析。

测试版本为线上有问题版本:

第一次测试记录:测试耗时:12h+,fd: 227 → 272

第二次测试记录:测试耗时:15h+,fd: 227 → 296

第三次测试记录:测试耗时:24h,fd: 227 → 313

e401608e5552f7fba79e2b6c1cef3e9b.png

句柄数量明显增加。

测试版本为还原更新的SDK版本:

第一次测试记录: 测试耗时:15h,fd: 193 → 198

第二次测试记录:测试耗时:21h,fd: 193 → 204,切换过账号一次

第三次测试记录:测试耗时:39h,fd: 193 → 210

句柄数量无明显增加。

对比 SDK 还原前后两个版本运行的数据得出结论,句柄异常增加的根因在线上版本所依赖的3个 SDK。这时候只需再依次对比3个依赖 SDK 的数据,定位到具体的问题 SDK 是时间的问题。同时我们也将排查定位进展同步给各自相关基础 so 库维护同学,发现之前其中一个 so 库的历史版本存在过句柄泄漏问题,和相应同学沟通了解相关信息。通过梳理底层 so 的句柄泄漏调用逻辑,增加调用日志,并对设备句柄数量持续观察,最终发现 so 侧存在一个逻辑:6小时轮询打开19个句柄,但是打开后没有正常关闭释放

此时问题的根因已大概定位,为了加快复现,我们把6小时轮询时间缩短为2分钟,运行一段时间后程序句柄数量达到1024上限发生崩溃,崩溃日志与线上崩溃日志完全相同,正向从用户角度复现了该问题。同时通过有无问题的两个版本 so 跑数据,对比后也证明问题的产生是由当前 so 库导致。至此,问题直接原因以及根本原因都已定位,问题修复后发版上线,线上验证通过。

为什么问题会集中爆发在 fwrite 方法的调用上?原因是业务其中一个场景需要30s轮询调用某个操作句柄的 API,高频调用 fwrite。它不能定位根本原因,不过暴露了直接原因,这也说明内存泄漏和句柄泄漏可能会报在任何代码位置。这里也让我们初期排查问题时,偏离了方向。不过这个就是这篇文章最想强调的内容,也是最想解决的问题,当出现这类问题,作为RD,我们要重点关注些什么?来快速纠正方向,快速定位问题。

下面我们简单介绍一下句柄泄漏是什么?如何处理?如何预防?Android中都有哪些常见的句柄?有助于我们后期快速排查定位句柄相关问题。

什么是句柄泄漏

句柄泄漏,就是当打开的资源未被正常释放,导致资源不能关闭回收。因为系统会为每个进程规定最大文件描述符上限数,一般 Linux 系统的进程最大句柄上限为1024,不过现在比较新的 Android 系统,上限升到32768,我们可以通过 adb shell ulimit -n 来查看:  

e64932a8fd72bff9e159fb0e107e0f64.png

截图为vivo findx2的设备文件描述符上限数

程序存在句柄泄漏问题时,对应的资源句柄不会被释放,当达到上限时,程序崩溃,报出异常信息。一般的异常信息有

  • Could not allocate dup blob fd

  • java.lang.RuntimeException: Could not read input channel file descriptors from parcel.

  • abort message 'could not create instance too many filesœ

  • java.io.IOException: Cannot run program "logcat": error=24, Too many open files

  • "Could not allocate JNI Env: %s", error_msg.c_str()

  • "Could not open input channel pair"

如果上报的日志信息包含这种,那大概就是句柄泄漏导致了。

注意:句柄泄漏和内存泄漏这类问题不属于业务逻辑问题,是进程分配的资源耗尽,导致再次分配时无足够的资源进行分配,所以当程序运行时,达到对应的资源上限后,就算普通的代码依然会直接报错,这个时候,上报的日志就是对应的代码位置,这点容易误导我们排查线上问题。

如何解决句柄泄漏

上面也提过,句柄泄漏和内存泄漏是一类问题,这类问题崩溃后,Crash 的位置可能不会明确的标出是哪里出现问题,最终的 Crash 日志也可能是普通代码。

问题用户操作路径存在共性的情况(复现难度较低)

  • 确定问题用户操作路径的共性

  • 根据用户操作路径,反复操作进行句柄数量监控并本地保存

  • 输出句柄异常增长情况,查看异常增长的句柄

  • 排查业务上,涉及句柄分配的代码

问题用户操作路径无共性的情况(复现难度较高)

  • 通过diff问题版本前后的代码,确定涉及句柄分配的代码改动

  • 通过排除法进行逐一回退对比,进行句柄数量监控和分析

  • 通过工具(so 库可以借助 IDA)进行问题代码定位,辅助分析问题

  • 确定问题代码或者问题依赖,再深入定位具体代码

以上的结论是建立在没有其它辅助手段基础上,实际排查过程中,我们还可以通过 Crash 平台上的辅助信息、梳理底层 so 库代码逻辑、积极与 so 侧同学沟通等角度进行辅助定位,也可以加速定位到问题代码。

定位句柄泄露相关命令和代码

1.查看设备句柄上限:adb shell ulimit -n

2.输出进程句柄:

private void listFd() {
    String tag = "FD_TAG";
    File fdFile = new File("/proc/" + android.os.Process.myPid() + "/fd");
    File[] files = fdFile.listFiles();
    int length = files.length;
    MerchantLogUtils.logFd(tag, "fd length: " + length);
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < length; i++) {
        File f = files[i];
        String strFile = Os.readlink(f.getAbsolutePath());
                sb.append(strFile + "\n");
    }
}

句柄泄漏常见问题以及分类

1.Andorid 常见的句柄泄漏问题

  • HandlerThread 的使用,要记得 release

  • IO 流操作打开后要在 finally 中 close

  • SQLite 数据库操作要把 cursor 实例 close

  • InputChannel相关,即 WindowManager.addView反复调用时,记得 removeView

  • Bitmap 进行 IPC 

这是常见的 Android 句柄泄漏的点,具体的原理分析资料很多。

2.部分 Android 的文件描述符的具体分类,可以缩小排查范围

c400686297c0e5bcf9f13f3db0bb8c07.png

如何预防和监控句柄泄漏

上面都是如何解决,其实更希望问题在线下或者灰度期间就暴露并且解决掉。

预防

  • 代码开发过程中,涉及句柄操作需“慎之又慎”,要经过充分的自测

  • 代码 CR 的 CheckList 增加“句柄相关代码的重点CR”,如上面介绍的 Android 常见的句柄泄露的场景

  • 版本需求改动涉及句柄创建时,QA 需重复多次操作对应路径,查看句柄情况

  • 测试粒度覆盖句柄,测试包定时输出句柄指标,达到对应的阈值后报警,端上同学介入排查句柄增长原因

  • 自动化测试中增加长时间运行时句柄数量的观测

观测

  • 线上定时获取设备句柄数量并且上报,建立句柄均值观测

  • 根据线上稳定期间的句柄数量均值,设置合理的报警阈值,及时感知到该类线上问题

总结

从 Crash 统计平台的相关数据可以看到,所有的 Crash 都在底层 so 库里,这类问题我们除了分析 Crash 统计平台提供的相关信息,是否还有其它可以辅助定位问题的手段呢?

  • 在全面定位过程中,也需要关注相同时间段的其他新增 Crash 信息,确认是否有相关性,如同为新增、机型、时间、地区、用户操作场景等等之间的相关性。

  • 通过对应的工具进行三方 so 的代码逻辑梳理,综合已有数据进行分析,如本次排查过程中使用 IDA 分析底层 so 库代码,定位到直接原因为 fwrite 之前未做空判断。由于系统 API 出现问题概率极小,如果之前处理过此类问题,综合 Crash 统计平台上的机型、运行时间等,从这就可以初步定位是句柄泄漏问题。

  • 积极同步进展以及所有可能的分析到底层 so 侧同学,我们定位到句柄泄漏,反向排除法定位问题过程中也同步到底层so同学,发现之前另一个底层so 解决过句柄泄漏问题,提前定位到问题 so,减少了问题定位的人力浪费。

  • 在与相关同学沟通时,要带有自己的分析和判断,有着重点的讨论,这样会极大地提升排查效率。

综上,当问题发生在平台底层库时,同时又无明显的用户操作路径,通过代码定位工具先尝试定位直接原因,再通过 Crash 平台上各方面的信息对比分析其相关性,配合测试数据,可以加快定位这类周期性问题。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Linux mx6ull-驱动(1)hello

编写第一个驱动&#xff0c;hello_drv 一、获取内核、编译内核。 这里为什么要获取内核呢&#xff0c;因为我们写的是驱动程序&#xff0c;而不是裸机程序。也就是我们的板子已经烧入进去了uboot、内核&#xff0c;根文件。然后我们要在这个板子的内核的基础上&#xff0c;来…

【uniapp】签名组件,兼容vue2vue3

网上找了个源码改吧改吧&#xff0c;清除了没用的功能和兼容性&#xff0c;基于uniapp开发的 样子 vue2 使用方法&#xff0c;具体的可以根据业务自行修改 <signature ref"signature" width"100%" height"410rpx"></signature>confi…

[鹏程杯2023]复现

SecretShare X的20个值和R的21个值已经被全部泄露&#xff0c;X和R都是1024bit的值&#xff0c;此时X总共泄露了32*20 640&#xff0c;于是&#xff0c;此时我们可以使用mt19937将其还原&#xff0c;还原之后&#xff0c;我们往前推20个1024bit的值&#xff0c;便可以求得A的…

C语言 预处理详解

目录 1.预定义符号 2.#define 2.1#define 定义标识符 2.2#define 定义宏 2.3#define 替换规则 2.4#和## 2.4.1# 的作用 2.4.2## 的作用 2.5 带有副作用的宏参数 2.6宏和函数的对比 对比 **2.7内联函数 2.8命名约定 3.#undef **4.命令行定义 5.条件编译 常…

AVL树性质和实现

AVL树 AVL是两名俄罗斯数学家的名字&#xff0c;以此纪念 与二叉搜索树的区别 AVL树在二叉搜索树的基础上增加了新的限制&#xff1a;需要时刻保证每个树中每个结点的左右子树高度之差的绝对值不超过1 因此&#xff0c;当向树中插入新结点后&#xff0c;即可降低树的高度&…

闪客网盘系统源码,已测试对接腾讯COS及本地和支付(支持限速+按时收费+文件分享+可对接易支付)- 修复版

正文概述 资源入口 支持对文件下载限速 对接易支付 推广赚钱啥的功能 源码非常的好 支持腾讯cos 阿里云cos 本地储存 远程存储 源码仅支持服务器搭建 php7.2 伪静态thinkphp 运行目录public 导入数据库 修改config目录下的database.php数据库信息 后台地址&#xff1a; 域名/ad…

如何在CentOS上安装SQL Server数据库并通过内网穿透工具实现远程访问

文章目录 前言1. 安装sql server2. 局域网测试连接3. 安装cpolar内网穿透4. 将sqlserver映射到公网5. 公网远程连接6.固定连接公网地址7.使用固定公网地址连接 正文开始前给大家推荐个网站&#xff0c;前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;…

CLIP Surgery论文阅读

CLIP Surgery for Better Explainability with Enhancement in Open-Vocabulary Tasks&#xff08;CVPR2023&#xff09; M norm ⁡ ( resize ⁡ ( reshape ⁡ ( F i ˉ ∥ F i ‾ ∥ 2 ⋅ ( F t ∥ F t ‾ ∥ 2 ) ⊤ ) ) ) M\operatorname{norm}\left(\operatorname{resize}\…

文件包含 [ZJCTF 2019]NiZhuanSiWei1

打开题目 代码审计 if(isset($text)&&(file_get_contents($text,r)"welcome to the zjctf")){ 首先isset函数检查text参数是否存在且不为空 用file_get_contents函数读取text制定的文件内容并与welcome to the zjctf进行强比较 echo "<br><h…

【C++】类型转换【4中类型转换】

目录 1. C语言中的类型转换 2. C的四种类型转换 2.1 static_cast 3.2 reinterpret_cast 3.3 const_cast 3.4 dynamic_cast 3. explict 4. RTTI&#xff08;了解&#xff09; 1. C语言中的类型转换 在 C 语言中&#xff0c;如果 赋值运算符左右两侧类型不同&#xff0…

如何用Excel做最小二乘法②

因为在Excel里面做最小二乘法是需要用到LINEST函数的&#xff0c;所以如果不知道怎么对数据进行最小二乘法时&#xff0c;就应该研究一下LINEST函数。 LINEST 函数语法 LINEST(known_ys, [known_xs], [const], [stats]) known_ys (必须) 因变量&#xff0c;单行/单列known_xs…

xxl-job 原理

一、xxl-job 架构设计 总体分两个部分&#xff1a; 调度中心&#xff1a;负责管理调度信息&#xff0c;按照调度配置发出调度请求&#xff0c;自身不承担业务代码。调度系统和任务解耦&#xff0c;提高了系统可用性和稳定性。通调度性能不在受限于任务模块。执行器&#xff1a…

Mysql配置主从复制-GTID模式

目录 主从复制 主从复制的定义 主从复制的原理 主从复制的优势 主从复制的形式 主从复制的模式 主从复制的类型 GTID模式 GTID的概念 GTID的优势 GTID的原理 GTID的配置 Mysql主服务器 ​编辑 Mysql从服务器 ​编辑 主从复制 主从复制的定义 是指把数据从一个…

Docker安装Mysql

Docker常用指令&#xff1a; docker search 镜像名&#xff1a;寻找镜像 docker pull 镜像名&#xff1a;拉去镜像 docker images :查看拥有镜像 docker ps :查看正在运行容器 docker pa -a &#xff1a;查看所有容器&#xff08;包含运行中的和停止的&#xff09; dock…

案例 - 拖拽上传文件,生成缩略图

直接看效果 实现代码 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>拖拽上传文件</title>&l…

Python 实现动态动画心形图

在抖音上刷到其他人用 matlab 实现的一个动态心形图&#xff0c;就想也用 Python 实现&#xff0c;摸索了两种实现方式&#xff0c;效果如下&#xff1a; 方法一&#xff1a; 利用循环&#xff0c;结合 pyplot 的 pause 与 clf 参数实现图像的动态刷新 import matplotlib.p…

【漏洞复现】weblogic-10.3.6-‘wls-wsat‘-XMLDecoder反序列化(CVE-2017-10271)

感谢互联网提供分享知识与智慧&#xff0c;在法治的社会里&#xff0c;请遵守有关法律法规 文章目录 1.1、漏洞描述1.2、漏洞等级1.3、影响版本1.4、漏洞复现1、基础环境2、漏洞扫描nacsweblogicScanner3、漏洞验证 说明内容漏洞编号CVE-2017-10271漏洞名称Weblogic < 10.3.…

【Unity细节】VS不能附加到Unity程序中解决方法大全

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! 本文由 秩沅 原创 &#x1f636;‍&#x1f32b;️收录于专栏&#xff1a;unity细节和bug &#x1f636;‍&#x1f32b;️优质专栏 ⭐【…

物联网AI MicroPython学习之语法 二进制与ASCII转换

学物联网&#xff0c;来万物简单IoT物联网&#xff01;&#xff01; ubinascii 介绍 ubinascii模块实现了二进制数据与各种ASCII编码之间的转换。 接口说明 a2b_base64 - 解码base64编码的数据 函数原型&#xff1a;ubinascii.a2b_base64(data)注意事项&#xff1a; 在解码…

GPT-4-Turbo的128K长度上下文性能如何?超过73K Tokens的数据支持依然不太好!

本文原文来自DataLearnerAI官方网站&#xff1a;GPT-4-Turbo的128K长度上下文性能如何&#xff1f;超过73K Tokens的数据支持依然不太好&#xff01; | 数据学习者官方网站(Datalearner)https://www.datalearner.com/blog/1051699526438975 GPT-4 Turbo是OpenAI最新发布的号称…