我們使用Linux作為服務器操作系統時,為了達到高並發處理能力,充分利用機器性能,經常會進行一些內核參數的調整優化,但不合理的調整常常也會引起意想不到的其他問題,本文就一次Linux服務器丟包故障的處理過程,結合Linux內核參數說明和TCP/IP協議棧相關的理論,介紹一些常見的丟包故障定位方法和解決思路。
在開始之前,我們先用一張圖解釋 linux 系統接收網絡報文的過程。
- 首先網絡報文通過物理網線發送到網卡
- 網絡驅動程序會把網絡中的報文讀出來放到 ring buffer 中,這個過程使用 DMA(Direct Memory Access),不需要 CPU 參與
- 內核從 ring buffer 中讀取報文進行處理,執行 IP 和 TCP/UDP 層的邏輯,最后把報文放到應用程序的 socket buffer 中
- 應用程序從 socket buffer 中讀取報文進行處理
在接收 UDP 報文的過程中,圖中任何一個過程都可能會主動或者被動地把報文丟棄,因此丟包可能發生在網卡和驅動,也可能發生在系統和應用。
之所以沒有分析發送數據流程,一是因為發送流程和接收類似,只是方向相反;另外發送流程報文丟失的概率比接收小,只有在應用程序發送的報文速率大於內核和網卡處理速率時才會發生。
本篇文章假定機器只有一個名字為 eth0 的 interface,如果有多個 interface 或者 interface 的名字不是 eth0,請按照實際情況進行分析。
NOTE:文中出現的 RX(receive) 表示接收報文,TX(transmit) 表示發送報文。
問題現象
本次故障的反饋現象是:從辦公網訪問公網服務器不穩定,服務器某些端口訪問經常超時,但Ping測試顯示客戶端與服務器的鏈路始終是穩定低延遲的。
通過在服務器端抓包,發現還有幾個特點:
- 從辦公網訪問服務器有多個客戶端,是同一個出口IP,有少部分是始終能夠穩定連接的,另一部分間歇訪問超時或延遲很高
- 同一時刻的訪問,無論哪個客戶端的數據包先到達,服務端會及時處理部分客戶端的SYN請求,對另一部分客戶端的SYN包“視而不見”,如tcpdump數據所示,源端口為56909的SYN請求沒有得到響應,同一時間源端口為50212的另一客戶端SYN請求馬上得到響應。
$ 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系統丟包的原因相對就很多,常見的有:UDP 報文錯誤、防火牆、UDP buffer size 不足、系統負載過高等,這里對這些丟包原因進行分析。
名詞解釋
# ifconfig em2 em2 Link encap:Ethernet HWaddr AC::3D:A9::0D inet addr:211.211.211.211 Bcast:211.211.211.255 Mask:255.255.255.0 UP BROADCAST RUNNING MULTICAST MTU: Metric: RX packets: errors: dropped: overruns: frame: TX packets: errors: dropped: overruns: carrier: collisions: txqueuelen: RX bytes: ( (1.3 TiB) Memory:94b00000-94b20000
- RX errors: 表示總的收包的錯誤數量,這包括 too-long-frames 錯誤,Ring Buffer 溢出錯誤,crc 校驗錯誤,幀同步錯誤,fifo overruns 以及 missed pkg 等等。
- RX dropped: 表示數據包已經進入了 Ring Buffer,但是由於內存不夠等系統原因,導致在拷貝到內存的過程中被丟棄。
- RX overruns: 表示了 fifo 的 overruns,這是由於 Ring Buffer(aka Driver Queue) 傳輸的 IO 大於 kernel 能夠處理的 IO 導致的,而 Ring Buffer 則是指在發起 IRQ 請求之前的那塊 buffer。很明顯,overruns 的增大意味着數據包沒到 Ring Buffer 就被網卡物理層給丟棄了,而 CPU 無法即使的處理中斷是造成 Ring Buffer 滿的原因之一,上面那台有問題的機器就是因為 interruprs 分布的不均勻(都壓在 core0),沒有做 affinity 而造成的丟包。
- RX frame: 表示 misaligned 的 frames。
對於 TX 的來說,出現上述 counter 增大的原因主要包括 aborted transmission, errors due to carrirer, fifo error, heartbeat erros 以及 windown error,而 collisions 則表示由於 CSMA/CD 造成的傳輸中斷。
dropped與overruns的區別 dropped,表示這個數據包已經進入到網卡的接收緩存fifo隊列,並且開始被系統中斷處理准備進行數據包拷貝(從網卡緩存fifo隊列拷貝到系統內存),但由於此時的系統原因(比如內存不夠等)導致這個數據包被丟掉,即這個數據包被Linux系統丟掉。 overruns,表示這個數據包還沒有被進入到網卡的接收緩存fifo隊列就被丟掉,因此此時網卡的fifo是滿的。為什么fifo會是滿的?因為系統繁忙,來不及響應網卡中斷,導致網卡里的數據包沒有及時的拷貝到系統內存,fifo是滿的就導致后面的數據包進不來,即這個數據包被網卡硬件丟掉。所以,個人覺得遇到overruns非0,需要檢測cpu負載與cpu中斷情
排查過程
服務器能正常接收到數據包,問題可以限定在兩種可能:部分客戶端發出的數據包本身異常;服務器處理部分客戶端的數據包時觸發了某種機制丟棄了數據包。因為出問題的客戶端能夠正常訪問公網上其他服務,后者的可能性更大。
有哪些情況會導致Linux服務器丟棄數據包?
確認有 UDP 丟包發生
要查看網卡是否有丟包,可以使用 ethtool -S eth0 查看,在輸出中查找 bad 或者 drop 對應的字段是否有數據,在正常情況下,這些字段對應的數字應該都是 0。如果看到對應的數字在不斷增長,就說明網卡有丟包。
另外一個查看網卡丟包數據的命令是 ifconfig,它的輸出中會有 RX(receive 接收報文)和 TX(transmit 發送報文)的統計數據:
~# ifconfig eth0 ... RX packets 3553389376 bytes 2599862532475 (2.3 TiB) RX errors 0 dropped 1353 overruns 0 frame 0 TX packets 3479495131 bytes 3205366800850 (2.9 TiB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 ...
此外,linux 系統也提供了各個網絡協議的丟包信息,可以使用 netstat -s 命令查看,加上 –udp 可以只看 UDP 相關的報文數據:
[root@holodesk02 GOD]# netstat -s -u IcmpMsg: InType0: 3 InType3: 1719356 InType8: 13 InType11: 59 OutType0: 13 OutType3: 1737641 OutType8: 10 OutType11: 263 Udp: 517488890 packets received 2487375 packets to unknown port received. 47533568 packet receive errors 147264581 packets sent 12851135 receive buffer errors 0 send buffer errors UdpLite: IpExt: OutMcastPkts: 696 InBcastPkts: 2373968 InOctets: 4954097451540 OutOctets: 5538322535160 OutMcastOctets: 79632 InBcastOctets: 934783053 InNoECTPkts: 5584838675
對於上面的輸出,關注下面的信息來查看 UDP 丟包的情況:
- packet receive errors 不為空,並且在一直增長說明系統有 UDP 丟包
- packets to unknown port received 表示系統接收到的 UDP 報文所在的目標端口沒有應用在監聽,一般是服務沒有啟動導致的,並不會造成嚴重的問題
- receive buffer errors 表示因為 UDP 的接收緩存太小導致丟包的數量
NOTE: 並不是丟包數量不為零就有問題,對於 UDP 來說,如果有少量的丟包很可能是預期的行為,比如丟包率(丟包數量/接收報文數量)在萬分之一甚至更低。
網卡或者驅動丟包
之前講過,如果 ethtool -S eth0
中有 rx_***_errors
那么很可能是網卡有問題,導致系統丟包,需要聯系服務器或者網卡供應商進行處理。
# ethtool -S eth0 | grep rx_ | grep errors rx_crc_errors: 0 rx_missed_errors: 0 rx_long_length_errors: 0 rx_short_length_errors: 0 rx_align_errors: 0 rx_errors: 0 rx_length_errors: 0 rx_over_errors: 0 rx_frame_errors: 0 rx_fifo_errors: 0
netstat -i
也會提供每個網卡的接發報文以及丟包的情況,正常情況下輸出中 error 或者 drop 應該為 0。
如果硬件或者驅動沒有問題,一般網卡丟包是因為設置的緩存區(ring buffer)太小,可以使用 ethtool 命令查看和設置網卡的 ring buffer。
ethtool -g
可以查看某個網卡的 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: 256 RX Mini: 0 RX Jumbo: 0 TX: 256
Pre-set 表示網卡最大的 ring buffer 值,可以使用 ethtool -G eth0 rx 8192
設置它的值。
UDP 報文錯誤
如果在傳輸過程中UDP 報文被修改,會導致 checksum 錯誤,或者長度錯誤,linux 在接收到 UDP 報文時會對此進行校驗,一旦發明錯誤會把報文丟棄。
如果希望 UDP 報文 checksum 及時有錯也要發送給應用程序,可以在通過 socket 參數禁用 UDP checksum 檢查:
int disable = 1; setsockopt(sock_fd, SOL_SOCKET, SO_NO_CHECK, (void*)&disable, sizeof(disable)
UDP buffer size 不足
linux 系統在接收報文之后,會把報文保存到緩存區中。因為緩存區的大小是有限的,如果出現 UDP 報文過大(超過緩存區大小或者 MTU 大小)、接收到報文的速率太快,都可能導致 linux 因為緩存滿而直接丟包的情況。
在系統層面,linux 設置了 receive buffer 可以配置的最大值,可以在下面的文件中查看,一般是 linux 在啟動的時候會根據內存大小設置一個初始值。
- /proc/sys/net/core/rmem_max:允許設置的 receive buffer 最大值
- /proc/sys/net/core/rmem_default:默認使用的 receive buffer 值
- /proc/sys/net/core/wmem_max:允許設置的 send buffer 最大值
- /proc/sys/net/core/wmem_dafault:默認使用的 send buffer 最大值
但是這些初始值並不是為了應對大流量的 UDP 報文,如果應用程序接收和發送 UDP 報文非常多,需要講這個值調大。可以使用 sysctl 命令讓它立即生效:
sysctl -w net.core.rmem_max=26214400 # 設置為 25M
也可以修改 /etc/sysctl.conf
中對應的參數在下次啟動時讓參數保持生效。
如果報文報文過大,可以在發送方對數據進行分割,保證每個報文的大小在 MTU 內。
另外一個可以配置的參數是 netdev_max_backlog
,它表示 linux 內核從網卡驅動中讀取報文后可以緩存的報文數量,默認是 1000,可以調大這個值,比如設置成 2000:
sudo sysctl -w net.core.netdev_max_backlog=2000
系統負載過高
系統 CPU、memory、IO 負載過高都有可能導致網絡丟包,比如 CPU 如果負載過高,系統沒有時間進行報文的 checksum 計算、復制內存等操作,從而導致網卡或者 socket buffer 出丟包;memory 負載過高,會應用程序處理過慢,無法及時處理報文;IO 負載過高,CPU 都用來響應 IO wait,沒有時間處理緩存中的 UDP 報文。
linux 系統本身就是相互關聯的系統,任何一個組件出現問題都有可能影響到其他組件的正常運行。對於系統負載過高,要么是應用程序有問題,要么是系統不足。對於前者需要及時發現,debug 和修復;對於后者,也要及時發現並擴容。
應用丟包
上面提到系統的 UDP buffer size,調節的 sysctl 參數只是系統允許的最大值,每個應用程序在創建 socket 時需要設置自己 socket buffer size 的值。
linux 系統會把接受到的報文放到 socket 的 buffer 中,應用程序從 buffer 中不斷地讀取報文。所以這里有兩個和應用有關的因素會影響是否會丟包:socket buffer size 大小以及應用程序讀取報文的速度。
對於第一個問題,可以在應用程序初始化 socket 的時候設置 socket receive buffer 的大小,比如下面的代碼把 socket buffer 設置為 20MB:
uint64_t receive_buf_size = 20*1024*1024; //20 MB setsockopt(socket_fd, SOL_SOCKET, SO_RCVBUF, &receive_buf_size, sizeof(receive_buf_size));
如果不是自己編寫和維護的程序,修改應用代碼是件不好甚至不太可能的事情。很多應用程序會提供配置參數來調節這個值,請參考對應的官方文檔;如果沒有可用的配置參數,只能給程序的開發者提 issue 了。
很明顯,增加應用的 receive buffer 會減少丟包的可能性,但同時會導致應用使用更多的內存,所以需要謹慎使用。
另外一個因素是應用讀取 buffer 中報文的速度,對於應用程序來說,處理報文應該采取異步的方式
包丟在什么地方
想要詳細了解 linux 系統在執行哪個函數時丟包的話,可以使用 dropwatch 工具,它監聽系統丟包信息,並打印出丟包發生的函數地址:
# dropwatch -l kas Initalizing kallsyms db dropwatch> start Enabling monitoring... Kernel monitoring activated. Issue Ctrl-C to stop monitoring 1 drops at tcp_v4_do_rcv+cd (0xffffffff81799bad) 10 drops at tcp_v4_rcv+80 (0xffffffff8179a620) 1 drops at sk_stream_kill_queues+57 (0xffffffff81729ca7) 4 drops at unix_release_sock+20e (0xffffffff817dc94e) 1 drops at igmp_rcv+e1 (0xffffffff817b4c41) 1 drops at igmp_rcv+e1 (0xffffffff817b4c41)
通過這些信息,找到對應的內核代碼處,就能知道內核在哪個步驟中把報文丟棄,以及大致的丟包原因。
此外,還可以使用 linux perf 工具監聽 kfree_skb(把網絡報文丟棄時會調用該函數) 事件的發生:
sudo perf record -g -a -e skb:kfree_skb sudo perf script
關於 perf 命令的使用和解讀,網上有很多文章可以參考。
關於UDP丟包的總結
- UDP 本身就是無連接不可靠的協議,適用於報文偶爾丟失也不影響程序狀態的場景,比如視頻、音頻、游戲、監控等。對報文可靠性要求比較高的應用不要使用 UDP,推薦直接使用 TCP。當然,也可以在應用層做重試、去重保證可靠性
- 如果發現服務器丟包,首先通過監控查看系統負載是否過高,先想辦法把負載降低再看丟包問題是否消失
- 如果系統負載過高,UDP 丟包是沒有有效解決方案的。如果是應用異常導致 CPU、memory、IO 過高,請及時定位異常應用並修復;如果是資源不夠,監控應該能及時發現並快速擴容
- 對於系統大量接收或者發送 UDP 報文的,可以通過調節系統和程序的 socket buffer size 來降低丟包的概率
- 應用程序在處理 UDP 報文時,要采用異步方式,在兩次接收報文之間不要有太多的處理邏輯
防火牆攔截
服務器端口無法連接,通常就是查看防火牆配置了,雖然這里已經確認同一個出口IP的客戶端有的能夠正常訪問,但也不排除配置了DROP特定端口范圍的可能性。
如果系統防火牆丟包,表現的行為一般是所有的 UDP 報文都無法正常接收,當然不排除防火牆只 drop 一部分報文的可能性。
如果遇到丟包比率非常大的情況,請先檢查防火牆規則,保證防火牆沒有主動 drop UDP 報文。
如何確認
查看iptables filter表,確認是否有相應規則會導致此丟包行為:
$ sudo iptables-save -t filter
這里容易排除防火牆攔截的可能性。
連接跟蹤表溢出
除了防火牆本身配置DROP規則外,與防火牆有關的還有連接跟蹤表nf_conntrack,Linux為每個經過內核網絡棧的數據包,生成一個新的連接記錄項,當服務器處理的連接過多時,連接跟蹤表被打滿,服務器會丟棄新建連接的數據包。
如何確認
通過dmesg可以確認是否有該情況發生:
$ dmesg |grep nf_conntrack
如果輸出值中有“nf_conntrack: table full, dropping packet”,說明服務器nf_conntrack表已經被打滿。
通過/proc文件系統查看nf_conntrack表實時狀態:
# 查看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。
$ 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標識:
$ 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當前設置:
# 查看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隊列溢出:
$ 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來解決:
$ sysctl -w net.core.netdev_max_backlog=2000
反向路由過濾
反向路由過濾機制是Linux通過反向路由查詢,檢查收到的數據包源IP是否可路由(Loose mode)、是否最佳路由(Strict mode),如果沒有通過驗證,則丟棄數據包,設計的目的是防范IP地址欺騙攻擊。rp_filter提供了
三種模式供配置:
- 0 - 不驗證
- 1 - RFC3704定義的嚴格模式:對每個收到的數據包,查詢反向路由,如果數據包入口和反向路由出口不一致,則不通過
- 2 - RFC3704定義的松散模式:對每個收到的數據包,查詢反向路由,如果任何接口都不可達,則不通過
如何確認
查看當前rp_filter策略配置:
$ cat /proc/sys/net/ipv4/conf/eth0/rp_filter
如果這里設置為1,就需要查看主機的網絡環境和路由策略是否可能會導致客戶端的入包無法通過反向路由驗證了。
從原理來看這個機制工作在網絡層,因此,如果客戶端能夠Ping通服務器,就能夠排除這個因素了。
如何解決
根據實際網絡環境將rp_filter設置為0或2:
$ 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可以確認是否有該情況發生:
$ dmesg | grep "TCP: drop open request from"
半連接隊列的連接數量可以通過netstat統計SYN_RECV狀態的連接得知
$ netstat -ant|grep SYN_RECV|wc -l 0
大多數情況下這個值應該是0或很小,因為半連接狀態從第一次握手完成時進入,第三次握手完成后退出,正常的網絡環境中這個過程發生很快,如果這個值較大,服務器極有可能受到了SYN Flood攻擊。
如何解決
tcp_max_syn_backlog的默認值是256,通常推薦內存大於128MB的服務器可以將該值調高至1024,內存小於32MB的服務器調低到128,同樣,該參數通過sysctl修改:
$ sysctl -w net.ipv4.tcp_max_syn_backlog=1024
另外,上述行為受到內核參數tcp_syncookies的影響,若啟用syncookie機制,當半連接隊列溢出時,並不會直接丟棄SYN包,而是回復帶有syncookie的SYC+ACK包,設計的目的是防范SYN Flood造成正常請求服務不可用。
$ 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驗證被丟棄的數據包統計:
$ 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:
$ 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外還被其他機制依賴,推薦繼續開啟:
$ sysctl -w net.ipv4.tcp_tw_recycle=0 $ sysctl -w net.ipv4.tcp_timestamps=1
怎么知道為什么數據包被丟棄
dropwatch
通過谷歌搜索,發現一個很酷的工具叫 dropwatch 。 沒有現成的 Ubuntu 安裝軟件包,但可以通過 github 下載:
https://github.com/pavel-odintsov/drop_watch
以下是我可以編譯的說明:
sudo apt-get install -y libnl-3-dev libnl-genl-3-dev binutils-dev libreadline6-dev git clone https://github.com/pavel-odintsov/drop_watch.git cd drop_watch/src make
這里是輸出! 它告訴我哪個內核函數丟失數據包,酷!
sudo ./dropwatch -l kas Initalizing kallsyms db dropwatch> start Enabling monitoring... Kernel monitoring activated. Issue Ctrl-C to stop monitoring 1 drops at tcp_v4_do_rcv+cd (0xffffffff81799bad) 10 drops at tcp_v4_rcv+80 (0xffffffff8179a620) 1 drops at sk_stream_kill_queues+57 (0xffffffff81729ca7) 4 drops at unix_release_sock+20e (0xffffffff817dc94e) 1 drops at igmp_rcv+e1 (0xffffffff817b4c41) 1 drops at igmp_rcv+e1 (0xffffffff817b4c41)
perf
用perf監控丟棄的數據包
還有另一個很酷的方法,用來調試發生什么。
可以使用 perf 監視 kfree_skb 事件,這將告訴你什么時候丟棄數據包(內核堆棧發生的地方):
sudo perf record -g -a -e skb:kfree_skb sudo perf script
擴展閱讀
還有這兩個很酷的文章:
監控和調優Linux網絡堆棧:接收數據
https://blog.packagecloud.io/eng/2016/06/22/monitoring-tuning-linux-networking-stack-receiving-data/
監控和調優Linux網絡堆棧:發送數據
https://blog.packagecloud.io/eng/2017/02/06/monitoring-tuning-linux-networking-stack-sending-data/
結論
Linux提供了豐富的內核參數供使用者調整,調整得當可以大幅提高服務器的處理能力,但如果調整不當,就會引進莫名其妙的各種問題,比如這次開啟tcp_tw_recycle導致丟包,實際也是為了減少TIME_WAIT連接數量而進行參數調優的結果。我們在做系統優化時,時刻要保持辯證和空杯的心態,不盲目吸收他人的果,而多去追求因,只有知其所以然,才能結合實際業務特點,得出最合理的優化配置。
https://jermine.vdo.pub/linux/linux%E6%9C%8D%E5%8A%A1%E5%99%A8%E4%B8%A2%E5%8C%85%E6%95%85%E9%9A%9C%E7%9A%84%E8%A7%A3%E5%86%B3/
https://cizixs.com/2018/01/13/linux-udp-packet-drop-debug/
轉自:
https://www.cnblogs.com/pugang/p/13609385.html