RestTemplate 效能調教

前言

當後端需要跟其他第三方服務、系統溝通時,最常用的方式為透過 Rest API 進行溝通。Spring Boot 專案中,提供了一個好用的
call API 工具 - RestTemplate , RestTemplate 提供了簡單的介面簡化了我們呼叫 API 的程式碼。

但是,RestTemplate 在發送大量請求時往往會發生效能瓶頸,故本篇文章將教學如何進行效能調教來解決 RestTemplate 速度慢的問題。

設定 ClientHttpRequestFactory

RestTemplate 底層預設發送 HTTP Request 的工具為使用 HttpURLConnection 來進行發送。此工具未支援 HTTP Connection
Pool 來縮短消耗的時間,我們可將其換成現今 Java 有支援 Connection Pool 的工具,像是 OkHttp、Apache HttpClient、
WebClient、FeignClient 等等。

而本篇將採用設定 Apache HttpClient 作為 RestTemplate 發送 HTTP Request 的工具 。

安裝 dependency

1
2
3
4
5
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version> <!-- or the latest version -->
</dependency>

建立 HttpComponentsClientHttpRequestFactory 來替換 HTTP Client

1
2
3
4
5
6
7
8
9
10
11
12
13

@Bean
public CloseableHttpClient httpClient() {
return HttpClientBuilder.create()
.setMaxConnTotal(100) // Set required maximum total connections
.setMaxConnPerRoute(20) // Set required maximum connections per route
.build();
}

@Bean
public RestTemplate restTemplate() {
return new RestTemplate(new HttpComponentsClientHttpRequestFactory(httpClient()));
}

設定 Connection Pool Manager

在 OSI 網路七層協定中,HTTP 是建構於 TCP 之上的通訊協定,如果每次請求都需要重新建立一次 Connection , 想必會非常的消耗資源 (TCP 三向交握的關係) 。因此 Apache Http Client 提供了 Connection Pool (連線池的機制) 。

概念上與 AP Server 在跟資料庫連線時的 Connection Pool 類似,都是先 Keep 住 Connection 不立即關閉,等下次有 Request 來時就會借用那個 Connection 來發送請求,大幅減少了每個 Request 都需要花費建立 Connection 的資源 。

至於如何設定請看下面程式碼說明

1
2
3
4
5
6
7
8
9
@Bean
public PoolingHtppClientConnectionManager customizedPoolingHtppClientConnectionManager(){
/*
設定每個Connection 在Connection Pool 中的維持時間 , 範例為 5分鐘 , 此參數須小心設定
*/
PoolingHtppClientConnectionManager connManager = new PoolingHtppClientConnectionManager(5, TimeUnit.MINUTES);
connManager.setMaxTotal(100); // Set required maximum total connections
connManager.setDefaultMaxPerRoute(20); // Set required maximum connections per route
}
  • max total : connection Pool 最大的總連線數量,預設為 20

  • defaultMaxPerRoute: 每個路徑最大的連線數量,預設為 2

設定 Keep Alive 機制

在 HTTP 中,Keep -Alive 的機制為讓一個 Connection 在一定時間內可以發送多個 Request 。為了避免 server side 已關閉連線,但 client 端卻還是維持該 Connection 的情況 (Connection Reset by Peer)。透過設定 Keep -Alive Header 來告知 Http Client Connection 要維持的時間。

通常若 server 端未給予 keep alive 的 timeout , 預設可以設定 30 或 60 秒來維持

1
2
3
4
5
6
7
8
9
10
11
12
public ConnectionKeepAliveStrategy connectionKeepAliveStrategy() {
return new DefaultConnectionKeepAliveStrategy() {
@Override
public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
long keepAliveDuration = super.getKeepAliveDuration(response, context);
if (keepAliveDuration < 0) {
return DEFAULT_KEEP_ALIVE_TIME_MILLIS;
}
return keepAliveDuration;
}
};
}

設定排程檢查並關閉 無效 or Idle timeout 的連線

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Bean
public Runnable idleConnectionMonitor(final PoolingHttpClientConnectionManager connectionManager) {
return new Runnable() {
@Override
@Scheduled(fixedDelay = 10000)
public void run() {
try {
if (connectionManager != null) {
LOGGER.trace("run IdleConnectionMonitor - Closing expired and idle connections...");
connectionManager.closeExpiredConnections();
connectionManager.closeIdleConnections(CLOSE_IDLE_CONNECTION_WAIT_TIME_SECS, TimeUnit.SECONDS);
} else {
LOGGER.trace("run IdleConnectionMonitor - Http Client Connection manager is not initialised");
}
} catch (Exception e) {
LOGGER.error("run IdleConnectionMonitor - Exception occurred. msg={}, e={}", e.getMessage(), e);
}
}
};
}

