【踩坑系列】文件描述符fd超标,导致应用OOM

4,885 阅读5分钟

背景

之前做过一段时间的客户端开发,当时使用go语言写的一个程序,跑在Android上,经常过一段时间就会崩溃。(很抱歉,“××”已停止运行。)

每当碰到这种情况就很崩溃,(内心OS:我只是个业余的安卓开发啊。。。)但是问题还是得解。

问题分析

幸好在安卓开发同事的帮助下,拿到了崩溃日志。

从日志来看,问题是java.lang.OutOfMemoryError: Could not allocate JNI Env。

网上大概找了下java.lang.OutOfMemoryError这个错误的原因,大致有以下几个:

  1. 文件描述符(fd)数目超限,即proc/pid/fd下文件数目突破/proc/pid/limits中的限制。可能的发生场景有:
  2. 短时间内大量请求导致socket的fd数激增,大量(重复)打开文件等 线程数超限,即proc/pid/status中记录的线程数(threads项)突破/proc/sys/kernel/threads-max中规定的最大线程数。可能的发生场景有:
  3. app内多线程使用不合理,如多个不共享线程池的OKhttpclient等等
  4. 传统的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

(soft limit: 是可以人工设置的限制值;hard limit: 是最大值,即soft limit设置的值不能超过hard limit)

从图中我们可以看到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进行如下的问题复现步骤:

改动前:

  1. 压测之前fd查看: TCP 11左右;总 500左右

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

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

在老版代码上,统一HTTP客户端,设置合适的连接数,以及超时时间之后,打出了新版的apk。在新版apk基础上,我们进行了以下的验证过程。

改动后:

  1. 新版压测之前:TCP 与总的文件描述符 基本与老版相同

2. 新版CMD与HTTP服务一起压测:TCP与总的文件描述符 基本没有变化,只增加了相应进程数(同时开了多个脚本)个的TCP fd。

可以看到新版的apk对http连接池做了有效的改进,在压测下,也没有提高太多的fd数量。

至此,整个应用崩溃问题的排查与解决过程算是结束了。

总结

  1. 在Linux系统下,Linux会对进程有各种限制,fd限制就是其中一种。
  2. HTTP连接也是会占用fd的,所以如果程序里HTTP服务使用比较频繁的话,一定要设置好HTTP连接池的参数。

Ref

不可思议的OOM

后记

总感觉要解决这个问题,我们开发人员需要具备以下几个基本常识:

  1. linux系统中一切皆文件,tcp连接也是fd。fd是一种资源,是资源就会有限制
  2. cat /proc/<pid>/limits 命令查看进程的fd限制,或其它限制
  3. lsof -p <pid> |wc -l 查看进程所有的fd总数