应用服务OkHttpClient创建大量对外连接时内存溢出


应用服务OkHttpClient创建大量对外连接时内存溢出

1 背景

最近工作中碰到一个生产问题,就是应用服务在使用 OkHttpClient 时,在创建大量对外连接时线程堆积导致内存溢出。

主要表现是在流量极低的情况下,即平均 qps 在 1~4 左右的情况下,各主要线程都很低,但是系统活跃线程却很高,超过了限制的阈值,如果持续下去,线程堆积过高则会导致应用程序直接挂掉。

2 排查

​2.1 原因

在对应用服务的其中一个 pod 的线程栈 dump 出来分析后,发现 dump 出来的文件中,有 266 个线程,其中 145 个都来自于同一个ConnectionPool(OkHttp ConnectionPool),而且是在流量不高的情况下。

经分析主要原因是应用中在创建 OkHttpClient 对象时,没有创建同一个 OkHttpClient 实例并重复使用,而是对于所有的 http 请求都重复创建一个新的实例,而每个实例都有自己的连接池和线程池,从而导致线程大量堆积。

应用服务OkHttpClient创建大量对外连接时内存溢出

而 OkHttpClient 默认的最大线程空闲数是 5,keepAlive 时间为 5 分钟,也就是发起一次网络连接后,5 分钟内不会断开连接,从而导致在创建大量对外连接时内存溢出。

应用服务OkHttpClient创建大量对外连接时内存溢出

而查看 OkHttpClient 类的源码发现,通过 OkHttp 创建每个OkHttpClient 实例的时候,每个客户端都会持有自己的连接池和线程池。

对于通过 OkHttp 创建的所有的 http 请求,在创建一个 OkHttpClient 实例并重复使用时,重用它的连接池和线程池可以减少延迟和节省内存。

应用服务OkHttpClient创建大量对外连接时内存溢出

2.2 验证过程

2.2.1 修改前

查看应用程序中的代码可以看到,在创建 OkHttpClient 实例的时候,并没有创建一个可复用的实例,而是每次创建 http 请求时,都会 new 一个新的 OkHttpClient 实例。

应用服务OkHttpClient创建大量对外连接时内存溢出

在 jmeter 模拟多个线程同时请求应用服务的接口,每次请求 300 个线程,通过多次请求之后,通过 jconsole 工具可以很清楚的看到线程数刚开始的时候基本为 0,但多次模拟请求过后很快就超过了 3000 个,然后电脑连续挂了 2 次。。。

应用服务OkHttpClient创建大量对外连接时内存溢出

并且从下图中,我们可以非常明显地看到,这些线程大部分都是 OkHttp ConnectionPool。

应用服务OkHttpClient创建大量对外连接时内存溢出

2.2.2 修改后

修改之后,创建一个共享的 OkHttpClient 对象,jmeter 每次模拟 300 请求,通过 jconsole 可以观察到,发现第一次模拟达到了一个峰值后,后面无论再怎么模拟,峰值基本都不再改变了。

而修改之前每次模拟 300 请求,模拟几次之后会发现线程数一直在蹭蹭往上涨,直至线程大量堆积导致应用程序崩溃。

应用服务OkHttpClient创建大量对外连接时内存溢出

3 解决

创建一个共享的 OkHttpClient 对象即可,而不是每次调用方法都单独创建一个 OkHttpClient 对象,每个 OkHttpClient 对象都有自己的连接池和线程池,会导致大量的线程堆积,从而可能会导致程序崩溃。

应用服务OkHttpClient创建大量对外连接时内存溢出