这一章在做什么

网络一慢,人们先怪网络。这一章教你分清:慢在网络本身,还是慢在两头的主机。它给出几种延迟的量法(建连、首字节、往返、重传),讲清 TCP 的两个连接队列、收发缓冲区、重传和拥塞控制,网卡、中断和软中断怎样把包交给协议栈,再用 ss、nstat、ip、sar 这些不要 root 的工具逐层看计数,最后才动用 BPF 工具和抓包。写网络服务的人最该带走的:读懂 Recv-Q、Send-Q 和那几个丢包计数。

从你的服务到网线:每一层用什么看 应用:accept、read、write 套接字:收发缓冲区、监听队列 TCP:窗口、重传、拥塞控制 IP:路由、分片 驱动:环形缓冲、中断、软中断 网卡和链路:协商速率、错误 curl -w、tcplife(连接寿命) ss -lnt、ss -tiepm nstat、netstat -s、tcpretrans ip route、nstat 的 Ip 计数 mpstat 的 %soft、/proc/softirqs ip -s link、sar -n DEV、ethtool tcpdump 只看线上
本站画的示意。越往上越靠近你的代码,越往下越靠近硬件。排查时通常从上下两头的计数看起,抓包放在最后。

什么时候翻这一章

这一章用到第 2 章的 USE 方法和负载特征,软中断和 mpstat 回第 6 章看,bpftrace 的写法在第 15 章。容器和云主机上的带宽限额在第 11 章。

先用中文过一遍

延迟有好几种:先说清量的是哪一段

说「网络延迟高」之前,先问清楚是哪一段。客户端发起一次请求,时间依次花在:域名解析、TCP 三次握手(建连延迟,connection latency)、服务端接受连接、调度线程、处理请求再发回第一个字节(首字节延迟,TTFB),然后才是传数据,最后连接关掉(连接寿命)。往返时间(RTT)是一个包在两端之间来回一趟的时间,ping 量的就是它的一个近似。

这几段的意义不一样。建连延迟里没有应用参与,它慢说明网络或两端的内核有问题;首字节延迟里包括服务端的处理时间和 CPU 排队,书里说它更像是在量应用和当前负载(§10.5.4)。重传会把任何一段拉长:数据包超时重传在 Linux 上至少 200 毫秒,握手阶段丢一个 SYN,客户端要等 1 秒才重发(§10.3.5、§10.4.1)。所以「平均 2 毫秒、偶尔 1 秒」这种分布,第一个要怀疑的就是重传。

客户端看到的一次请求(比例是示意) 解析域名 三次握手 服务端 accept、调度、处理 传数据 关闭 建连延迟 约一个 RTT 首字节延迟(TTFB) 多半是你的服务 连接寿命:建立到关闭 SYN 被丢:干等 1 秒再重发 握手这一段从毫秒级变成 1 秒以上 curl -w 能分别给出 time_connect 和 time_starttransfer;tcplife 记连接寿命。
本站画的示意。不是每次请求都有握手:长连接(keep-alive)把握手的成本摊到很多请求上,这也是连接池的意义。

两个连接队列:backlog 满了会怎样

服务端 listen() 之后,新连接先后经过两个队列:握手还没完成的放在 SYN 队列(半连接),握手完成、等你的程序 accept() 的放在 accept 队列(书里叫 listen backlog)。listen(fd, backlog) 的第二个参数定的是后一个队列的上限,它还会被系统的 net.core.somaxconn 截断(§10.4.3)。

应用 accept 得太慢,accept 队列就满。满了以后 Linux 丢掉新来的 SYN,计数 ListenOverflows 和 ListenDrops 各加一;客户端收不到回应,1 秒后重发,再不行就在第 3 秒、第 7 秒……(从第一次发 SYN 算起,间隔每次加倍)这就是「偶尔连接要一秒」的来历。书里把这两件事(backlog 丢弃和 SYN 重传)当作主机过载的信号(§10.3.7)。本站在 WSL2 上还看到另一条路:握手的最后一个 ACK 到达时队列刚好满了,服务端把 ACK 丢掉、连接留在 SYN-RECV,客户端却已经认为连接建立了。对客户端来说,connect() 成功不等于对方已经能 accept 它。下面的动手和模拟器都围绕这件事。

一个连接从 SYN 到 accept() 客户端 SYN SYN 队列 握手进行中 accept 队列 上限 = backlog accept() 满了:新 SYN 直接丢,不回任何包 ListenOverflows、ListenDrops 各加一 客户端没等到回应:1 秒后重发,然后 3 秒、7 秒…… ss -lnt 的 LISTEN 行: Recv-Q = accept 队列里现在有几个 Send-Q = 上限(backlog,被 somaxconn 截断)
本站画的示意,把书里讲的两个队列和计数、工具放在一起。SYN 队列满时可以用 SYN cookies 绕过,accept 队列满了没有这种绕法,只能让应用取得更快或者把队列开大。

缓冲区、窗口和重传:吞吐被谁限住了

每个 TCP 套接字有一个发送缓冲区和一个接收缓冲区。你的 write() 只是把数据拷进发送缓冲区;内核按两个上限往外发:对端通告的接收窗口(rwnd,等于对端接收缓冲区还空着多少),和自己按网络状况估的拥塞窗口(cwnd)。对端的程序不读,接收缓冲区填满,窗口变成 0,发送方就只能停下来等,你的 write() 也会阻塞(§10.4.1、§10.4.3)。

所以「吞吐上不去」有好几种原因,ss -ti 会直接告诉你是哪一种:rwnd_limited 是被对端的接收窗口限住,sndbuf_limited 是自己的发送缓冲区太小,app_limited 是应用自己没给够数据(§10.6.1)。拥塞窗口在丢包后缩小,丢包靠两种方式发现:等重传定时器超时,或者收到三个重复 ACK 立即快速重传。拥塞控制算法决定窗口怎么涨怎么缩,Linux 默认是 CUBIC,BBR 是另一种思路:不像前几种那样主要靠丢包调窗口,而是通过探测估计路径的带宽和 RTT。

数据从一个 write() 走到对面的 read() write() 发送缓冲区ss 的 Send-Q 网络上在途 ≤ cwnd 接收缓冲区ss 的 Recv-Q read() 接收方通告窗口 rwnd = 接收缓冲区还空着多少 对端不 read():Recv-Q 涨满 → 窗口通告为 0 → 发送方停住 发送方 ss -ti 显示 rwnd_limited,Send-Q 越积越多,write() 阻塞 吞吐低的三种常见原因:对端读得慢(rwnd)、自己缓冲小(sndbuf)、网络丢包让 cwnd 缩小。
本站画的示意,红框那一行是下面第二个动手实验在 WSL2 上实际看到的。

