
干后端开发和运维这些年最怕遇到一种情况业务代码没有问题数据库也扛得住但用户就是反馈连接慢、请求超时、服务“假死”。翻遍日志只看到一堆 connection timeout 和 connection reset by peer。一开始我以为是负载均衡或者网关的问题后来一步步排查才发现根子往往在 Linux 内核的 TCP/IP 协议栈参数上。这套默认参数在低并发、低流量场景下完全够用可一旦业务量上来三次握手、缓冲区分配、拥塞控制这些内核机制就会成为新的瓶颈。去年我调优一个在线交易系统的接入层高峰期每秒新建连接数千个当时的问题就出在两处全连接队列太小导致 accept 排队以及 SYN 重传太保守导致偶发丢包时客户端要等好几秒。那次之后我把从三次握手到拥塞控制这条链路上的关键参数完整梳理了一遍结合线上实测和压测数据形成了一套可复用的调优方法。这篇文章就把这套方法完整分享出来覆盖连接建立阶段的队列与重传参数、数据交互阶段的缓冲区配置、拥塞控制算法的选型以及最容易被忽视的常见故障排查技巧。主要面向后端开发、运维、SRE 同学做嵌入式 Linux 网络方向的朋友同样可以参考。文中所有参数都以 CentOS 7/8内核 3.10和 Ubuntu 20.04/22.04内核 5.x实测为准命令基于 sysctl 和 iproute2没有涉及需要编译内核的部分拿到手上就能验证。1. 协议栈调优的整体思路1.1 为什么内核默认参数扛不住真实业务很多人有个误区觉得 Linux 内核的默认参数是“经过精心调校”的适合所有场景。实际上内核默认参数追求的是“在各种环境下都不出大问题”而不是“在某个特定业务下性能最优”。拿全连接队列来说net.core.somaxconn默认值是 128很多应用层程序在 listen 的时候会把 backlog 设置为 4096 甚至更大但内核最终采用的队列长度是min(backlog, somaxconn)也就是取两者中较小的值。你以为自己给了 4096 的容量实际上内核只开了 128高并发下新连接照样排队。我压测过一个 Spring Boot 服务并发 2000 个短连接请求应用本身 CPU 才 30%但 p99 延迟涨了 10 倍。用ss -lnt一看Send-Q那一列显示 128而Recv-Q一直在跳动说明全连接队列已经打满了三次握手已经完成但应用 accept 的速度跟不上连接只能积压在内核里。这就是默认参数和真实业务之间的落差。所以调优的第一步不是急着改参数而是先搞明白 TCP 连接的整个生命周期里内核到底在哪些环节做了限制。三次握手不是简单的三次报文交换背后隐藏着两条队列的长度上限、超时重传的次数、以及是否启用 SYN Cookies 等机制。这些限制都是可以调的但必须搞清楚每个参数的作用域和副作用。1.2 调优前必须坚守的三个原则在我调过的所有系统里真正让我记住教训的往往不是改了什么参数而是改参数时犯的错。这里分享三条原则每个做协议栈调优的人都应该先刻在脑子里。第一先测量后调整。任何参数在修改之前都要先看基线数据。至少要看三个来源ss -lnt看当前队列状态netstat -s或/proc/net/netstat看 TCP 层的统计计数sar -n TCP,ETCP看历史趋势。没有基线数据就动参数等于闭着眼睛开车出了问题根本不知道是自己改的还是本来就有的。第二一次只改一个维度。TCP 调优涉及的参数之间是有联动关系的比如缓冲区调大后如果拥塞窗口不匹配可能内存消耗上去了但吞吐没变化。同时改 tcp_rmem 和拥塞控制算法出问题后定位根因的时间会翻倍。我在压测时曾经同时改了tcp_rmem和initcwnd吞吐出现断崖式下跌排查了半天才发现是两个参数叠加后导致内存回收压力过大跟网络本身没有关系。第三参数必须持久化。用sysctl -w改的参数重启后立即失效这在小规模验证时没问题但生产环境一定要写入/etc/sysctl.d/目录下的独立配置文件通过sysctl --system重新加载。我习惯为每个业务模块单独建一个文件比如/etc/sysctl.d/99-tcp-tuning.conf这样参数来源清晰也方便回滚。1.3 按连接生命周期拆解调优范围TCP 一条连接从诞生到销毁大致经历四个阶段建立连接、数据交互、拥塞调整、断开连接。每个阶段涉及的参数组完全不同我建议按这个生命周期做模块化排查而不是一把梭把所有参数都调一遍。建立连接阶段核心是半连接队列SYN Queue和全连接队列Accept Queue对应参数包括net.ipv4.tcp_max_syn_backlog、net.core.somaxconn、net.ipv4.tcp_syn_retries、net.ipv4.tcp_synack_retries还有tcp_syncookies。数据交互阶段核心是内核为 socket 分配的收发缓冲区对应net.ipv4.tcp_rmem、net.ipv4.tcp_wmem、net.core.rmem_max、net.core.wmem_max以及应用层的TCP_NODELAY选项。拥塞调整阶段核心是拥塞控制算法和拥塞窗口cwnd的初始值对应net.ipv4.tcp_congestion_control、路由项的initcwnd、tcp_slow_start_after_idle。断开连接阶段核心是 TIME_WAIT 状态的处理对应net.ipv4.tcp_fin_timeout、net.ipv4.tcp_tw_reuse、net.ipv4.tcp_max_tw_buckets。这四个阶段是串行的任何一个环节出问题都会影响最终的业务体验但每个阶段的排查手段和参数调整逻辑是完全不同的。下面按顺序展开。2. 三次握手阶段的调优细节2.1 半连接队列与全连接队列握手背后的两条流水线很多人讲到三次握手就画三条箭头SYN、SYNACK、ACK看起来很简洁但实际上内核在握手过程中维护了两条独立的队列。第一条是半连接队列SYN Queue存放的是已经收到 SYN、正在等待 ACK 的连接请求。客户端发来 SYN 后服务端内核在其中创建一条半连接记录状态是 SYN_RECV然后回复 SYNACK。第二条是全连接队列Accept Queue存放的是三次握手已经完成、等待应用调用 accept() 取走的连接。这里有个非常关键的细节当全连接队列满了之后内核的行为取决于是否开启tcp_syncookies。启用了 SYN Cookies内核不会丢弃还在半连接队列里的请求而是通过一种特殊的编码方式在 SYNACK 里携带连接信息等客户端 ACK 回来后再重建连接。但如果全连接队列本身满了即使半连接处理得再快连接也没地方放最终还是会失败。所以两条队列就像两道闸门任何一道堵塞水流都过不去。判断队列是否溢出最直接的方法是看ss -lnt的输出。全连接队列的当前积压量显示在Recv-Q列队列上限显示在Send-Q列。如果Recv-Q长期逼近Send-Q说明应用的 accept 速度跟不上连接到达速度。而半连接队列的溢出则要看netstat -s里的SYNs to LISTEN sockets dropped计数。2.2 半连接队列参数tcp_max_syn_backlog 的真实上限半连接队列的长度不是由单个参数决定的它受到net.ipv4.tcp_max_syn_backlog和内存限制的共同作用。默认值通常是 1024内核 5.x 之后部分发行版调整到了 4096但对于高并发短连接场景1024 很容易被打穿。调大tcp_max_syn_backlog之后还需要关注另一个老生常谈的问题如果开启了 SYN Cookies半连接队列的长度上限实际上不再由tcp_max_syn_backlog控制而是取决于tcp_syncookies机制本身。SYN Cookies 的好处是在队列打满时依然能处理新连接代价是放弃了部分 TCP 选项比如大窗口、时间戳而且服务端无法保存半连接的状态某些情况下会对后续的数据传输优化产生影响。所以生产环境我通常建议保留tcp_syncookies1但不要把 SYN Cookies 当成解决队列溢出的唯一手段根本方案还是提高队列上限和应用层的 accept 速度。另一个隐藏参数是net.ipv4.tcp_syn_retries它控制本机作为客户端发起连接时SYN 的重传次数。这个值默认是 6意味着在约 127 秒后才会放弃连接。注意这个参数是客户端的不是服务端的。很多运维同学在服务端改了tcp_syn_retries发现没效果因为服务端收到 SYN 之后回复 SYNACK 的重传由tcp_synack_retries控制默认是 5对应约 63 秒。在内网环境、网络质量好的前提下这两个重传次数完全可以调小。我一般会设成tcp_syn_retries 3、tcp_synack_retries 2这样 SYN 建连失败最快几秒内就能反馈给上层应用可以快速重试其他节点而不是傻等一两分钟。2.3 全连接队列somaxconn 与 backlog 的短板效应全连接队列的有效长度是min(backlog, somaxconn)这里的 backlog 是应用调用 listen() 时传入的参数。Nginx 默认给的 backlog 是 511Redis 默认是 511 或 1024Java 的 Netty 默认也可以配置。很多应用层开发以为设置了大的 backlog 就万事大吉但如果没有同步调整内核的net.core.somaxconn最终队列长度会被内核参数卡死。net.core.somaxconn的默认值在 CentOS 7 里是 128Ubuntu 22.04 里是 4096两者差距很大。如果你的应用部署在 CentOS 7 上并且没改过这个参数那么无论 Tomcat、Netty 设置的 backlog 有多大全连接队列上限都是 128。这就是我在开头提到的那个交易系统的核心瓶颈。调优方式分两步。先改内核参数cat /etc/sysctl.d/99-tcp-tuning.conf EOF net.core.somaxconn 4096 net.ipv4.tcp_max_syn_backlog 8192 EOF sysctl --system再改应用。Nginx 在 nginx.conf 的 listen 指令里加backlog4096Java 的 ServerSocket 在 bind 之前设置 backlogNetty 则在ServerBootstrap.option(ChannelOption.SO_BACKLOG, 4096)。注意如果应用的 backlog 设置小于内核的 somaxconn那还是应用的限制生效所以两边要一起调。另外还有一个很容易忽略的点net.core.netdev_max_backlog控制的是网卡收包队列的长度默认 1000在万兆网卡多队列场景下也是潜在瓶颈。它和全连接队列不一样但它决定数据包能不能及时进入协议栈。如果遇到网卡丢包但应用层压力不大优先查这个参数。2.4 握手队列与 syncookies 的联动判断我现在判断握手层有没有问题习惯用一个标准化的排查模板在这里直接分享给你。先看全连接队列积压ss -lnt | awk {print $1, $2, $3, $4, $5, $6}如果 Recv-Q 长时间等于 Send-Q说明全连接队列在满负荷运行。再看半连接netstat -s | grep -i SYNs to LISTEN数值在持续增长说明有 SYN 到达但没进入全连接队列要么是半连接队列溢出要么是 accept 太慢。最后抓包确认tcpdump -i eth0 tcp[tcpflags] (tcp-syn|tcp-ack) ! 0 -c 100如果看到大量 SYN 重传的报文相同序号出现多次那多半是服务端处理不过来或队列溢出导致丢包。配合sar -n ETCP 1看listen queue overflows和listen drops基本就能定位是半连接队列还是全连接队列的问题。3. 数据交互阶段收发缓冲区到底该怎么调3.1 缓冲区是吞吐量的隐形天花板三次握手完成只是开始真正的性能大考在数据交互阶段。TCP 的可靠传输依赖“发送缓冲区—网卡—接收缓冲区—应用”这条链路其中任意一环的缓冲区不够大吞吐量就上不去。这就像一个水管系统无论水泵多有力只要某个位置的管径细流量就会被卡在那里。Linux 内核为每个 TCP socket 维护了发送缓冲区和接收缓冲区。发送缓冲区的作用是暂存应用写入但尚未被 ACK 的数据接收缓冲区的作用是暂存已到达但尚未被应用读取的数据。缓冲区的大小不是固定的内核会根据网络状况和内存压力动态调整但调整范围受tcp_rmem和tcp_wmem两个参数约束。这里有一个生活化的类比缓冲区像停车场应用是客户网络是公路。停车场太小车来了没地方停只能堵在公路上停车场太大虽然不会堵车但占用了大量土地内存可能影响其他功能区。调优的实质是找到匹配业务流量特征的最佳停车场上限。3.2 tcp_rmem 与 tcp_wmem 的三元组含义net.ipv4.tcp_rmem和net.ipv4.tcp_wmem的格式都是三个数字最小值、默认值、最大值单位是字节。net.ipv4.tcp_rmem 4096 131072 6291456 net.ipv4.tcp_wmem 4096 16384 4194304内核会在这个范围内自动调整缓冲区大小具体调整策略由net.ipv4.tcp_moderate_rcvbuf控制默认开启表示接收缓冲区会根据 BDP 自动调节。但要注意自动调节的上限还受net.core.rmem_max和net.core.wmem_max制约这两个是系统级的最大值。如果应用要手动设置 socket 缓冲区比如在 Java 里调用setReceiveBufferSize内核会把应用设置的值与net.core.rmem_max做比较超过的部分会被截断。所以在调优时不要只改 tcp_rmemnet.core.rmem_max和net.core.wmem_max也要同步调大否则应用层设置的缓冲区起不到预期效果。我的习惯配置是net.core.rmem_max 16777216 net.core.wmem_max 16777216 net.ipv4.tcp_rmem 4096 262144 16777216 net.ipv4.tcp_wmem 4096 65536 16777216这里有一个经验公式缓冲区大小至少要大于等于带宽时延积BDP。BDP 等于链路带宽与往返时延RTT的乘积。举个例子内网 10Gbps 带宽RTT 0.5msBDP 就是 10Gbps × 0.0005s 5Mb 625KB。如果接收缓冲区只有 128KB即使网络是 10G单连接吞吐最高也只能跑到约 2Gbps因为窗口大小限制了在途数据量。所以判断缓冲区够不够先算 BDP再对配置。一般我会留出 2 倍余量把单连接接收缓冲区调到 BDP 的两倍以上这样既不会浪费内存也不会限制吞吐。3.3 TCP_NODELAY 与延迟确认一对经常打架的参数数据交互阶段最常见的性能杀手其实是应用层设置不当的两个机制Nagle 算法和延迟确认Delayed ACK。Nagle 算法的作用是合并小包发送端有未确认数据时不允许发送新的小数据包必须等之前的包被 ACK 后再发。这个机制在 telnet 这种交互式场景下可以减少小包数量但对高吞吐的 API 服务没有任何好处反而会带来额外的延迟。所以大多数网络编程框架默认都会在 socket 上设置TCP_NODELAY关闭 Nagle 算法。问题在于关闭了 Nagle 之后如果内核的延迟确认机制还在就会产生一个经典的 40ms 延迟问题。延迟确认的意图是减少 ACK 包数量接收方收到数据后不立即回 ACK而是等待最多 40ms 或与下一个数据包一起回复。当发送端关了 Nagle 快速发送数据接收端却迟迟不回 ACK发送端的窗口就得不到释放。这个问题的另一面同样存在如果发送端开着 Nagle 而接收端开着延迟确认就形成了“Nagle 等 ACK、延迟确认等数据”的互相等待典型表现就是连续几个小包之后卡顿一下。在跨地域的 API 调用里这个卡顿会直接被用户感知为“偶尔慢一下”。生产环境我的做法是应用层一律开启TCP_NODELAY同时把内核的tcp_autotuning和延迟确认控制在合理范围。Linux 上延迟确认的时间不由单独参数控制它由tcp_delack_min决定但该参数不建议随意调小调小了会增加 ACK 包数量对高吞吐场景反而有负面效果。更稳妥的方案是在应用层优化减少不必要的零碎小包写入尽量用批量写入、批量 flush。3.4 实测一个典型大流量传输场景的参数组合我之前调过一个日志采集服务单机每天处理约 2TB 日志模式是大量并发长连接持续推送数据。当时的症状是带宽利用率只有 30%CPU 不高但每个连接的吞吐始终上不去。定位过程是这样的先看ss -i里的 cwnd 和 rwnd 数值发现 rwnd 很小只有几十 KB说明接收缓冲区限制了窗口。再查net.core.rmem_max确实是默认的 212992 字节约 208KB。结合内网 10G 带宽、RTT 0.3ms 算 BDP理论需要的窗口是 10Gbps × 0.0003s ≈ 3.75Mb ≈ 469KB。默认的 208KB 直接砍掉了一大半。当时的调整方案net.core.rmem_max 8388608 net.core.wmem_max 8388608 net.ipv4.tcp_rmem 4096 262144 8388608 net.ipv4.tcp_wmem 4096 65536 8388608调整后重新压测单连接吞吐从约 3Gbps 提升到了接近 8Gbps带宽利用率翻了接近三倍而且因为缓冲区上限才 8MB即使有 2000 个并发连接最坏情况内存占用也不会超过 16GB属于可以接受的范围。这里还要提醒一个坑调大缓冲区之后如果应用读数据的速度跟不上接收缓冲区会积压大量数据表现为Recv-Q持续走高最终触发系统内存压力。所以大缓冲区一定要配合应用的消费能力来评估不要盲目设成几十 MB。4. 拥塞控制从默认算法选型到初始窗口调整4.1 拥塞控制的本质探测网络容量而不是把数据一股脑发出去TCP 的可靠传输建立在“确认”机制之上但确认本身是有成本的。发送方不能无限地向网络里灌数据因为路由器、交换机的缓冲能力有限如果发送速率超过中间设备处理能力数据包就会被丢弃而丢包又会触发重传最终让吞吐反而下降。拥塞控制就是一套用来探测网络可用带宽、并动态调整发送速率的机制。拥塞控制的核心变量是拥塞窗口cwnd它表示发送方在未收到 ACK 的情况下允许发送的最大字节数。cwnd 不是固定的它遵循一条经典的锯齿曲线慢启动阶段指数增长探测到丢包后减半然后进入拥塞避免阶段线性增长再次丢包再次减半。在这个循环里发送方不断逼近网络的容量上限又不断因为超调而退回去。理解这个机制对调优很重要因为很多参数的作用对象就是 cwnd 的变化节奏。比如初始 cwnd 太小一条新连接要花很多个 RTT 才能把发送速率提上来再比如空闲后 cwnd 会重置导致突发流量在冷连接上表现很差。针对这些问题内核提供了对应的调整手段。4.2 拥塞控制算法选型cubic、bbr 还是不想换Linux 默认的拥塞控制算法是 cubic它基于丢包反馈来调节窗口在高带宽、大时延的网络下表现不错但在链路本身有抖动或丢包率较高的环境里cubic 会把随机丢包误判为拥塞导致窗口大幅收缩吞吐波动明显。Google 提出的 BBR 算法则是基于瓶颈带宽和往返时延的测量来调节发送速率而不是依赖丢包。BBR 在长肥网络和高丢包率链路上通常能获得比 cubic 更高的吞吐这也是为什么很多 CDN、视频推流服务愿意切到 BBR。但它也不是万能的BBR 在深队列的网络设备上可能侵占其他流量的带宽在浅队列场景表现也有分化而且对内核版本有要求Linux 4.9 才支持。那么生产环境怎么选我给一个实用建议如果业务主要跑在丢包率低于 0.1% 的可靠网络比如云内网、同城专线cubic 完全够用没必要折腾如果链路跨地域、长距离、或者经过无线/共享网络BBR 通常效果更好。切换方法是sysctl net.ipv4.tcp_congestion_controlbbr如果没有 bbr先确认内核是否加载了模块modprobe tcp_bbr对于某些场景我还会用tcp_vegas它基于 RTT 的增大来提前降低发送速率比 cubic 更温和适合延迟敏感型业务。但 vegas 与网络设备自身的 buffering 配合度要求高在深缓冲网络里效果不如 cubic。所以我最终的生产配置基本是核心交易链路用 cubic跨地域大数据传输链路用 bbr。还有一个容易忽略的点拥塞控制算法的选择会影响接收端窗口的利用率。如果网络延迟不稳定cubic 的窗口会频繁收缩这时候即使你调大了缓冲区吞吐也可能上不去。所以我在调 BBR 时经常会同时配合调大缓冲区两者作用方向一致能获得叠加效果。4.3 初始拥塞窗口与空闲重置两个隐藏较深的参数很多新连接在起步阶段都会白白浪费几个 RTT。TCP 默认的初始拥塞窗口initcwnd在 Linux 2.6.39 之后是 10 个 MSSMSS 通常为 1460 字节也就是约 14.6KB。对于内网来说这个值够用因为 RTT 很短但对于跨地域的请求RTT 高达几十毫秒每增加一个 RTT 就多几十毫秒延迟慢启动的前几个 RTT 几乎都耗在探路上数据量没发多少。我曾经在一个 API 网关压测中发现一个 30KB 的响应体在 RTT 约 40ms 的长连接上要将近 300ms 才能传输完但抓包发现实际数据传输只有几个 RTT 就完成了剩下的时间全在等 ACK 和调整窗口。后来把initcwnd从 10 调大到 32响应时间下降非常明显。调整初始窗口的操作不是通过 sysctl而是通过 ip route 命令ip route change default via 192.168.1.1 dev eth0 initcwnd 32 initrwnd 32注意initcwnd和initrwnd分别控制发送和接收的初始窗口。如果服务器有多个路由规则需要分别调整。如果不想用命令临时改可以配置 NetworkManager 或 systemd-networkd 的脚本在网卡启动时自动执行。另一个参数是net.ipv4.tcp_slow_start_after_idle它控制连接空闲一段时间后是否重置 cwnd。默认是 1也就是重置。对于长连接会闲置几秒但希望复用既有窗口的应用比如数据库连接池、消息推送长连接可以设置为 0sysctl -w net.ipv4.tcp_slow_start_after_idle0这样空闲后的连接不需要重新慢启动能够更快恢复高速传输。但这个参数对短连接没有意义因为短连接根本活不到“空闲”那一刻。4.4 实操用 ss 观测 cwnd 的变化全过程调拥塞控制参数最怕调了之后没有反馈不知道是否生效。我习惯用ss -i来实时观察 socket 的 cwnd 和 rwnd。以一条本机自连测试连接为例ss -i输出的关键字段包括cwnd拥塞窗口、rwnd接收窗口、ssthresh慢启动阈值、rtt平滑后的往返时延。在传输大文件时观察 cwnd 是否从初始值快速上升直到超过 ssthresh 后进入线性增长阶段就能判断慢启动和拥塞避免是否正常工作。如果发现 cwnd 增长很慢先确认是不是触发了tcp_slow_start_after_idle的重置再看initcwnd是否生效。若initcwnd改成了 32但 ss 里新连接的 cwnd 仍然显示 10多半是路由规则没生效检查ip route show里目标网段是否走的是你改过的那条路由。还有一个建议在压测阶段抓包看丢包重传确认拥塞控制算法是否在正确工作。用 tcpdump 抓到重复 ACK 或乱序包后对照 cwnd 曲线可以清楚看到算法何时触发了快速重传、何时进入拥塞避免。这一步对理解 BBR 和 cubic 的行为差异尤其有效。5. 最常翻车的三个问题TIME_WAIT、队列溢出、窗口不足5.1 TIME_WAIT 堆积与客户端端口耗尽高并发短连接场景下连接关闭后主动关闭方会进入 TIME_WAIT 状态持续 2MSLLinux 上默认 60 秒。对于服务端大量 TIME_WAIT 本身不一定导致故障因为它只占一个 socket 表项真正要命的是客户端。客户端主动关闭连接后本地端口会被占用 60 秒。如果客户端的并发连接数高于本地端口范围默认从net.ipv4.ip_local_port_range指定的范围开始通常是 32768 到 60999约 28000 个端口端口就会耗尽新连接无法建立。我见过一个爬虫系统在高峰期直接报出Cannot assign requested address就是因为调用外部 HTTP 接口时单机短连接数超过了端口上限。解决方案有几种。第一调大本地端口范围net.ipv4.ip_local_port_range 1024 65535第二开启tcp_tw_reusenet.ipv4.tcp_tw_reuse 1tcp_tw_reuse允许内核在满足条件时复用处于 TIME_WAIT 状态的连接前提是启用 TCP 时间戳。这个参数对发起连接的一方有效。这里有一个很多老教程还在讲的坑tcp_tw_recycle在 NAT 环境下会导致大量连接被丢弃因为这个机制依赖时间戳做校验NAT 后面多台机器的时钟偏差会让校验失败。Linux 4.12 之后内核已经移除了tcp_tw_recycle所以新内核上不用再考虑它。第三从应用层做长连接改造。短连接带来的 TIME_WAIT 无论怎么调参数都只是延缓问题真正的治本方案是连接复用也就是把 HTTP 短连接改成 HTTP/1.1 keep-alive 或 gRPC 长连接。我自己的项目里通常优先做应用层改造内核参数只是兜底手段。5.2 排除诡异故障的一个技巧看 ListenDrops 而不是只看握手日志很多时候你觉得连接建不上但应用日志里没有异常抓包也看到三次握手完成了。这种“握手成功但连接消失”的现象多半是连接被内核的某种机制丢弃最常见的就是全连接队列溢出。抓包看不出来队列溢出因为队列溢出发生在三次握手完成之后、应用 accept 之前这一瞬间内核已经把连接从半连接队列移到了全连接队列然后发现队列满了直接丢包。应用完全感知不到客户端却会一直等待数据响应直到超时。所以排查时不要只看握手包要看内核统计cat /proc/net/netstat | grep Listen如果ListenOverflows和ListenDrops一直在涨说明全连接队列是真的不够。配合ss -lnt确认 Send-Q 等于上限基本就能锁定问题。另一个隐藏场景是 CPU 软中断跑满导致内核无法及时处理 ACK 包和应用无法及时 accept。这种情况即使队列没满新连接也会因为处理不过来而被丢弃表现出和队列溢出类似的症状。5.3 缓冲区不足导致吞吐长期上不去吞吐上不去第一个要排查的点是接收窗口是否是瓶颈。前面提到过 BDP 的计算这里再补充一个实操检查方法。用ss -i看 rwnd 和 cwnd如果 rwnd 远小于 cwnd说明接收方窗口限制了下行速度即使发送方想发快也发不了。如果 cwnd 远小于 rwnd大概是发送方的拥塞窗口上不去此时重点看当前使用的拥塞控制算法、网络丢包率、路由的 mtu 等。还有一种容易误判的场景单连接吞吐上不去但并发调大后总吞吐反而更差。这通常是每个连接的缓冲区之和超过了内存承受能力系统频繁进行页面回收和 swap反而拖慢整体性能。这种时候正确的做法是把单连接缓冲区调小一点用更多连接来分摊总吞吐。5.4 调优工具与验证方法速查最后整理一份我常用的工具清单方便你直接对照使用工具用法用途sysctlsysctl -a查看或设置内核参数ssss -lnt, ss -i查看队列状态、socket 窗口和拥塞窗口netstatnetstat -s查看 TCP 协议统计、丢包计数sarsar -n TCP,ETCP 1观察 TCP 连接数、队列溢出趋势tcpdumptcpdump -i eth0 tcp port 8080抓包分析握手和重传过程ip routeip route show查看路由项与 initcwnd 配置压测时一定要控制变量。我通常的做法是先跑一个 5 分钟基线记录吞吐、延迟、丢包数改一个参数后再跑 5 分钟对比两组数据。如果同时改了好几个参数必须能明确区分每个参数贡献了多少。方法是在同一台机器上做 A/B 对比或者用 tc 的 netem 模块模拟不同延迟和丢包验证调参效果后才能上生产。类似的经验还有一个小扩展如果你们团队用了 4.x 以上内核tcp_available_congestion_control可以快速查看当前可用的算法模块。查看当前生效的算法sysctl net.ipv4.tcp_congestion_control如果发现系统缺乏某个算法模块优先安装内核模块而不是直接改 sysctl否则参数会在重启后失效。我自己的体会是TCP/IP 协议栈调优不是改几个参数就能一劳永逸的事它更像是一个持续测量、调整、验证的循环。每次上线新业务或更换网络架构比如从同机房迁移到跨云都要把从三次握手到拥塞控制这条链路的基线数据重新跑一遍确保参数匹配新的网络特征。另外改动生产参数前至少先在一台低流量实例上观察一段时间确认内存、CPU、网络丢包都没有异常再全量下发这样能大幅降低调优引发的风险。