背景
之前做过一段时间的客户端开发,当时使用go语言写的一个程序,跑在Android上,经常过一段时间就会崩溃。(很抱歉,“××”已停止运行。)
每当碰到这种情况就很崩溃,(内心OS:我只是个业余的安卓开发啊。。。)但是问题还是得解。
问题分析
幸好在安卓开发同事的帮助下,拿到了崩溃日志。

网上大概找了下java.lang.OutOfMemoryError这个错误的原因,大致有以下几个:
- 文件描述符(fd)数目超限,即proc/pid/fd下文件数目突破/proc/pid/limits中的限制。可能的发生场景有:
- 短时间内大量请求导致socket的fd数激增,大量(重复)打开文件等 线程数超限,即proc/pid/status中记录的线程数(threads项)突破/proc/sys/kernel/threads-max中规定的最大线程数。可能的发生场景有:
- app内多线程使用不合理,如多个不共享线程池的OKhttpclient等等
- 传统的java堆内存超限,即申请堆内存大小超过了 Runtime.getRuntime().maxMemory() (低概率)32为系统进程逻辑空间被占满导致OOM.
对应到:Could not allocate JNI Env错误,就是文件描述符fd超限了。
题外话:希望不要有好奇宝宝问我,为啥go写的程序,是java的日志。如果有,请搜索下go语言,安卓程序关键字~
我们知道在Linux系统中很重要的设计思想就是一切皆文件,网络是文件,键盘等外设也是文件。打开常规文件、新建TCP连接等操作都会占用fd,既然是fd超限,那么Linux系统默认对线程的fd限制是多少呢?
执行cat /proc/<pid>/limits

从图中我们可以看到Max open files 设置的最大数是1024,也就是说如果我们的进程使用的fd数超过了这个限制就会被系统kill掉,导致应用崩溃。
分析collector应用(自己写的垃圾代码),collector对常规文件的IO在个位数之内,但是对HTTP请求倒是不少,所以初步判断,collector的崩溃是由于HTTP请求太多并且没有释放连接(会占用fd)导致的。
问题复现与解决:
复现思路:
在collector上对其两个HTTP服务(HTTP上报数据模块、CMD指令执行模块)注册HTTP 接口,通过脚本来压测,并查看实时的fd。
- HTTP上报数据模块: 大概就是以固定频率搜集设备端信息,以HTTP方式上传给服务端后台。
- CMD指令执行模块:其它应用调用colletor的http服务,执行某种操作
脚本如下:
#!/bin/bash
for((i=0;i<40000000;i=i))
do
# sleep 1s
# 拿到默认应用列表
curl --request POST \
--url http://172.18.156.13:8086/doorplate/iot/devices/properties \
--header 'cache-control: no-cache' \
--header 'content-type: application/json' \
--header 'postman-token: c373eb5d-8a53-d3da-65ef-6b6709827252' \
--data '[{"field":"device_status","value":"BOOTsadfasdf"}]'
curl --request POST \
--url http://172.18.156.13:8086/benchmark/cmd \
--header 'cache-control: no-cache' \
--form action=get \
--form module=com.cvte.maxhub.settings \
--form key=ccas
curl --request GET \
--url 'http://172.18.156.13:8086/benchmark/set?key=af'
curl --request POST \
--url http://172.18.156.13:8086/doorplate/iot/devices/properties \
--header 'content-type: application/json' \
--data '[{"field":"device_status",\n"value":"BOOTsadfasdf"}]'
done
提前铺垫下几个命令:
lsof -p 25163 |grep TCP |wc -l 查看进程25163的TCP相关的fd总数
lsof -p 25163 |wc -l 查看进程25163所有的fd总数
下文,统一称改进之前(有崩溃)的collector为老版,改进之后为新版
我们对老版apk进行如下的问题复现步骤:
改动前:
-
压测之前fd查看: TCP 11左右;总 500左右

-
压测CMD服务:TCP 15左右基本不变 ;总 510左右基本不变,可以得出结论:和CMD服务无关;

-
压测HTTP服务:fd暴增到1257,持续增加 ;并且都无回落。可以得到结论:和HTTP服务有关


在老版代码上,统一HTTP客户端,设置合适的连接数,以及超时时间之后,打出了新版的apk。在新版apk基础上,我们进行了以下的验证过程。
改动后:
- 新版压测之前:TCP 与总的文件描述符 基本与老版相同


至此,整个应用崩溃问题的排查与解决过程算是结束了。
总结
- 在Linux系统下,Linux会对进程有各种限制,fd限制就是其中一种。
- HTTP连接也是会占用fd的,所以如果程序里HTTP服务使用比较频繁的话,一定要设置好HTTP连接池的参数。
Ref
后记
总感觉要解决这个问题,我们开发人员需要具备以下几个基本常识:
- linux系统中一切皆文件,tcp连接也是fd。fd是一种资源,是资源就会有限制
cat /proc/<pid>/limits命令查看进程的fd限制,或其它限制lsof -p <pid> |wc -l查看进程所有的fd总数