收包路径:网卡、中断和软中断

一个包从网线进来,网卡用 DMA 把它放进内核的环形缓冲区,然后发中断。Linux 的 NAPI 让中断只负责「叫醒」:真正的处理放到软中断(softirq)里,包多的时候干脆关掉中断、轮询着取,一次取一批(§10.4.3)。这样吞吐高,代价是一点延迟;网卡驱动的中断合并(interrupt coalescing)是同一种取舍。

和排查最相关的是:软中断在哪个 CPU 上跑,协议栈的处理就压在哪个 CPU 上。网卡只有一个队列、又没有在软件里分流时,所有收包都挤在一个核上,mpstat -P ALL 里那个核的 %soft 很高、别的核闲着(§6.6.3)。多队列网卡用 RSS 按连接哈希分到不同队列和 CPU;不支持的网卡可以开 RPS 在软件里分。书里提到负载均衡和代理这类收包特别多的程序尤其容易碰到这个问题。嵌入式板子上常见的单队列网卡也值得留意。

英文中文一句话
bandwidth / throughput带宽 / 吞吐带宽是链路的上限,吞吐是现在实际传了多少;全双工时每个方向各算 §10.1 术语页
RTT (round-trip time)往返时间一个包来回一趟的时间;ss -ti 里的 rtt、ping 量的都是它 §10.3.5 术语页
connection latency建连延迟从发 SYN 到握手完成;没有应用参与,包含 SYN 重传的时间 §10.3.5
first-byte latency (TTFB)首字节延迟连接建立到收到第一个字节;包含服务端的处理和排队 §10.3.5
backlog (SYN / listen)连接队列半连接和等 accept 的两个队列;后者的上限是 listen 的参数 §10.4.3 术语页
socket buffer套接字缓冲区每个连接的收发缓冲,Linux 会按连接的活动自动调大 §10.4.3 术语页
receive window (rwnd)接收窗口接收方告诉发送方还能收多少;为 0 时发送方停住 §10.4.1
congestion window (cwnd)拥塞窗口发送方自己估的、不等 ACK 最多能发多少;丢包后缩小 §10.4.1 术语页
retransmit (RTO / fast)重传(超时 / 快速)定时器超时重发(Linux 至少 200 毫秒),或收到重复 ACK 立即重发 §10.4.1 术语页
TIME_WAITTIME_WAIT 状态主动关闭的一方要等一段时间,短连接太多会占满本地端口 §10.5.7 术语页
MTU / MSS最大传输单元 / 最大报文段一帧最多多大(常见 1500 字节);MSS 是扣掉头部后 TCP 一段的数据量 §10.3.4 术语页
NAPI / interrupt coalescingNAPI / 中断合并包多时少发中断、改成轮询成批处理,换吞吐、费一点延迟 §10.4.3
softirq软中断协议栈收包处理跑在这里;mpstat 的 %soft §10.4.3 术语页
RSS / RPS接收端分流(硬件 / 软件)按连接把收包分到多个 CPU,避免一个核被软中断压满 §10.4.3
bufferbloat缓冲膨胀路由器、交换机缓冲过大,包排很久的队,延迟变高 §10.3.6

方法:按什么顺序查

书里建议先用这四种:性能监控、USE 方法、静态性能调优、负载特征(§10.5);抓包代价高,放到最后。落到一次「网络好像慢」的排查上,本站整理成下面几步。前五步都不需要 root。

  1. 慢在哪一段?

    先拆延迟:curl -w 的 time_connect 和 time_starttransfer,或者你自己的客户端在 connect 前后、收到首字节时各记一次时间。建连慢往网络和服务端内核查;首字节慢、建连快,多半是服务自己慢,回第 5、6 章。要在空闲和负载下各量一次。§10.3.5§10.5.4

  2. 有没有丢、有没有错?

    错误最好查也最好解释,先看。nstat -z 隔几秒跑两次,第二次显示的就是增量(加 -a 给的是开机以来的累计值):TcpRetransSegs 比 TcpOutSegs、TcpExtListenOverflows、TcpExtTCPSynRetrans。ip -s -s link 看网卡的 errors、dropped、missed。§10.5.2§10.6.4

  3. 监听队列满没满?

    ss -lnt:LISTEN 行的 Recv-Q 贴着 Send-Q,就是应用 accept 不过来。先查 accept 的那个线程为什么慢,再考虑把 listen() 的 backlog 和 somaxconn 调大。§10.3.7§10.6.1

  4. 某条连接被什么限住了?

    ss -tiepm 看重要的连接:rtt 和 minrtt 比一比看抖动,retrans,rwnd_limited、sndbuf_limited 各占了多少时间,有没有 app_limited 标记(它只是个标记,不带时间)。§10.6.1§10.5.7

  5. 链路满了吗?

    sar -n DEV 1 分方向看 rxkB/s、txkB/s,除以协商速率(ethtool 或 /sys/class/net/网卡/speed)。云主机上要按限额算,不是按网卡标称。顺便看 mpstat -P ALL 1 有没有一个核的 %soft 特别高。§10.5.2§10.6.6

  6. 重传落在哪些连接、什么状态?

    tcpretrans-bpfcc(要 root):ESTABLISHED 状态的重传多,指向网络;SYN_SENT 状态的多,指向对端服务器来不及处理 SYN。tcplife-bpfcc 记每条连接活了多久、传了多少。§10.6.9§10.6.11

  7. 还不清楚,再抓包

    tcpdump 只抓几秒、带上过滤条件、写到文件里再看。高包速下抓包本身就耗 CPU,内核还可能丢包不给 tcpdump,输出末尾会报丢了多少。§10.5.6§10.6.13

工具速查

