SNMP 計數器¶
本文件解釋了 SNMP 計數器的含義。
通用 IPv4 計數器¶
所有第4層資料包和 ICMP 資料包都會改變這些計數器,但第2層資料包(如 STP)或 ARP 資料包不會改變這些計數器。
IpInReceives
IP 層接收到的資料包數量。它在 ip_rcv 函式開始時增加,並始終與 IpExtInOctets 一起更新。即使資料包稍後被丟棄(例如,由於 IP 報頭無效或校驗和錯誤等),它也會增加。它表示 GRO/LRO 後聚合段的數量。
IpInDelivers
傳遞到上層協議的資料包數量。例如 TCP、UDP、ICMP 等。如果沒有應用程式偵聽原始套接字,則只傳遞核心支援的協議;如果有應用程式偵聽原始套接字,則所有有效的 IP 資料包都會被傳遞。
IpOutRequests
透過 IP 層傳送的資料包數量,包括單播和多播資料包,並始終與 IpExtOutOctets 一起更新。
IpExtInOctets 和 IpExtOutOctets
它們是 Linux 核心擴充套件,沒有 RFC 定義。請注意,RFC1213 確實定義了 ifInOctets 和 ifOutOctets,但它們是不同的。ifInOctets 和 ifOutOctets 包含 MAC 層報頭大小,而 IpExtInOctets 和 IpExtOutOctets 不包含,它們只包含 IP 層報頭和 IP 層資料。
IpExtInNoECTPkts, IpExtInECT1Pkts, IpExtInECT0Pkts, IpExtInCEPkts
它們表示四種 ECN IP 資料包的數量,請參閱 顯式擁塞通知 (Explicit Congestion Notification) 獲取更多詳細資訊。
這 4 個計數器計算每種 ECN 狀態下接收到的資料包數量。它們計算實際幀數,無論 LRO/GRO。因此對於同一個資料包,您可能會發現 IpInReceives 計數為 1,但 IpExtInNoECTPkts 計數為 2 或更多。
IpInHdrErrors
定義於 RFC1213 ipInHdrErrors。它表示資料包因 IP 報頭錯誤而被丟棄。這可能發生在 IP 輸入路徑和 IP 轉發路徑中。
IpInAddrErrors
定義於 RFC1213 ipInAddrErrors。它會在兩種情況下增加:(1) IP 地址無效。(2) 目標 IP 地址不是本地地址且未啟用 IP 轉發。
IpExtInNoRoutes
此計數器表示當 IP 協議棧接收到資料包但無法從路由表中找到其路由時,資料包被丟棄。這可能發生在啟用 IP 轉發且目標 IP 地址不是本地地址且沒有目標 IP 地址路由的情況下。
IpInUnknownProtos
定義於 RFC1213 ipInUnknownProtos。如果核心不支援第4層協議,此計數器將增加。如果應用程式使用原始套接字,核心將始終將資料包傳遞到原始套接字,此計數器不會增加。
IpExtInTruncatedPkts
對於 IPv4 資料包,這意味著實際資料大小小於 IPv4 報頭中的“總長度”欄位。
IpInDiscards
定義於 RFC1213 ipInDiscards。它表示資料包在 IP 接收路徑中被丟棄,原因在於核心內部原因(例如記憶體不足)。
IpOutDiscards
定義於 RFC1213 ipOutDiscards。它表示資料包在 IP 傳送路徑中被丟棄,原因在於核心內部原因。
IpOutNoRoutes
定義於 RFC1213 ipOutNoRoutes。它表示資料包在 IP 傳送路徑中被丟棄,且未找到其路由。
ICMP 計數器¶
IcmpInMsgs 和 IcmpOutMsgs
定義於 RFC1213 icmpInMsgs 和 RFC1213 icmpOutMsgs
如 RFC1213 中所述,這兩個計數器包含錯誤,即使 ICMP 資料包型別無效,它們也會增加。ICMP 輸出路徑會檢查原始套接字的報頭,因此即使 IP 報頭由使用者空間程式構建,IcmpOutMsgs 仍會更新。
ICMP 命名型別
每種 ICMP 型別都有兩個計數器:“In” 和 “Out”。例如,對於 ICMP Echo 資料包,它們是 IcmpInEchos 和 IcmpOutEchos。它們的含義很直接。“In” 計數器表示核心接收到此類資料包,“Out” 計數器表示核心傳送此類資料包。
ICMP 數字型別
它們是 IcmpMsgInType[N] 和 IcmpMsgOutType[N],其中 [N] 表示 ICMP 型別編號。這些計數器跟蹤所有型別的 ICMP 資料包。ICMP 型別編號定義可以在 ICMP 引數 文件中找到。
例如,如果 Linux 核心傳送一個 ICMP Echo 資料包,IcmpMsgOutType8 會增加 1。如果核心收到一個 ICMP Echo Reply 資料包,IcmpMsgInType0 會增加 1。
IcmpInCsumErrors
此計數器表示 ICMP 資料包的校驗和錯誤。核心在更新 IcmpInMsgs 之後、更新 IcmpMsgInType[N] 之前驗證校驗和。如果資料包的校驗和錯誤,IcmpInMsgs 會更新,但 IcmpMsgInType[N] 不會更新。
IcmpInErrors 和 IcmpOutErrors
定義於 RFC1213 icmpInErrors 和 RFC1213 icmpOutErrors
當 ICMP 資料包處理路徑中發生錯誤時,這兩個計數器將更新。接收資料包路徑使用 IcmpInErrors,傳送資料包路徑使用 IcmpOutErrors。當 IcmpInCsumErrors 增加時,IcmpInErrors 也總是增加。
ICMP 計數器的關係¶
IcmpMsgOutType[N] 的總和總是等於 IcmpOutMsgs,因為它們同時更新。IcmpMsgInType[N] 與 IcmpInErrors 的總和應等於或大於 IcmpInMsgs。當核心接收到一個 ICMP 資料包時,核心遵循以下邏輯:
增加 IcmpInMsgs
如果存在任何錯誤,更新 IcmpInErrors 並結束程序
更新 IcmpMsgOutType[N]
根據型別處理資料包,如果存在任何錯誤,更新 IcmpInErrors 並結束程序
因此,如果所有錯誤都發生在步驟 (2) 中,則 IcmpInMsgs 應等於 IcmpMsgOutType[N] 和 IcmpInErrors 的總和。如果所有錯誤都發生在步驟 (4) 中,則 IcmpInMsgs 應等於 IcmpMsgOutType[N] 的總和。如果錯誤同時發生在步驟 (2) 和步驟 (4) 中,則 IcmpInMsgs 應小於 IcmpMsgOutType[N] 和 IcmpInErrors 的總和。
通用 TCP 計數器¶
TcpInSegs
TCP 層接收到的資料包數量。如 RFC1213 中所述,它包括錯誤接收到的資料包,例如校驗和錯誤、無效 TCP 報頭等。只有一個錯誤不會被包含:如果第2層目標地址不是 NIC 的第2層地址。這可能發生在資料包是多播或廣播資料包,或 NIC 處於混雜模式時。在這些情況下,資料包將傳遞到 TCP 層,但 TCP 層會在增加 TcpInSegs 之前丟棄這些資料包。TcpInSegs 計數器不感知 GRO。因此,如果兩個資料包被 GRO 合併,TcpInSegs 計數器只會增加 1。
TcpOutSegs
TCP 層傳送的資料包數量。如 RFC1213 中所述,它不包括重傳的資料包。但它包括 SYN、ACK 和 RST 資料包。與 TcpInSegs 不同,TcpOutSegs 感知 GSO,因此如果一個數據包透過 GSO 分割成 2 個,TcpOutSegs 將增加 2。
TcpActiveOpens
它表示 TCP 層傳送一個 SYN,並進入 SYN-SENT 狀態。每次 TcpActiveOpens 增加 1,TcpOutSegs 應始終增加 1。
TcpPassiveOpens
它表示 TCP 層接收一個 SYN,回覆一個 SYN+ACK,進入 SYN-RCVD 狀態。
TcpExtTCPRcvCoalesce
當 TCP 層接收到資料包但應用程式未讀取時,TCP 層會嘗試合併它們。此計數器表示在這種情況下合併了多少個數據包。如果啟用了 GRO,大量資料包將由 GRO 合併,這些資料包將不計入 TcpExtTCPRcvCoalesce。
TcpExtTCPAutoCorking
傳送資料包時,TCP 層會嘗試將小資料包合併為更大的資料包。在這種情況下,每合併一個數據包,此計數器增加 1。有關更多詳細資訊,請參閱 LWN 文章:https://lwn.net/Articles/576263/
TcpExtTCPOrigDataSent
此計數器由核心 commit f19c29e3e391 解釋,我將解釋貼上在下面
TCPOrigDataSent: number of outgoing packets with original data (excluding
retransmission but including data-in-SYN). This counter is different from
TcpOutSegs because TcpOutSegs also tracks pure ACKs. TCPOrigDataSent is
more useful to track the TCP retransmission rate.
TCPSynRetrans
此計數器由核心 commit f19c29e3e391 解釋,我將解釋貼上在下面
TCPSynRetrans: number of SYN and SYN/ACK retransmits to break down
retransmissions into SYN, fast-retransmits, timeout retransmits, etc.
TCPFastOpenActiveFail
此計數器由核心 commit f19c29e3e391 解釋,我將解釋貼上在下面
TCPFastOpenActiveFail: Fast Open attempts (SYN/data) failed because
the remote does not accept it or the attempts timed out.
TcpExtListenOverflows 和 TcpExtListenDrops
當核心從客戶端接收到 SYN,並且如果 TCP accept 佇列已滿時,核心將丟棄 SYN 並將 TcpExtListenOverflows 增加 1。同時,核心也會將 TcpExtListenDrops 增加 1。當 TCP 套接字處於 LISTEN 狀態,並且核心需要丟棄一個數據包時,核心總是將 TcpExtListenDrops 增加 1。因此,增加 TcpExtListenOverflows 會讓 TcpExtListenDrops 同時增加,但 TcpExtListenDrops 也可以在 TcpExtListenOverflows 不增加的情況下增加,例如記憶體分配失敗也會導致 TcpExtListenDrops 增加。
注意:以上解釋基於核心 4.10 或更高版本,在舊核心上,當 TCP accept 佇列已滿時,TCP 協議棧有不同的行為。在舊核心上,TCP 協議棧不會丟棄 SYN,它會完成三次握手。由於 accept 佇列已滿,TCP 協議棧會將套接字保留在 TCP 半開佇列中。由於它處於半開佇列中,TCP 協議棧將以指數退避計時器傳送 SYN+ACK,在客戶端回覆 ACK 後,TCP 協議棧檢查 accept 佇列是否仍然已滿,如果未滿,則將套接字移動到 accept 佇列,如果已滿,則將套接字保留在半開佇列中,下次客戶端回覆 ACK 時,此套接字將獲得另一次機會移動到 accept 佇列。
TCP Fast Open¶
TcpEstabResets
TcpAttemptFails
TcpOutRsts
定義於 RFC1213 tcpOutRsts。RFC 規定此計數器表示“傳送的包含 RST 標誌的段”,但在 Linux 核心中,此計數器表示核心嘗試傳送的段。傳送過程可能由於某些錯誤(例如記憶體分配失敗)而失敗。
TcpExtTCPSpuriousRtxHostQueues
當 TCP 協議棧想要重傳一個數據包,並發現該資料包並未在網路中丟失,但尚未傳送時,TCP 協議棧將放棄重傳並更新此計數器。這可能發生在資料包在 qdisc 或驅動程式佇列中停留時間過長的情況下。
TcpEstabResets
套接字在 Establish 或 CloseWait 狀態下接收到 RST 資料包。
TcpExtTCPKeepAlive
此計數器表示傳送了多少個保活資料包。保活預設不會啟用。使用者空間程式可以透過設定 SO_KEEPALIVE 套接字選項來啟用它。
TcpExtTCPSpuriousRTOs
由 F-RTO 演算法檢測到的偽重傳超時。
TCP 快速路徑¶
當核心接收到 TCP 資料包時,它有兩條路徑來處理資料包,一條是快速路徑,另一條是慢速路徑。核心程式碼中的註釋對此提供了很好的解釋,我將其貼上在下面
It is split into a fast path and a slow path. The fast path is
disabled when:
- A zero window was announced from us
- zero window probing
is only handled properly on the slow path.
- Out of order segments arrived.
- Urgent data is expected.
- There is no buffer space left
- Unexpected TCP flags/window values/header lengths are received
(detected by checking the TCP header against pred_flags)
- Data is sent in both directions. The fast path only supports pure senders
or pure receivers (this means either the sequence number or the ack
value must stay constant)
- Unexpected TCP option.
除非滿足上述任何條件,否則核心會嘗試使用快速路徑。如果資料包亂序,核心將以慢速路徑處理它們,這意味著效能可能不太好。如果使用“延遲 ACK”,核心也會進入慢速路徑,因為在使用“延遲 ACK”時,資料是雙向傳送的。當不使用 TCP 視窗縮放選項時,核心會在連線進入建立狀態時立即嘗試啟用快速路徑,但如果使用 TCP 視窗縮放選項,核心會首先停用快速路徑,並在接收到資料包後嘗試啟用它。
TcpExtTCPPureAcks 和 TcpExtTCPHPAcks
如果資料包設定了 ACK 標誌且沒有資料,則它是一個純 ACK 資料包;如果核心在快速路徑中處理它,TcpExtTCPHPAcks 將增加 1;如果核心在慢速路徑中處理它,TcpExtTCPPureAcks 將增加 1。
TcpExtTCPHPHits
如果 TCP 資料包包含資料(即它不是純 ACK 資料包),並且此資料包在快速路徑中處理,則 TcpExtTCPHPHits 將增加 1。
TCP 中止¶
TcpExtTCPAbortOnData
這意味著 TCP 層有待傳輸的資料,但需要關閉連線。因此 TCP 層會向對端傳送一個 RST,表示連線並未優雅關閉。增加此計數器的一個簡單方法是使用 SO_LINGER 選項。請參閱 socket 手冊頁 的 SO_LINGER 部分。
預設情況下,當應用程式關閉連線時,close 函式會立即返回,核心將嘗試非同步傳送未傳送的資料。如果您使用 SO_LINGER 選項,將 l_onoff 設定為 1,l_linger 設定為正數,則 close 函式不會立即返回,而是等待未傳送的資料被對端確認,最大等待時間為 l_linger 秒。如果將 l_onoff 設定為 1 並將 l_linger 設定為 0,當應用程式關閉連線時,核心將立即傳送一個 RST 並增加 TcpExtTCPAbortOnData 計數器。
TcpExtTCPAbortOnClose
此計數器表示當應用程式想要關閉 TCP 連線時,TCP 層中仍有未讀取的資料。在這種情況下,核心將向 TCP 連線的對端傳送一個 RST。
TcpExtTCPAbortOnMemory
當應用程式關閉 TCP 連線時,核心仍需要跟蹤該連線,讓它完成 TCP 斷開過程。例如,一個應用程式呼叫套接字的 close 方法,核心向連線的對端傳送 FIN,此後應用程式與該套接字不再有任何關係,但核心需要保留該套接字,此套接字變為孤兒套接字,核心等待對端的回覆,並最終進入 TIME_WAIT 狀態。當核心沒有足夠的記憶體來保留孤兒套接字時,核心將向對端傳送一個 RST,並刪除該套接字,在這種情況下,核心會將 TcpExtTCPAbortOnMemory 增加 1。兩種情況會觸發 TcpExtTCPAbortOnMemory:
1. TCP 協議使用的記憶體高於 tcp_mem 的第三個值。請參閱 TCP 手冊頁 中的 tcp_mem 部分。
孤兒套接字計數高於 net.ipv4.tcp_max_orphans
TcpExtTCPAbortOnTimeout
當任何 TCP 定時器過期時,此計數器將增加。在這種情況下,核心不會發送 RST,只會放棄連線。
TcpExtTCPAbortOnLinger
當 TCP 連線進入 FIN_WAIT_2 狀態時,核心可以傳送一個 RST 並立即刪除套接字,而不是等待對端的 FIN 資料包。這不是 Linux 核心 TCP 協議棧的預設行為。透過配置 TCP_LINGER2 套接字選項,您可以讓核心遵循此行為。
TcpExtTCPAbortFailed
如果滿足 RFC2525 2.17 節,核心 TCP 層將傳送 RST。如果在此過程中發生內部錯誤,TcpExtTCPAbortFailed 將增加。
TCP 混合慢啟動¶
混合慢啟動演算法是傳統 TCP 擁塞視窗慢啟動演算法的增強。它使用兩種資訊來檢測 TCP 路徑的最大頻寬是否已接近。這兩種資訊是 ACK 序列長度和資料包延遲增加。有關詳細資訊,請參閱 混合慢啟動論文。無論是 ACK 序列長度還是資料包延遲達到特定閾值,擁塞控制演算法都將進入擁塞避免狀態。直到 v4.20,有兩種擁塞控制演算法使用混合慢啟動,它們是 cubic(預設擁塞控制演算法)和 cdg。四個 SNMP 計數器與混合慢啟動演算法相關。
TcpExtTCPHystartTrainDetect
檢測到 ACK 序列長度閾值的次數
TcpExtTCPHystartTrainCwnd
由 ACK 序列長度檢測到的 CWND 總和。將此值除以 TcpExtTCPHystartTrainDetect 即為由 ACK 序列長度檢測到的平均 CWND。
TcpExtTCPHystartDelayDetect
檢測到資料包延遲閾值的次數。
TcpExtTCPHystartDelayCwnd
由資料包延遲檢測到的 CWND 總和。將此值除以 TcpExtTCPHystartDelayDetect 即為由資料包延遲檢測到的平均 CWND。
TCP 重傳和擁塞控制¶
TCP 協議有兩種重傳機制:SACK 和快速恢復。它們是互斥的。當啟用 SACK 時,核心 TCP 協議棧將使用 SACK,否則核心將使用快速恢復。SACK 是一個 TCP 選項,定義於 RFC2018 中;快速恢復定義於 RFC6582 中,也稱為“Reno”。
TCP 擁塞控制是一個龐大而複雜的話題。要理解相關的 SNMP 計數器,我們需要了解擁塞控制狀態機的狀態。有 5 種狀態:Open(開放)、Disorder(亂序)、CWR(擁塞視窗減小)、Recovery(恢復)和 Loss(丟失)。有關這些狀態的詳細資訊,請參閱本文件的第 5 頁和第 6 頁:https://pdfs.semanticscholar.org/0e9c/968d09ab2e53e24c4dca5b2d67c7f7140f8e.pdf
TcpExtTCPRenoRecovery 和 TcpExtTCPSackRecovery
當擁塞控制進入 Recovery 狀態時,如果使用 SACK,TcpExtTCPSackRecovery 增加 1;如果未使用 SACK,TcpExtTCPRenoRecovery 增加 1。這兩個計數器表示 TCP 協議棧開始重傳丟失的資料包。
TcpExtTCPSACKReneging
一個數據包已被 SACK 確認,但接收方已丟棄該資料包,因此傳送方需要重傳該資料包。在這種情況下,傳送方將 TcpExtTCPSACKReneging 增加 1。接收方可以丟棄已由 SACK 確認的資料包,儘管這不常見,但 TCP 協議允許這樣做。傳送方並不知道接收方發生了什麼。傳送方只是等待該資料包的 RTO 超時,然後傳送方假設該資料包已被接收方丟棄。
TcpExtTCPRenoReorder
重排序資料包由快速恢復檢測。只有當 SACK 被停用時才會使用它。快速恢復演算法透過重複 ACK 號來檢測重排序。例如,如果觸發重傳,並且原始重傳資料包沒有丟失,只是亂序了,接收方會多次確認,一次用於重傳資料包,另一次用於原始亂序資料包的到達。因此,傳送方會發現比預期更多的 ACK,並且傳送方知道發生了亂序。
TcpExtTCPTSReorder
當一個“空洞”被填補時,會檢測到重排序資料包。例如,假設傳送方傳送資料包 1,2,3,4,5,接收順序是 1,2,4,5,3。當傳送方收到資料包 3 的 ACK(這將填補空洞)時,兩個條件將使 TcpExtTCPTSReorder 增加 1:(1) 如果資料包 3 尚未被再次重傳。(2) 如果資料包 3 已被重傳,但資料包 3 的 ACK 的時間戳早於重傳時間戳。
TcpExtTCPSACKReorder
由 SACK 檢測到的重排序資料包。SACK 有兩種檢測重排序的方法:(1) 傳送方收到 DSACK。這意味著傳送方多次傳送了相同的資料包。唯一的原因是傳送方認為亂序資料包丟失了,所以再次傳送該資料包。(2) 假設傳送方傳送了資料包 1,2,3,4,5,並且傳送方已收到資料包 2 和 5 的 SACK,現在傳送方收到資料包 4 的 SACK 並且傳送方尚未重傳該資料包,傳送方就會知道資料包 4 是亂序的。核心的 TCP 協議棧會在上述兩種情況下增加 TcpExtTCPSACKReorder。
TcpExtTCPSlowStartRetrans
TCP 協議棧想要重傳一個數據包且擁塞控制狀態為“Loss”(丟失)。
TcpExtTCPFastRetrans
TCP 協議棧想要重傳一個數據包且擁塞控制狀態不是“Loss”(丟失)。
TcpExtTCPLostRetransmit
SACK 指出重傳資料包再次丟失。
TcpExtTCPRetransFail
TCP 協議棧嘗試將重傳資料包傳遞給下層,但下層返回錯誤。
TcpExtTCPSynRetrans
TCP 協議棧重傳一個 SYN 資料包。
DSACK¶
DSACK 定義於 RFC2883。接收方使用 DSACK 向傳送方報告重複資料包。有兩種重複:(1) 已確認的資料包重複。(2) 亂序資料包重複。TCP 協議棧在接收方和傳送方都計算這兩種重複。
TcpExtTCPDSACKOldSent
TCP 協議棧收到一個已被確認的重複資料包,因此它向傳送方傳送一個 DSACK。
TcpExtTCPDSACKOfoSent
TCP 協議棧收到一個亂序重複資料包,因此它向傳送方傳送一個 DSACK。
TcpExtTCPDSACKRecv
TCP 協議棧收到一個 DSACK,表示接收到一個已確認的重複資料包。
TcpExtTCPDSACKOfoRecv
TCP 協議棧收到一個 DSACK,表示接收到一個亂序重複資料包。
無效的 SACK 和 DSACK¶
當 SACK(或 DSACK)塊無效時,相應的計數器將更新。驗證方法基於 SACK 塊的起始/結束序列號。有關更多詳細資訊,請參閱核心原始碼中 tcp_is_sackblock_valid 函式的註釋。一個 SACK 選項最多可以有 4 個塊,它們是單獨檢查的。例如,如果 SACK 的 3 個塊無效,則相應的計數器將更新 3 次。commit 18f02545a9a1(“[TCP] MIB: Add counters for discarded SACK blocks”)的註釋有額外的解釋
TcpExtTCPSACKDiscard
此計數器表示有多少個 SACK 塊無效。如果無效 SACK 塊是由 ACK 記錄引起的,TCP 協議棧將只忽略它,並且不會更新此計數器。
TcpExtTCPDSACKIgnoredOld 和 TcpExtTCPDSACKIgnoredNoUndo
當 DSACK 塊無效時,這兩個計數器中的一個將被更新。哪個計數器將被更新取決於 TCP 套接字的 undo_marker 標誌。如果 undo_marker 未設定,TCP 協議棧不太可能重傳任何資料包,並且我們仍然收到一個無效的 DSACK 塊,原因可能是資料包在網路中間重複。在這種情況下,TcpExtTCPDSACKIgnoredNoUndo 將更新。如果 undo_marker 已設定,TcpExtTCPDSACKIgnoredOld 將更新。正如其名稱所暗示的,它可能是一箇舊資料包。
SACK 移位¶
Linux 網路協議棧將資料儲存在 sk_buff 結構(簡稱 skb)中。如果一個 SACK 塊跨越多個 skb,TCP 協議棧會嘗試重新安排這些 skb 中的資料。例如,如果一個 SACK 塊確認了序列號 10 到 15,skb1 包含序列號 10 到 13,skb2 包含序列號 14 到 20。那麼 skb2 中的序列號 14 和 15 將被移動到 skb1。這個操作是“移位”(shift)。如果一個 SACK 塊確認了序列號 10 到 20,skb1 包含序列號 10 到 13,skb2 包含序列號 14 到 20。skb2 中的所有資料都將被移動到 skb1,並且 skb2 將被丟棄,這個操作是“合併”(merge)。
TcpExtTCPSackShifted
一個 skb 被移位
TcpExtTCPSackMerged
一個 skb 被合併
TcpExtTCPSackShiftFallback
一個 skb 應該被移位或合併,但 TCP 協議棧由於某些原因沒有執行此操作。
TCP 亂序¶
TcpExtTCPOFOQueue
TCP 層收到一個亂序資料包並有足夠的記憶體將其排隊。
TcpExtTCPOFODrop
TCP 層收到一個亂序資料包但記憶體不足,因此將其丟棄。此類資料包不會計入 TcpExtTCPOFOQueue。
TcpExtTCPOFOMerge
收到的亂序資料包與前一個數據包有重疊。重疊部分將被丟棄。所有 TcpExtTCPOFOMerge 資料包也將計入 TcpExtTCPOFOQueue。
TCP PAWS¶
PAWS(Protection Against Wrapped Sequence numbers,防止序列號迴繞)是一種用於丟棄舊資料包的演算法。它依賴於 TCP 時間戳。有關詳細資訊,請參閱 時間戳維基百科 和 PAWS 的 RFC。
TcpExtPAWSActive
在 Syn-Sent 狀態下,資料包被 PAWS 丟棄。
TcpExtPAWSEstab
在 Syn-Sent 以外的任何狀態下,資料包被 PAWS 丟棄。
TCP ACK 跳過¶
在某些情況下,核心會避免過於頻繁地傳送重複 ACK。請在 sysctl 文件 的 tcp_invalid_ratelimit 部分找到更多詳細資訊。當核心由於 tcp_invalid_ratelimit 而決定跳過一個 ACK 時,核心會更新以下計數器之一,以指示 ACK 在哪種情況下被跳過。只有當收到的資料包是 SYN 資料包或不包含資料時,ACK 才會被跳過。
TcpExtTCPACKSkippedSynRecv
在 Syn-Recv 狀態下跳過 ACK。Syn-Recv 狀態意味著 TCP 協議棧收到一個 SYN 並回復 SYN+ACK。現在 TCP 協議棧正在等待一個 ACK。通常,TCP 協議棧在 Syn-Recv 狀態下不需要傳送 ACK。但在幾種情況下,TCP 協議棧需要傳送 ACK。例如,TCP 協議棧重複接收相同的 SYN 資料包,接收到的資料包未透過 PAWS 檢查,或者接收到的資料包序列號超出視窗。在這些情況下,TCP 協議棧需要傳送 ACK。如果 ACK 傳送頻率高於 tcp_invalid_ratelimit 允許的頻率,TCP 協議棧將跳過傳送 ACK 並增加 TcpExtTCPACKSkippedSynRecv。
TcpExtTCPACKSkippedPAWS
由於 PAWS(防止序列號迴繞)檢查失敗而跳過 ACK。如果在 Syn-Recv、Fin-Wait-2 或 Time-Wait 狀態下 PAWS 檢查失敗,則跳過的 ACK 將計入 TcpExtTCPACKSkippedSynRecv、TcpExtTCPACKSkippedFinWait2 或 TcpExtTCPACKSkippedTimeWait。在所有其他狀態下,跳過的 ACK 將計入 TcpExtTCPACKSkippedPAWS。
TcpExtTCPACKSkippedSeq
序列號超出視窗,時間戳透過 PAWS 檢查,且 TCP 狀態不是 Syn-Recv、Fin-Wait-2 和 Time-Wait。
TcpExtTCPACKSkippedFinWait2
在 Fin-Wait-2 狀態下跳過 ACK,原因可能是 PAWS 檢查失敗或接收到的序列號超出視窗。
TcpExtTCPACKSkippedTimeWait
在 Time-Wait 狀態下跳過 ACK,原因可能是 PAWS 檢查失敗或接收到的序列號超出視窗。
TcpExtTCPACKSkippedChallenge
如果 ACK 是一個挑戰 ACK,則跳過該 ACK。RFC 5961 定義了 3 種挑戰 ACK,請參閱 RFC 5961 3.2 節、RFC 5961 4.2 節 和 RFC 5961 5.2 節。除了這三種情況,在某些 TCP 狀態下,如果 ACK 號在第一個未確認號之前(比 RFC 5961 5.2 節 更嚴格),Linux TCP 協議棧也會發送挑戰 ACK。
TCP 接收視窗¶
TcpExtTCPWantZeroWindowAdv
根據當前記憶體使用情況,TCP 協議棧嘗試將接收視窗設定為零。但接收視窗可能仍然是非零值。例如,如果前一個視窗大小為 10,並且 TCP 協議棧接收了 3 個位元組,即使根據記憶體使用情況計算的視窗大小為零,當前視窗大小也將為 7。
TcpExtTCPToZeroWindowAdv
TCP 接收視窗從非零值設定為零。
TcpExtTCPFromZeroWindowAdv
TCP 接收視窗從零值設定為非零值。
延遲 ACK¶
TCP 延遲 ACK 是一種用於減少網路中資料包數量的技術。有關更多詳細資訊,請參閱 延遲 ACK 維基百科。
TcpExtDelayedACKs
延遲 ACK 定時器到期。TCP 協議棧將傳送一個純 ACK 資料包並退出延遲 ACK 模式。
TcpExtDelayedACKLocked
延遲 ACK 定時器到期,但由於套接字被使用者空間程式鎖定,TCP 協議棧無法立即傳送 ACK。TCP 協議棧稍後將傳送一個純 ACK(在使用者空間程式解鎖套接字之後)。當 TCP 協議棧稍後傳送純 ACK 時,TCP 協議棧也將更新 TcpExtDelayedACKs 並退出延遲 ACK 模式。
TcpExtDelayedACKLost
當 TCP 協議棧收到一個已確認的資料包時,它將被更新。延遲 ACK 丟失可能會導致此問題,但它也可能由其他原因觸發,例如網路中資料包重複。
尾部丟失探測 (TLP)¶
TLP 是一種用於檢測 TCP 資料包丟失的演算法。有關更多詳細資訊,請參閱 TLP 論文。
TcpExtTCPLossProbes
傳送了一個 TLP 探測資料包。
TcpExtTCPLossProbeRecovery
資料包丟失被 TLP 檢測到並恢復。
TCP Fast Open 描述¶
TCP Fast Open 是一種允許在三次握手完成之前傳輸資料的技術。有關一般描述,請參閱 TCP Fast Open 維基百科。
TcpExtTCPFastOpenActive
當 TCP 協議棧在 SYN-SENT 狀態下收到一個 ACK 資料包,並且該 ACK 資料包確認了 SYN 資料包中的資料時,TCP 協議棧明白 TFO cookie 已被對端接受,然後更新此計數器。
TcpExtTCPFastOpenActiveFail
此計數器表示 TCP 協議棧發起了 TCP Fast Open,但失敗了。此計數器將在三種情況下更新:(1) 對端未確認 SYN 資料包中的資料。(2) 帶有 TFO cookie 的 SYN 資料包至少超時一次。(3) 三次握手後,重傳超時發生了 net.ipv4.tcp_retries1 次,因為某些中間裝置可能在握手後阻止快速開啟。
TcpExtTCPFastOpenPassive
此計數器表示 TCP 協議棧接受快速開啟請求的次數。
TcpExtTCPFastOpenPassiveFail
此計數器表示 TCP 協議棧拒絕快速開啟請求的次數。這可能是由於 TFO cookie 無效或 TCP 協議棧在套接字建立過程中發現錯誤導致的。
TcpExtTCPFastOpenListenOverflow
當待處理的快速開啟請求數量大於 fastopenq->max_qlen 時,TCP 協議棧將拒絕快速開啟請求並更新此計數器。當此計數器更新時,TCP 協議棧不會更新 TcpExtTCPFastOpenPassive 或 TcpExtTCPFastOpenPassiveFail。fastopenq->max_qlen 由 TCP_FASTOPEN 套接字操作設定,且不能大於 net.core.somaxconn。例如
setsockopt(sfd, SOL_TCP, TCP_FASTOPEN, &qlen, sizeof(qlen));
TcpExtTCPFastOpenCookieReqd
此計數器表示客戶端想要請求 TFO cookie 的次數。
挑戰 ACK¶
有關挑戰 ACK 的詳細資訊,請參閱 TcpExtTCPACKSkippedChallenge 的解釋。
TcpExtTCPChallengeACK
傳送的挑戰 ACK 數量。
TcpExtTCPSYNChallenge
響應 SYN 資料包傳送的挑戰 ACK 數量。更新此計數器後,TCP 協議棧可能會發送一個挑戰 ACK 並更新 TcpExtTCPChallengeACK 計數器,或者它也可能跳過傳送挑戰並更新 TcpExtTCPACKSkippedChallenge。
修剪¶
當套接字面臨記憶體壓力時,TCP 協議棧會嘗試從接收佇列和亂序佇列中回收記憶體。其中一種回收方法是“合併”(collapse),這意味著分配一個大的 skb,將連續的 skb 複製到單個大的 skb 中,並釋放這些連續的 skb。
TcpExtPruneCalled
TCP 協議棧嘗試為套接字回收記憶體。更新此計數器後,TCP 協議棧將嘗試合併亂序佇列和接收佇列。如果記憶體仍然不足,TCP 協議棧將嘗試從亂序佇列中丟棄資料包(並更新 TcpExtOfoPruned 計數器)。
TcpExtOfoPruned
TCP 協議棧嘗試丟棄亂序佇列中的資料包。
TcpExtRcvPruned
在“合併”並從亂序佇列中丟棄資料包後,如果實際使用的記憶體仍大於最大允許記憶體,此計數器將更新。這意味著“修剪”失敗。
TcpExtTCPRcvCollapsed
此計數器表示在“合併”過程中釋放了多少個 skb。
示例¶
ping 測試¶
對公共 DNS 伺服器 8.8.8.8 執行 ping 命令
nstatuser@nstat-a:~$ ping 8.8.8.8 -c 1
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=119 time=17.8 ms
--- 8.8.8.8 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 17.875/17.875/17.875/0.000 ms
nstat 結果
nstatuser@nstat-a:~$ nstat
#kernel
IpInReceives 1 0.0
IpInDelivers 1 0.0
IpOutRequests 1 0.0
IcmpInMsgs 1 0.0
IcmpInEchoReps 1 0.0
IcmpOutMsgs 1 0.0
IcmpOutEchos 1 0.0
IcmpMsgInType0 1 0.0
IcmpMsgOutType8 1 0.0
IpExtInOctets 84 0.0
IpExtOutOctets 84 0.0
IpExtInNoECTPkts 1 0.0
Linux 伺服器傳送了一個 ICMP Echo 資料包,因此 IpOutRequests、IcmpOutMsgs、IcmpOutEchos 和 IcmpMsgOutType8 增加了 1。伺服器從 8.8.8.8 接收到 ICMP Echo Reply,因此 IpInReceives、IcmpInMsgs、IcmpInEchoReps 和 IcmpMsgInType0 增加了 1。ICMP Echo Reply 透過 IP 層傳遞到 ICMP 層,因此 IpInDelivers 增加了 1。預設 ping 資料大小為 48,因此一個 ICMP Echo 資料包及其對應的 Echo Reply 資料包由以下部分構成:
14 位元組 MAC 報頭
20 位元組 IP 報頭
16 位元組 ICMP 報頭
48 位元組資料(ping 命令的預設值)
因此 IpExtInOctets 和 IpExtOutOctets 均為 20+16+48=84。
TCP 三次握手¶
在伺服器端,我們執行
nstatuser@nstat-b:~$ nc -lknv 0.0.0.0 9000
Listening on [0.0.0.0] (family 0, port 9000)
在客戶端,我們執行
nstatuser@nstat-a:~$ nc -nv 192.168.122.251 9000
Connection to 192.168.122.251 9000 port [tcp/*] succeeded!
伺服器監聽 TCP 9000 埠,客戶端連線到它,它們完成了三次握手。
在伺服器端,我們可以找到以下 nstat 輸出
nstatuser@nstat-b:~$ nstat | grep -i tcp
TcpPassiveOpens 1 0.0
TcpInSegs 2 0.0
TcpOutSegs 1 0.0
TcpExtTCPPureAcks 1 0.0
在客戶端,我們可以找到以下 nstat 輸出
nstatuser@nstat-a:~$ nstat | grep -i tcp
TcpActiveOpens 1 0.0
TcpInSegs 1 0.0
TcpOutSegs 2 0.0
當伺服器收到第一個 SYN 時,它回覆了一個 SYN+ACK,並進入 SYN-RCVD 狀態,因此 TcpPassiveOpens 增加了 1。伺服器收到 SYN,傳送 SYN+ACK,收到 ACK,因此伺服器傳送了 1 個數據包,接收了 2 個數據包,TcpInSegs 增加了 2,TcpOutSegs 增加了 1。三次握手中的最後一個 ACK 是一個不帶資料的純 ACK,因此 TcpExtTCPPureAcks 增加了 1。
當客戶端傳送 SYN 時,客戶端進入 SYN-SENT 狀態,因此 TcpActiveOpens 增加了 1。客戶端傳送 SYN,收到 SYN+ACK,傳送 ACK,因此客戶端傳送了 2 個數據包,接收了 1 個數據包,TcpInSegs 增加了 1,TcpOutSegs 增加了 2。
TCP 正常流量¶
在伺服器上執行 nc
nstatuser@nstat-b:~$ nc -lkv 0.0.0.0 9000
Listening on [0.0.0.0] (family 0, port 9000)
在客戶端執行 nc
nstatuser@nstat-a:~$ nc -v nstat-b 9000
Connection to nstat-b 9000 port [tcp/*] succeeded!
在 nc 客戶端輸入一個字串(在我們的示例中為“hello”)
nstatuser@nstat-a:~$ nc -v nstat-b 9000
Connection to nstat-b 9000 port [tcp/*] succeeded!
hello
客戶端 nstat 輸出
nstatuser@nstat-a:~$ nstat
#kernel
IpInReceives 1 0.0
IpInDelivers 1 0.0
IpOutRequests 1 0.0
TcpInSegs 1 0.0
TcpOutSegs 1 0.0
TcpExtTCPPureAcks 1 0.0
TcpExtTCPOrigDataSent 1 0.0
IpExtInOctets 52 0.0
IpExtOutOctets 58 0.0
IpExtInNoECTPkts 1 0.0
伺服器端 nstat 輸出
nstatuser@nstat-b:~$ nstat
#kernel
IpInReceives 1 0.0
IpInDelivers 1 0.0
IpOutRequests 1 0.0
TcpInSegs 1 0.0
TcpOutSegs 1 0.0
IpExtInOctets 58 0.0
IpExtOutOctets 52 0.0
IpExtInNoECTPkts 1 0.0
再次在 nc 客戶端輸入一個字串(在我們的示例中為“world”)
nstatuser@nstat-a:~$ nc -v nstat-b 9000
Connection to nstat-b 9000 port [tcp/*] succeeded!
hello
world
客戶端 nstat 輸出
nstatuser@nstat-a:~$ nstat
#kernel
IpInReceives 1 0.0
IpInDelivers 1 0.0
IpOutRequests 1 0.0
TcpInSegs 1 0.0
TcpOutSegs 1 0.0
TcpExtTCPHPAcks 1 0.0
TcpExtTCPOrigDataSent 1 0.0
IpExtInOctets 52 0.0
IpExtOutOctets 58 0.0
IpExtInNoECTPkts 1 0.0
伺服器端 nstat 輸出
nstatuser@nstat-b:~$ nstat
#kernel
IpInReceives 1 0.0
IpInDelivers 1 0.0
IpOutRequests 1 0.0
TcpInSegs 1 0.0
TcpOutSegs 1 0.0
TcpExtTCPHPHits 1 0.0
IpExtInOctets 58 0.0
IpExtOutOctets 52 0.0
IpExtInNoECTPkts 1 0.0
比較第一個客戶端 nstat 輸出和第二個客戶端 nstat 輸出,我們可以發現一個區別:第一個有“TcpExtTCPPureAcks”,但第二個有“TcpExtTCPHPAcks”。第一個伺服器端 nstat 輸出和第二個伺服器端 nstat 輸出也有一個區別:第二個伺服器端 nstat 有 TcpExtTCPHPHits,但第一個伺服器端 nstat 沒有。網路流量模式完全相同:客戶端向伺服器傳送一個數據包,伺服器回覆一個 ACK。但核心以不同的方式處理它們。當不使用 TCP 視窗縮放選項時,核心會在連線進入建立狀態時立即嘗試啟用快速路徑,但如果使用 TCP 視窗縮放選項,核心會首先停用快速路徑,並在接收到資料包後嘗試啟用它。我們可以使用“ss”命令來驗證是否使用了視窗縮放選項。例如,在伺服器或客戶端上執行以下命令:
nstatuser@nstat-a:~$ ss -o state established -i '( dport = :9000 or sport = :9000 )
Netid Recv-Q Send-Q Local Address:Port Peer Address:Port
tcp 0 0 192.168.122.250:40654 192.168.122.251:9000
ts sack cubic wscale:7,7 rto:204 rtt:0.98/0.49 mss:1448 pmtu:1500 rcvmss:536 advmss:1448 cwnd:10 bytes_acked:1 segs_out:2 segs_in:1 send 118.2Mbps lastsnd:46572 lastrcv:46572 lastack:46572 pacing_rate 236.4Mbps rcv_space:29200 rcv_ssthresh:29200 minrtt:0.98
“wscale:7,7”表示伺服器和客戶端都將視窗縮放選項設定為 7。現在我們可以解釋測試中的 nstat 輸出
在客戶端的第一個 nstat 輸出中,客戶端傳送了一個數據包,伺服器回覆了一個 ACK,當核心處理這個 ACK 時,快速路徑沒有啟用,所以 ACK 被計入“TcpExtTCPPureAcks”。
在客戶端的第二個 nstat 輸出中,客戶端再次傳送了一個數據包,並從伺服器收到了另一個 ACK,此時快速路徑已啟用,並且該 ACK 符合快速路徑的條件,因此由快速路徑處理,該 ACK 被計入 TcpExtTCPHPAcks。
在伺服器端的第一個 nstat 輸出中,快速路徑未啟用,因此沒有“TcpExtTCPHPHits”。
在伺服器端的第二個 nstat 輸出中,快速路徑已啟用,並且從客戶端收到的資料包符合快速路徑的條件,因此被計入“TcpExtTCPHPHits”。
TcpExtTCPAbortOnClose¶
在伺服器端,我們執行以下 Python 指令碼
import socket
import time
port = 9000
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('0.0.0.0', port))
s.listen(1)
sock, addr = s.accept()
while True:
time.sleep(9999999)
這個 Python 指令碼監聽 9000 埠,但不從連線中讀取任何資料。
在客戶端,我們透過 nc 傳送字串“hello”
nstatuser@nstat-a:~$ echo "hello" | nc nstat-b 9000
然後,我們回到伺服器端,伺服器已經收到了“hello”資料包,並且 TCP 層已經確認了這個資料包,但應用程式尚未讀取它。我們輸入 Ctrl-C 終止伺服器指令碼。然後我們可以在伺服器端發現 TcpExtTCPAbortOnClose 增加了 1
nstatuser@nstat-b:~$ nstat | grep -i abort
TcpExtTCPAbortOnClose 1 0.0
如果在伺服器端執行 tcpdump,我們可以發現伺服器在我們輸入 Ctrl-C 後傳送了一個 RST。
TcpExtTCPAbortOnMemory 和 TcpExtTCPAbortOnTimeout¶
以下是一個讓孤兒套接字計數高於 net.ipv4.tcp_max_orphans 的示例。在客戶端將 tcp_max_orphans 更改為較小的值
sudo bash -c "echo 10 > /proc/sys/net/ipv4/tcp_max_orphans"
客戶端程式碼(建立 64 個連線到伺服器)
nstatuser@nstat-a:~$ cat client_orphan.py
import socket
import time
server = 'nstat-b' # server address
port = 9000
count = 64
connection_list = []
for i in range(64):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((server, port))
connection_list.append(s)
print("connection_count: %d" % len(connection_list))
while True:
time.sleep(99999)
伺服器程式碼(接受客戶端的 64 個連線)
nstatuser@nstat-b:~$ cat server_orphan.py
import socket
import time
port = 9000
count = 64
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('0.0.0.0', port))
s.listen(count)
connection_list = []
while True:
sock, addr = s.accept()
connection_list.append((sock, addr))
print("connection_count: %d" % len(connection_list))
在伺服器和客戶端執行 Python 指令碼。
在伺服器上
python3 server_orphan.py
在客戶端上
python3 client_orphan.py
在伺服器上執行 iptables
sudo iptables -A INPUT -i ens3 -p tcp --destination-port 9000 -j DROP
在客戶端輸入 Ctrl-C,停止 client_orphan.py。
在客戶端檢查 TcpExtTCPAbortOnMemory
nstatuser@nstat-a:~$ nstat | grep -i abort
TcpExtTCPAbortOnMemory 54 0.0
在客戶端檢查孤兒套接字計數
nstatuser@nstat-a:~$ ss -s
Total: 131 (kernel 0)
TCP: 14 (estab 1, closed 0, orphaned 10, synrecv 0, timewait 0/0), ports 0
Transport Total IP IPv6
* 0 - -
RAW 1 0 1
UDP 1 1 0
TCP 14 13 1
INET 16 14 2
FRAG 0 0 0
測試解釋:執行 server_orphan.py 和 client_orphan.py 後,我們在伺服器和客戶端之間建立了 64 個連線。執行 iptables 命令後,伺服器將丟棄來自客戶端的所有資料包,在 client_orphan.py 上鍵入 Ctrl-C,客戶端系統將嘗試關閉這些連線,在它們優雅關閉之前,這些連線變成了孤兒套接字。由於伺服器的 iptables 阻止了來自客戶端的資料包,伺服器將不會收到來自客戶端的 FIN 包,因此客戶端上的所有連線都將停留在 FIN_WAIT_1 階段,因此它們將一直作為孤兒套接字直到超時。我們將 10 回顯到 /proc/sys/net/ipv4/tcp_max_orphans,因此客戶端系統將只保留 10 個孤兒套接字,對於所有其他孤兒套接字,客戶端系統傳送了 RST 並將其刪除。我們有 64 個連線,因此“ss -s”命令顯示系統有 10 個孤兒套接字,TcpExtTCPAbortOnMemory 的值為 54。
關於孤兒套接字計數的補充說明:您可以透過“ss -s”命令找到精確的孤兒套接字計數,但當核心決定是否增加 TcpExtTCPAbortOnMemory 併發送 RST 時,核心並不總是檢查精確的孤兒套接字計數。為了提高效能,核心首先檢查一個近似計數,如果近似計數大於 tcp_max_orphans,核心會再次檢查精確計數。因此,如果近似計數小於 tcp_max_orphans,但精確計數大於 tcp_max_orphans,您會發現 TcpExtTCPAbortOnMemory 根本沒有增加。如果 tcp_max_orphans 足夠大,這種情況就不會發生,但如果您將 tcp_max_orphans 減少到像我們測試中的小值,您可能會發現這個問題。因此在我們的測試中,儘管 tcp_max_orphans 為 10,客戶端仍建立了 64 個連線。如果客戶端只建立了 11 個連線,我們就無法發現 TcpExtTCPAbortOnMemory 的變化。
繼續之前的測試,我們等待了幾分鐘。由於伺服器上的 iptables 阻止了流量,伺服器不會收到 FIN,客戶端所有孤兒套接字最終都會在 FIN_WAIT_1 狀態下超時。因此我們等待幾分鐘,可以在客戶端發現 10 個超時
nstatuser@nstat-a:~$ nstat | grep -i abort
TcpExtTCPAbortOnTimeout 10 0.0
TcpExtTCPAbortOnLinger¶
伺服器端程式碼
nstatuser@nstat-b:~$ cat server_linger.py
import socket
import time
port = 9000
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('0.0.0.0', port))
s.listen(1)
sock, addr = s.accept()
while True:
time.sleep(9999999)
客戶端程式碼
nstatuser@nstat-a:~$ cat client_linger.py
import socket
import struct
server = 'nstat-b' # server address
port = 9000
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack('ii', 1, 10))
s.setsockopt(socket.SOL_TCP, socket.TCP_LINGER2, struct.pack('i', -1))
s.connect((server, port))
s.close()
在伺服器上執行 server_linger.py
nstatuser@nstat-b:~$ python3 server_linger.py
在客戶端上執行 client_linger.py
nstatuser@nstat-a:~$ python3 client_linger.py
執行 client_linger.py 後,檢查 nstat 的輸出
nstatuser@nstat-a:~$ nstat | grep -i abort
TcpExtTCPAbortOnLinger 1 0.0
TcpExtTCPRcvCoalesce¶
在伺服器上,我們執行一個程式監聽 TCP 埠 9000,但不讀取任何資料
import socket
import time
port = 9000
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('0.0.0.0', port))
s.listen(1)
sock, addr = s.accept()
while True:
time.sleep(9999999)
將以上程式碼儲存為 server_coalesce.py,並執行
python3 server_coalesce.py
在客戶端,將以下程式碼儲存為 client_coalesce.py
import socket
server = 'nstat-b'
port = 9000
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((server, port))
執行
nstatuser@nstat-a:~$ python3 -i client_coalesce.py
我們使用“-i”進入互動模式,然後是一個數據包
>>> s.send(b'foo')
3
再次傳送一個數據包
>>> s.send(b'bar')
3
在伺服器上,執行 nstat
ubuntu@nstat-b:~$ nstat
#kernel
IpInReceives 2 0.0
IpInDelivers 2 0.0
IpOutRequests 2 0.0
TcpInSegs 2 0.0
TcpOutSegs 2 0.0
TcpExtTCPRcvCoalesce 1 0.0
IpExtInOctets 110 0.0
IpExtOutOctets 104 0.0
IpExtInNoECTPkts 2 0.0
客戶端傳送了兩個資料包,伺服器沒有讀取任何資料。當第二個資料包到達伺服器時,第一個資料包仍在接收佇列中。因此 TCP 層合併了這兩個資料包,我們可以發現 TcpExtTCPRcvCoalesce 增加了 1。
TcpExtListenOverflows 和 TcpExtListenDrops¶
在伺服器上,執行 nc 命令,監聽埠 9000
nstatuser@nstat-b:~$ nc -lkv 0.0.0.0 9000
Listening on [0.0.0.0] (family 0, port 9000)
在客戶端,在不同的終端中執行 3 個 nc 命令
nstatuser@nstat-a:~$ nc -v nstat-b 9000
Connection to nstat-b 9000 port [tcp/*] succeeded!
nc 命令只接受 1 個連線,並且接受佇列長度為 1。在當前的 Linux 實現中,將佇列長度設定為 n 意味著實際佇列長度為 n+1。現在我們建立 3 個連線,1 個被 nc 接受,2 個在接受佇列中,因此接受佇列已滿。
在執行第 4 個 nc 之前,我們清除伺服器上的 nstat 歷史記錄
nstatuser@nstat-b:~$ nstat -n
在客戶端執行第 4 個 nc
nstatuser@nstat-a:~$ nc -v nstat-b 9000
如果 nc 伺服器執行在核心 4.10 或更高版本上,您將不會看到“Connection to ... succeeded!”字串,因為如果接受佇列已滿,核心將丟棄 SYN。如果 nc 客戶端執行在舊核心上,您將看到連線成功,因為核心將完成三次握手並將套接字保留在半開佇列中。我在核心 4.15 上進行了測試。以下是伺服器上的 nstat 輸出
nstatuser@nstat-b:~$ nstat
#kernel
IpInReceives 4 0.0
IpInDelivers 4 0.0
TcpInSegs 4 0.0
TcpExtListenOverflows 4 0.0
TcpExtListenDrops 4 0.0
IpExtInOctets 240 0.0
IpExtInNoECTPkts 4 0.0
TcpExtListenOverflows 和 TcpExtListenDrops 都為 4。如果第 4 個 nc 和 nstat 之間的時間更長,TcpExtListenOverflows 和 TcpExtListenDrops 的值會更大,因為第 4 個 nc 的 SYN 被丟棄,客戶端正在重試。
IpInAddrErrors, IpExtInNoRoutes 和 IpOutNoRoutes¶
伺服器 A IP 地址:192.168.122.250 伺服器 B IP 地址:192.168.122.251 在伺服器 A 上準備,新增一條到伺服器 B 的路由
$ sudo ip route add 8.8.8.8/32 via 192.168.122.251
在伺服器 B 上準備,停用所有介面的 send_redirects
$ sudo sysctl -w net.ipv4.conf.all.send_redirects=0
$ sudo sysctl -w net.ipv4.conf.ens3.send_redirects=0
$ sudo sysctl -w net.ipv4.conf.lo.send_redirects=0
$ sudo sysctl -w net.ipv4.conf.default.send_redirects=0
我們希望讓伺服器 A 向 8.8.8.8 傳送資料包,並將資料包路由到伺服器 B。當伺服器 B 收到此類資料包時,它可能會向伺服器 A 傳送 ICMP Redirect 訊息,將 send_redirects 設定為 0 將停用此行為。
首先,生成 InAddrErrors。在伺服器 B 上,我們停用 IP 轉發
$ sudo sysctl -w net.ipv4.conf.all.forwarding=0
在伺服器 A 上,我們向 8.8.8.8 傳送資料包
$ nc -v 8.8.8.8 53
在伺服器 B 上,我們檢查 nstat 的輸出
$ nstat
#kernel
IpInReceives 3 0.0
IpInAddrErrors 3 0.0
IpExtInOctets 180 0.0
IpExtInNoECTPkts 3 0.0
由於我們已讓伺服器 A 將 8.8.8.8 路由到伺服器 B,並且我們在伺服器 B 上停用了 IP 轉發,伺服器 A 向伺服器 B 傳送資料包,然後伺服器 B 丟棄了資料包並增加了 IpInAddrErrors。由於 nc 命令在未收到 SYN+ACK 時會重發 SYN 資料包,我們可能會發現多個 IpInAddrErrors。
其次,生成 IpExtInNoRoutes。在伺服器 B 上,我們啟用 IP 轉發
$ sudo sysctl -w net.ipv4.conf.all.forwarding=1
檢查伺服器 B 的路由表並刪除預設路由
$ ip route show
default via 192.168.122.1 dev ens3 proto static
192.168.122.0/24 dev ens3 proto kernel scope link src 192.168.122.251
$ sudo ip route delete default via 192.168.122.1 dev ens3 proto static
在伺服器 A 上,我們再次聯絡 8.8.8.8
$ nc -v 8.8.8.8 53
nc: connect to 8.8.8.8 port 53 (tcp) failed: Network is unreachable
在伺服器 B 上,執行 nstat
$ nstat
#kernel
IpInReceives 1 0.0
IpOutRequests 1 0.0
IcmpOutMsgs 1 0.0
IcmpOutDestUnreachs 1 0.0
IcmpMsgOutType3 1 0.0
IpExtInNoRoutes 1 0.0
IpExtInOctets 60 0.0
IpExtOutOctets 88 0.0
IpExtInNoECTPkts 1 0.0
我們在伺服器 B 上啟用了 IP 轉發,當伺服器 B 收到目標 IP 地址為 8.8.8.8 的資料包時,伺服器 B 將嘗試轉發此資料包。我們已刪除預設路由,因此沒有 8.8.8.8 的路由,因此伺服器 B 增加了 IpExtInNoRoutes 並向伺服器 A 傳送了“ICMP Destination Unreachable”訊息。
第三,生成 IpOutNoRoutes。在伺服器 B 上執行 ping 命令
$ ping -c 1 8.8.8.8
connect: Network is unreachable
在伺服器 B 上執行 nstat
$ nstat
#kernel
IpOutNoRoutes 1 0.0
我們已刪除伺服器 B 上的預設路由。伺服器 B 無法找到 8.8.8.8 IP 地址的路由,因此伺服器 B 增加了 IpOutNoRoutes。
TcpExtTCPACKSkippedSynRecv¶
在此測試中,我們從客戶端向伺服器傳送 3 個相同的 SYN 資料包。第一個 SYN 將讓伺服器建立一個套接字,將其設定為 Syn-Recv 狀態,並回復一個 SYN/ACK。第二個 SYN 將讓伺服器再次回覆 SYN/ACK,並記錄回覆時間(重複 ACK 回覆時間)。第三個 SYN 將讓伺服器檢查之前的重複 ACK 回覆時間,並決定跳過重複 ACK,然後增加 TcpExtTCPACKSkippedSynRecv 計數器。
執行 tcpdump 捕獲一個 SYN 資料包
nstatuser@nstat-a:~$ sudo tcpdump -c 1 -w /tmp/syn.pcap port 9000
tcpdump: listening on ens3, link-type EN10MB (Ethernet), capture size 262144 bytes
開啟另一個終端,執行 nc 命令
nstatuser@nstat-a:~$ nc nstat-b 9000
由於 nstat-b 沒有監聽埠 9000,它應該回復一個 RST,nc 命令也立即退出。這足以讓 tcpdump 命令捕獲一個 SYN 資料包。Linux 伺服器可能使用硬體解除安裝進行 TCP 校驗和計算,因此 /tmp/syn.pcap 中的校驗和可能不正確。我們呼叫 tcprewrite 來修復它
nstatuser@nstat-a:~$ tcprewrite --infile=/tmp/syn.pcap --outfile=/tmp/syn_fixcsum.pcap --fixcsum
在 nstat-b 上,我們執行 nc 監聽埠 9000
nstatuser@nstat-b:~$ nc -lkv 9000
Listening on [0.0.0.0] (family 0, port 9000)
在 nstat-a 上,我們阻止了來自埠 9000 的資料包,否則 nstat-a 會向 nstat-b 傳送 RST
nstatuser@nstat-a:~$ sudo iptables -A INPUT -p tcp --sport 9000 -j DROP
向 nstat-b 重複傳送 3 個 SYN
nstatuser@nstat-a:~$ for i in {1..3}; do sudo tcpreplay -i ens3 /tmp/syn_fixcsum.pcap; done
在 nstat-b 上檢查 SNMP 計數器
nstatuser@nstat-b:~$ nstat | grep -i skip
TcpExtTCPACKSkippedSynRecv 1 0.0
正如我們所料,TcpExtTCPACKSkippedSynRecv 為 1。
TcpExtTCPACKSkippedPAWS¶
要觸發 PAWS,我們可以傳送一箇舊的 SYN。
在 nstat-b 上,讓 nc 監聽埠 9000
nstatuser@nstat-b:~$ nc -lkv 9000
Listening on [0.0.0.0] (family 0, port 9000)
在 nstat-a 上,執行 tcpdump 捕獲一個 SYN
nstatuser@nstat-a:~$ sudo tcpdump -w /tmp/paws_pre.pcap -c 1 port 9000
tcpdump: listening on ens3, link-type EN10MB (Ethernet), capture size 262144 bytes
在 nstat-a 上,執行 nc 作為客戶端連線 nstat-b
nstatuser@nstat-a:~$ nc -v nstat-b 9000
Connection to nstat-b 9000 port [tcp/*] succeeded!
現在 tcpdump 已經捕獲到 SYN 並退出。我們應該修復校驗和
nstatuser@nstat-a:~$ tcprewrite --infile /tmp/paws_pre.pcap --outfile /tmp/paws.pcap --fixcsum
傳送 SYN 資料包兩次
nstatuser@nstat-a:~$ for i in {1..2}; do sudo tcpreplay -i ens3 /tmp/paws.pcap; done
在 nstat-b 上,檢查 SNMP 計數器
nstatuser@nstat-b:~$ nstat | grep -i skip
TcpExtTCPACKSkippedPAWS 1 0.0
我們透過 tcpreplay 傳送了兩個 SYN,它們都會導致 PAWS 檢查失敗。nstat-b 對第一個 SYN 回覆了 ACK,跳過了第二個 SYN 的 ACK,並更新了 TcpExtTCPACKSkippedPAWS。
TcpExtTCPACKSkippedSeq¶
要觸發 TcpExtTCPACKSkippedSeq,我們傳送具有有效時間戳(透過 PAWS 檢查)但序列號超出視窗的資料包。Linux TCP 協議棧會避免在資料包有資料時跳過,所以我們需要一個純 ACK 資料包。為了生成這樣的資料包,我們可以建立兩個套接字:一個在埠 9000 上,另一個在埠 9001 上。然後我們在埠 9001 上捕獲一個 ACK,將源/目標埠號更改為與埠 9000 套接字匹配。然後我們可以透過這個資料包觸發 TcpExtTCPACKSkippedSeq。
在 nstat-b 上,開啟兩個終端,執行兩個 nc 命令分別監聽埠 9000 和 埠 9001
nstatuser@nstat-b:~$ nc -lkv 9000
Listening on [0.0.0.0] (family 0, port 9000)
nstatuser@nstat-b:~$ nc -lkv 9001
Listening on [0.0.0.0] (family 0, port 9001)
在 nstat-a 上,執行兩個 nc 客戶端
nstatuser@nstat-a:~$ nc -v nstat-b 9000
Connection to nstat-b 9000 port [tcp/*] succeeded!
nstatuser@nstat-a:~$ nc -v nstat-b 9001
Connection to nstat-b 9001 port [tcp/*] succeeded!
在 nstat-a 上,執行 tcpdump 捕獲一個 ACK
nstatuser@nstat-a:~$ sudo tcpdump -w /tmp/seq_pre.pcap -c 1 dst port 9001
tcpdump: listening on ens3, link-type EN10MB (Ethernet), capture size 262144 bytes
在 nstat-b 上,透過埠 9001 套接字傳送一個數據包。例如,我們在示例中傳送了字串“foo”
nstatuser@nstat-b:~$ nc -lkv 9001
Listening on [0.0.0.0] (family 0, port 9001)
Connection from nstat-a 42132 received!
foo
在 nstat-a 上,tcpdump 應該已經捕獲到 ACK。我們應該檢查兩個 nc 客戶端的源埠號
nstatuser@nstat-a:~$ ss -ta '( dport = :9000 || dport = :9001 )' | tee
State Recv-Q Send-Q Local Address:Port Peer Address:Port
ESTAB 0 0 192.168.122.250:50208 192.168.122.251:9000
ESTAB 0 0 192.168.122.250:42132 192.168.122.251:9001
執行 tcprewrite,將埠 9001 更改為埠 9000,將埠 42132 更改為埠 50208
nstatuser@nstat-a:~$ tcprewrite --infile /tmp/seq_pre.pcap --outfile /tmp/seq.pcap -r 9001:9000 -r 42132:50208 --fixcsum
現在 /tmp/seq.pcap 是我們需要的資料包。將其傳送到 nstat-b
nstatuser@nstat-a:~$ for i in {1..2}; do sudo tcpreplay -i ens3 /tmp/seq.pcap; done
在 nstat-b 上檢查 TcpExtTCPACKSkippedSeq
nstatuser@nstat-b:~$ nstat | grep -i skip
TcpExtTCPACKSkippedSeq 1 0.0