記一次網(wǎng)絡(luò)請(qǐng)求連接超時(shí)的事故
本文轉(zhuǎn)載自微信公眾號(hào)「搬運(yùn)工來(lái)架構(gòu)」,作者cocodroid。轉(zhuǎn)載本文請(qǐng)聯(lián)系搬運(yùn)工來(lái)架構(gòu)公眾號(hào)。
前言
從HTTP請(qǐng)求超時(shí)、重試機(jī)制、操作系統(tǒng)網(wǎng)絡(luò)等層面剖析了事故的原因,最終解決業(yè)務(wù)問(wèn)題。
這里先拋兩個(gè)問(wèn)題:
1)你遭遇過(guò)由于網(wǎng)絡(luò)連接或請(qǐng)求超時(shí)造成的生產(chǎn)事故嗎?
2)你知道操作系統(tǒng)默認(rèn)的網(wǎng)絡(luò)連接超時(shí)是多少秒?
先思考下,可以將你的答案寫在評(píng)論區(qū)哦。
問(wèn)題背景
最近同事出現(xiàn)這么一個(gè)問(wèn)題,簡(jiǎn)單業(yè)務(wù)場(chǎng)景:
服務(wù)A使用HTTP請(qǐng)求服務(wù)B接口m。服務(wù)A起了一個(gè)定時(shí)任務(wù)Task:
從db查詢數(shù)據(jù)總共有1200+條,每條記錄對(duì)應(yīng)一次請(qǐng)求,循環(huán)調(diào)用m接口。服務(wù)B收到請(qǐng)求會(huì)使用TCP連接其它服務(wù)器機(jī)器,進(jìn)行命令的交互。注意這里并不是異步并發(fā)去請(qǐng)求接口,因?yàn)槿绻惒讲l(fā)請(qǐng)求,可能就造成服務(wù)B的處理線程很快用光,從而造成不會(huì)再很好的處理更多請(qǐng)求,甚至?xí)斐纱竺娣e請(qǐng)求超時(shí)或服務(wù)宕機(jī)等等問(wèn)題。
此時(shí)定時(shí)任務(wù)到時(shí)間跑起來(lái)了,過(guò)不了多久,服務(wù)A出現(xiàn)向B請(qǐng)求Hand住,最終出現(xiàn)超時(shí)。
如下超時(shí)日志Read timed out:
雖然服務(wù)A自身查詢DB等服務(wù)是正常的,但是服務(wù)A和服務(wù)B之間的交互也很重要,如果兩者之間出現(xiàn)問(wèn)題,必然會(huì)對(duì)業(yè)務(wù)處理或者系統(tǒng)等方面造成影響。
所以到底是為什么,這里涉及了什么問(wèn)題呢?
問(wèn)題解決
1、重試機(jī)制加快問(wèn)題出現(xiàn)
此時(shí)在服務(wù)A上進(jìn)行排查,通過(guò)elk日志發(fā)現(xiàn)異常日志,異常日志數(shù)量激增。如下截圖:
異常日志明細(xì):
org.apache.http.impl.execchain.RetryExec,由此可知應(yīng)該跟http重試機(jī)制相關(guān)。
由RetryExec源碼可知,當(dāng)http執(zhí)行請(qǐng)求時(shí),如果正常請(qǐng)求則立即返回;否則IOException異常時(shí),則進(jìn)入重試環(huán)節(jié)。
這里要注意下,for循環(huán)進(jìn)行重試是死循環(huán)的方式,這里的重試次數(shù)由實(shí)現(xiàn)者控制,如果無(wú)需重試,默認(rèn)則不會(huì)進(jìn)行重試,而是直接拋出異常。
查看RetryHandler的自定義實(shí)現(xiàn)源碼:
- @Component
- public class HttpRequestRetryHandlerServer implements HttpRequestRetryHandler {
- protected static final Logger LOG = LoggerFactory.getLogger(HttpRequestRetryHandlerServer.class);
- @Override
- public boolean retryRequest(IOException e, int retryCount, HttpContext httpCtx) {
- if (retryCount >= 3) {
- LOG.warn("Maximum tries reached, exception would be thrown to outer block");
- return false;
- }
- if (e instanceof org.apache.http.NoHttpResponseException) {
- LOG.warn("No response from server on {} call", retryCount);
- return true;
- }
- return false;
- }
- }
從源碼知道,重試次數(shù)最多3次,并且只針對(duì)這種異常NoHttpResponseException,從命名知道這是HTTP無(wú)響應(yīng)異常(源碼注釋是:Signals that the target server failed to respond with a valid HTTP response.)。
那么服務(wù)A為什么會(huì)進(jìn)入重試流程呢?
由上面的異常知道,可以排除是由于網(wǎng)絡(luò)連接超時(shí)出現(xiàn)的異常,而是正常請(qǐng)求,但是由于可能某種原因,遲遲沒(méi)有得到正常響應(yīng)結(jié)果。由前面的異常Read timed out知道是出現(xiàn)讀超時(shí)異常,這里就考慮到可能是跟網(wǎng)絡(luò)數(shù)據(jù)傳輸?shù)葏?shù)相關(guān)。
查看默認(rèn)配置:
由此可知,6秒是數(shù)據(jù)傳輸?shù)淖铋L(zhǎng)時(shí)間(讀超時(shí))。http請(qǐng)求時(shí)等待數(shù)據(jù)結(jié)果如果超過(guò)6秒,就會(huì)中斷當(dāng)前的請(qǐng)求,拋出Read timed out異常。所以基本上就可以知道這個(gè)異常的原由了。
2、重試機(jī)制加快問(wèn)題-解決方法:
分析當(dāng)前場(chǎng)景,于是做下調(diào)整:
1)由于此場(chǎng)景http請(qǐng)求無(wú)需進(jìn)行重試,則將其關(guān)閉:
- @Bean
- public CloseableHttpClient noRetryHttpClient(HttpClientBuilder clientBuilder) {
- // 重試次數(shù)為0,不進(jìn)行重試
- clientBuilder.setRetryHandler(new DefaultHttpRequestRetryHandler(0, false));
- return clientBuilder.build();
- }
2)由于此業(yè)務(wù)請(qǐng)求,服務(wù)B可能出現(xiàn)超過(guò)6秒的處理時(shí)長(zhǎng),則socketTimeout調(diào)整為15秒:
- # http pool config
- http:
- maxTotal: 500
- defaultMaxPerRoute: 100
- connectTimeout: 5000
- connectionRequestTimeout: 3000
- socketTimeout: 15000
- maxIdleTime: 1
- keepAliveTimeOut: 65
3、機(jī)器連接超時(shí)的鍋
接下來(lái)再排查服務(wù)B到底是怎么了?也就是上圖的右側(cè)“閃電”。為什么需要這么長(zhǎng)的處理時(shí)間。
服務(wù)A發(fā)起Http請(qǐng)求時(shí),服務(wù)B接收請(qǐng)求后進(jìn)行跟服務(wù)器進(jìn)行連接后交互數(shù)據(jù)。服務(wù)B與服務(wù)機(jī)器通信只要是使用Tcp ssh的方式,也就是會(huì)進(jìn)行網(wǎng)絡(luò)通信。
經(jīng)過(guò)排查服務(wù)B的日志:
可知,進(jìn)行連接服務(wù)器時(shí)出現(xiàn)異常。注意這個(gè)連接耗時(shí)時(shí)長(zhǎng)為:63秒左右。并且確認(rèn)排查目標(biāo)服務(wù)器確實(shí)沒(méi)有正常工作,而是已經(jīng)停機(jī)許久。
由于進(jìn)行的是Unix平臺(tái)的網(wǎng)絡(luò)連接,當(dāng)前的操作系統(tǒng)是Linux CentOS。那么為什么這個(gè)超時(shí)時(shí)間是63秒,而不是5秒、15秒、60秒等比較規(guī)整的數(shù)據(jù)呢?
此時(shí)查看網(wǎng)絡(luò)連接的代碼:
- connection.connect();
可知這里并沒(méi)有指定連接超時(shí)等參數(shù),那么應(yīng)該是使用了操作系統(tǒng)內(nèi)核的默認(rèn)參數(shù)了。
Linux 系統(tǒng)默認(rèn)的建立 TCP 連接的超時(shí)時(shí)間為 127 秒。這個(gè)對(duì)于客戶端一般都是比較長(zhǎng)了,更多的業(yè)務(wù)場(chǎng)景基本不會(huì)使用默認(rèn)值,而是根據(jù)業(yè)務(wù)場(chǎng)景進(jìn)行設(shè)置合理的連接超時(shí)時(shí)間。那么這個(gè)時(shí)間是怎么來(lái)的?為什么是127?
其實(shí)這個(gè)時(shí)間參數(shù)是由:net.ipv4.tcp_syn_retries配置的等級(jí)來(lái)確定的。
net.ipv4.tcp_syn_retries 的設(shè)置,表示應(yīng)用程序進(jìn)行connect()系統(tǒng)調(diào)用時(shí),在對(duì)方不返回SYN + ACK的情況下(也就是超時(shí)的情況下),第一次發(fā)送之后,內(nèi)核最多重試幾次發(fā)送SYN包;并且決定了等待時(shí)間。
Linux上的默認(rèn)值是 net.ipv4.tcp_syn_retries = 6 ,也就是說(shuō)如果是本機(jī)主動(dòng)發(fā)起連接,(即主動(dòng)開(kāi)啟TCP三次握手中的第一個(gè)SYN包),如果一直收不到對(duì)方返回SYN + ACK ,那么應(yīng)用程序最大的超時(shí)時(shí)間就是127秒。
第 1 次發(fā)送 SYN 報(bào)文后等待 1s(2 的 0 次冪),如果超時(shí),則重試;
第 2 次發(fā)送后等待 2s(2 的 1 次冪),如果超時(shí),則重試;
第 3 次發(fā)送后等待 4s(2 的 2 次冪),如果超時(shí),則重試;
第 4 次發(fā)送后等待 8s(2 的 3 次冪),如果超時(shí),則重試;
第 5 次發(fā)送后等待 16s(2 的 4 次冪),如果超時(shí),則重試;
第 6 次發(fā)送后等待 32s(2 的 5 次冪),如果超時(shí),則重試;
第 7 次發(fā)送后等待 64s(2 的 6 次冪),如果超時(shí),則超時(shí)失敗。
接下來(lái)查看我們的機(jī)器上的tcp syn 參數(shù):
而我們的服務(wù)器設(shè)置的tcp_syn_retries為5,即默認(rèn)超時(shí)為=1+2+4+8+16+32=63秒。剛好與當(dāng)前問(wèn)題完美符合,這就是為什么出現(xiàn)63秒超時(shí)原由了。
4、那么在Windows平臺(tái),又是怎么樣的呢?
(本來(lái)這部分不準(zhǔn)備闡述的,希望讀者自行查閱資料,但是還是做個(gè)完整的吧。)
因?yàn)槲沂怯肳indows10作為開(kāi)發(fā)機(jī)器的,所以順便想了解下在Windows平臺(tái)下,它的超時(shí)時(shí)間是多少。寫了個(gè)測(cè)試用例,一測(cè),竟然是21秒左右。這又是什么原理??
查閱相關(guān)資料:
TcpMaxConnectRetransmissions
Determines how many times TCP retransmits an unanswered request for a new connection. TCP retransmits new connection requests until they are answered, or until this value expires.
TCP/IP adjusts the frequency of retransmissions over time. The delay between the original transmission and the first retransmissions for each interface is determined by the value of TcpInitialRTT (by default, it is 3 seconds). This delay is doubled after each attempt. After the final attempt, TCP/IP waits for an interval equal to double the last delay and then abandons the connection request.
TcpInitialRTT
Determines how quickly TCP retransmits a connection request if it doesn't receive a response to the original request for a new connection.
By default, the retransmission timer is initialized to 3 seconds, and the request (SYN) is sent twice, as specified in the value of TcpMaxConnectRetransmissions.
由資料可知,在Windows平臺(tái)上是由此參數(shù):TcpMaxConnectRetransmissions和TcpInitialRTT控制,TcpMaxConnectRetransmissions默認(rèn)值一般為2,TcpInitialRTT默認(rèn)是3秒。
也就是會(huì)進(jìn)行2次重試,每次是上次的2倍時(shí)間,即21秒為:3+3*2+(3*2)*2=3+6+12=21秒。
通過(guò)命令查詢Windows參數(shù):
- netsh interface tcp show global
這個(gè)最大SYN重新傳輸次數(shù)我的公司開(kāi)發(fā)機(jī)器是2,但是我的個(gè)人機(jī)器卻為4(那么默認(rèn)連接超時(shí)時(shí)長(zhǎng)為:3+6+12+24+48=93秒),雖然都是Windows10系統(tǒng),但是為什么不同這個(gè)就不得而知了。
5、機(jī)器連接超時(shí)的鍋-解決方法:
服務(wù)B網(wǎng)絡(luò)連接服務(wù)器時(shí)設(shè)置連接超時(shí)時(shí)間為5秒:
- connection.connect(null, 5000, kexTimout);
這樣只要超過(guò)5秒還沒(méi)能連接上,就做超時(shí)異常處理,及早釋放資源,不再阻塞當(dāng)前處理線程。
6、結(jié)果:
通過(guò)相關(guān)的調(diào)整優(yōu)化,重新發(fā)布服務(wù)驗(yàn)證,最終服務(wù)穩(wěn)定運(yùn)行,不會(huì)再出現(xiàn)異常等情況。
perfect!
總結(jié)
1)雖然這次事故造成的罪魁禍?zhǔn)撞皇欠?wù)A的HTTP重試機(jī)制,但是也是它加快了問(wèn)題的出現(xiàn)速度。
所以我們要清楚是否需要重試機(jī)制,如果不需要就不要設(shè)置,不然非常浪費(fèi)資源,甚至?xí)嚎宸?wù)提供方系統(tǒng)等問(wèn)題。
2)網(wǎng)絡(luò)連接一般有TCP和HTTP等,防止超時(shí)時(shí)間太久影響業(yè)務(wù)、甚至造成服務(wù)宕機(jī)等嚴(yán)重問(wèn)題,一般都要設(shè)置合理的超時(shí)時(shí)間(連接超時(shí)時(shí)間和數(shù)據(jù)傳輸時(shí)間等)。
因?yàn)椴僮飨到y(tǒng)設(shè)置的是比較通用的默認(rèn)參數(shù),并不會(huì)考慮具體的業(yè)務(wù)場(chǎng)景。
網(wǎng)絡(luò)數(shù)據(jù)傳輸時(shí)間:具體的業(yè)務(wù)場(chǎng)景是非常不同的,比如默認(rèn)6秒的數(shù)據(jù)傳輸時(shí)間,在實(shí)際的場(chǎng)景下并不一定合理,此時(shí)需要根據(jù)實(shí)際進(jìn)行調(diào)整,比如我這邊的情況是調(diào)整為15秒。
網(wǎng)絡(luò)連接超時(shí):比如Windows平臺(tái)網(wǎng)絡(luò)連接超時(shí)默認(rèn)一般是21秒,Linux(Centos)有默認(rèn)階梯超時(shí)機(jī)制,默認(rèn)127秒,而在我這臺(tái)機(jī)器上則是63秒。
3)學(xué)習(xí)操作系統(tǒng)超時(shí)機(jī)制。比如Linux或Windows,在連接超時(shí)時(shí)可以在前面的超時(shí)時(shí)間增加倍數(shù),可以學(xué)以致用到我們的業(yè)務(wù)開(kāi)發(fā)中去。