想知道敲什么看哪里节
一次请求慢在哪段curl -s -o /dev/null -w '%{time_connect} %{time_starttransfer}\n' URL两个数都是从开始算起的秒数;前者是建连完成,后者减前者主要是服务端处理§10.3.5
监听队列满没满ss -lntLISTEN 行 Recv-Q 是 accept 队列现在的长度,Send-Q 是上限;Recv-Q 贴着上限就是满了§10.6.1
丢没丢连接nstat -z TcpExtListenOverflows TcpExtListenDrops不带 -a 时显示自上次运行以来的增量(-a 给累计值);在涨就是 accept 队列溢出§10.6.4
某条连接卡在哪ss -tiepm dst 10.0.0.5rtt、rto、cwnd、retrans;rwnd_limited 是对端读得慢,sndbuf_limited 是自己发送缓冲小§10.6.1
重传率nstat -z TcpRetransSegs TcpOutSegs隔一段时间跑第二次,两个增量相除。多高算高看客户端:面向互联网的服务天然比机房内部高§10.5.1
协议栈的各种计数netstat -s | grep -iE 'listen|retrans|prune'listen queue overflowed、segments retransmitted、因为缓冲区不够被裁掉的包§10.6.5
网卡错误和丢包ip -s -s linkRX 的 errors、dropped、missed、overrun,TX 的 errors、carrier;是自启动以来的累计值,看它涨不涨§10.6.2
吞吐和利用率sar -n DEV 1rxkB/s、txkB/s 分方向除以协商速率;%ifutil 取两个方向里大的那个§10.6.6
连接速率和重传的趋势sar -n TCP,ETCP 1passive/s 是每秒新进来的连接,retrans/s 是每秒重传的段§10.6.6
协商速率、offload、驱动计数ethtool eth0
ethtool -k eth0
ethtool -S eth0
Speed、Duplex 有没有掉到低档;-k 看 TSO、GRO 开没开;-S 是驱动自己定义的计数§10.6.8
软中断压在一个核上没有mpstat -P ALL 1
grep NET_RX /proc/softirqs
某个核 %soft 远高于别的核,或 NET_RX 只在一列涨§10.4.3
每条连接活了多久tcplife-bpfcc每关掉一条连接打一行:进程、两端地址、收发 KB、寿命(毫秒);开销小,可以常开§10.6.9
谁在重传tcpretrans-bpfccSTATE 列:ESTABLISHED 多,可能是网络的问题;SYN_SENT 多,可能是对端服务器来不及处理 SYN§10.6.11
监听队列离满还有多远tcpsynbl-bpfcc
tcpsynbl.bt
每个 backlog 上限一张队列长度直方图;.bt 版在队列超过上限时还会打印 dropping a SYN§10.6.12
包的细节tcpdump -ni eth0 -w /tmp/x.pcap 'port 8080'
tcpdump -nr /tmp/x.pcap -ttt
短时间抓、带过滤;结束时看 dropped by kernel;-ttt 显示相邻两个包的时间差§10.6.13

ss、ip、nstat 在 iproute2 包里,是内核网络开发者维护的,最可能先支持内核的新特性;netstat 和 ifconfig 在 net-tools 包里,书里说内核开发者已经把它们当作过时的工具。sar、mpstat 在 sysstat 里;ethtool、tcpdump 各自一个包,tcpdump 要 root(或 CAP_NET_RAW)。tcplife、tcpretrans、tcpsynbl 在 Debian 的 bpfcc-tools 包里带 -bpfcc 后缀,bpftrace 包也带了 .bt 版本,都要 root。上表从 curl 到 sar 的几行都不需要 root。

动手:在自己的机器上试

backlog 很小、迟迟不 accept:看队列怎么满

服务端 listen(fd, 4),前 3 秒一个都不 accept,之后每半秒才 accept 一个;客户端同时发起 10 个非阻塞 connect,记下每个连接用了多久。先猜:10 个连接里有几个能马上建立?剩下的要等多久?

// slowacc.c:listen 的 backlog 很小,前 3 秒一个都不 accept,之后每 0.5 秒才 accept 一个
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <time.h>
#include <unistd.h>

int main(int argc, char **argv) {
    int backlog = argc > 1 ? atoi(argv[1]) : 4;
    int fd = socket(AF_INET, SOCK_STREAM, 0), on = 1;
    setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof on);
    struct sockaddr_in a = { .sin_family = AF_INET, .sin_port = htons(30100) };
    inet_pton(AF_INET, "127.0.0.1", &a.sin_addr);
    if (bind(fd, (struct sockaddr *)&a, sizeof a) || listen(fd, backlog)) { perror("bind/listen"); return 1; }
    alarm(9);                                    // 9 秒后自己退出
    sleep(3);                                    // 假装忙:这 3 秒里完全不 accept
    struct timespec half = { 0, 500000000 };
    for (;;) {                                   // 之后慢慢 accept,一个 0.5 秒
        int c = accept(fd, NULL, NULL);
        if (c >= 0) close(c);
        nanosleep(&half, NULL);
    }
}
// burst.c:同时发起 N 个非阻塞 connect,记下每个连接用了多久才建立
#include <arpa/inet.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <time.h>
#include <unistd.h>

static double now_ms(void) {
    struct timespec t;
    clock_gettime(CLOCK_MONOTONIC, &t);
    return t.tv_sec * 1e3 + t.tv_nsec / 1e6;
}

int main(int argc, char **argv) {
    int n = argc > 1 ? atoi(argv[1]) : 10;
    if (n > 64) n = 64;
    struct pollfd p[64];
    double t0 = now_ms(), done[64];
    struct sockaddr_in a = { .sin_family = AF_INET, .sin_port = htons(30100) };
    inet_pton(AF_INET, "127.0.0.1", &a.sin_addr);
    for (int i = 0; i < n; i++) {
        p[i].fd = socket(AF_INET, SOCK_STREAM, 0);
        p[i].events = POLLOUT;
        done[i] = -1;
        fcntl(p[i].fd, F_SETFL, O_NONBLOCK);
        connect(p[i].fd, (struct sockaddr *)&a, sizeof a);   // 返回 EINPROGRESS
    }
    int left = n;
    while (left > 0 && now_ms() - t0 < 8000) {               // 最多等 8 秒
        if (poll(p, n, 100) <= 0) continue;
        for (int i = 0; i < n; i++)
            if (done[i] < 0 && (p[i].revents & POLLOUT)) { done[i] = now_ms() - t0; p[i].events = 0; left--; }
    }
    sleep(2);                                                // 连接先别关,留时间给 ss 看
    for (int i = 0; i < n; i++)
        if (done[i] < 0) printf("conn %2d: not connected after 8 s\n", i);
        else printf("conn %2d: connected in %7.1f ms\n", i, done[i]);
    return 0;
}
$ gcc -O2 slowacc.c -o slowacc && gcc -O2 burst.c -o burst
# 记下当前的计数,之后的 nstat 只显示这之后的增量
$ nstat -n
$ ./slowacc 4 & sleep 0.3; ./burst 10 > burst.out &
$ sleep 0.5; ss -lnt 'sport = :30100'
# 大约 3.7 秒时,分别从客户端和服务端两边数连接的状态
$ sleep 3; ss -tn 'dport = :30100' | awk 'NR>1{print $1}' | sort | uniq -c
$ ss -tan 'sport = :30100' | awk 'NR>1 && $1!="TIME-WAIT"{print $1}' | sort | uniq -c
$ sleep 6; cat burst.out; nstat | grep -E 'Listen|SynRetrans'

