是否应复用javax.ws.rs.client.Client实例?负载测试与问题分析
javax.ws.rs.client.Client复用的疑问与测试分析 API 的主要入口是 ClientBuilder,用于创建 Client 实例——这些实例是可配置的重量级对象,负责管理底层通信基础设施,是访问任何 Web 资源的根对象。
我不确定这段描述指的是Client还是ClientBuilder实例。经网络搜索,似乎是指Client,但……
我开展了一项简单的负载测试:启动10个线程,每个线程执行50次调用,调用间隔为100-500ms的随机值。
每次测试调用实际包含2个HTTP请求:
- 使用
javax.ws.rs.client.Client手动请求获取访问令牌; - MicroProfile REST Client 管理的最终请求,通过
@ClientHeaderParam(name = HttpHeaders.AUTHORIZATION, ...)注入上述访问令牌。
测试环境为WildFly 26实例。
第一种实现——每次调用执行ClientBuilder.newClient()
注:每次使用后关闭Response和Client。
- 最小值:31 ms
- 最大值:196 ms
- 平均值:约48 ms
第二种实现——共享ClientBuilder实例
注:每次使用后关闭Response和Client。
- 最小值:25 ms
- 最大值:72 ms
- 平均值:约32 ms
此方案性能有所提升,可见共享ClientBuilder确实有益。两种实现均运行正常。
第三种实现——共享Client实例
注:每次使用后关闭Response。
该测试无法完整执行,每个线程执行到第7-10次迭代时开始抛出错误,因此结果有限:
- 最小值:14 ms
- 最大值:53 ms
- 平均值:约21 ms
共享实例看似是最优选择,但错误导致此方案不可行。
错误信息:java.net.SocketException: Software caused connection abort: recv failed
第四种实现——使用commons-pool2实现共享池化Client实例
注:每次使用后关闭Response,并将Client归还至池。
创建了一个最大容量为20的通用池,Client工厂使用共享的ClientBuilder,运行时按需创建10个Client(对应负载测试的10个线程)。但该测试同样在每个线程执行到第9-12次迭代时抛出相同错误,结果也有限:
- 最小值:15 ms
- 最大值:67 ms(存在约5500 ms的异常值)
- 平均值:约20 ms(忽略异常值)
可见使用池化共享Client实例并无收益。
观察结果
表面上Client实例应被复用,但实际使用多次后会出现故障。
初步检查MicroProfile REST Client发现,它似乎总是使用新的ClientBuilder,但这是不同的实现,无法深入研究。
我的问题
我是否应该复用javax.ws.rs.client.Client实例?
用例说明
我有一个MicroProfile REST Client连接至认证端点,采用OpenID Connect进行认证。
使用@ClientHeaderParam(name = HttpHeaders.AUTHORIZATION, ...)将认证头注入MP客户端请求,该注解指向一个认证头生成器,通过与OpenID服务器(KeyCloak)进行client_credential认证获取有效令牌,并返回给MP方法调用。
编辑1
我启用了部分日志,发现大量如下调试输出:
[org.apache.http.impl.conn.PoolingHttpClientConnectionManager] (Thread-*) Connection request: [route: {}->http://localhost:80][total available: 0; route allocated: 50 of 50; total allocated: 50 of 50]
该日志代表对REST端点的请求,而非OpenID服务器。
现在我怀疑问题并非直接出在Client实例,而是MicroProfile REST Client。当共享Client实例时,MP连接池释放租用连接的速度过慢,导致等待可用连接时出现阻塞;而不共享Client实例时,连接释放更快,从未出现allocated: 50 of 50的情况。
基于此发现,我将调用隔离至Callable中,解决了错误问题,但平均耗时却飙升至荒谬的2秒!
我现在有一个新问题:Client实例共享与MicroProfile REST Client连接池有何关联?
编辑2
无论如何构建REST客户端,最终都会使用org.apache.http.impl.conn.PoolingHttpClientConnectionManager。
当使用@Inject @RestClient时,连接无法复用,释放速度极慢,很快就达到50/50的分配上限,进而开始锁定资源。
当使用RestClientBuilder.newBuilder().baseUri(uri).build(Client.class)手动构建代理时,始终使用全新的连接管理器(分配1/50),且连接管理器会在每次连接使用后立即关闭。
我认为MicroProfile REST Client构建器会共享构建器,它尝试共享客户端,但对于同一端点的每次请求(令牌过期时请求头会变化),无法复用池中的客户端连接。
内容的提问来源于stack exchange,提问作者Claudio Weiler

