首頁 > 易卦

超時與重試淺析

作者:由 ksfzhaohui 發表于 易卦日期:2021-09-30

超時重試如何設定睡眠時間

前言

超時可以說是除了空指標我們最熟悉的異常了,從系統的接入層,到服務層,再到資料庫層等等都能看到超時的身影;超時很多情況下同時伴隨著重試,因為某些情況下比如網路抖動問題等,重試是可以成功的;當然重試往往也會指定重試次數上限,因為如果程式確實存在問題,重試多少次都無濟於事,那其實也是對資源的浪費。

為什麼要設定超時

對於開發人員來說我們平時最常見的就是設定超時時間,比如資料庫超時設定、快取超時設定、中介軟體客戶端超時設定、HttpClient超時設定、可能還有業務超時;為什麼要設定超時時間,因為如果不設定超時時間,可能因為某個請求無法即時響應導致整個鏈路處於長時間等待狀態,這種請求如果過多,直接導致整個系統癱瘓,丟擲超時異常其實也是及時止損;縱觀各種超時時間設定,可以看到其實大多數都是圍繞網路超時,而網路超時不得不提Socket超時設定。

Socket超時

Socket是作為網路通訊最基礎的類,要進行通訊基本分為兩步:

建立連線:在進行讀寫訊息之前必須首先建立連線;連線階段會有連線超時設定ConnectTimeout;

讀寫操作:讀寫也就是雙方正式交換資料,此階段會有讀寫超時設定ReadTimeOut;

連線超時

Socket提供的connect方法提供了連線超時設定:

public void connect(SocketAddress endpoint) throws IOExceptionpublic void connect(SocketAddress endpoint, int timeout) throws IOException

不設定

timeout

預設是0,理論上應該是沒有時間限制,經測試預設還是有一個時間限制大概21秒左右;

在建立連線的時候可能會丟擲多種異常,常見的比如:

ProtocolException:基礎協議中存在錯誤,例如TCP錯誤;

java。net。ProtocolException: Protocol error

ConnectException:遠端拒絕連線(例如,沒有程序正在偵聽遠端地址/埠);

java。net。ConnectException: Connection refused

SocketTimeoutException:套接字讀取(read)或接受(accept)發生超時;

java。net。SocketTimeoutException: connect timed out java。net。SocketTimeoutException: Read timed out

UnknownHostException:指示無法確定主機的IP地址;

java。net。UnknownHostException: localhost1

NoRouteToHostException:連線到遠端地址和埠時出錯。通常,由於防火牆的介入,或者中間路由器關閉,無法訪問遠端主機;

java。net。NoRouteToHostException: Host unreachable java。net。NoRouteToHostException: Address not available

SocketException:建立或訪問套接字時出錯;

java。net。SocketException: Socket closed java。net。SocketException: connect failed

這裡我們重點要討論的是

SocketTimeoutException

,同時

Connection refused

也經常出現,這裡做一個簡單的對比

Connect timed out

本地可以直接使用一個不存在的ip嘗試連線:

SocketAddress endpoint = new InetSocketAddress(“111。1。1。1”, 8080);socket。connect(endpoint, 2000);

嘗試連線報如下錯誤:

java。net。SocketTimeoutException: connect timed out at java。net。DualStackPlainSocketImpl。waitForConnect(Native Method) at java。net。DualStackPlainSocketImpl。socketConnect(DualStackPlainSocketImpl。java:85) at java。net。AbstractPlainSocketImpl。doConnect(AbstractPlainSocketImpl。java:350) at java。net。AbstractPlainSocketImpl。connectToAddress(AbstractPlainSocketImpl。java:206) at java。net。AbstractPlainSocketImpl。connect(AbstractPlainSocketImpl。java:188) at java。net。PlainSocketImpl。connect(PlainSocketImpl。java:172) at java。net。SocksSocketImpl。connect(SocksSocketImpl。java:392) at java。net。Socket。connect(Socket。java:589)

Connection refused

本地測試可以使用127。x。x。x來進行模擬,嘗試連線報如下錯誤:

java。net。ConnectException: Connection refused: connect at java。net。DualStackPlainSocketImpl。waitForConnect(Native Method) at java。net。DualStackPlainSocketImpl。socketConnect(DualStackPlainSocketImpl。java:85) at java。net。AbstractPlainSocketImpl。doConnect(AbstractPlainSocketImpl。java:350) at java。net。AbstractPlainSocketImpl。connectToAddress(AbstractPlainSocketImpl。java:206) at java。net。AbstractPlainSocketImpl。connect(AbstractPlainSocketImpl。java:188) at java。net。PlainSocketImpl。connect(PlainSocketImpl。java:172) at java。net。SocksSocketImpl。connect(SocksSocketImpl。java:392) at java。net。Socket。connect(Socket。java:589)

對比

Connection refused:表示從本地客戶端到目標IP地址的路由是正常的,但是該目標埠沒有程序在監聽,然後服務端拒絕掉了連線;127開頭用作本地環回測試(loopback test)本主機的程序之間的通訊,所以資料報不會發送給網路,路由都是正常的;

Connect timed out:超時的可能性比較多常見的如伺服器無法ping通、防火牆丟棄了請求報文、網路間隙性問題等;

讀寫超時

Socket可以設定

SoTimeout

表示讀寫的超時時間,如果不設定預設為0,表示沒有時間限制;可以簡單做一個模擬,模擬伺服器端業務處理延遲10秒,而客戶端設定的讀寫超時時間為2秒:

Socket socket = new Socket();SocketAddress endpoint = new InetSocketAddress(“127。0。0。1”, 8189);socket。connect(endpoint, 2000);//設定連線超時為2秒socket。setSoTimeout(1000);//設定讀寫超時為1秒InputStream inStream = socket。getInputStream();inStream。read();//讀取操作

因為伺服器端做了延遲處理,所以超過客戶端設定的讀寫超時時間,直接報如下錯誤:

java。net。SocketTimeoutException: Read timed out at java。net。SocketInputStream。socketRead0(Native Method) at java。net。SocketInputStream。socketRead(SocketInputStream。java:116) at java。net。SocketInputStream。read(SocketInputStream。java:171) at java。net。SocketInputStream。read(SocketInputStream。java:141) at java。net。SocketInputStream。read(SocketInputStream。java:224)

NIO超時

以上是基於傳統Socket的超時配置,

NIO

提供的

SocketChannel

也同樣存在超時的情況;

NIO

模式提供了阻塞模式和非阻塞模式,阻塞模式和傳統的Socket是一樣的,而且存在對應關係;而非阻塞模式並沒有提供超時時間的設定;

阻塞模式

SocketChannel client = SocketChannel。open();//阻塞模式client。configureBlocking(true);InetSocketAddress endpoint = new InetSocketAddress(“128。5。50。12”, 8888);client。socket()。connect(endpoint, 1000);

以上阻塞模式下可以透過

client。socket()

可以獲取到

SocketChannel

對應的

Socket

,設定連線超時時間,報如下錯誤:

java。net。SocketTimeoutException at sun。nio。ch。SocketAdaptor。connect(SocketAdaptor。java:118)

非阻塞模式

SocketChannel client = SocketChannel。open();// 非阻塞模式client。configureBlocking(false);// select註冊Selector selector = Selector。open();client。register(selector, SelectionKey。OP_CONNECT);InetSocketAddress endpoint = new InetSocketAddress(“127。0。0。1”, 8888);client。connect(endpoint);

同樣模擬以上兩種情況,報如下錯誤:

//連線超時異常java。net。ConnectException: Connection timed out: no further information at sun。nio。ch。SocketChannelImpl。checkConnect(Native Method) at sun。nio。ch。SocketChannelImpl。finishConnect(SocketChannelImpl。java:717)//連線拒絕異常java。net。ConnectException: Connection refused: no further information at sun。nio。ch。SocketChannelImpl。checkConnect(Native Method) at sun。nio。ch。SocketChannelImpl。finishConnect(SocketChannelImpl。java:717)

常見超時

瞭解了

Socket

超時,那麼瞭解其他因為網路而引發的超時就簡單多了,常見的網路讀寫超時設定包括:資料庫客戶端超時、快取客戶端超時、

RPC

客戶端超時、

HttpClient

超時,閘道器層超時;以上幾種情況其實都是以客戶端的角度來進行的超時時間設定,像Web容器在伺服器端也做了超時處理;當然除了網路相關的超時可能也有一些業務超時的情況,下面分別介紹;

網路超時

這裡重點看一下客戶端相關的超時設定,服務端重點看一下Web容器;

資料庫客戶端超時

Mysql

為例,最簡單的超時時間設定只需要在url後面新增即可:

jdbc:mysql://localhost:3306/ds0?connectTimeout=2000&socketTimeout=200

connectTimeout:連線超時時間;

socketTimeout:讀寫超時時間;

除了資料庫驅動本身提供的超時時間配置,我們一般都直接使用

ORM

框架,比如

Mybatis

等,這些框架本身也會提供相應的超時時間:

defaultStatementTimeout:設定超時時間,它決定資料庫驅動等待資料庫響應的秒數。

快取客戶端超時

Redis

為例,使用

Jedis

為例,在建立連線的時候同樣可以配置超時時間:

public Jedis(final String host, final int port, final int timeout)

這裡只配置了一個超時時間,但其實連線和讀寫超時共用一個值,可以檢視

Connection

原始碼:

public void connect() { if (!isConnected()) { try { socket = new Socket(); socket。setReuseAddress(true); socket。setKeepAlive(true); socket。setTcpNoDelay(true); socket。setSoLinger(true, 0); //timeout連線超時設定 socket。connect(new InetSocketAddress(host, port), timeout); //timeout讀寫超時設定 socket。setSoTimeout(timeout); outputStream = new RedisOutputStream(socket。getOutputStream()); inputStream = new RedisInputStream(socket。getInputStream()); } catch (IOException ex) { throw new JedisConnectionException(ex); } } }

RPC客戶端超時

Dubbo

為例,可以直接在

xml

中配置超時時間:

預設時間為1000ms,

Dubbo

作為

RPC

框架,底層使用的是

Netty

等通訊框架,但是

Dubbo

透過

Future

實現了自己的超時機制,可以直接檢視

DefaultFuture

,部分程式碼如下所示:

// 內部鎖 private final Lock lock = new ReentrantLock(); private final Condition done = lock。newCondition(); // 在指定時間內不能獲取直接返回TimeoutException public Object get(int timeout) throws RemotingException { if (timeout <= 0) { timeout = Constants。DEFAULT_TIMEOUT; } if (!isDone()) { long start = System。currentTimeMillis(); lock。lock(); try { while (!isDone()) { done。await(timeout, TimeUnit。MILLISECONDS); if (isDone() || System。currentTimeMillis() - start > timeout) { break; } } } catch (InterruptedException e) { throw new RuntimeException(e); } finally { lock。unlock(); } if (!isDone()) { throw new TimeoutException(sent > 0, channel, getTimeoutMessage(false)); } } return returnFromResponse(); }

HttpClient超時

HttpClient可以說是我們最常使用的Http客戶端了,可以透過

RequestConfig

來設定超時時間:

RequestConfig requestConfig = RequestConfig。custom()。setSocketTimeout(2000)。setConnectTimeout(1000) 。setConnectionRequestTimeout(3000)。build();

其中可以配置三個超時時間分別是:

socketTimeout:連線建立成功,讀寫超時時間;

connectTimeout:連線超時時間;

connectionRequestTimeout:從連線管理器請求連線時使用的超時;

閘道器層超時

以常見的Nginx為例,作為代理轉發,從下游Web伺服器的角度來看,Nginx作為轉發器其實就是客戶端,同樣需要配置連線、讀寫等超時時間:

server { listen 80; server_name localhost; location / { // 超時配置 proxy_connect_timeout 2s; proxy_read_timeout 2s; proxy_send_timeout 2s; //重試機制 proxy_next_upstream error timeout; proxy_next_upstream_tries 5; proxy_next_upstream_timeout 5; } }

相關超時時間配置:

proxy_connect_timeout:與後端伺服器建立連線超時時間,預設60s;

proxy_read_timeout:從後端伺服器讀取響應的超時時間,預設60s;

proxy_send_timeout:往後端伺服器傳送請求的超時時間,預設60s;

Nginx作為代理伺服器,同樣提供了重試機制,對於上游伺服器往往會配置多臺來實現負載均衡,相關配置如下:

proxy_next_upstream:什麼情況下需要請求下一臺後端伺服器進行重試,預設error timeout;

proxy_next_upstream_tries:重試次數,預設為0表示不限次數;

proxy_next_upstream_timeout:重試最大超時時間,預設為0表示不限次數;

服務端超時

以上幾種情況我們都是站在客戶端的角度,也是作為開發人員最常使用的超時配置,其實在伺服器端也同樣可以配置相應的超時時間,比如最常見的Web容器Tomcat、上面介紹的Nginx等,下面看一下Tomcat的相關超時配置:

connectionTimeout:聯結器在接受連線後,指定時間內沒有接收到請求URI行,則表示連線超時;

socket。soTimeout:從客戶端讀取請求資料的超時時間,默認同connectionTimeout;

asyncTimeout:非同步請求的超時時間;

disableUploadTimeout和connectionUploadTimeout:檔案上傳使用的超時時間;

keepAliveTimeout:設定Http長連線超時時間;

更多配置:Tomcat8。5

業務超時

基本上我們用到的中介軟體都提供了超時設定,當然業務中某些情況也需要我們自己做超時處理,比如某個功能需要呼叫多個服務,每個服務都有自己的超時時間,但是此功能有個總的超時時間,這時候我們可以參考

Dubbo

使用

Future

來解決超時問題。

重試

重試往往伴隨著超時一起出現,因為超時可能是因為某些特殊原因導致暫時性的請求失敗,也就是說重試是有可能出現請求再次成功的;其實現在很多提供負載均衡的系統,不僅是在超時的時候重試,出現任何異常都會重試,比如類似Nginx的閘道器,RPC,MQ等;下面具體看看各種系統都是如何實現重試的;

RPC重試

RPC系統一般都會提供註冊中心,服務提供方會提供多個節點,所以如果某個服務端節點異常,消費端會重新選擇其他的節點;以

Dubbo

為例,提供了容錯機制類

FailoverClusterInvoker

,預設會失敗重試兩次,具體重試是透過

for

迴圈來實現的:

for (int i = 0; i < len; i++) { try{ //負載均衡選擇一個服務端 Invoker invoker = select(loadbalance, invocation, copyinvokers, invoked); //執行 Result result = invoker。invoke(invocation); } catch (Throwable e) { //出現異常並不會退出 le = new RpcException(e。getMessage(), e); } }

以上透過

for

迴圈捕獲異常來實現重試是一種比較好的方式,比在

catch

子句中再實現重試更方便;

MQ重試

很多訊息系統都提供了重試機制比如ActiveMQ、RocketMQ、Kafka等;

ActiveMQ中的

ActiveMQMessageConsumer

類的

rollback

提供了重試機制,最大的重發次數

DEFAULT_MAXIMUM_REDELIVERIES=6

RocketMQ在訊息量大,網路有波動的情況下,重試也是一個大機率事件;Producer中的

setRetryTimesWhenSendFailed

設定在同步方式下自動重試的次數,預設值為2;

閘道器重試

閘道器作為一個負載均衡器,其中一個核心功能就是重試機制,除了此機制外還有健康檢測機制,事先把有問題的業務邏輯節點排除掉,這樣也減少了重試的機率,重試本身也是很浪費時間的;Nginx相關重試的配置上節中已經介紹,這裡不在重複;

HttpClient重試

HttpClient內部其實提供了重試機制,實現類

RetryExec

,預設重試次數為3次,程式碼部分如下:

for (int execCount = 1;; execCount++) { try { return this。requestExecutor。execute(route, request, context, execAware); } catch (final IOException ex) { // 重試異常檢查 }}

只有發生IOExecetion時才會發生重試;

InterruptedIOException、UnknownHostException、ConnectException、SSLException,發生這4種異常不重試;

可以發現

SocketTimeoutException

繼承於

InterruptedIOException

,所以並不會重試;

定時器重試

之前有遇到過需要通知外部系統的情況,因為實時性沒那麼高,而且很多外部系統都不是那麼穩定,不一定什麼時候就進入維護中;採用

資料庫+定時器

的方式來進行重試,每條通知記錄會儲存下一次重試的時間(重試時間採用遞增的方式),定時器定期查詢哪些下一次重試時間在當前時間內的,如果成功更新狀態為成功,如果失敗更新下一次重試時間,重試次數+1,當然也會設定最大重試值;

注意點

當然重試也需要注意是查詢類的還是更新類的,如果是查詢類的多次重試並不影響結果,如果是更新類的,需要做好冪等性。

總結

合理的設定超時與重試機制,是保證系統高可用的前提之一;太多故障因為不合理的設定超時時間導致的,所以我們在開發過程中一定要注意;另外一點就是可用多看看一些中介軟體的原始碼,很多解決方案都可用在這些中介軟體中找到答案,比如

Dubbo

中的超時重試機制,可用作為一個很好的參考。