本站在 WSL2 上看到的(每段只留关键的几行):

ss -lnt:   State   Recv-Q  Send-Q  Local Address:Port
           LISTEN  5       4       127.0.0.1:30100

客户端一侧:   8 ESTAB       2 CLOSE-WAIT
服务端一侧:   4 ESTAB       4 SYN-RECV    2 FIN-WAIT-2

conn  0: connected in     2.7 ms     (0 到 4 号都是 2.7 ms)
conn  5: connected in  3060.5 ms     (5 到 9 号都是 3060.5 ms)

TcpExtListenOverflows   21
TcpExtListenDrops       21
TcpExtTCPSynRetrans     19
  1. listen(fd, 4) 时,Send-Q 是 4,Recv-Q 却停在 5:在这台机器上,accept 队列实际能放 backlog + 1 个。前 5 个连接 2.7 毫秒就建立了,它们只是排在队列里,还没有人 accept。
  2. 后 5 个的 SYN 被丢了,客户端 1 秒后重发,又被丢;3 秒时服务端 accept 了一个,队列空出位置,第二次重发的 SYN 得到了回应。所以它们的建连时间是整整 3 秒,这正是书里说的「连接延迟里包含重传」。
  3. 3.7 秒左右,客户端认为 10 个连接都建立了(8 个 ESTAB,加上 2 个已被服务端 accept 后关掉的),服务端却只有 4 个在 accept 队列里(ESTAB),另有 4 个停在 SYN-RECV:这 4 个连接握手的最后一个 ACK 到达时队列又满了,被丢掉。客户端这时发出的请求,要等这个连接真正进了服务端的 accept 队列才会被处理,中间可能还要重传。
  4. ListenOverflows 记了 21 次,比 5 个连接多得多:每一次重发的 SYN、每一个被丢的 ACK 都算一次。所以看这个计数要看它的增长速度,不要拿它当「丢了几个客户端」。
  5. 把 backlog 改成 100000 再跑 ss -lnt,Send-Q 显示 4096:这台机器的 somaxconn 是 4096,listen() 的参数被它截断了,而且不报错。
WSL2
本站实测过上面全部命令(Debian 13,内核 6.6,iproute2 6.15),不需要 root,只用 127.0.0.1 的 30100 端口。nstat 的增量是相对你自己上一次运行算的(它把上次的值存在一个历史文件里),机器上别的程序在 127.0.0.1 上的连接也会算进去。
服务器
别在线上对真实端口做这个实验。排查线上服务时,同样的三个动作:ss -lnt 看 Recv-Q 和 Send-Q,nstat -z TcpExtListenOverflows 隔几秒看增量,客户端那边看 TcpExtTCPSynRetrans。要看队列长度的分布用 tcpsynbl-bpfcc(要 root)。
ARM 板
busybox 如果编进了 netstat,-lnt 能看到 LISTEN 行的 Recv-Q,但不显示队列上限;有 iproute2 就用 ss。没有 nstat 时直接读 /proc/net/netstat:TcpExt 那两行,第一行是名字,第二行是值,ListenOverflows 在里面。gcc 交叉编译这两个程序就能在板子上跑。

对端不读:Recv-Q 涨满、窗口变零

接收方接受连接后 4 秒不读,发送方用阻塞的 write() 往里灌 32 MB。先猜:数据会卡在哪一边的缓冲区里?发送方的 write() 会怎样?

// lazyread.c:接受一个连接,然后 4 秒不读,之后把数据读完退出
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdio.h>
#include <sys/socket.h>
#include <unistd.h>

int main(void) {
    int fd = socket(AF_INET, SOCK_STREAM, 0), on = 1;
    setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof on);
    struct sockaddr_in a = { .sin_family = AF_INET, .sin_port = htons(30101) };
    inet_pton(AF_INET, "127.0.0.1", &a.sin_addr);
    if (bind(fd, (struct sockaddr *)&a, sizeof a) || listen(fd, 16)) { perror("bind/listen"); return 1; }
    int c = accept(fd, NULL, NULL);
    sleep(4);                                    // 应用忙别的去了,不读 socket
    static char buf[65536];
    long total = 0;
    ssize_t n;
    while ((n = read(c, buf, sizeof buf)) > 0) total += n;
    printf("read %ld bytes\n", total);
    return 0;
}

// blast.c:连上 30101,用阻塞的 write 往里灌 32 MB(另存一个文件)
int main(void) {
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in a = { .sin_family = AF_INET, .sin_port = htons(30101) };
    inet_pton(AF_INET, "127.0.0.1", &a.sin_addr);
    if (connect(fd, (struct sockaddr *)&a, sizeof a)) { perror("connect"); return 1; }
    static char buf[65536];
    for (int i = 0; i < 512; i++)                // 512 × 64 KB = 32 MB
        if (write(fd, buf, sizeof buf) < 0) { perror("write"); return 1; }
    close(fd);
    return 0;
}
# blast.c 用和 lazyread.c 一样的几个头文件
$ gcc -O2 lazyread.c -o lazyread && gcc -O2 blast.c -o blast
$ nstat -n
$ ./lazyread & sleep 0.3; ./blast &
$ sleep 1.5; ss -tni 'sport = :30101 or dport = :30101'
$ wait; nstat | grep -i window

本站在 WSL2 上看到的(ss -tni 每条连接一大行,只摘了要看的字段):

接收方:  ESTAB  Recv-Q 127680  Send-Q 0
发送方:  ESTAB  Recv-Q 0       Send-Q 2498752
发送方:  rwnd_limited:1504ms(100.0%)  backoff:2
发送方:  notsent:2498752

