Java Apache HttpClient4.X Connection Reset 問題

前言

若要透過 Java 呼叫 API 端點, 可透過多種不同的程式工具進行呼叫,像是 Apache HTTP Client 、OkHTTP、WevFlux 等。今天將介紹使用 Apache HTTP Client 開發所遇到的坑,以及對應的解決辦法。

發生原因

由於因業務邏輯需求要呼叫多次 API 。可能會重複使用建立的 HTTP Client Instance 來去呼叫 API, 若 API 的時間連線時間太長, 可能會出現 TCP Idle connection , 導致程式無法順利執行完成,拋出 Socket Exception: Connection Reset 問題

以下為發生的程式碼範例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void main(String[] args) throws IOException {
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
List<String> urlList = new ArrayList<>();
// multiple urls using single HttpClient
try (CloseableHttpClient httpClient = httpClientBuilder.build()) {
for (String url : urlList) {

HttpGet getRequest = new HttpGet(url);
try (CloseableHttpResponse response = httpClient.execute(getRequest)) {
HttpEntity entity = response.getEntity();
if (entity != null && response.getStatusLine().getStatusCode() == 200) {
String result = EntityUtils.toString(entity);
// handle response logic ...
}
} catch (IOException e) {
log.error(e.getMessage(), e);
}
}
} catch (IOException e) {
log.error(e.getMessage(), e);
}
}

解決辦法

解法一

單次 request 就 重新 new 一個 CloseableHttpClient

然後使用完畢後就 close (try with resources)。這樣的做法為每次進行呼叫 API 都重新進行一次 TCP Connection。

依照官法文件說法,每次重新建立一個新的 HTTP Client Instance 是成本很高的事情,因此假設程式若有效能需求的情況下。此解決方法可能不盡理想

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

@Slf4j
public class HttpClientExample {
public static void main(String[] args) throws IOException {
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();

List<String> urlList = new ArrayList<>();
for (String url : urlList) {
try (CloseableHttpClient httpClient = httpClientBuilder.build()) {
HttpGet getRequest = new HttpGet(url);
try (CloseableHttpResponse response = httpClient.execute(getRequest)) {
HttpEntity entity = response.getEntity();
if (entity != null && response.getStatusLine().getStatusCode() == 200) {
String result = EntityUtils.toString(entity);
// handle response logic ...
}
} catch (IOException e) {
log.error(e.getMessage(), e);
}

} catch (IOException e) {
log.error(e.getMessage(), e);
}
}

}
}

解法二

使用 connection pool 的方式來驗證 connection 是否為 idle connection, 同時也能減少多次呼叫 API 時所吃的資源與效能。

設定 Connection Pool 參數來定期驗證 TCP Connection 是否為過期或無效的 Connection。

1
2
3
4
5
6
7
8
9
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
cm.setValidateAfterInactivity(500);
CloseableHttpClient httpclient = HttpClients.custom()
.setConnectionManager(cm)
.evictExpiredConnections()
.evictIdleConnections(5L, TimeUnit.SECONDS)
.setRetryHandler(DefaultHttpRequestRetryHandler.INSTANCE)
.build();

補充說明

  • max per route : connection manager 預設每個 domain 最大的 connection 數量為 5

  • evictExpiredConnections 與 evictIdleConnections 用於設置在背景中清理過期的 connection

  • setValidateAfterInactivity : 每次取得連線時,假設該連線空閒超過該時間,則會驗證是否可用。默認值為 2000ms

  • 設定重試機制:預設試 3 次,但假設 pool 中的 max per route 是五個 connection , 可能還是會出現例外。最好的保險是重是次數需大於 MaxPerRoute , 保證都失效後,重新進行連接

參考資料