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 数据包的数量,有关更多详细信息,请参阅 显式拥塞通知。
这 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 回显数据包,它们是 IcmpInEchos 和 IcmpOutEchos。它们的含义很直接。“In”计数器表示内核接收到这样的数据包,“Out”计数器表示内核发送了这样的数据包。
ICMP 数字类型
它们是 IcmpMsgInType[N] 和 IcmpMsgOutType[N],[N] 表示 ICMP 类型编号。这些计数器跟踪所有类型的 ICMP 数据包。ICMP 类型编号定义可以在 ICMP 参数 文档中找到。
例如,如果 Linux 内核发送一个 ICMP 回显数据包,则 IcmpMsgOutType8 将增加 1。如果内核收到一个 ICMP 回显回复数据包,则 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 接受队列已满时,内核将丢弃 SYN 并将 TcpExtListenOverflows 加 1。同时,内核还会将 TcpExtListenDrops 加 1。当 TCP 套接字处于 LISTEN 状态,并且内核需要丢弃一个数据包时,内核总是会将 TcpExtListenDrops 加 1。因此,增加 TcpExtListenOverflows 会导致 TcpExtListenDrops 同时增加,但 TcpExtListenDrops 也可能在不增加 TcpExtListenOverflows 的情况下增加,例如,内存分配失败也会导致 TcpExtListenDrops 增加。
注意:以上解释基于内核 4.10 或更高版本,在旧内核上,当 TCP 接受队列已满时,TCP 堆栈的行为会有所不同。在旧内核上,TCP 堆栈不会丢弃 SYN,它将完成三次握手。由于接受队列已满,TCP 堆栈会将套接字保留在 TCP 半连接队列中。由于它在半连接队列中,TCP 堆栈会以指数退避定时器发送 SYN+ACK,在客户端回复 ACK 后,TCP 堆栈会检查接受队列是否仍然已满,如果未满,则将套接字移动到接受队列,如果已满,则将套接字保留在半连接队列中,下次客户端回复 ACK 时,此套接字将有另一次机会移动到接受队列。
TCP 快速打开¶
TcpEstabResets
TcpAttemptFails
TcpOutRsts
定义在 RFC1213 tcpOutRsts. RFC 说此计数器指示“发送的包含 RST 标志的段”,但在 Linux 内核中,此计数器指示内核尝试发送的段。由于某些错误(例如内存分配失败),发送过程可能会失败。
TcpExtTCPSpuriousRtxHostQueues
当 TCP 堆栈想要重传一个数据包,并且发现该数据包并未在网络中丢失,但该数据包尚未发送时,TCP 堆栈会放弃重传并更新此计数器。如果数据包在 qdisc 或驱动程序队列中停留的时间过长,可能会发生这种情况。
TcpEstabResets
套接字在 Establish 或 CloseWait 状态下收到 RST 数据包。
TcpExtTCPKeepAlive
此计数器指示发送了多少 keepalive 数据包。默认情况下不会启用 keepalive。用户空间程序可以通过设置 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 man page 的 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 man page 中的 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
当拥塞控制进入恢复状态时,如果使用 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 计数器加 1。
TcpExtTCPSlowStartRetrans
TCP 协议栈想要重传数据包,并且拥塞控制状态为“丢失”。
TcpExtTCPFastRetrans
TCP 协议栈想要重传数据包,并且拥塞控制状态不是“丢失”。
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:为丢弃的 SACK 块添加计数器”)的注释中有额外的解释。
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。此操作是“移位”。如果一个 SACK 块确认序列号 10 到 20,skb1 的序列号为 10 到 13,skb2 的序列号为 14 到 20。skb2 中的所有数据都将被移动到 skb1,并且 skb2 将被丢弃,此操作是“合并”。
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
ACK 在 Syn-Recv 状态下被跳过。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
ACK 由于 PAWS(防止序列号回绕)检查失败而被跳过。如果 PAWS 检查在 Syn-Recv、Fin-Wait-2 或 Time-Wait 状态下失败,则跳过的 ACK 将被计入 TcpExtTCPACKSkippedSynRecv、TcpExtTCPACKSkippedFinWait2 或 TcpExtTCPACKSkippedTimeWait。在所有其他状态下,跳过的 ACK 将被计入 TcpExtTCPACKSkippedPAWS。
TcpExtTCPACKSkippedSeq
序列号超出窗口,并且时间戳通过了 PAWS 检查,并且 TCP 状态不是 Syn-Recv、Fin-Wait-2 和 Time-Wait。
TcpExtTCPACKSkippedFinWait2
ACK 在 Fin-Wait-2 状态下被跳过,原因可能是 PAWS 检查失败或接收到的序列号超出窗口。
TcpExtTCPACKSkippedTimeWait
ACK 在 Time-Wait 状态下被跳过,原因可能是 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 时,它还将更新 TcpExtDelayedACKs 并退出延迟 ACK 模式。
TcpExtDelayedACKLost
当 TCP 协议栈收到已确认的数据包时,此计数器会更新。延迟 ACK 丢失可能会导致此问题,但也可能由其他原因触发,例如数据包在网络中重复。
尾部丢包探测 (TLP)¶
TLP 是一种用于检测 TCP 数据包丢失的算法。有关更多详细信息,请参考TLP 论文。
TcpExtTCPLossProbes
已发送 TLP 探测数据包。
TcpExtTCPLossProbeRecovery
TLP 检测到并恢复了数据包丢失。
TCP 快速打开描述¶
TCP 快速打开是一种允许在三次握手完成之前传输数据的技术。请参考TCP 快速打开维基以获取一般描述。
TcpExtTCPFastOpenActive
当 TCP 协议栈在 SYN-SENT 状态下收到 ACK 数据包,并且该 ACK 数据包确认了 SYN 数据包中的数据时,TCP 协议栈会理解为 TFO Cookie 被对方接受,然后更新此计数器。
TcpExtTCPFastOpenActiveFail
此计数器表示 TCP 协议栈发起的 TCP 快速打开失败。此计数器将在以下三种情况下更新:(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 协议栈将尝试从接收队列和乱序队列中回收内存。其中一种回收方法是“折叠”,这意味着分配一个大的 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
nstayt 的结果
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 减小到一个像我们测试中的小值,您可能会发现这个问题。因此,在我们的测试中,客户端建立了 64 个连接,尽管 tcp_max_orphans 为 10。如果客户端只建立了 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 客户端运行在旧内核上,您会看到连接成功,因为内核将完成 3 次握手并将套接字保留在半开队列中。我在内核 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 重定向消息,将 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 目标不可达”消息。
第三,生成 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
重复发送 3 个 SYN 到 nstat-b
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