TcpExtTCPWantZeroWindowAdv   1
TcpExtTCPToZeroWindowAdv     1
TcpExtTCPFromZeroWindowAdv   1
  1. 接收方的 Recv-Q 停在 12 万多字节:接收缓冲区满了。Linux 的接收缓冲区会随着应用读数据自动长大,应用一直不读,它就停在很小的值上。
  2. 发送方的 Send-Q 积了 2.4 MB,其中 notsent 是还没发出去的。发送缓冲区也满了以后,blast 的 write() 就阻塞在那里。书里说网卡饱和不好直接量,可以看应用线程阻塞在发送上的时间(§10.5.2);这里堵住它的不是网卡,是对端。
  3. 发送方 rwnd_limited 占了 100% 的时间:一直被对端的接收窗口限着。nstat 里接收方通告过一次零窗口(ToZeroWindowAdv),后来读数据时又重新打开(FromZeroWindowAdv);backoff 是退避次数,这里多半来自零窗口期间的探测。
  4. 换成真实场景:你的服务往下游写得越来越慢,先看下游连接的 ss -ti。看到 rwnd_limited,调大自己的发送缓冲区没有用,问题在对端的读。
WSL2
本站实测过,不需要 root,只用 127.0.0.1 的 30101 端口。loopback 的 MTU 是 65536,所以 mss 比真实网卡大得多,数字只看相对关系。
服务器
对线上的连接用 ss -tiepm dst 对端IP 看同样的字段,不影响业务。-m 多给出 skmem,能看到缓冲区实际分配了多少。
ARM 板
busybox 一般没有 ss;cat /proc/net/tcp 里每条连接一行,tx_queue:rx_queue 那一列(十六进制)就是 Send-Q 和 Recv-Q。拿不到 rwnd_limited 这类细节,但能看出数据堆在哪一边。

动手:模拟器

先猜:新连接每秒平均来 200 个,应用每秒平均能 accept 250 个,backlog 是 16。平均处理得过来,会不会丢?再按一次「应用卡住 2 秒」(比如一次长时间的日志刷盘或锁等待),这 2 秒里来的约 400 个连接会怎样,客户端看到的建连时间分布会变成什么样?

模拟器

连接队列溢出

新连接随机到达,进 accept 队列排队,应用一个一个 accept 走。队列满了,新 SYN 被丢,客户端按 1、3、7、15……秒重发。拖动到达速率、accept 速度,换 backlog,点「跑 10 秒」看结果;「应用卡住 2 秒」模拟 accept 线程短暂停顿。

accept 队列

最近 30 秒,每秒收到的 SYN(绿:进了队列,红:被丢)

客户端看到的建连时间

还没开始。点「跑 10 秒」。
0 s
模拟时间
0
丢掉的 SYN(ListenOverflows)
—
首次 SYN 被丢的比例
—
公式 M/M/1/K(不计重发)
—
建连时间 平均 / p99
0
还在重发 / 放弃了
  1. 默认参数跑 10 秒,丢弃率接近公式算的 0.5%。然后按「应用卡住 2 秒」再跑 10 秒:卡住的 2 秒几乎全是红柱,卡住结束后好几秒里还在丢:被丢的客户端在 1、3、7 秒后成批回来重发,叠在正常到达上,又把队列挤满。平均处理得过来,不代表不丢。
  2. 把 backlog 从 16 换成 1024,重置后重复上一步。卡住 2 秒来的约 400 个连接都能排进队列,没有 SYN 被丢,建连时间也几乎全在 1 秒以下:队列吸收了突发,代价是排在后面的请求多等了一会儿。
  3. 取消「客户端重发 SYN」,把到达调到 300、accept 调到 250(到达比处理快),跑 30 秒:丢弃率趋向公式给的值(约 17%)。再勾上重发:被丢的连接回来再排队,实际负载比 300 还高,丢弃率比公式高,还在重发的连接越积越多。

成书之后

somaxconn 的默认值从 128 变成了 4096

书里
调优一节建议把 net.core.somaxconn 从默认值调大,举的例子是 1024(§10.8.1);书里的示例跑在 5.3 内核上。
今天
Linux 5.4 把 somaxconn 的默认值从用了二十多年的 128 提到 4096。读者的 WSL2(6.6)上读出来就是 4096。所以在新内核上看到 accept 队列上限很小,先查程序自己 listen() 传的是多少,再考虑改 sysctl。
核对于 2026-10。出处:Eric Dumazet 的内核提交「net: increase SOMAXCONN to 4096」(Linux 5.4);内核文档 ip-sysctl 对 somaxconn 的说明;本站在 WSL2 上读 /proc/sys/net/core/somaxconn。

两个 TCP 参数早已不起作用:tcp_tw_recycle 和 tcp_fack

书里
TCP 选项的示例里列了 tcp_fack = 1 和 tcp_tw_recycle = 0,并说 Linux 默认支持 FACK(§10.4.1、§10.8.1)。
今天
tcp_tw_recycle 在 Linux 4.12 里删除了,配置文件里还写着它,sysctl 会报找不到这个文件。FACK 的丢包检测在 4.15 被移除,由 RACK 取代,tcp_fack 的值从那时起被忽略。从网上抄 sysctl 配置时,这两行可以删掉。
核对于 2026-10。出处:tcp(7) 手册页(man-pages 项目)对 tcp_tw_recycle 的说明;内核文档 ip-sysctl 对 tcp_fack 的说明(4.15 起写明不再起作用);内核提交「tcp: retire FACK loss detection」(Yuchung Cheng,2017)。

QUIC 和 HTTP/3 已经成为标准

书里
QUIC 由 Google 设计、跑在 UDP 上,IETF 正在标准化 QUIC 本身和 HTTP over QUIC(即 HTTP/3)(§10.4.1)。
今天
QUIC 在 2021 年 5 月作为 RFC 9000 发布,HTTP/3 在 2022 年 6 月作为 RFC 9114 发布。对排查的影响:走 QUIC 的流量在内核看来是 UDP,ss、nstat 里的 TCP 计数和 tcpretrans 都看不到它的重传和拥塞状态,要靠应用或 QUIC 库自己的统计。
核对于 2026-10。出处:RFC Editor 上 RFC 9000、RFC 9114 的记录。

分节地图

