zuul 網關超時優化
1. 概述
前段時間,線上的服務不知道為啥,突然全部的服務都超時,所有的請求經過網關都超時,后來進行鏈路追蹤排查,發現有一個服務鏈接 RDS 數據庫,一個查詢花費了 20S 的查詢時間,導致后續調用該服務的應用都超時。然后超時的連接占滿了 zuul 的轉發池,最終導致了所有經過 gateway 的服務都在等待,導致全體服務全部超時。
原因找出來之后,我就在納悶,為何一個服務超時會導致所有服務都延時,作為一個高可用的網關, zuul 的設計應該不會這么差吧,并且,當時系統的 QPS 也不是很高,所以,必須找出這次超時的問題所在,于是,我就開始了此次網關超時的排查和優化!
下圖是出現問題是的相關監控

因為公司的服務是 java 和 node 應用都存在,所以使用的 zuul 是簡單路由轉發,未使用 服務注冊中心之類的,所以,熔斷器和
2. 問題分析
首先,因為網關的相關配置都是之前填的,有些參數不是特別的清楚,下面的配置文件是我當時系統的相關配置。
zuul:
host:
connect-timeout-millis: 30000
socket-timeout-millis: 60000
ribbon:
ConnectTimeout: 300000
ReadTimeout: 60000
eureka:
enabled: false
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 70000
2.1 zuul 參數解釋
connect-timeout-millis
此參數為 zuul 網關連接服務的時間,單位為毫秒,我這里配置的是 30S,如果 30S 之內未連接到待轉發的下一服務,則轉發將報錯,此請求也就將結束。這段時間為下圖的第 1 步的時間
如下圖所示:

socket-timeout-millis
zuul 網關連接到服務并且服務返回結果這一部分時間,單位為毫秒,我這里配置的是 60S,如果 60S 之內下一服務還沒有返回, gateway 將報轉發超時。
此時間為上圖的 1+2+3 三部分時間
ribbon.ConnectTimeout
此為 ribbon 轉發的連接時間,如果 zuul 使用的服務調用,則將采用此時間
ribbon.ReadTimeout
此為 ribbon 轉發到返回的時間,如果 zuul 使用的是服務調用,則將采用此時間
超時時間采用哪個?
以上四個都是 zuul 的超時時間,但是問題來了,四個時間,到底采用哪兩個呢?
我通過閱讀 zuul 的相關文檔,了解到,如果 zuul 使用的服務發現,則將會使用 rebbon進行負載均衡,即 ribbon.ConnectTimeout 和 ribbon.ReadTimeout。 如果 zuul 使用的是簡單路由(通過配置 url 進行路由轉發),則將采用 socket-timeout-millis 和 connect-timeout-millis。
If you want to configure the socket timeouts and read timeouts for requests proxied through Zuul, you have two options, based on your configuration:
- If Zuul uses service discovery, you need to configure these timeouts with the ribbon.ReadTimeout and ribbon.SocketTimeout Ribbon properties.
- If you have configured Zuul routes by specifying URLs, you need to use zuul.host.connect-timeout-millis and zuul.host.socket-timeout-millis.
2.2 問題分析
因為我的服務未使用服務注冊中心,所以,很明顯, 配置的 ribbon 并未生效,zuul 超時時間使用的 socket-timeout-millis 和 connect-timeout-millis。
但是問題又來了,我 zuul 的超時時間為 60S,為何我的服務相應時間平均達到了 3 分鐘,遠遠超過了我設置的 60S,所以肯定是 zuul 還出現了相應的問題。
后續通過了解到 zuul 源碼和架構,明白 zuul 是 NIO 架構,即一個請求進來,經過 zuul 攔截器 Filter,最終交給 HttpClient 進行請求,zuul 為了高性能,使用 HttpClient 連接池。
獲取連接源碼
下面是 zuul 獲取 HttpClient 連接池的代碼
AbstractConnPool.getPoolEntryBlocking,看這個名字就知道。這是一個阻塞獲取池資源的方法
private E getPoolEntryBlocking(
final T route, final Object state,
final long timeout, final TimeUnit timeUnit,
final Future<E> future) throws IOException, InterruptedException, ExecutionException, TimeoutException {
Date deadline = null;
if (timeout > 0) {
deadline = new Date (System.currentTimeMillis() + timeUnit.toMillis(timeout));
}
this.lock.lock();
try {
final RouteSpecificPool<T, C, E> pool = getPool(route);
E entry;
for (;;) {
Asserts.check(!this.isShutDown, "Connection pool shut down");
if (future.isCancelled()) {
throw new ExecutionException(operationAborted());
}
for (;;) {
entry = pool.getFree(state);
if (entry == null) {
break;
}
if (entry.isExpired(System.currentTimeMillis())) {
entry.close();
}
if (entry.isClosed()) {
this.available.remove(entry);
pool.free(entry, false);
} else {
break;
}
}
if (entry != null) {
this.available.remove(entry);
this.leased.add(entry);
onReuse(entry);
return entry;
}
// New connection is needed
final int maxPerRoute = getMax(route);
// Shrink the pool prior to allocating a new connection
final int excess = Math.max(0, pool.getAllocatedCount() + 1 - maxPerRoute);
if (excess > 0) {
for (int i = 0; i < excess; i++) {
final E lastUsed = pool.getLastUsed();
if (lastUsed == null) {
break;
}
lastUsed.close();
this.available.remove(lastUsed);
pool.remove(lastUsed);
}
}
if (pool.getAllocatedCount() < maxPerRoute) {
final int totalUsed = this.leased.size();
final int freeCapacity = Math.max(this.maxTotal - totalUsed, 0);
if (freeCapacity > 0) {
final int totalAvailable = this.available.size();
if (totalAvailable > freeCapacity - 1) {
if (!this.available.isEmpty()) {
final E lastUsed = this.available.removeLast();
lastUsed.close();
final RouteSpecificPool<T, C, E> otherpool = getPool(lastUsed.getRoute());
otherpool.remove(lastUsed);
}
}
final C conn = this.connFactory.create(route);
entry = pool.add(conn);
this.leased.add(entry);
return entry;
}
}
boolean success = false;
try {
pool.queue(future);
this.pending.add(future);
if (deadline != null) {
success = this.condition.awaitUntil(deadline);
} else {
this.condition.await();
success = true;
}
if (future.isCancelled()) {
throw new ExecutionException(operationAborted());
}
} finally {
// In case of 'success', we were woken up by the
// connection pool and should now have a connection
// waiting for us, or else we're shutting down.
// Just continue in the loop, both cases are checked.
pool.unqueue(future);
this.pending.remove(future);
}
// check for spurious wakeup vs. timeout
if (!success && (deadline != null && deadline.getTime() <= System.currentTimeMillis())) {
break;
}
}
throw new TimeoutException("Timeout waiting for connection");
} finally {
this.lock.unlock();
}
}
- 代碼已建立有一個deadline ,然后判斷timeout ,這個timeout要注意。如果大于零才會賦值deadline, 如果為0 則不會賦值deadline 也就是說deadline始終為null
Date deadline = null;
if (timeout > 0) {
//如果超時時間有效,則設定deadline
deadline = new Date (System.currentTimeMillis() + tunit.toMillis(timeout));
}
- 進入鎖代碼。pool.getFree 獲取池資源。如果獲取到了,并且Connect的檢驗并沒有被關閉,則直接return entry
Asserts.check(!this.isShutDown, "Connection pool shut down");
for (;;) {
//獲取池資源
entry = pool.getFree(state);
if (entry == null) {
break;
}
//校驗超時
if (entry.isExpired(System.currentTimeMillis())) {
entry.close();
}
if (entry.isClosed()) {
this.available.remove(entry);
pool.free(entry, false);
} else {
break;
}
}
if (entry != null) {
this.available.remove(entry);
this.leased.add(entry);
onReuse(entry);
return entry;
}
- 如果沒有獲取到 進行接下來的代碼。
- 判斷是否達到了host配置的最大池數量,是否需要增加, 如果需要增加,則會在增加新連接之前縮小池,然后再分配返回entry
// New connection is needed 獲取是否需要創建新的連接
final int maxPerRoute = getMax(route);
// Shrink the pool prior to allocating a new connection
final int excess = Math.max(0, pool.getAllocatedCount() + 1 - maxPerRoute);
if (excess > 0) {
for (int i = 0; i < excess; i++) {
final E lastUsed = pool.getLastUsed();
if (lastUsed == null) {
break;
}
lastUsed.close();
this.available.remove(lastUsed);
pool.remove(lastUsed);
}
}
if (pool.getAllocatedCount() < maxPerRoute) {
final int totalUsed = this.leased.size();
final int freeCapacity = Math.max(this.maxTotal - totalUsed, 0);
if (freeCapacity > 0) {
final int totalAvailable = this.available.size();
if (totalAvailable > freeCapacity - 1) {
if (!this.available.isEmpty()) {
final E lastUsed = this.available.removeLast();
lastUsed.close();
final RouteSpecificPool<T, C, E> otherpool = getPool(lastUsed.getRoute());
otherpool.remove(lastUsed);
}
}
final C conn = this.connFactory.create(route);
entry = pool.add(conn);
this.leased.add(entry);
return entry;
}
}
- 如果并不是上面的情況,實際情況就是池子被用光了,而且還達到了最大。就不能從池子中獲取資源了。只能等了……
- 等待的時候會判斷deadline , 如果deadline不為null 就會await一個時間。如果為null,那么等待就會無限等待,直到有資源。
boolean success = false;
try {
if (future.isCancelled()) {
throw new InterruptedException("Operation interrupted");
}
pool.queue(future);
this.pending.add(future);
//判斷deadline是否有效
if (deadline != null) {
//如果有效就等待至deadline
success = this.condition.awaitUntil(deadline);
} else {
//如果無效就一直等待,沒有超時時間
this.condition.await();
success = true;
}
if (future.isCancelled()) {
throw new InterruptedException("Operation interrupted");
}
} finally {
// In case of 'success', we were woken up by the
// connection pool and should now have a connection
// waiting for us, or else we're shutting down.
// Just continue in the loop, both cases are checked.
pool.unqueue(future);
this.pending.remove(future);
}
創建連接池源碼
問題通過以上的源碼就發現了,關鍵問題是線程池的等待時間,設置一個連接的等待時間即可解決,使得線程不會一直等待 HttpCllient 連接,我找到相應的創建 CloseableHttpClient 衛士,位于 SimpleHostRoutingFilter#newClient#newClient(),源碼如下
protected CloseableHttpClient newClient() {
final RequestConfig requestConfig = RequestConfig.custom()
.setConnectionRequestTimeout(
// 設置超時鏈接時間
this.hostProperties.getConnectionRequestTimeoutMillis())
.setSocketTimeout(this.hostProperties.getSocketTimeoutMillis())
.setConnectTimeout(this.hostProperties.getConnectTimeoutMillis())
.setCookieSpec(CookieSpecs.IGNORE_COOKIES).build();
return httpClientFactory.createBuilder().setDefaultRequestConfig(requestConfig)
.setConnectionManager(this.connectionManager).disableRedirectHandling()
.build();
}
此處可看到,主要是從 this.hostProperties.getConnectionRequestTimeoutMillis(),拿到超時時間,最終我找到了 Springboot 配置 connectionRequestTimeoutMillis 位置,即 ZuulProperties.Host#connectionRequestTimeoutMillis,代碼如下
/**
* Represents a host.
*/
public static class Host {
/**
* The maximum number of total connections the proxy can hold open to backends.
*/
private int maxTotalConnections = 200;
/**
* The maximum number of connections that can be used by a single route.
*/
private int maxPerRouteConnections = 20;
/**
* The socket timeout in millis. Defaults to 10000.
*/
private int socketTimeoutMillis = 10000;
/**
* The connection timeout in millis. Defaults to 2000.
*/
private int connectTimeoutMillis = 2000;
/**
* The timeout in milliseconds used when requesting a connection from the
* connection manager. Defaults to -1, undefined use the system default.
* 此處時間為 -1,即永久等待
*/
private int connectionRequestTimeoutMillis = -1;
/**
* The lifetime for the connection pool.
*/
private long timeToLive = -1;
/**
* The time unit for timeToLive.
*/
private TimeUnit timeUnit = TimeUnit.MILLISECONDS;
public Host() {
}
// get set and toString
}
通過源碼可看到,zuul 有如下的相關配置:
- maxTotalConnections:HttpClient 總連接數,默認值為200
- maxPerRouteConnections:HttpClient 單個服務(即服務發現中的每個服務)連接數,默認為 20
- socketTimeoutMillis:連接服務時間,單位為毫秒,默認為10秒
- connectTimeoutMillis:服務返回時間,單位為毫秒,默認時間為20秒
- connectionRequestTimeoutMillis:連接 HttpClient 等待時間,默認為-1,即永久!
下面來對為何超時進行一個復現:
假設 100 個請求進來,HttpClient 連接池大小為 20,其中 20 個請求一直在等待遠程服務返回,其余 80 個請求一直在等待連接池的空閑連接,所以連接一直在等待,最終導致其它服務無法進入,最終導致所有服務都癱瘓了。
因為我的 gateway 未使用路由發現,所以,微服務中的熔斷器和負載均衡,均使用不上。所以只能采用如下方法:
- 設置 HttpClient 連接時間,即 connectionRequestTimeoutMillis 設置為 10S
- 增大 HttpClient 連接池大小,使得有足夠多的連接數,來增大并發量,即設置 maxTotalConnections 和 maxPerRouteConnections,這里我設置成了 500 和 250
下面是我調優后的參數:
zuul:
host:
connect-timeout-millis: 30000
socket-timeout-millis: 60000
max-total-connections: 500
max-per-route-connections: 250
connection-request-timeout-millis: 10000
3. 后續
雖然增大了 HttpClient 連接池大小,修改了連接 HttpClient 的時間,但是進行壓測時,依舊會出現某些請求,時間超過了 60S,我們以上的設置為 60S 再加上搶占連接池,總時間也不過 70S,如果超過70S,則會報搶占 HttpClient 連接池異常。但是我壓測后的結果,并不如此,以下是我壓測的結果

以上壓測顯示,95% 請求時間為 164 S,遠遠超過了我們預測的 70S,并且壓測的 QPS 很低,才 5.1,所以 zuul 的效率還不是很高,還待優化!
預知后事如何,請期待接下來的博客

浙公網安備 33010602011771號