超時重試如何設定睡眠時間
前言
超時可以說是除了空指標我們最熟悉的異常了,從系統的接入層,到服務層,再到資料庫層等等都能看到超時的身影;超時很多情況下同時伴隨著重試,因為某些情況下比如網路抖動問題等,重試是可以成功的;當然重試往往也會指定重試次數上限,因為如果程式確實存在問題,重試多少次都無濟於事,那其實也是對資源的浪費。
為什麼要設定超時
對於開發人員來說我們平時最常見的就是設定超時時間,比如資料庫超時設定、快取超時設定、中介軟體客戶端超時設定、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
以上透過
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
中的超時重試機制,可用作為一個很好的參考。