← 返回 Atlas
Linux 内核

Linux 内核网络栈

深夜两点,线上 Nginx 偶发 Connection refused,CPU 却纹丝不动。防火墙、后端、磁盘查遍,最后 `ss -lnt` 一看,443 的 Send-Q 写着 128——应用 backlog 写了 65535,却被 `net.core.somaxconn` 悄悄掐掉。协议栈的『静默截断』天天有。从网卡到 socket,这一路是 NAPI 的中断活锁、CUBIC 的锯齿、TIME_WAIT 的 60 秒,处处讨价还价。

内核 · 协议栈

一次 2.4.20 的生死抉择:关掉中断去轮询

2001 年 Gbit 网卡刚普及时,满速收包等于灾难:每来一个包就触发一次硬中断,CPU 被中断风暴打到 100%,吞吐却跌破 100Mbps——这就是中断活锁。内核 2.4.20(2002 年)合入 NAPI,由 Jamal Hadi Salim 等人实现:关掉中断,把网卡挂进软中断的 poll_list,由 net_rx_action 每轮按配额取包。今天驱动丢包先看这条路径:rx_no_buffer_count 增长,十有八九是 NAPI 轮询配额或 ring buffer 不够。反面教材同样真实:早期 tg3 在轻载下频繁开关中断反而多耗约 8% CPU,直到驱动学会编程硬件中断合并门限。

CUBIC 统治了十五年,BBR 凭什么截胡

2005 年 CUBIC 进内核,作者来自 NC State 的 Rhee 团队。它把拥塞窗口恢复画成一条三次曲线:凹段快速逼近上次丢包点,凸段放缓试探,2.6.19 起成为默认,一坐十五年。它的毛病也写在曲线上——靠丢包当反馈信号,高 BDP 的跨洋链路永远估不满带宽,对 bufferbloat 无感。2016 年 4.9 合入的 BBR 换了个思路:不问『丢了多少』,用瓶颈带宽 × 最小 RTT 直接建模链路容量(四态机见下文)。Google 两年内把搜索、YouTube、GCP 全部切到 BBR;到今天 sysctl net.ipv4.tcp_congestion_control 默认仍是 cubic。

listen(65535) 为什么只收 128:背压队列的静默规则

listen(fd, 65535) 能拿到 65535 的队列吗?不能。accept 队列长度取 min(应用 backlog, net.core.somaxconn),somaxconn 默认 128,65535 被静默截断成 128。ss -lnt 的 Send-Q 显示真实长度:Nginx 常见 511。Recv-Q 贴着 Send-Q 时说明 accept 队列饱和,内核丢新 SYN,表现是『偶发 Connection refused』。排查第一站:先看 Send-Q 是否被 somaxconn 掐了。只改内核不改进程(Nginx listen 443 backlog=65535;),问题原样还在。

TIME_WAIT 不是病,病的是想治它的教程

TIME_WAIT 常被当成内存泄漏,其实它是 PAWS 的守门员:连接关闭后要等 2*MSL(默认 60 秒),防止迟到的旧段串扰新连接,大量短连接挂着上万条是常态。真正翻车的是老教程爱教的 tcp_tw_recycle=1——它丢『时间戳比已回收连接更早』的入向包,NAT 后面的客户端时间戳一乱序就间歇性连不上,该参数新内核已移除。tcp_tw_reuse 常被误读:只对出站连接有效,服务端无效,还得 tcp_timestamps=1。调参先定量:ss -tan state time-wait | wc -l,再用 ip_local_port_range(28k 端口)除以 60 秒得 470,算并发上限。

实战示例

ss -lnt 上 Nginx 443 的 Send-Q 写着 511,而你 listen(fd, 65535) 的服务只有 128——内核取 min(backlog, somaxconn):前者是发行版把 somaxconn 调到了 511,后者被默认的 128 静默截断,不报错。

子关键词

