问题说明
OkHttp ConnectionPool 对象居然有近2000个, 占用了太多的内存, 导致宕机
内存溢出
本次问题的原因是, OkHttp 使用不规范, 封装的工具类不科学
原工具类中提供的 post 方法, 每次调用都会创建一个新的 OkHttpClient 对象, 调用请求后该对象就会被抛弃, 但是该对象内部持有一个 ConnectionPool 连接池对象, 其中的连接对象, 默认会保持存活5分钟, 导致 OkHttpClient 对象(及 ConnectionPool) 不能及时被回收释放, 对象数量越来越多, 占用内存越来越大, 最终内存溢出
内存泄漏
Response 或 ResponseBody 没有正确且及时关闭, 可能会导致下面的内存泄漏警告
WARN okhttp3.OkHttpClient - A connection to http://host:port was leaked. Did you forget to close a response body? To see where this was allocated, set the OkHttpClient logger level to FINE: Logger.getLogger(OkHttpClient.class.getName()).setLevel(Level.FINE);
重新封装 OkHttpKit
确保解决 内存溢出 和 内存泄漏 两种问题, 兼顾方便好用的需求
以下是基于 com.squareup.okhttp3:okhttp:4.11.0 的 OkHttpKit 工具包, OkHttp 从版本4起使用了 Kotlin 编写, 使用和之前的版本稍有不同
package com.mrathena.toolkit;
import lombok.Setter;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import okhttp3.ConnectionPool;
import okhttp3.Cookie;
import okhttp3.CookieJar;
import okhttp3.FormBody;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.LongAdder;
import java.util.function.Consumer;
/**
* 在高并发场景下, 一定要确保使用同一个 OkHttpClient 对象, 否则容易造成内存溢出
* 因为每个 OkHttpClient 对象都有一个 connectionPool 属性, 其中的连接在使用后默认会继续存活5分钟, 即该对象只有到期后才会被释放
* 而且每个池中有一个线程池对象, 会创建一个清理连接的死循环线程, 当有大量连接池对象时, 也会有大量该线程, 造成无谓的资源浪费
* 在高并发场景下, 如果每次请求都创建一个新的 OkHttpClient 对象(及 ConnectionPool), 这些对象会随着时间推移(默认5分钟内)占用越来越多的内存, 最终导致内存爆炸
* 参考: https://blog.csdn.net/mrathena/article/details/123267897
* <p>
* 不正确的封装与使用可能会报如下内存泄漏异常, 原因是 Response / ResponseBody 没有正确且及时关闭, 响应关闭应封装到公共方法中而不能靠方法调用者的自觉关闭(非常不靠谱)
* WARN okhttp3.OkHttpClient - A connection to http://host:port was leaked. Did you forget to close a response body? To see where this was allocated, set the OkHttpClient logger level to FINE: Logger.getLogger(OkHttpClient.class.getName()).setLevel(Level.FINE);
* <p>
* 使用异步 enqueue 时, 最终会调用到 OkHttpClient 内 Dispatcher 对象内部维护的一个线程池, 该线程池中的线程默认有60秒的存活时间(导致之行结束不能及时停止), 这里可更换自定义的线程池. 不使用异步 enqueue 没有该问题
* new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS, new SynchronousQueue<>(), Util.threadFactory("OkHttp Dispatcher", false));
* new OkHttpClient.Builder().dispatcher(new Dispatcher(executor)).build();
* <p>
* 通过 response.body().byteStream() 可以拿到 InputStream, 可依此实现下载功能, buffer=8196, 可依此实现下载进度功能
*/
@Slf4j
@Setter
@Accessors(chain = true, fluent = true)
public final class OkHttpKit {
/**
* 媒体类型
*/
private static final MediaType MEDIA_TYPE_JSON = MediaType.parse("application/json; charset=utf-8");
private static final MediaType MEDIA_TYPE_FILE = MediaType.parse("application/octet-stream");
/**
* 单例 OkHttpClient 对象 (默认 连接超时/读超时/写超时 都是1秒)
*/
private static volatile OkHttpClient SINGLETON;
/**
* 获取多例 OkHttpClient 对象
*/
public static OkHttpClient generateOkHttpClient(int connect, int write, int read) {
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.connectTimeout(connect, TimeUnit.SECONDS);
builder.writeTimeout(write, TimeUnit.SECONDS);
builder.readTimeout(read, TimeUnit.SECONDS);
builder.setCookieJar$okhttp(new CookieJar() {
// https://www.rfc-editor.org/rfc/rfc6265#section-5.1.3, Domain Matching
// https://www.rfc-editor.org/rfc/rfc6265#section-5.3, 如何处理 Cookie
// https://www.rfc-editor.org/rfc/rfc6265#section-5.3, 6. 如何处理 Cookie 的 Domain
// 结构应该是 Map<Domain, Map<CookieName, CookieValue>>, 而不是用 host 做 key
private final Map<String, Map<String, Cookie>> cookies = new ConcurrentHashMap<>();
@Override
public void saveFromResponse(@NotNull HttpUrl url, @NotNull List<Cookie> list) {
// 入参的 cookies 里存在多个 cookie 有相同 name, 不同 domain 的情况, 所以不能直接弄转成 map
for (Cookie cookie : list) {
if (!cookie.value().isEmpty() && cookie.matches(url)) {
String domain = cookie.domain();
this.cookies.putIfAbsent(domain, new HashMap<>());
this.cookies.get(domain).put(cookie.name(), cookie);
}
}
log.debug("========== ========== ========== ========== ========== ========== ========== ========== ========== ==========");
this.cookies.forEach((domain, value) -> {
value.forEach((name, cookie) -> log.debug("Save [{}].[{} = {}]", domain, name, cookie.value()));
});
log.debug("========== ========== ========== ========== ========== ========== ========== ========== ========== ==========");
}
@NotNull
@Override
public List<Cookie> loadForRequest(@NotNull HttpUrl url) {
String host = url.host();
log.debug("Host: [{}], Url: {}", host, url);
List<Cookie> cookieList = new LinkedList<>();
this.cookies.forEach((domain, map) -> {
for (Cookie cookie : map.values()) {
// 这里的匹配逻辑不是我们自己瞎比猜的, 看上面的参考文档, 另外 OkHttp3.Cookie 已经按国际标准实现了 cookie 是否匹配 url 的验证方法
if (cookie.matches(url)) {
cookieList.add(cookie);
log.debug("Load [{}].[{} = {}]", domain, cookie.name(), cookie.value());
}
}
});
return cookieList;
}
});
return builder.build();
}
/**
* 获取多例 OkHttpClient 对象
*/
public static OkHttpClient generateOkHttpClient() {
return generateOkHttpClient(1, 1, 1);
}
/**
* 获取单例 OkHttpClient 对象
*/
public static OkHttpClient getOkHttpClient() {
if (null == SINGLETON) {
synchronized (OkHttpClient.class) {
if (null == SINGLETON) {
SINGLETON = generateOkHttpClient();
}
}
}
return SINGLETON;
}
/**
* 请求参数
*/
private String url;
private Map<String, String> headers;
private Map<String, Object> parameters;
private String json;
private Map<String, File> files;
private OkHttpKit() {}
public static OkHttpKit create() {
return new OkHttpKit();
}
public OkHttpKit headers(Map<String, String> headers) {
if (null == this.headers) {
this.headers = new HashMap<>();
}
this.headers.putAll(headers);
return this;
}
public OkHttpKit header(String key, String value) {
if (null == this.headers) {
this.headers = new HashMap<>();
}
this.headers.put(key, value);
return this;
}
public OkHttpKit cookie(String value) {
return header("cookie", value);
}
public OkHttpKit userAgent(String userAgent) {
return header("user-agent", userAgent);
}
public OkHttpKit ua(String userAgent) {
return header("user-agent", userAgent);
}
public OkHttpKit referer(String userAgent) {
return header("referer", userAgent);
}
public OkHttpKit parameters(Map<String, Object> parameters) {
if (null == this.parameters) {
this.parameters = new HashMap<>();
}
this.parameters.putAll(parameters);
return this;
}
public OkHttpKit parameter(String key, Object value) {
if (null == this.parameters) {
this.parameters = new HashMap<>();
}
this.parameters.put(key, value);
return this;
}
public OkHttpKit file(String key, File file) {
if (null == this.files) {
this.files = new HashMap<>();
}
this.files.put(key, file);
return this;
}
public OkHttpKit files(Map<String, File> files) {
if (null == this.files) {
this.files = new HashMap<>();
}
this.files.putAll(files);
return this;
}
public void get(OkHttpClient client, Consumer<Response> consumer) throws IOException {
if (null != this.parameters) {
StringBuilder sb = new StringBuilder();
this.parameters.forEach((key, value) -> {
if (null != key && null != value) {
sb.append("&").append(key).append("=").append(value);
}
});
if (!this.url.contains("?")) {
sb.deleteCharAt(0).insert(0, "?");
}
this.url += sb.toString();
}
Request.Builder requestBuilder = new Request.Builder().url(this.url);
if (null != this.headers) {
this.headers.forEach(requestBuilder::addHeader);
}
Request request = requestBuilder.build();
try (Response response = client.newCall(request).execute()) {
consumer.accept(response); // 消耗掉该响应
}
}
public void get(Consumer<Response> consumer) throws IOException {
get(getOkHttpClient(), consumer);
}
public void post(OkHttpClient client, Consumer<Response> consumer) throws IOException {
Request.Builder requestBuilder = new Request.Builder().url(this.url);
if (null != this.headers) {
this.headers.forEach(requestBuilder::addHeader);
}
if (null != this.files) {
MultipartBody.Builder multipartBodyBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM);
for (Map.Entry<String, File> entry : this.files.entrySet()) {
String name = entry.getKey();
File file = entry.getValue();
RequestBody fileBody = RequestBody.create(file, MEDIA_TYPE_FILE);
multipartBodyBuilder.addFormDataPart(name, file.getName(), fileBody);
}
requestBuilder.post(multipartBodyBuilder.build());
} else if (null != this.json) {
requestBuilder.post(FormBody.create(this.json, MEDIA_TYPE_JSON));
} else {
FormBody.Builder formBodyBuilder = new FormBody.Builder();
if (null != this.parameters) {
for (Map.Entry<String, Object> entry : this.parameters.entrySet()) {
formBodyBuilder.add(entry.getKey(), entry.getValue().toString());
}
}
requestBuilder.post(formBodyBuilder.build());
}
Request request = requestBuilder.build();
try (Response response = client.newCall(request).execute()) {
consumer.accept(response); // 消耗掉该响应
}
}
public void post(Consumer<Response> consumer) throws IOException {
post(getOkHttpClient(), consumer);
}
public static void main(String[] args) {
try {
OkHttpKit.create().url("https://www.baidu.com").get(response -> {
try {
if (response.isSuccessful()) {
ResponseBody body = response.body();
if (null == body) {
log.info("Response Body is null");
return;
}
System.out.println(body.string());
} else {
System.out.println(response.protocol());
System.out.println(response.code());
System.out.println(response.message());
}
} catch (Throwable cause) {
log.error("", cause);
}
});
} catch (Throwable cause) {
log.error("", cause);
}
}
}