应用服务OkHttpClient创建大量对外连接时内存溢出
1 背景
最近工作中碰到一个生产问题,就是应用服务在使用 OkHttpClient 时,在创建大量对外连接时线程堆积导致内存溢出。
主要表现是在流量极低的情况下,即平均 qps 在 1~4 左右的情况下,各主要线程都很低,但是系统活跃线程却很高,超过了限制的阈值,如果持续下去,线程堆积过高则会导致应用程序直接挂掉。
2 排查
2.1 原因
在对应用服务的其中一个 pod 的线程栈 dump 出来分析后,发现 dump 出来的文件中,有 266 个线程,其中 145 个都来自于同一个ConnectionPool(OkHttp ConnectionPool),而且是在流量不高的情况下。
经分析主要原因是应用中在创建 OkHttpClient 对象时,没有创建同一个 OkHttpClient 实例并重复使用,而是对于所有的 http 请求都重复创建一个新的实例,而每个实例都有自己的连接池和线程池,从而导致线程大量堆积。
而 OkHttpClient 默认的最大线程空闲数是 5,keepAlive 时间为 5 分钟,也就是发起一次网络连接后,5 分钟内不会断开连接,从而导致在创建大量对外连接时内存溢出。
而查看 OkHttpClient 类的源码发现,通过 OkHttp 创建每个OkHttpClient 实例的时候,每个客户端都会持有自己的连接池和线程池。
对于通过 OkHttp 创建的所有的 http 请求,在创建一个 OkHttpClient 实例并重复使用时,重用它的连接池和线程池可以减少延迟和节省内存。
2.2 验证过程
2.2.1 修改前
查看应用程序中的代码可以看到,在创建 OkHttpClient 实例的时候,并没有创建一个可复用的实例,而是每次创建 http 请求时,都会 new 一个新的 OkHttpClient 实例。
在 jmeter 模拟多个线程同时请求应用服务的接口,每次请求 300 个线程,通过多次请求之后,通过 jconsole 工具可以很清楚的看到线程数刚开始的时候基本为 0,但多次模拟请求过后很快就超过了 3000 个,然后电脑连续挂了 2 次。。。
并且从下图中,我们可以非常明显地看到,这些线程大部分都是 OkHttp ConnectionPool。
2.2.2 修改后
修改之后,创建一个共享的 OkHttpClient 对象,jmeter 每次模拟 300 请求,通过 jconsole 可以观察到,发现第一次模拟达到了一个峰值后,后面无论再怎么模拟,峰值基本都不再改变了。
而修改之前每次模拟 300 请求,模拟几次之后会发现线程数一直在蹭蹭往上涨,直至线程大量堆积导致应用程序崩溃。
3 解决
创建一个共享的 OkHttpClient 对象即可,而不是每次调用方法都单独创建一个 OkHttpClient 对象,每个 OkHttpClient 对象都有自己的连接池和线程池,会导致大量的线程堆积,从而可能会导致程序崩溃。