天天看點

zuul網關_zuul 網關逾時優化 解決問題

zuul網關_zuul 網關逾時優化 解決問題

1. 概述

前段時間,線上的服務不知道為啥,突然全部的服務都逾時,所有的請求經過網關都逾時,後來進行鍊路追蹤排查,發現有一個服務連結 RDS 資料庫,一個查詢花費了 20S 的查詢時間,導緻後續調用該服務的應用都逾時。然後逾時的連接配接占滿了 zuul 的轉發池,最終導緻了所有經過 gateway 的服務都在等待,導緻全體服務全部逾時。

原因找出來之後,我就在納悶,為何一個服務逾時會導緻所有服務都延時,作為一個高可用的網關, zuul 的設計應該不會這麼差吧,并且,當時系統的 QPS 也不是很高,是以,必須找出這次逾時的問題所在,于是,我就開始了此次網關逾時的排查和優化!

下圖是出現問題的相關監控

zuul網關_zuul 網關逾時優化 解決問題
因為公司的服務是 java 和 node 應用都存在,是以使用的 zuul 是簡單路由轉發,未使用 服務注冊中心之類的,是以,熔斷器和

2. 問題分析

首先,因為網關的相關配置都是之前填的,有些參數不是特别的清楚,下面的配置檔案是我當時系統的相關配置。

zuul:host:connect-timeout-millis: 30000socket-timeout-millis: 60000ribbon:ConnectTimeout: 300000ReadTimeout: 60000eureka:enabled: falsehystrix:command:default:execution:isolation:thread:timeoutInMilliseconds: 70000
           

2.1 zuul 參數解釋

connect-timeout-millis

此參數為 zuul 網關連接配接服務的時間,機關為毫秒,我這裡配置的是 30S,如果 30S 之内未連接配接到待轉發的下一服務,則轉發将報錯,此請求也就将結束。這段時間為下圖的第 1 步的時間

如下圖所示:

zuul網關_zuul 網關逾時優化 解決問題

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 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 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 neededfinal int maxPerRoute = getMax(route);// Shrink the pool prior to allocating a new connectionfinal 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 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. timeoutif (!success && (deadline != null && deadline.getTime() <= System.currentTimeMillis())) {break;
                }
            }throw new TimeoutException("Timeout waiting for connection");
        } finally {this.lock.unlock();
        }
    }
           
  1. 代碼已建立有一個deadline ,然後判斷timeout ,這個timeout要注意。如果大于零才會指派deadline, 如果為0 則不會指派deadline 也就是說deadline始終為null
Date deadline = null;if (timeout > 0) {//如果逾時時間有效,則設定deadline
            deadline = new Date (System.currentTimeMillis() + tunit.toMillis(timeout));
        }
           
  1. 進入鎖代碼。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;
                }
           
  1. 如果沒有擷取到 進行接下來的代碼。
  2. 判斷是否達到了host配置的最大池數量,是否需要增加, 如果需要增加,則會在增加新連接配接之前縮小池,然後再配置設定傳回entry
// New connection is needed  擷取是否需要建立新的連接配接final int maxPerRoute = getMax(route);// Shrink the pool prior to allocating a new connectionfinal 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 otherpool = getPool(lastUsed.getRoute());
                                otherpool.remove(lastUsed);
                            }
                        }final C conn = this.connFactory.create(route);
                        entry = pool.add(conn);this.leased.add(entry);return entry;
                    }
                }
           
  1. 如果并不是上面的情況,實際情況就是池子被用光了,而且還達到了最大。就不能從池子中擷取資源了。隻能等了……
  2. 等待的時候會判斷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: 30000socket-timeout-millis: 60000max-total-connections: 500max-per-route-connections: 250connection-request-timeout-millis: 10000
           

3. 後續

雖然增大了 HttpClient 連接配接池大小,修改了連接配接 HttpClient 的時間,但是進行壓測時,依舊會出現某些請求,時間超過了 60S,我們以上的設定為 60S 再加上搶占連接配接池,總時間也不過 70S,如果超過70S,則會報搶占 HttpClient 連接配接池異常。但是我壓測後的結果,并不如此,以下是我壓測的結果

zuul網關_zuul 網關逾時優化 解決問題

以上壓測顯示,95% 請求時間為 164 S,遠遠超過了我們預測的 70S,并且壓測的 QPS 很低,才 5.1,是以 zuul 的效率還不是很高,還待優化!

預知後事如何,請期待接下來的部落格

标題:zuul 網關逾時優化 - 1.解決問題

作者:boolean-dev

位址:https://blog.booleandev.xyz/articles/2020/09/25/1600963319310.html

本站使用「CC BY 4.0」 創作共享協定,轉載請在文章明顯位置注明作者及出處。

  • 1. 概述
  • 2. 問題分析
  • 2.1 zuul 參數解釋
  • connect-timeout-millis
  • socket-timeout-millis
  • ribbon.ConnectTimeout
  • ribbon.ReadTimeout
  • 逾時時間采用哪個?
  • 2.2 問題分析
  • 擷取連接配接源碼
  • 建立連接配接池源碼
  • 3. 後續

繼續閱讀