SNMP 计数器

本文档解释了 SNMP 计数器的含义。

通用 IPv4 计数器

所有第4层数据包和 ICMP 数据包都会改变这些计数器,但第2层数据包(如 STP)或 ARP 数据包不会改变这些计数器。

  • IpInReceives

定义于 RFC1213 ipInReceives

IP 层接收到的数据包数量。它在 ip_rcv 函数开始时增加,并始终与 IpExtInOctets 一起更新。即使数据包稍后被丢弃(例如,由于 IP 报头无效或校验和错误等),它也会增加。它表示 GRO/LRO 后聚合段的数量。

  • IpInDelivers

定义于 RFC1213 ipInDelivers

传递到上层协议的数据包数量。例如 TCP、UDP、ICMP 等。如果没有应用程序侦听原始套接字,则只传递内核支持的协议;如果有应用程序侦听原始套接字,则所有有效的 IP 数据包都会被传递。

  • IpOutRequests

定义于 RFC1213 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 icmpInMsgsRFC1213 icmpOutMsgs

如 RFC1213 中所述,这两个计数器包含错误,即使 ICMP 数据包类型无效,它们也会增加。ICMP 输出路径会检查原始套接字的报头,因此即使 IP 报头由用户空间程序构建,IcmpOutMsgs 仍会更新。

  • ICMP 命名类型

这些计数器包括大多数常见 ICMP 类型,它们是:
IcmpInDestUnreachs: RFC1213 icmpInDestUnreachs
IcmpInTimeExcds: RFC1213 icmpInTimeExcds
IcmpInParmProbs: RFC1213 icmpInParmProbs
IcmpInSrcQuenchs: RFC1213 icmpInSrcQuenchs
IcmpInRedirects: RFC1213 icmpInRedirects
IcmpInEchos: RFC1213 icmpInEchos
IcmpInEchoReps: RFC1213 icmpInEchoReps
IcmpInTimestamps: RFC1213 icmpInTimestamps
IcmpInTimestampReps: RFC1213 icmpInTimestampReps
IcmpInAddrMasks: RFC1213 icmpInAddrMasks
IcmpInAddrMaskReps: RFC1213 icmpInAddrMaskReps
IcmpOutDestUnreachs: RFC1213 icmpOutDestUnreachs
IcmpOutTimeExcds: RFC1213 icmpOutTimeExcds
IcmpOutParmProbs: RFC1213 icmpOutParmProbs
IcmpOutSrcQuenchs: RFC1213 icmpOutSrcQuenchs
IcmpOutRedirects: RFC1213 icmpOutRedirects
IcmpOutEchos: RFC1213 icmpOutEchos
IcmpOutEchoReps: RFC1213 icmpOutEchoReps
IcmpOutTimestamps: RFC1213 icmpOutTimestamps
IcmpOutTimestampReps: RFC1213 icmpOutTimestampReps
IcmpOutAddrMasks: RFC1213 icmpOutAddrMasks
IcmpOutAddrMaskReps: RFC1213 icmpOutAddrMaskReps

每种 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 icmpInErrorsRFC1213 icmpOutErrors

当 ICMP 数据包处理路径中发生错误时,这两个计数器将更新。接收数据包路径使用 IcmpInErrors,发送数据包路径使用 IcmpOutErrors。当 IcmpInCsumErrors 增加时,IcmpInErrors 也总是增加。

ICMP 计数器的关系

IcmpMsgOutType[N] 的总和总是等于 IcmpOutMsgs,因为它们同时更新。IcmpMsgInType[N] 与 IcmpInErrors 的总和应等于或大于 IcmpInMsgs。当内核接收到一个 ICMP 数据包时,内核遵循以下逻辑:

  1. 增加 IcmpInMsgs

  2. 如果存在任何错误,更新 IcmpInErrors 并结束进程

  3. 更新 IcmpMsgOutType[N]

  4. 根据类型处理数据包,如果存在任何错误,更新 IcmpInErrors 并结束进程

因此,如果所有错误都发生在步骤 (2) 中,则 IcmpInMsgs 应等于 IcmpMsgOutType[N] 和 IcmpInErrors 的总和。如果所有错误都发生在步骤 (4) 中,则 IcmpInMsgs 应等于 IcmpMsgOutType[N] 的总和。如果错误同时发生在步骤 (2) 和步骤 (4) 中,则 IcmpInMsgs 应小于 IcmpMsgOutType[N] 和 IcmpInErrors 的总和。

通用 TCP 计数器

  • TcpInSegs

定义于 RFC1213 tcpInSegs

TCP 层接收到的数据包数量。如 RFC1213 中所述,它包括错误接收到的数据包,例如校验和错误、无效 TCP 报头等。只有一个错误不会被包含:如果第2层目标地址不是 NIC 的第2层地址。这可能发生在数据包是多播或广播数据包,或 NIC 处于混杂模式时。在这些情况下,数据包将传递到 TCP 层,但 TCP 层会在增加 TcpInSegs 之前丢弃这些数据包。TcpInSegs 计数器不感知 GRO。因此,如果两个数据包被 GRO 合并,TcpInSegs 计数器只会增加 1。

  • TcpOutSegs

定义于 RFC1213 tcpOutSegs

TCP 层发送的数据包数量。如 RFC1213 中所述,它不包括重传的数据包。但它包括 SYN、ACK 和 RST 数据包。与 TcpInSegs 不同,TcpOutSegs 感知 GSO,因此如果一个数据包通过 GSO 分割成 2 个,TcpOutSegs 将增加 2。

  • TcpActiveOpens

定义于 RFC1213 tcpActiveOpens

它表示 TCP 层发送一个 SYN,并进入 SYN-SENT 状态。每次 TcpActiveOpens 增加 1,TcpOutSegs 应始终增加 1。

  • TcpPassiveOpens

定义于 RFC1213 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

定义于 RFC1213 tcpEstabResets

  • TcpAttemptFails

定义于 RFC1213 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 部分。

  1. 孤儿套接字计数高于 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 的次数。

SYN cookies

SYN cookies 用于缓解 SYN 洪水攻击,详情请参阅 SYN cookies 维基百科

  • TcpExtSyncookiesSent

它表示发送了多少个 SYN cookie。

  • TcpExtSyncookiesRecv

TCP 协议栈接收到多少个 SYN cookie 的回复数据包。

  • TcpExtSyncookiesFailed

从 SYN cookie 解码的 MSS 无效。当此计数器更新时,收到的数据包将不被视为 SYN cookie,并且 TcpExtSyncookiesRecv 计数器不会更新。

挑战 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