設定請求參數

1
2
3
4
5
6
7
8
9
private RequestConfig requestConfig(){
RequestConfig requestConfig =
RequestConfig.custom()
.setConnectionRequestTimeout(REQUEST_TIMEOUT)
.setConnectTimeout(CONNECT_TIMEOUT)
.setSocketTimeout(SOCKET_TIMEOUT)
.build();
return requestConfig;
}
  • connection request timeout : client 端從 connection pool 取得連線的最大時效
  • connection timeout : 建立連線前的最大時效
  • socket timeout : client 等待 server 端回應資料的最大時效

完整設定範例

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
@Configuration
@Log4j2
@RequiredArgsConstructor
public class HttpClientConfig {

// Determines the timeout in milliseconds until a connection is established.
private static final int CONNECT_TIMEOUT = 30000;
// The timeout when requesting a connection from the connection manager.
private static final int REQUEST_TIMEOUT = 30000;
// The timeout for waiting for data
private static final int SOCKET_TIMEOUT = 60000;

private static final int DEFAULT_KEEP_ALIVE_TIME_MILLIS = 20 * 1000;
private static final int CLOSE_IDLE_CONNECTION_WAIT_TIME_SECS = 30;

private final EnvironmentConfig environmentConfig;

@Value("${http.conn-pool.max-total-conn:100}")
private Integer maxTotalConnection;

@Value("${http.conn-pool.max-per-route-conn:20}")
private Integer maxPerRouteConnection;

@Bean
public PoolingHttpClientConnectionManager poolingConnectionManager() {
PoolingHttpClientConnectionManager poolingConnectionManager =
new PoolingHttpClientConnectionManager();
poolingConnectionManager.setMaxTotal(maxTotalConnection);
poolingConnectionManager.setDefaultMaxPerRoute(maxPerRouteConnection);
return poolingConnectionManager;
}

public ConnectionKeepAliveStrategy connectionKeepAliveStrategy() {
return new DefaultConnectionKeepAliveStrategy() {
@Override
public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
long keepAliveDuration = super.getKeepAliveDuration(response, context);
if (keepAliveDuration < 0) {
return DEFAULT_KEEP_ALIVE_TIME_MILLIS;
}
return keepAliveDuration;
}
};
}

@Bean
public CloseableHttpClient httpClient() {
RequestConfig requestConfig =
RequestConfig.custom()
.setConnectionRequestTimeout(REQUEST_TIMEOUT)
.setConnectTimeout(CONNECT_TIMEOUT)
.setSocketTimeout(SOCKET_TIMEOUT)
.build();
DefaultProxyRoutePlanner proxyRoutePlanner = proxyRouter();

HttpClientBuilder httpClientBuilder =
HttpClients.custom()
.setDefaultRequestConfig(requestConfig)
.setConnectionManager(poolingConnectionManager())
.setKeepAliveStrategy(connectionKeepAliveStrategy())
.setConnectionManagerShared(true);
if (proxyRoutePlanner != null) {
httpClientBuilder.setRoutePlanner(proxyRoutePlanner);
}
return httpClientBuilder.build();
}

@Bean
public Runnable idleConnectionMonitor(
final PoolingHttpClientConnectionManager connectionManager) {

return new Runnable() {
@Override
@Scheduled(fixedDelay = 10000)
public void run() {
try {
if (connectionManager != null) {
log.trace(
"run IdleConnectionMonitor - Closing expired and idle connections...");
connectionManager.closeExpiredConnections();
connectionManager.closeIdleConnections(
CLOSE_IDLE_CONNECTION_WAIT_TIME_SECS, TimeUnit.SECONDS);
} else {
log.trace(
"run IdleConnectionMonitor - Http Client Connection manager is not initialised");
}
} catch (Exception e) {
log.error(
"run IdleConnectionMonitor - Exception occurred. msg={}, e={}",
e.getMessage(),
e);
}
}
};
}
/*
設定thread pool ,讓idle monitor的thread 能被執行
*/
@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setThreadNamePrefix("poolScheduler");
scheduler.setPoolSize(50);
return scheduler;
}
}

參考資料