这一章按书的固定套路走:背景(§10.1–10.3)、架构(§10.4)、方法(§10.5)、工具(§10.6)、实验(§10.7)、调优(§10.8)。先读的是延迟、缓冲、backlog 这几个概念,Linux 网络栈的实现(连接队列、缓冲区、NAPI 和多 CPU 分流),USE 和延迟分析,以及 ss、nstat、netstat、sar、tcpretrans 几个最常用的工具。协议的细节、硬件和各种调优参数,用到再查。

  1. As systems become more distributed,…约 300 词开篇网络常被当替罪羊;这一章的学习目标和六个部分。
  2. Terminology约 200 词术语接口、包和帧、带宽和吞吐、延迟的两种含义。
  3. Models约 100 词模型三个简单模型的引子。
  4. Network Interface约 100 词网络接口接口和物理端口的对应,写网络服务的人都知道。
  5. Controller约 100 词控制器网卡上的控制器是什么,同上,跳过不影响后面。
  6. Protocol Stack约 200 词协议栈TCP/IP 和 OSI 两种分层,各层的消息叫什么。
  7. Concepts约 100 词概念一组概念的引子。
  8. Networks and Routing约 300 词网络与路由子网、路由、单播和组播,网络基础课内容。
  9. Protocols约 200 词协议不同协议、不同版本走不同的内核路径,性能也不同。
  10. Encapsulation约 100 词封装加头加尾,基础知识。
  11. Packet Size约 400 词包的大小1500 的 MTU 从哪来,巨帧为什么推不开,offload 补了多少。
  12. Latency约 700 词延迟几种延迟各量的是哪一段,哪些包含服务端的处理时间。
  13. Buffering约 200 词缓冲两端的缓冲撑起吞吐;中间设备缓冲太大反而拖慢。
  14. Connection Backlog约 100 词连接队列backlog 满了丢 SYN,客户端重发;这是主机过载的信号。
  15. Interface Negotiation约 100 词接口协商速率和双工是协商出来的,坏线会让它掉档。
  16. Congestion Avoidance约 100 词拥塞避免以太网、IP、TCP 各自怎么避免拥塞。
  17. Utilization约 100 词利用率按方向、按协商速率算;只给包数的工具算不出利用率。
  18. Local Connections约 200 词本机连接本机进程之间用 127.0.0.1 还是 Unix 域套接字。
  19. Architecture约 100 词架构协议、硬件、软件三部分的引子。
  20. Protocols约 2200 词协议IP、TCP、UDP、QUIC 的性能特性。要查握手、重传、拥塞控制、Nagle 和延迟 ACK 时回来看。
  21. Hardware约 1000 词硬件网卡、控制器和 PCIe 带宽、交换机、路由器、防火墙各自可能成为瓶颈。
  22. Software约 1900 词软件Linux 网络栈:两个连接队列、缓冲区、GSO/TSO、qdisc、NAPI、多 CPU 分流、绕过内核。
  23. Methodology约 100 词方法方法总表和作者推荐先用的四种。
  24. Tools Method约 300 词工具法把几个工具挨个跑一遍各看什么。
  25. USE Method约 400 词USE 方法网卡每个方向查利用率、饱和度、错误;饱和度为什么难量。
  26. Workload Characterization约 300 词负载特征吞吐、包速、连接速率三个基本数,加一张进阶问题清单。
  27. Latency Analysis约 200 词延迟分析尽量多量几种延迟来缩小范围;重传造成的离群值怎么找。
  28. Performance Monitoring约 100 词性能监控长期该记哪几个网络指标。
  29. Packet Sniffing约 400 词抓包抓包为什么贵,内核里过滤和环形缓冲怎么省开销。
  30. TCP Analysis约 200 词TCP 分析该看的 TCP 行为,以及短连接太多时的临时端口冲突。
  31. Static Performance Tuning约 200 词静态性能调优一张检查配置的问题清单:速率、双工、MTU、路由、限额。
  32. Resource Controls约 200 词资源控制带宽限制、QoS 和人为加的延迟。
  33. Micro-Benchmarking约 100 词微基准测试先用基准工具确认网络本身跑得到预期速度。
  34. Observability Tools约 200 词观测工具工具总表;iproute2 和 net-tools 两套工具的关系。
  35. ss约 400 词ssss -tiepm 的各个字段,尤其是几种 limited 标志;ss 读的是 netlink。
  36. ip约 200 词ipip -s link 的错误计数和路由表。
  37. ifconfig约 100 词ifconfig已被 ip 取代,计数和 ip 一样。
  38. nstat约 200 词nstat按 SNMP 名字打印内核的网络计数,默认给增量。
  39. netstat约 500 词netstat-i 和 -s 两种用法,-s 里该盯哪几项;监控工具该读 /proc。
  40. sar约 200 词sar-n 的几组选项,按秒看吞吐和 TCP 计数,能存历史。
  41. nicstat约 200 词nicstat直接给出网卡利用率和饱和度的工具。
  42. ethtool约 100 词ethtool驱动计数、驱动信息和 offload 开关。
  43. tcplife约 200 词tcplife跟踪状态变化记录每条连接的寿命和流量,开销小。
  44. tcptop约 100 词tcptop按进程和连接排的 TCP 流量 top。
  45. tcpretrans约 200 词tcpretrans逐个打印重传和当时的 TCP 状态;状态说明问题在哪边。
  46. bpftrace约 1000 词bpftrace套接字和 TCP 的跟踪点,tcpsynbl 看监听队列长度。要自己写跟踪时回来查。
  47. tcpdump约 300 词tcpdump抓到文件再看,过滤表达式,时间戳选项。
  48. Wireshark约 100 词Wireshark图形界面看抓包文件,按连接归类。
  49. Other Tools约 100 词其他工具别的章和 BPF 那本书里的网络工具,以及 strace、lsof 等。
  50. Experimentation约 100 词实验主动发包测试的工具。
  51. ping约 100 词pingICMP 往返时间;路由器可能给 ICMP 不同的优先级。
  52. traceroute约 300 词traceroute逐跳的路径和 RTT;全是星号时换成 TCP 试。
  53. pathchar约 100 词pathchar测逐跳带宽的老工具,原版已经很难跑起来。
  54. iperf约 200 词iperf测两台机器之间的最大吞吐,可开多个并行连接。
  55. netperf约 100 词netperf测请求-响应的往返延迟。
  56. tc约 100 词tc用 netem 人为加丢包、加延迟,测试时模拟坏网络。
  57. Other Tools约 100 词其他工具内核发包器、组合测试、带统计的路由追踪、重放抓包:pktgen、Flent、mtr、tcpreplay。
  58. Tuning约 100 词调优默认值通常已经不错,先搞清负载再动参数。
  59. System-Wide约 900 词系统级参数缓冲区、两个 backlog、设备队列、拥塞控制、qdisc 的 sysctl。改之前对照自己内核的文档。
  60. Socket Options约 100 词套接字选项写服务的人能在代码里改的那几个:缓冲大小、REUSEPORT、NODELAY。
  61. Configuration约 100 词配置巨帧、链路聚合、防火墙打优先级标记。
  62. Exercises约 200 词习题习题;动手部分本页已经给了替代。

