0%

OkHttp 介绍与问题总结

OkHttpClient 最早用于安卓客户端请求服务端场景,优势是请求稳定,低延迟,甚至可以多数据中心负载均衡。

特性:

  • 支持 HTTP/2,对于相同的 host 会共享一个 socket 连接
  • 在不支持HTTP/2,情况下,实现连接池,从而有效降低建立连接的请求延迟
  • 响应缓存,避免重复请求
  • 支持同步或异步请求

连接

参考文档:

https://square.github.io/okhttp/connections/

https://www.cnblogs.com/duanxz/p/11066227.html

Address

定义了目标站点的域名(类似:github.com),所有的静态配置,端口号,以及通信协议。

在同一个地址下的 URL,可能会共享同一个 TCP 连接,OKHttp 使用连接池来自动管理回收这些连接。

Routes

对同一个 Address 可能会有多个 Routes,因为同一个域名可能会对应多个服务器 IP 地址。

发起连接和请求流程

当你使用 OkHttpClient 发一起一个 URL 请求时,它会有以下逻辑:

  1. 使用URL中的scheme, hostname, port,以及 OkHttpClient 的配置信息,创建了一个 Address,这个Address定义了我们应该怎么连接到服务器。
  2. 尝试从连接池(connection pool)中,复用一个以 Address 定义的连接
  3. 如果没找到可用连接,它会选择一个 Route ,(通常是通过DNS反查服务器的IP地址等等),一个host 可能对应多个服务器的IP。
  4. 如果是一个新的 Route,他会直接创建一个 socket 连接。
  5. 发送 Http 请求,并且接收响应。

如果连接出现了问题,OkHttp 会尝试另一个 Route

如果接收到响应,这个连接将会返回到连接池中以备未来使用,连接池会清理一段时间过期的连接。

实例化

OkHttpClient 实例应该是共享的。

应该创建一个独立的 OkHttpClient 实例发起所有请求,因为每个 OkHttpClient 会拥有 独立的连接池和线程池。复用连接和线程能够有效的降低延迟减少内存开销。

可以使用以下两种方式创建实例:

1
2
3
4
5
x = new OkHttpClient()

x = new OkHttpClient.Builder()
.retryOnConnectionFailure(false)
.build();

然后,通过 newBuilder() 定制 共享实例,会构建一个派生的 client,使用同一个连接池和线程池

1
2
3
4
x = client
.newBuilder()
.readTimeout(500, TimeUnit.MILLISECONDS)
.build();

问题总结

Gzip

Okhttp 通过查看源码,如果用户没有指定 Accept-Encoding 的 header,会默认发送 Gzip 的头,同时自动解压缩。这个操作是在拦截器中做的。

如果用户手动配置这个头,则需要手动解压缩。

同时还会自动发送 Keep-Alive, User-Agent 等 header。

源码位置:

1
okhttp3.internal.http.BridgeInterceptor.Java

连接池泄露问题(重要)

每次都实例化新的 OkHttpClient 的实例,又没有使用共享的连接池,这样在实例化时每次默认都会创建一个新的连接池。从而导致连接泄露。

内存可能泄露的问题(重要)

虽然每次使用共享的连接池,但是每次都实例化新的 OkHttpClient 实例,会导致内存回收不够及时,FullGC 频繁导致内存占用巨大。(可能有内存泄漏问题)

连接池大小的配置(重要)

连接池第一个参数:最大空闲连接,不是指总共的连接池大小。

而是针对某个 Address 的最大空闲连接。针对同一个站点,如有多个IP对照的情况(baidu.com),会创建很多的Address,从而导致连接池崩溃,内存崩溃。

不同场景的测试结果分析

请求内网地址(100线程共请求5W次)

  1. 每次都 new okhttpclient 实例化的场景,FullGC 频繁:
  • 200M JVM内存,连接池 (10,10);连接数(21,57),new okhttpclient ,71 FullGC,1200 GC,总时间 28秒,成功率100%
  • 200M JVM内存,连接池 (1,10);连接数(13,15),new okhttpclient ,51FullGC,1141 GC,总时间54秒,成功率80%
  • 200M JVM内存,连接池 (5,10);连接数(8,12),new okhttpclient ,71FullGC,1188 GC,总时间28秒,成功率100%
  • 200M JVM内存,连接池 (5,60);连接数(14,29),new okhttpclient ,72FullGC,1194GC,总时间28秒,成功率100%
  • 200M JVM内存,连接池 (50,10);连接数(100,100),new okhttpclient ,71FullGC,1197GC,总时间4秒,成功率100%
  1. 使用 newbuilder 实例化场景:
  • 200M JVM内存,连接池 (10,10);连接数(88,100),newbuilder,0FullGC,133 GC,总时间 4秒,成功率100%
  • 200M JVM内存,连接池 (1,10);连接数(84,98),newbuilder,0FullGC,131 GC,总时间4秒,成功率100%
  • 200M JVM内存,连接池 (5,10);连接数(90,99),newbuilder,0FullGC,128GC,总时间5秒,成功率100%
  • 200M JVM内存,连接池 (5,60);连接数(86,98),newbuilder,0FullGC,136 GC,总时间4秒,成功率100%
  • 200M JVM内存,连接池 (50,10);连接数(93,99),newbuilder,0FullGC,135 GC,总时间4秒,成功率100%
  • 200M JVM内存,连接池 (5,10);连接数(95, 100),not new,0FullGC,130GC,总时间4秒,成功率100%