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 数据包的数量,有关更多详细信息,请参阅 显式拥塞通知

这 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 回显数据包,它们是 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 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 接受队列已满时,内核将丢弃 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

定义在 RFC1213 tcpEstabResets.

  • TcpAttemptFails

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

  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

当拥塞控制进入恢复状态时,如果使用 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 的次数。

SYN Cookies

SYN Cookies 用于缓解 SYN 洪水攻击,有关详细信息,请参考SYN Cookies 维基

  • TcpExtSyncookiesSent

表示已发送的 SYN Cookies 的数量。

  • TcpExtSyncookiesRecv

TCP 协议栈收到的 SYN Cookies 的回复数据包的数量。

  • TcpExtSyncookiesFailed

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

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