词与短语

只收先读和用到再查两档的节。斜体那一行是原句里连续的几个词,在电子书里搜它就能找到那一句。

§0 开篇

blame the unknown
… its inherent complexity (blame the unknown). This …
看不清的东西最好怪
作者的调侃:网络复杂又看不见,所以出了问题先怪它。
exonerate
… which may exonerate the network so …
洗清(网络的)嫌疑
本是法律用语,宣告无罪。查清以后,常常证明不是网络的错。

§10.3.4 Packet Size

confluence
The confluence of two components has …
(两件事)凑到一起
come into play
… the misconfigured firewalls come into play: …
登场,开始起作用
不是「来玩」。前面说完旧硬件,现在轮到配错的防火墙出场。
stick to
… many systems stick to the 1,500 …
一直守着(默认值)不改

§10.3.5 Latency

think time
… includes the think time of the target …
(服务端的)处理时间
性能领域的说法,指服务端收到请求后自己花掉的时间,不是谁在「思考」。

§10.3.6 Buffering

throttle
… on the hosts, which throttles performance.
压低,节流
本义是掐住喉咙、关小油门。拥塞避免一启动,发送速度就被压下来。
be best served by
… may be best served by the endpoints …
最好交给……来做
serve 在这里是「满足(某个功能的需要)」,不是「服务」。缓冲放在两端的主机上最合适。

§10.3.7 Connection Backlog

in time
… for the process to accept in time, …
及时(来得及)
不是「按时」(on time)。连接来得太多,进程来不及一个个 accept。

§10.3.9 Congestion Avoidance

become overwhelmed
Hosts can also become overwhelmed when …
被压垮,应付不过来

§10.3.10 Utilization

as straightforward as it sounds
… isn’t as straightforward as it sounds.
没有听起来那么简单
往轻里说:速率和双工都可能是协商出来的,分母本身就会变。

§10.3.11 Local Connections

not merged
… called TCP friends, but was not merged …
没被合入(内核主线)
merge 在内核开发里专指补丁被接受进主线,不是一般的「合并」。

§10.4.1 Protocols

while hurting performance on others
… on some network paths, while hurting performance …
同时在另一些路径上反而更差
作者对 BBR 的评价有保留:不是换上就一定快。
in the pipeline
… if there is data in the pipeline and …
在途(已发出、还没确认)
不是 CPU 流水线,也不是「在计划中」。Nagle 只在已有数据在路上时才攒小包。
span
… large enough to span the transmitted data …
覆盖(全部要发的数据)
初始窗口大到一次就能把整个响应发完,就不用等 ACK 了。

§10.4.2 Hardware

line rate
… drive them both at line rate at …
线速:链路标称的满速率
网络行话。两个 10 GbE 口插在带宽不够的插槽上,没法同时跑满。
I know this from practice
… same time (I know this from practice!)
(这是)我吃过亏才知道的
作者的口吻:括号里的感叹,表示这不是纸上算的。
element of mystery
This element of mystery on the network …
(网络上)看不清的那部分

§10.4.3 Software

bogus
… listening TCP port from bogus IP addresses.
伪造的
staging area
… can act as a staging area for …
暂存区,候场区
本指演出前候场或货物中转的地方。可疑的连接先放在第一个队列,握手完成才升到第二个。
at some cost in latency
… though at some cost in latency.
代价是延迟高一点
at the cost of 是「以……为代价」,some 表示代价不大。
packet storm
… being overwhelmed by packet storms.
包风暴:短时间涌来大量的包
spin on CPU
(spin on CPU until an event occurs)
在 CPU 上空转
spin 是「打转」:不睡眠,一直占着 CPU 查有没有事件,换更低的延迟。

§10.5.1 Tools Method

what constitutes
What constitutes a “high” retransmit rate …
什么才算
constitute 是「构成、算作」。什么样的重传率算高,要看客户端在哪。

§10.5.2 USE Method

readily available
… usually readily available as statistics and …
随手就能拿到
at any hop
… caused by problems at any hop.
(路径上)任何一跳
hop 是包经过的一个路由器或一段链路。重传说明路上某处有问题,不一定在你的机器上。

§10.5.4 Latency Analysis

narrow down
… to narrow down the real source of …
缩小范围
transient
… latency are transient and only occur …
一时的,只在某些时候出现

§10.5.6 Packet Sniffing

the last resort
… this may be the last resort, as …
最后一招
resort 在这里是「求助的手段」,不是度假胜地。别的办法都不行再抓包。

§10.6.1 ss

application-bound
… that the connection is application-bound.
受限于应用
-bound 是「被……限住」(CPU-bound、IO-bound),不是「绑定到应用」。
workaround
A workaround I’ve found is to …
变通办法
ad hoc
… as a source for ad hoc reporting, …
临时现做的
拉丁语,本义「为此」。临时写个 awk 出报表,不是正式的监控系统。

§10.6.5 netstat

This may be expected
… unreliable network. This may be expected …
这可能是正常的
expected 是「在预料之中」。面向互联网的服务,重传率高一些不算异常。

§10.6.11 tcpretrans

consume
… not consuming its SYN backlog fast enough.
(从队列里)取走、处理
队列的「消费者」,不是「消耗」。应用 accept 得不够快。
negligible
… the overhead should be negligible.
可以忽略不计

§10.8 Tuning

performance wins
… leading to much greater performance wins.
性能收益
win 在性能领域指一次改进带来的收益,不是「胜利」。

§10.8.1 System-Wide

not a recipe
… a point-in-time example, not a recipe.
不是照抄就行的配方
作者提醒:那组参数只是某个时间点的例子,别原样搬到自己的机器上。

诊断题:换成你来查

场景和输出都是本站编的。先自己判断,再点选项看解释。

客户端说「偶尔连不上,重试就好」,这组数说明什么?§10.3.7

场景:你的 C++ 网关监听 8080,高峰期客户端偶尔报连接超时。你在网关机器上隔 10 秒各跑一次:

ss -lnt:  State   Recv-Q  Send-Q  Local:Port
          LISTEN  129     128     *:8080
nstat:    TcpExtListenOverflows   3412
          TcpExtListenDrops       3412