NAPI 与 net_rx_action 软中断轮询取包的配额机制
net_rx_action 是 NAPI 软中断入口,从 poll_list 配额取包。丢包先看 rx_no_buffer_count,上涨就扩 ring buffer 或调 net.core.netdev_max_backlog
CUBIC 的三次曲线 默认拥塞控制算法的凹增长
CUBIC 把窗口恢复画成以丢包点为拐点的三次函数,凹段快速逼近上界、凸段试探。2.6.19 起成为默认直到今天。它的反馈信号是丢包,高 BDP 链路上估不满带宽,这正是 BBR 切入的地方。
BBR 的四态机 瓶颈带宽乘最小 RTT 建模
STARTUP 指数提速探测 BtlBw,DRAIN 排空队列,PROBE_BW 周期换带宽估测,PROBE_RTT 每 10 秒收敛一次 RTT。不靠丢包反馈,能跑满长肥链路,混跑公平性靠 BBRv2/v3 补。
TIME_WAIT 与 2MSL 防旧段串扰的 60 秒等待
四次挥手后主动关闭方停留 2*MSL(默认 60s),防止旧段被当成新连接数据。大量短连接下上万条是常态,先定量再决定动不动参。用 bind+SO_REUSEADDR 硬绕过会破坏 PAWS,别学。
网络命名空间 netns 每个容器独立的协议栈
2.6.24 由 Eric W. Biederman 引入,每 netns 有独立路由表、iptables 和 net.* sysctl。宿主机改了 net.* 不会进容器,镜像里要自己设;ip netns exec 可窥容器协议栈。
epoll 的 O(1) 唤醒 万级 fd 上的可扩展事件模型
Davide Libenzi 2003 年随 2.6 合入,把 select/poll 的 O(n) 全量扫描换成就绪链表加回调,注册的 fd 只在就绪时被唤醒。这是『万级连接一个线程』能成立的底层原因。

衍生角度

设计模式:中断换轮询

NAPI 把『每包一次硬中断』换成软中断轮询,是典型的拉模式替换推模式。代价也真实:轻载下频繁开关中断反而多耗约 8% CPU(tg3 早期回归),所以现代驱动加中断合并门限做负载感知。

概念拆解:拥塞窗口 vs BDP

一条 TCP 连接跑多快,由 min(瓶颈带宽, 窗口/RTT) 决定。CUBIC 按丢包恢复窗口,BBR 估 BtlBw×minRTT,分歧从 `ss -i` 的 cwnd 与 rtt 看起。

风格架构:可插拔的协议栈

拥塞控制是模块(tcp_cubic.ko/tcp_bbr.ko),路由表按命名空间隔离,netfilter 是钩子框架。各部件可替换,BBR 才得以两年内换掉默认算法而不动协议主体。

原理分析:静默的背压

协议栈传递压力靠『悄悄丢』:accept 队列满丢 SYN、conntrack 满丢新连接、backlog 超限直接取小。它不报错,只体现在计数器和偶发超时里——排障要先看计数器,别先怀疑路由。

实际用法与坑

  • 排偶发 Connection refused,先 `ss -lnt` 看 Send-Q 是否被 somaxconn 截断,再查应用 backlog。
  • 换 BBR 先 `sysctl net.ipv4.tcp_available_congestion_control` 确认有 bbr,再 `sysctl -w` 切换。
  • 收包丢包先 `ethtool -S eth0 | grep rx_no_buffer_count`,增长就 `ethtool -G eth0 rx 4096` 扩 ring buffer。
  • 动 TIME_WAIT 前先 `ss -tan state time-wait | wc -l` 定量,用端口范围除以 60s 估并发上限。
  • 调 sysctl 写进 /etc/sysctl.d/99-tuning.conf(字典序靠后能压住别人),改完 `sysctl --system` 生效。

下一步动手做

ss -lnt 看 Send-Q,sysctl net.ipv4.tcp_congestion_control 查算法,异常写进 /etc/sysctl.d/99-tuning.conf,再 sysctl --system

相关书籍

TCP/IP 详解(卷1:协议)

TCP 状态机、TIME_WAIT/PAWS、拥塞控制的权威出处,看 Vol.1 第 20 章与现代算法。

深入理解 Linux 网络技术内幕

内核侧 NAPI/软中断与 sk_buff 实现,补 tcpip 的协议行为视角,看接收路径章节。

关联关键词