前言
超時,應該是程序員很不愛處理的一種狀態。當我們調用某服務、某個中間件、db時,希望對方能快速回復,正確就正常,錯誤就錯誤,而不是一直不回覆。目前在後端領域來說,如java領域,調用服務時以同步阻塞調用爲主,此時一般會阻塞當前線程,等待結果。如果我們設置了超時時間還好,一段時間等不到就報錯了,要是超時時間沒設置或者過長,會導致線程動不了,即hang住了,多來幾個這種線程,線程池也就全都hang住了,此時,我們也就沒辦法響應前端了。
由於業務代碼或者底層框架編碼時不注意超時問題,這個問題經常會在線上纔出現(比如依賴的某個服務A,長時間運行的情況下,會出現響應慢問題,但是在平時開發環境服務A經常重啓,把問題掩蓋了,我們依賴方在開發環境測,當然也就沒注意A可能超時,等已上線,A一超時,我們就完蛋了)。
我前面幾篇文章的起源,也就是研究線上一個問題,就是懷疑我們服務中的數據庫連接池的連接被db或者防火牆幹掉了,導致我們這邊因爲也沒設置超時時間,進而卡死。
當時我就想模擬oracle數據庫不響應的情況,發現還是很不好模擬,後面經過各種查資料,才發現現在使用的這種iptables防火牆丟棄oracle返回的數據包的方式。
實驗環境
我們要模擬的事情如下:
oracle我就不模擬了,原理一樣的,那個網絡包要複雜一些,講起來就重點發散了。
這個圖裏,服務A就是相當於oracle,我們這裏是簡單啓動一個http服務:
python -m SimpleHTTPServer 8000
我們直接請求看下結果:
然後,我們後臺服務部署在10.80.121.46服務器的8084端口,我們正常請求該端口的接口,它就會去請求服務A,將服務A的響應返回給我們。
@PostMapping("/")
@ApiOperation(value = "訪問遠程服務")
public String test() {
String s = HttpClientUtil.doGet("http://10.80.121.115:8000/");
log.info("resp:{}",s);
return s;
}
實驗步驟
iptables安裝啓動
yum install -y iptables-services
systemctl start iptables
systemctl stop iptables
systemctl status iptables
如果提示綠色的“active (exited)”,則iptables已經啓動成功。
允許瀏覽器訪問8084端口
允許訪問服務端口,否則沒法測試,另外,如果對iptables完全不懂的,先去看下我前一篇再回來看。
iptables -I INPUT -p tcp -m tcp --dport 8084 -j ACCEPT
思路
要模擬出服務A不返回數據的效果,其實有兩種思路,一種是,把我們程序發給服務A的包丟掉;一種是,不丟棄我們發出去的包,但是,把服務A回給我們的包丟掉。
思路2:丟棄服務A返回的數據包
我是一開始就用的這個思路,所以先講這種。
這種的話,有的人可能覺得很簡單,實際上不是那麼簡單。我們不能簡單地把服務A返回給我們的包,全丟,因爲這樣的話,tcp連接都沒法建立。所以,我們要丟服務A返回的數據包,但是,tcp三次握手的包不能丟。
我們來分析下,這兩種包的不同之處。
三次握手時,對方返回的包長這樣:
即,tcp標誌位設置了SYN/ACK。
再來看看返回數據時的報文:
可以看到,標誌不一樣,這次是PSH/ACK。
所以,我們可以根據標誌來進行區分。
所以,最終我們的丟包策略是:
iptables -I INPUT 1 -p tcp -m tcp --tcp-flags PSH,ACK PSH,ACK --sport 8000 -j DROP
上面的意思是,對於服務A返回的包(源端口--sport爲8000),如果tcp標誌爲--tcp-flags PSH,ACK PSH,ACK
,就drop
。
這個tcp-flags的語法,詳細如下:
[!] --tcp-flags mask comp
Match when the TCP flags are as specified. The first argument mask is the flags which we should examine, written as a comma-separated list, and the
second argument comp is a comma-separated list of flags which must be set. Flags are: SYN ACK FIN RST URG PSH ALL NONE. Hence the command
iptables -A FORWARD -p tcp --tcp-flags SYN,ACK,FIN,RST SYN
will only match packets with the SYN flag set, and the ACK, FIN and RST flags unset.
後面接了兩端,第一段是掩碼,表示要檢查哪些標誌位(這裏指定n個要檢查的標誌位,別的沒說要檢查的,咱們就不管了),第二段是我們預期應該要設置的標誌,比如,我們這裏預期是要設置了PSH和ACK的纔算匹配。
ok,加完這個,我們再請求一次,看看效果,我們看前臺是轉圈,後臺日誌呢,是過了很久之後,顯示超時:
然後,我在後臺服務機器抓了包,可以看到,下面全是超時重傳,因爲服務A的數據發過來,我們丟棄了,服務A以爲我們沒收到,一直重發:
而我們服務這邊,代碼也是有點問題的,超時時間用的默認的,沒設置:
過了很久,都快3分鐘了,一直等不到迴應,纔去斷開連接,這要是在線上,不是又悲劇了嗎?所以,這個模擬超時,還是可以找出一些我們代碼問題的,有點用。
另外,我們看到,對方還給我們回覆了RST,我之前遇到過一種情況,對方回覆RST後,我們這邊連接就斷開了,報錯是:broken pipe,而不是read time out,如果,我們必須要模擬出read time out這種異常的話,可以把對方的RST也給丟了。
iptables -I INPUT 1 -p tcp -m tcp --tcp-flags RST RST --sport 8000 -j DROP
思路1:丟棄我們發出去的包
這個思路呢,和上面的原理類似,也是看看我們要發出去的包有什麼特徵,然後識別出來丟棄。
我最終寫出來的規則如下:
iptables -I OUTPUT 1 -p tcp -m tcp --tcp-flags PSH,ACK PSH,ACK --dport 8000 -j DROP
首先,這個是出去的包,所以,-I OUTPUT表示把規則寫入OUTPUT鏈,然後tcp標誌和上面是一樣的(這個需要自己結合tcpdump去觀察這些標誌),然後目的端口是8000,這樣的包,就是我們程序發出去的包,丟棄。
另外,我也觀察了tcpdump抓包,這次,意外的是:
整個過程,在tcpdump看來,只發現有三次握手的包,而程序發出去的包,被iptables丟棄後直接沒進協議棧,壓根就沒再繼續了,所以tcpdump也就看不到。
這個思路看起來更簡單暴力一些。
最終的效果還是一樣的:
結語
整個過程就講完了,iptables暫時告一段落,最近要忙點其他的事情了。