LISTEN 行 Recv-Q 129 已经超过 Send-Q 128,十秒里 ListenOverflows 涨了三千多,这是 accept 队列溢出,丢 SYN 的是你自己的机器。只调 somaxconn 不一定够:Send-Q 显示的是程序 listen() 传的值和 somaxconn 中小的那个。新内核 somaxconn 默认 4096,这个 128 多半是代码里传的,要改代码;5.4 以前的内核 somaxconn 默认正好是 128,两处都得查(sysctl net.core.somaxconn)。更根本的是 accept 线程为什么取得慢(被别的工作占住、锁、事件循环阻塞)。SYN cookies 只对第一个队列管用,对 accept 队列满没有帮助。

建连时间这样分布,最可能是什么?§10.3.5

场景:一个 ARM 网关板子每分钟上报数据,每次新建连接。你在客户端打了建连耗时的日志,统计一天:

connect time    count
  < 5 ms        86012
  1.0 - 1.1 s     402
  3.0 - 3.1 s      37
  > 7 s             3

建连时间集中在 1、3、7 秒这几个整数附近,正好是 SYN 重传的退避间隔,几乎可以断定是握手阶段丢了包。服务端处理慢会体现在首字节延迟上,不在建连时间里;DNS 超时一般是几秒到几十秒、和这几个点不对齐,而且如果是用 IP 直连就不存在;调度延迟不会这么整齐。下一步在服务端看 nstat 的 ListenOverflows 和 ip -s link 的 dropped,在客户端看 TcpExtTCPSynRetrans。

往日志服务器写得越来越慢,该调哪里?§10.6.1

场景:你的服务通过一条长连接把日志推给日志服务器,最近 write() 常常阻塞。在你这边看这条连接:

ESTAB  Recv-Q 0  Send-Q 4194304
rtt:0.4/0.1  cwnd:10  retrans:0/0
rwnd_limited:9120ms(91.2%)

rwnd_limited 占了 91%,说明大部分时间对端通告的窗口不够,数据堆在你的发送缓冲区里(Send-Q 4 MB)。RTT 很小、没有重传,网络是好的。调大发送缓冲只会让数据堆得更多、晚一点阻塞;换拥塞控制也没用,限住你的是接收窗口不是拥塞窗口。去日志服务器上看那条连接的 Recv-Q 和它的读线程。

这台代理机器的瓶颈在哪?§10.4.3

场景:一台 8 核机器跑你写的 UDP 转发代理,包速一高就开始丢包,可 top 里总的 CPU 才用了 15%。mpstat 截了一秒:

CPU   %usr  %sys  %soft  %idle
  0    2.0   3.0   94.0    1.0
  1    9.0   4.0    0.0   87.0
  2    8.0   5.0    0.0   87.0
(其余各核和 1 号相近)

0 号核的时间几乎全在 %soft:协议栈收包处理都挤在这一个核上,它满了就开始丢,别的核再闲也帮不上。多开应用线程没用,瓶颈在应用之前。看 /proc/interrupts 里网卡有几个队列、中断落在哪些核上,grep NET_RX /proc/softirqs 看是不是只有一列在涨;单队列网卡可以开 RPS,在软件里按连接分到多个 CPU。

服务调用下游时偶尔连不上,这是怎么回事?§10.5.7

场景:你的服务每个请求都新建一条到同一个下游(同一 IP、同一端口)的短连接,用完就主动关闭,平时每秒约 450 个请求。高峰时 connect() 偶尔失败。

ss -s:  timewait 27950
sysctl: ip_local_port_range = 32768 60999

两端 IP 和下游端口都固定,能区分连接的只剩本地临时端口,这台机器只有 28232 个可用。每秒四百五十个主动关闭的连接都要在 TIME_WAIT 里待 60 秒左右,同时就有两万七千个左右,已经贴着端口的上限;高峰时请求稍一多,就挑不出不在 TIME_WAIT 里的端口,connect() 报 EADDRNOTAVAIL。书里给的办法有复用 TIME_WAIT(tcp_tw_reuse)、多用几个 IP;对写服务的人来说,最直接的是长连接或连接池,握手的成本也一起省了。

值得停下来的句子

This chapter will show how to figure out what is really happening, which may exonerate the network so that analysis can move on.— §10 开篇。本站译:这一章要教你弄清到底发生了什么,查完也许能还网络一个清白,分析好继续往下走。

写服务的人和运维、网络组之间最常见的一句话是「是不是网络的问题」。作者把这一章的目的定成先查清,而且直说结果多半是给网络洗清嫌疑。这改变了排查的姿态:不是去证明网络有错,而是用建连延迟、重传计数、队列溢出这些数,快速判断问题在不在网络,不在就回到自己的服务上。上面第一道诊断题就是典型:超时看起来像网络,丢包的却是自己的 accept 队列。

Backlog drops and SYN retransmits are indicators of host overload.— §10.3.7。本站译:backlog 丢弃和 SYN 重传,说明主机过载了。

短短一句,把两个看起来属于「网络」的现象归到了主机头上。对写 C/C++ 服务的人,这句话的意思是:ListenOverflows 在涨,第一个该查的是自己的 accept 循环和它所在的线程,而不是交换机。也别只盯着计数本身:模拟器里可以看到,平均处理能力够,一次短暂的停顿也会让队列溢出,被丢的客户端还会在 1 秒、3 秒后成批回来。

带走的几条

一句话:先拆延迟、先看计数,确认问题在不在网络;accept 队列和收发缓冲区,是写服务的人自己能管的两处。

延迟先拆段

建连慢查网络和内核,首字节慢多半是服务自己。curl -w 或在代码里打点,各量一次。

§10.3.5
整秒的延迟是重传

建连时间落在 1、3、7 秒附近,是 SYN 被丢后在重发;数据重传至少 200 毫秒。p99 突然很高,先想到它。

§10.4.1
ss -lnt 看监听队列

LISTEN 行 Recv-Q 是排队数,Send-Q 是上限。贴着上限、ListenOverflows 在涨,就是 accept 跟不上。

§10.6.1
backlog 要在代码里给够

listen() 的参数会被 somaxconn 截断;新内核的 somaxconn 已是 4096,上限小时先查代码里 listen() 传了多少。

§10.4.3
ss -ti 告诉你被谁限住

rwnd_limited 是对端读得慢,sndbuf_limited 是自己的发送缓冲小,app_limited 是应用没给够数据。

§10.6.1
计数看增量

nstat 默认给自上次以来的增量;ip -s link、netstat -s 是累计值,隔几秒跑两次再比。

§10.6.4
一个核 %soft 满

收包的软中断挤在一个核上,别的核闲着也帮不上。查网卡队列数,开 RSS 或 RPS。

§10.4.3