我們使用Linux作為服務器操作系統時,為了達到高並發處理能力,充分利用機器性能,經常會進行一些內核參數的調整優化,但不合理的調整常常也會引起意想不到的其他問題,本文就一次Linux服務器丟包故障的處理過程,結合Linux內核參數說明和TCP/IP協議棧相關的理論,介紹一些常見的丟包故障定位方法和解決思路。
問題現象
本次故障的反饋現象是:從辦公網訪問公網服務器不穩定,服務器某些端口訪問經常超時,但Ping測試顯示客戶端與服務器的鏈路始終是穩定低延遲的。
通過在服務器端抓包,發現還有幾個特點:
- 從辦公網訪問服務器有多個客戶端,是同一個出口IP,有少部分是始終能夠穩定連接的,另一部分間歇訪問超時或延遲很高
- 同一時刻的訪問,無論哪個客戶端的數據包先到達,服務端會及時處理部分客戶端的SYN請求,對另一部分客戶端的SYN包“視而不見”,如tcpdump數據所示,源端口為56909的SYN請求沒有得到響應,同一時間源端口為50212的另一客戶端SYN請求馬上得到響應。
1
2
3
4
5
6
7
8
9
10
11
|
$ sudo tcpdump -i eth0 port 22 and "tcp[tcpflags] & (tcp-syn) != 0"
18:56:37.404603 IP CLIENT.56909 > SERVER.22: Flags [S], seq 1190606850, win 29200, options [mss 1448,sackOK,TS val 198321481 ecr 0,nop,wscale 7], length 0
18:56:38.404582 IP CLIENT.56909 > SERVER.22: Flags [S], seq 1190606850, win 29200, options [mss 1448,sackOK,TS val 198321731 ecr 0,nop,wscale 7], length 0
18:56:40.407289 IP CLIENT.56909 > SERVER.22: Flags [S], seq 1190606850, win 29200, options [mss 1448,sackOK,TS val 198322232 ecr 0,nop,wscale 7], length 0
18:56:44.416108 IP CLIENT.56909 > SERVER.22: Flags [S], seq 1190606850, win 29200, options [mss 1448,sackOK,TS val 198323234 ecr 0,nop,wscale 7], length 0
18:56:45.100033 IP CLIENT.50212 > SERVER.22: Flags [S], seq 4207350463, win 65535, options [mss 1366,nop,wscale 5,nop,nop,TS val 821068631 ecr 0,sackOK,eol], length 0
18:56:45.100110 IP SERVER.22 > CLIENT.50212: Flags [S.], seq 1281140899, ack 4207350464, win 27960, options [mss 1410,sackOK,TS val 1709997543 ecr 821068631,nop,wscale 7], length 0
18:56:52.439086 IP CLIENT.56909 > SERVER.22: Flags [S], seq 1190606850, win 29200, options [mss 1448,sackOK,TS val 198325240 ecr 0,nop,wscale 7], length 0
18:57:08.472825 IP CLIENT.56909 > SERVER.22: Flags [S], seq 1190606850, win 29200, options [mss 1448,sackOK,TS val 198329248 ecr 0,nop,wscale 7], length 0
18:57:40.535621 IP CLIENT.56909 > SERVER.22: Flags [S], seq 1190606850, win 29200, options [mss 1448,sackOK,TS val 198337264 ecr 0,nop,wscale 7], length 0
18:57:40.535698 IP SERVER.22 > CLIENT.56909: Flags [S.], seq 3621462255, ack 1190606851, win 27960, options [mss 1410,sackOK,TS val 1710011402ecr 198337264,nop,wscale 7], length 0
|
排查過程
服務器能正常接收到數據包,問題可以限定在兩種可能:部分客戶端發出的數據包本身異常;服務器處理部分客戶端的數據包時觸發了某種機制丟棄了數據包。因為出問題的客戶端能夠正常訪問公網上其他服務,后者的可能性更大。
有哪些情況會導致Linux服務器丟棄數據包?
防火牆攔截
服務器端口無法連接,通常就是查看防火牆配置了,雖然這里已經確認同一個出口IP的客戶端有的能夠正常訪問,但也不排除配置了DROP特定端口范圍的可能性。
如何確認
查看iptables filter表,確認是否有相應規則會導致此丟包行為:
1
|
$ sudo iptables-save -t filter
|
這里容易排除防火牆攔截的可能性。
連接跟蹤表溢出
除了防火牆本身配置DROP規則外,與防火牆有關的還有連接跟蹤表nf_conntrack,Linux為每個經過內核網絡棧的數據包,生成一個新的連接記錄項,當服務器處理的連接過多時,連接跟蹤表被打滿,服務器會丟棄新建連接的數據包。
如何確認
通過dmesg可以確認是否有該情況發生:
1
|
$ dmesg |grep nf_conntrack
|
如果輸出值中有“nf_conntrack: table full, dropping packet”,說明服務器nf_conntrack表已經被打滿。
通過/proc文件系統查看nf_conntrack表實時狀態:
1
2
3
4
5
6
|
# 查看nf_conntrack表最大連接數
$ cat /proc/sys/net/netfilter/nf_conntrack_max
65536
# 查看nf_conntrack表當前連接數
$ cat /proc/sys/net/netfilter/nf_conntrack_count
7611
|
當前連接數遠沒有達到跟蹤表最大值,排除這個因素。
如何解決
如果確認服務器因連接跟蹤表溢出而開始丟包,首先需要查看具體連接判斷是否正遭受DOS攻擊,如果是正常的業務流量造成,可以考慮調整nf_conntrack的參數:
nf_conntrack_max決定連接跟蹤表的大小,默認值是65535,可以根據系統內存大小計算一個合理值:CONNTRACK_MAX = RAMSIZE(in bytes)/16384/(ARCH/32),如32G內存可以設置1048576;
nf_conntrack_buckets決定存儲conntrack條目的哈希表大小,默認值是nf_conntrack_max的1/4,延續這種計算方式:BUCKETS = CONNTRACK_MAX/4,如32G內存可以設置262144;
nf_conntrack_tcp_timeout_established決定ESTABLISHED狀態連接的超時時間,默認值是5天,可以縮短到1小時,即3600。
1
2
3
|
$ sysctl -w net.netfilter.nf_conntrack_max=1048576
$ sysctl -w net.netfilter.nf_conntrack_buckets=262144
$ sysctl -w net.netfilter.nf_conntrack_tcp_timeout_established=3600
|
Ring Buffer溢出
排除了防火牆的因素,我們從底向上來看Linux接收數據包的處理過程,首先是網卡驅動層。
如下圖所示,物理介質上的數據幀到達后首先由NIC(網絡適配器)讀取,寫入設備內部緩沖區Ring Buffer中,再由中斷處理程序觸發Softirq從中消費,Ring Buffer的大小因網卡設備而異。當網絡數據包到達(生產)的速率快於內核處理(消費)的速率時,Ring Buffer很快會被填滿,新來的數據包將被丟棄。
如何確認
通過ethtool或/proc/net/dev可以查看因Ring Buffer滿而丟棄的包統計,在統計項中以fifo標識:
1
2
3
4
5
6
7
|
$ ethtool -S eth0|grep rx_fifo
rx_fifo_errors: 0
$ cat /proc/net/dev
Inter-| Receive | Transmit
face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed
eth0: 17253386680731 42839525880 0 0 0 0 0 244182022 14879545018057 41657801805 0 0 0 0 0 0
|
可以看到服務器的接收方向的fifo丟包數並沒有增加,這里自然也排除這個原因。
如何解決
如果發現服務器上某個網卡的fifo數持續增大,可以去確認CPU中斷是否分配均勻,也可以嘗試增加Ring Buffer的大小,通過ethtool可以查看網卡設備Ring Buffer最大值,修改Ring Buffer當前設置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
# 查看eth0網卡Ring Buffer最大值和當前設置
$ ethtool -g eth0
Ring parameters for eth0:
Pre-set maximums:
RX: 4096
RX Mini: 0
RX Jumbo: 0
TX: 4096
Current hardware settings:
RX: 1024
RX Mini: 0
RX Jumbo: 0
TX: 1024
# 修改網卡eth0接收與發送硬件緩存區大小
$ ethtool -G eth0 rx 4096 tx 4096
Pre-set maximums:
RX: 4096
RX Mini: 0
RX Jumbo: 0
TX: 4096
Current hardware settings:
RX: 4096
RX Mini: 0
RX Jumbo: 0
TX: 4096
|
netdev_max_backlog溢出
netdev_max_backlog是內核從NIC收到包后,交由協議棧(如IP、TCP)處理之前的緩沖隊列。每個CPU核都有一個backlog隊列,與Ring Buffer同理,當接收包的速率大於內核協議棧處理的速率時,CPU的backlog隊列不斷增長,當達到設定的netdev_max_backlog值時,數據包將被丟棄。
如何確認
通過查看/proc/net/softnet_stat可以確定是否發生了netdev backlog隊列溢出:
1
2
3
4
5
|
$ cat /proc/net/softnet_stat
01a7b464 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
01d4d71f 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
0349e798 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
017e0826 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
|
其中:
每一行代表每個CPU核的狀態統計,從CPU0依次往下;
每一列代表一個CPU核的各項統計:第一列代表中斷處理程序收到的包總數;第二列即代表由於netdev_max_backlog隊列溢出而被丟棄的包總數。
從上面的輸出可以看出,這台服務器統計中,並沒有因為netdev_max_backlog導致的丟包。
如何解決
netdev_max_backlog的默認值是1000,在高速鏈路上,可能會出現上述第二列統計不為0的情況,可以通過修改內核參數net.core.netdev_max_backlog來解決:
1
|
$ sysctl -w net.core.netdev_max_backlog=2000
|
反向路由過濾
反向路由過濾機制是Linux通過反向路由查詢,檢查收到的數據包源IP是否可路由(Loose mode)、是否最佳路由(Strict mode),如果沒有通過驗證,則丟棄數據包,設計的目的是防范IP地址欺騙攻擊。rp_filter提供了三種模式供配置:
- 0 - 不驗證
- 1 - RFC3704定義的嚴格模式:對每個收到的數據包,查詢反向路由,如果數據包入口和反向路由出口不一致,則不通過
- 2 - RFC3704定義的松散模式:對每個收到的數據包,查詢反向路由,如果任何接口都不可達,則不通過
如何確認
查看當前rp_filter策略配置:
1
2
|
$ cat /proc/sys/net/ipv4/conf/eth0/rp_filter
0
|
如果這里設置為1,就需要查看主機的網絡環境和路由策略是否可能會導致客戶端的入包無法通過反向路由驗證了。
從原理來看這個機制工作在網絡層,因此,如果客戶端能夠Ping通服務器,就能夠排除這個因素了。
如何解決
根據實際網絡環境將rp_filter設置為0或2:
1
2
3
4
5
|
$ sysctl -w net.ipv4.conf.all.rp_filter=2
或
$ sysctl -w net.ipv4.conf.eth0.rp_filter=2
|
半連接隊列溢出
半連接隊列指的是TCP傳輸中服務器收到SYN包但還未完成三次握手的連接隊列,隊列大小由內核參數tcp_max_syn_backlog定義。
當服務器保持的半連接數量達到tcp_max_syn_backlog后,內核將會丟棄新來的SYN包。
如何確認
通過dmesg可以確認是否有該情況發生:
1
|
$ dmesg | grep "TCP: drop open request from"
|
半連接隊列的連接數量可以通過netstat統計SYN_RECV狀態的連接得知
1
2
|
$ netstat -ant|grep SYN_RECV|wc -l
0
|
大多數情況下這個值應該是0或很小,因為半連接狀態從第一次握手完成時進入,第三次握手完成后退出,正常的網絡環境中這個過程發生很快,如果這個值較大,服務器極有可能受到了SYN Flood攻擊。
如何解決
tcp_max_syn_backlog的默認值是256,通常推薦內存大於128MB的服務器可以將該值調高至1024,內存小於32MB的服務器調低到128,同樣,該參數通過sysctl修改:
1
|
$ sysctl -w net.ipv4.tcp_max_syn_backlog=1024
|
另外,上述行為受到內核參數tcp_syncookies的影響,若啟用syncookie機制,當半連接隊列溢出時,並不會直接丟棄SYN包,而是回復帶有syncookie的SYC+ACK包,設計的目的是防范SYN Flood造成正常請求服務不可用。
1
2
|
$ sysctl -w net.ipv4.tcp_syncookies=1
net.ipv4.tcp_syncookies = 1
|
PAWS
PAWS全名Protect Againest Wrapped Sequence numbers,目的是解決在高帶寬下,TCP序列號在一次會話中可能被重復使用而帶來的問題。
如上圖所示,客戶端發送的序列號為A的數據包A1因某些原因在網絡中“迷路”,在一定時間沒有到達服務端,客戶端超時重傳序列號為A的數據包A2,接下來假設帶寬足夠,傳輸用盡序列號空間,重新使用A,此時服務端等待的是序列號為A的數據包A3,而恰巧此時前面“迷路”的A1到達服務端,如果服務端僅靠序列號A就判斷數據包合法,就會將錯誤的數據傳遞到用戶態程序,造成程序異常。
PAWS要解決的就是上述問題,它依賴於timestamp機制,理論依據是:在一條正常的TCP流中,按序接收到的所有TCP數據包中的timestamp都應該是單調非遞減的,這樣就能判斷那些timestamp小於當前TCP流已處理的最大timestamp值的報文是延遲到達的重復報文,可以予以丟棄。在上文的例子中,服務器已經處理數據包Z,而后到來的A1包的timestamp必然小於Z包的timestamp,因此服務端會丟棄遲到的A1包,等待正確的報文到來。
PAWS機制的實現關鍵是內核保存了Per-Connection的最近接收時間戳,如果加以改進,就可以用來優化服務器TIME_WAIT狀態的快速回收。
TIME_WAIT狀態是TCP四次揮手中主動關閉連接的一方需要進入的最后一個狀態,並且通常需要在該狀態保持2*MSL(報文最大生存時間),它存在的意義有兩個:
1.可靠地實現TCP全雙工連接的關閉:關閉連接的四次揮手過程中,最終的ACK由主動關閉連接的一方(稱為A)發出,如果這個ACK丟失,對端(稱為B)將重發FIN,如果A不維持連接的TIME_WAIT狀態,而是直接進入CLOSED,則無法重傳ACK,B端的連接因此不能及時可靠釋放。
2.等待“迷路”的重復數據包在網絡中因生存時間到期消失:通信雙方A與B,A的數據包因“迷路”沒有及時到達B,A會重發數據包,當A與B完成傳輸並斷開連接后,如果A不維持TIME_WAIT狀態2*MSL時間,便有可能與B再次建立相同源端口和目的端口的“新連接”,而前一次連接中“迷路”的報文有可能在這時到達,並被B接收處理,造成異常,維持2*MSL的目的就是等待前一次連接的數據包在網絡中消失。
TIME_WAIT狀態的連接需要占用服務器內存資源維持,Linux內核提供了一個參數來控制TIME_WAIT狀態的快速回收:tcp_tw_recycle,它的理論依據是:
在PAWS的理論基礎上,如果內核保存Per-Host的最近接收時間戳,接收數據包時進行時間戳比對,就能避免TIME_WAIT意圖解決的第二個問題:前一個連接的數據包在新連接中被當做有效數據包處理的情況。這樣就沒有必要維持TIME_WAIT狀態2*MSL的時間來等待數據包消失,僅需要等待足夠的RTO(超時重傳),解決ACK丟失需要重傳的情況,來達到快速回收TIME_WAIT狀態連接的目的。
但上述理論在多個客戶端使用NAT訪問服務器時會產生新的問題:同一個NAT背后的多個客戶端時間戳是很難保持一致的(timestamp機制使用的是系統啟動相對時間),對於服務器來說,兩台客戶端主機各自建立的TCP連接表現為同一個對端IP的兩個連接,按照Per-Host記錄的最近接收時間戳會更新為兩台客戶端主機中時間戳較大的那個,而時間戳相對較小的客戶端發出的所有數據包對服務器來說都是這台主機已過期的重復數據,因此會直接丟棄。
如何確認
通過netstat可以得到因PAWS機制timestamp驗證被丟棄的數據包統計:
1
2
3
|
$ netstat -s |grep -e "passive connections rejected because of time stamp" -e "packets rejects in established connections because of timestamp”
387158 passive connections rejected because of time stamp
825313 packets rejects in established connections because of timestamp
|
通過sysctl查看是否啟用了tcp_tw_recycle及tcp_timestamp:
1
2
3
4
|
$ sysctl net.ipv4.tcp_tw_recycle
net.ipv4.tcp_tw_recycle = 1
$ sysctl net.ipv4.tcp_timestamps
net.ipv4.tcp_timestamps = 1
|
這次問題正是因為服務器同時開啟了tcp_tw_recycle和timestamps,而客戶端正是使用NAT來訪問服務器,造成啟動時間相對較短的客戶端得不到服務器的正常響應。
如何解決
如果服務器作為服務端提供服務,且明確客戶端會通過NAT網絡訪問,或服務器之前有7層轉發設備會替換客戶端源IP時,是不應該開啟tcp_tw_recycle的,而timestamps除了支持tcp_tw_recycle外還被其他機制依賴,推薦繼續開啟:
1
2
|
$ sysctl -w net.ipv4.tcp_tw_recycle=0
$ sysctl -w net.ipv4.tcp_timestamps=1
|
結論
Linux提供了豐富的內核參數供使用者調整,調整得當可以大幅提高服務器的處理能力,但如果調整不當,就會引進莫名其妙的各種問題,比如這次開啟tcp_tw_recycle導致丟包,實際也是為了減少TIME_WAIT連接數量而進行參數調優的結果。我們在做系統優化時,時刻要保持辯證和空杯的心態,不盲目吸收他人的果,而多去追求因,只有知其所以然,才能結合實際業務特點,得出最合理的優化配置。