Linux底层网络实战:UDP/TCP收发链路、状态机与调优
2026/9/9 14:51:58 网站建设 项目流程

搞过 Linux 网络问题的人应该都有这种感觉:应用层代码写得挺顺,数据一上网络就变得神神秘秘。TCP 粘包、UDP 丢包、连接突然被 reset、端口一会儿能绑一会儿不能绑——这些表象背后全是内核协议栈在起作用。这次我把 Linux 底层 UDP/TCP 的收发链路、状态切换、缓冲区机制重新走了一遍,也把调试过程中踩过的坑和验证过的工具链整理成文。无论你是做 C++ 服务端、嵌入式 Linux,还是只想知道 WSL2 里 UDP 通讯为什么比原生系统更难搞,这篇内容都值得花十分钟看完。

1. 到底哪一层在干活:先拆清 Linux 网络的分层地图

1.1 应用层看到的 socket 和内核看到的 socket,是两个世界

很多教程一上来就讲三次握手、四次挥手,但实际调问题的时候,真正的难点往往在上层应用对 socket 的认知和内核的实际情况对不上。比如你用socket(AF_INET, SOCK_STREAM, 0)创建一个 fd,这个 fd 在用户态只是一个整数,但内核里它关联着一整套结构体链:struct socketstruct sockstruct sk_buff,还有收发两个方向上各自的队列和等待队列。

我自己的理解是,应用层拿到的 fd 只是一个“遥控器”,真正处理数据的是内核里的sock对象。这个对象管理着连接状态、接收缓冲区、发送缓冲区、拥塞控制状态、重传定时器等一大堆东西。你调用send()的时候,数据并没有直接跑到网卡上,而是先拷贝到内核的发送缓冲区,然后由内核协议栈在合适的时机把它封装成数据包发出去。这也解释了为什么send()返回成功只代表数据进了内核缓冲区,不代表对端一定收到了。

1.2 数据从应用到网卡的完整旅程

从应用层角度看,一次 send 的数据路径大致是:应用调用send()→ 系统调用进入内核 →sock_sendmsg()→ 传输层协议的处理函数(TCP 走tcp_sendmsg(),UDP 走udp_sendmsg())→ 网络层(IP 层)路由和分片 → 邻居子系统(ARP/ND)→ 网卡驱动 → 真正发到物理链路上。

这个过程里最容易忽略的是:TCP 的tcp_sendmsg()不一定把你给的数据立刻封装成包发出去,它要考虑 Nagle 算法、拥塞窗口、发送窗口等一堆因素;而 UDP 的udp_sendmsg()则相对“直来直去”,基本上你调用一次,它就尝试把一个数据报发出去,但同样要经过路由、邻居子系统和网卡队列。如果网卡的队列满了,数据包会被丢弃,这个丢包点是在驱动层,应用层很难感知到,只能靠对端没回应来反推。

所以说,“底层”两个字不是指你去改内核源码,而是你要对整个链路的数据流动和缓冲位置有概念。这样遇到“UDP 收发不稳定”的问题时,才知道先查应用层缓冲区、再查内核缓冲区、最后看网卡队列的排查顺序。

1.3 理解端口、地址和连接的本质

还有一个经常让人绕晕的话题:端口和连接到底是什么关系。TCP 连接是由四元组唯一确定的:源 IP、源端口、目的 IP、目的端口。你可以用同一个本地端口去连接多个不同的远端服务,只要四元组不重复就合法。而 UDP 更宽松,同一个 UDP socket 可以收发任意对端的数据,除非你调用了connect()去固定对端地址。

搞懂这个结构,很多报错就很好理解了,比如bind: only one usage of each socket address通常意味着你要绑定的 IP 和端口组合已经被占用,可能是别的进程在监听,也可能是你自己有个 socket 处于 TIME_WAIT 状态没释放。后面我会专门讲这些排查场景。

2. TCP 的状态机:握手、挥手和那些“卡住”的连接

2.1 三次握手不是三次发送,是三个状态变迁

TCP 三次握手是面试高频题,但真正去ss -tnp看连接状态时,很多人会疑惑“为什么服务端会看到 SYN_RECV?”“为什么客户端会先看到 SYN_SENT?”

三次握手本质上是在客户端和服务端之间完成序列号同步和窗口参数协商。客户端先发 SYN,服务端收到后进入 SYN_RECV 状态并回复 SYN+ACK,客户端收到后进入 ESTABLISHED 并回 ACK,服务端收到这个 ACK 才进入 ESTABLISHED。注意,服务端的 ESTABLISHED 比客户端晚一步,这个细节在某些高并发场景下会导致客户端以为连接可用、服务端还没来得及把连接放进 accept 队列的情况。

如果ss里看到大量 SYN_RECV,基本可以判断是服务端收到 SYN 之后回包给客户端失败,或者客户端根本不回 ACK。这时候要查的可能不是应用层,而是 syn backlog 队列是否溢出、防火墙是否丢弃了回包。

2.2 四次挥手和时间等待的坑

四次挥手相比握手要更微妙。主动关闭方发送 FIN 进入 FIN_WAIT_1,对端回 ACK 进入 FIN_WAIT_2,对端再发 FIN 进入 TIME_WAIT,最后主动方回 ACK 然后等待 2MSL 才彻底关闭。TIME_WAIT 的 2MSL 等待期是为了保证最后一个 ACK 如果丢了,对端重发的 FIN 还能被正确处理。

TIME_WAIT 本身是设计上的必要机制,但问题在于服务端如果主动关闭连接,会产生大量 TIME_WAIT,在短连接高并发的场景下,端口会被占用,导致bind失败或connect报错。网上很多方案是调net.ipv4.tcp_tw_reusetcp_tw_recycle,这里要给个忠告:tcp_tw_recycle在 NAT 环境下会导致严重问题,现在的新内核很多已经默认移除了这个选项,千万别为了省 TIME_WAIT 引入更大的坑。

2.3 收到 RST 和收到 FIN 是两码事

很多 C++ 服务端开发者会困惑“为什么客户端直接断开时,服务端读到的不是 0 而是 ECONNRESET”。这里要区分 FIN 和 RST:正常关闭连接发送 FIN,收到 FIN 后read()返回 0;异常关闭则直接发 RST,收到 RST 后,如果之前有未读数据或继续发起读写,就会报Connection reset by peer

实际排查中,curl: (35) TCP connection reset by peer这个报错很常见,通常不是服务端没起来,而是服务端 accept 之后又立刻把连接关了,或者中间网络设备发了 RST。要定位是哪个环节,最有效的办法是两端同时抓包,看 RST 的发送方是谁。

3. UDP 为什么“看起来简单,用起来想骂人”

3.1 无连接不代表可以随便写

UDP socket 创建之后可以直接sendto()给任意地址发数据,不需要连接过程,这样设计的好处是延迟低、实现简单,但也带来一个典型的坑:你发出去的每个数据报都是独立的 IP 包,底层没有任何确认、重传、排序机制,所以丢包、乱序是常态而不是异常。

在 Linux 上做 UDP 通讯时,尤其要注意发送缓冲区大小和接收缓冲区大小。发送缓冲区如果满了,sendto()会返回 EAGAIN 或 EWOULDBLOCK(非阻塞模式下);接收缓冲区如果满了,新到的数据报会被内核直接丢弃,而应用层根本不知道——这就是“UDP 收着收着突然少了一包”的主要原因。

3.2 UDP 的接收缓冲区和发送缓冲区怎么配

UDP 的收发缓冲区分别对应内核参数net.core.rmem_default/net.core.rmem_maxnet.core.wmem_default/net.core.wmem_max。但要注意,设置 socket 缓冲区时,内核实际允许的最大值受rmem_maxwmem_max的限制,你调setsockopt(SO_RCVBUF)传一个大于上限的值会被静默截断,而且只有在进程启动早期调用才有效。

比较稳妥的做法是先通过sysctl调大内核上限,再在代码里用setsockopt设置目标值。比如你要在 UDP 高吞吐场景下收流,建议把/proc/sys/net/core/rmem_max调到 16MB 以上,然后在代码里设 8MB 左右的接收缓冲。另外,UDP 单包大小也有限制,默认 MTU 1500 的网络上,UDP 负载建议控制在 1472 字节以内,否则会触发 IP 分片,分片包一旦丢失,整个数据报都废了,反而更容易丢数据。

3.3 UDP 和 TCP 的选择不是“快就行”

每次有人问我“TCP 和 UDP 怎么选”,我第一反应是先反问:你的业务能不能接受丢包重传?能不能容忍乱序?比如工业场景里的 Modbus TCP 就必须选 TCP,因为它要可靠的请求响应;而音视频传输、游戏位置同步这类对实时性要求高、可以容忍少量丢失的场景,UDP 是更合理的选择。

ROS 机器人通信里大量使用 UDP 也是同样的理由:传感器数据高频产生,丢一帧还可以接受,但如果为了等一帧重传把后面的数据全堵住,整个控制环路就崩了。这个场景要特别注意的是,UDP 虽然快,但如果你在局域网里跑,反而要关注交换机是否有流控策略在丢弃 UDP 组播/广播包——很多交换机默认对广播包做了速率限制。

4. 关键工具与实测:怎么把“底层”真正看到

4.1 ss 和 netstat:先看状态再看队列

排查任何网络问题,第一步不是抓包,而是看当前系统的连接状态。我现在基本不用 netstat 了,ss输出更快也更详细。常用组合是ss -tnp看 TCP 连接的状态和对应进程,ss -unp看 UDP socket 信息。

值得多看一眼的是ss -tnp state time-wait能快速列出所有 TIME_WAIT 连接,ss -tnp | grep SYN_RECV能找到半连接。如果你发现 service 端口明明在监听但客户端连接失败,第一件事就是看ss -lnp确认监听地址是不是 127.0.0.1 而不是 0.0.0.0,这个经典问题至少坑过我两次。

4.2 tcpdump 抓包看底层的真实动作

很多人习惯直接看 Wireshark,但在服务器上没法开图形界面,tcpdump才是标配工具。抓 TCP 握手用tcpdump -i any tcp port 8080 -nn,抓 UDP 打流用tcpdump -i eth0 udp port 5005 -nn -c 100。抓包文件保存下来再拉到本地导入 Wireshark,可以看握手延迟、重传、乱序、窗口变化等细节。

有一次我排查一个 TCP 传输速率上不去的服务,抓包发现客户端持续在发零窗口通告,说明接收端应用层消费太慢,接收缓冲区被占满,这不是网络问题而是应用处理瓶颈。这种问题不看抓包是永远猜不到的。抓包时要注意缓冲区可能丢掉部分包,如果需要精确统计,用-s 0抓全长包,并且尽量在终端别打印太多内容,直接-w写文件。

4.3 iperf3 打流:验证你的协议栈性能

想验证两台 Linux 主机之间的 UDP/TCP 底层最大吞吐和丢包率,iperf3 是最顺手的工具。TCP 打流直接iperf3 -s在服务端跑起来,客户端iperf3 -c <server_ip>就能测出带宽。UDP 打流要用iperf3 -u -c <server_ip> -b 100M指定目标带宽,测试结束后会输出实际吞吐、丢包率和抖动。

这个工具的厉害之处在于能直接逼出收发缓冲区配置不当的问题。我测试时经常发现:UDP 打流到 50Mbps 还好,一旦调到 200Mbps,丢包率直接飙到 5% 以上——但线上业务流量明明没那么大,后来才反应过来是默认的 212KB 接收缓冲区根本扛不住瞬时突发流量。用iperf3配合ss -unp查看 socket 的接收队列积压情况,基本就能实锤缓冲区问题。

实测建议:iperf3 的 UDP 测试命令加-R可以测反向流量,加-t 30指定测试时长,建议跑 30 秒以上避免冷启动阶段的干扰。

4.4 用 /proc 和 sysctl 查看当前内核网络参数

很多网络参数不用重启就能改,也不一定需要装额外工具,直接看 /proc 就行。cat /proc/sys/net/ipv4/tcp_tw_reusecat /proc/sys/net/core/rmem_max这些命令简单直接。批量看的话用sysctl -a | grep -E 'tcp_|udp_|rmem|wmem'

我习惯在写网络服务之前先跑一组自定义脚本把关键参数打出来,这样调优有据可依。具体的参数包括:

  • net.core.rmem_max:所有协议栈的接收缓冲上限调大依据
  • net.core.wmem_max:发送缓冲上限
  • net.ipv4.tcp_rmem:TCP 自动调优的接收窗口最小值/默认值/最大值
  • net.ipv4.tcp_wmem:TCP 发送窗口三档
  • net.ipv4.tcp_max_syn_backlog:半连接队列长度
  • net.core.somaxconn:全连接队列上限

这几个参数单独拎出来都要看应用场景,不能一味调大。比如全连接队列调太大,应用 accept 速度跟不上,积压的连接反而会消耗内存。

5. 高频报错和排查实录:从 bind 失败到 Connection reset

5.1 端口绑定失败:address already in use 的三种可能

后台服务重启时经常碰到bind: Address already in use,第一反应是进程没退干净,但排查完发现根本没有对应进程。这时候大概率是 TIME_WAIT 状态的连接占用了本地端口。解决方案有几个:

  1. 如果是服务端端口被占用且无法重启服务,用ss -tlnp找到 PID,kill 掉或者等待释放。
  2. 如果是客户端端口不够用导致无法发起连接,调大net.ipv4.ip_local_port_range范围,或者打开net.ipv4.tcp_tw_reuse(只对客户端有意义)。
  3. 如果是因为SO_REUSEADDR没设置,服务端重启时会失败。在监听 socket 上启用SO_REUSEADDR是标配操作,C++ 里就是在 bind 之前调用setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, ...)

还有一种是 Docker 部署场景的报错:error response from daemon: ports are not available: exposing port tcp 0.0.0.0:xxxx。这个本质是宿主机端口被占用了,用ss -tlnp | grep xxxx查到占用进程后处理即可,不一定非要去 Docker 配置里改动。

5.2 connect 被 reset 和 bind 冲突的代码级原因

curl: (35) TCP connection reset by peer基本涵盖了这几类情况:

  • 服务端端口开着,但 listen 队列已满,内核直接回了 RST
  • 服务端 accept 之后立刻 close,客户端还没来得及发数据就被 RST
  • 中间有防火墙或负载均衡设备主动发了 RST
  • 服务端并发模型写错了,子进程继承了监听 fd 导致 accept 混乱

从代码角度,服务端不要对还没完成业务处理的连接随意 close,尤其是 HTTP 服务,不要因为框架默认行为就直接断开。抓包确认 RST 是服务端发出的还是网络中间设备发出的,能省下大量瞎猜的时间。

5.3 WSL2 里 UDP 与 Windows 宿主机通讯的特殊问题

WSL2 的网络是 NAT 模式,虚拟机有自己的 IP,跟 Windows 共享一套端口映射。很多用户会遇到“Windows 上跑 UDP 客户端连不上 WSL2 里的 UDP 服务端”,这通常是因为 WSL2 没有端口转发到 Windows 宿主。如果你在 WSL2 里跑 UDP 服务端,Windows 里的程序要用localhost去访问,一般需要 Windows 侧做一次netsh interface portproxy的 UDP 转发;而 WSL2 访问 Windows 宿主反而比较简单,直接用 Windows 的局域网 IP 或特殊 DNS 解析地址即可。

内核参数方面,WSL2 里跑高吞吐 UDP 也会受默认缓冲区限制,同样通过/etc/sysctl.conf调整net.core.rmem_maxnet.core.wmem_max即可,但要注意 WSL2 重启后参数会恢复默认值,需要配置启动脚本加载。

5.4 UDP 多线程收发乱序和丢包

多线程收发 UDP 时容易遇到两个问题:一是多个线程同时recvfrom()同一个 socket,内核会把数据报分给不同线程,处理完再发出去就会出现顺序变化;二是线程的接收缓冲区如果独立设置过小,某个线程稍微慢一点就会导致内核队列溢出丢包。

从架构上解决顺序问题,通常是一个接收线程收包后丢进无锁队列或带序号的消息队列,再由工作线程池处理。如果需要并行收包又保证顺序,可以用多队列网卡配合SO_REUSEPORT,让每个线程绑一个独立的 socket,但这时顺序性同样要靠协议层序号保证。

UDP 丢包问题除了缓冲区,还有一个容易忽略的坑是校验和。新内核上 UDP 接收会校验 checksum,如果硬件网卡的 checksum offload 功能和驱动配合不好,可能出现收到的全是坏包然后被静默丢弃。可以用ethtool -k eth0查看 rx-checksumming 是否开启,遇到怪异的丢包问题可以尝试关闭 offload 验证。

6. 从 socket 参数到内核调优:核心服务器的缓冲区和队列配置

6.1 TCP 的接收窗口和发送窗口为什么不能只调一个

TCP 的收发是滑动窗口机制,发送方发的数据不能超过接收方通告的窗口大小。这个窗口大小由接收方的接收缓冲区剩余空间决定,但又受tcp_rmem的最大值限制。所以你只调大发送方的tcp_wmem没有用,因为数据能不能发出去,最终要看接收方有没有足够空间收。

在 C/S 两边都是 Linux 的情况下,正确做法是两边同时调大tcp_rmemtcp_wmem。以千兆网络传输大文件为例,理想状态是带宽延迟积(BDP)等于窗口大小,1000Mbps 带宽、10ms RTT 的话,窗口至少要 1.25MB 才能跑满带宽。默认的 16KB 初始窗口当然跑不满,必须调大。

6.2 TCP 连接队列:accept 之前发生了什么

进程调用listen(fd, backlog)之后,内核会维护两个队列:半连接队列(SYN Queue)和全连接队列(Accept Queue)。三次握手还在进行、没有完成最后一步的连接进半连接队列;完成握手的连接等应用调 accept 时从全连接队列取走。

backlog参数和内核参数net.core.somaxconn共同决定全连接队列的最大长度。比如你调listen(fd, 128),但somaxconn是 128,那么全连接队列就是 min(128, 128)=128。很多框架如 Nginx 的backlog配 511,就要求 somaxconn 至少调到 1024。

全连接队列满了之后,后续的握手成功连接会被内核丢弃或者直接回 RST,表现上就是客户端 connect 成功率下降、服务端压力不大但请求失败。用ss -lnt看 Send-Q 可以查全连接队列的大小,ss -tnp看溢出计数则要看netstat -s | grep overflow

6.3 常用系统级参数速查和建议值

以下是我在长期维护服务端时总结出的一组基准值,不是万能药,但作为起步配置非常管用:

参数默认值建议值适用场景
net.core.somaxconn1281024高并发短连接
net.ipv4.tcp_max_syn_backlog1281024SYN 洪水防护或突发连接
net.core.rmem_max21299216777216大流量 UDP/TCP 接收
net.core.wmem_max21299216777216大流量发送
net.ipv4.tcp_rmem4096 87380 62914564096 87380 16777216高 BDP 传输
net.ipv4.tcp_wmem4096 16384 41943044096 16384 16777216高 BDP 传输
net.ipv4.ip_local_port_range32768 609991024 65535客户端连接数不足

改完用sysctl -p生效,如果希望重启后保留,写入/etc/sysctl.conf或独立的/etc/sysctl.d/99-network-tuning.conf。还要注意:Docker 容器内看到的是容器自己的 sysctl 参数,和宿主机不是同一个命名空间,需要--sysctl参数或宿主机级调优才能影响容器。

7. Linux 网络底层知识在实际工作里怎么用

做嵌入式 Linux 开发和传统服务器开发的人,对 UDP/TCP 底层的需求有一点差异。嵌入式场景更关心弱网环境下的表现和多网卡路由选择;服务器场景更关心大并发连接和吞吐调优。但底层机制是通用的:理解内核缓冲区模型、理解连接状态机、理解抓包工具的输出,这三点打底,大多数问题都能定位到大致范围。

有不少读者问过我“怎么才算真正理解 TCP/IP”,我的答案是你看到一个新的网络性能问题,能先说出它可能发生在用户态、内核态、驱动层还是物理链路的哪一段。比如延迟高,是发送队列排队导致的还是对端处理慢;比如收包慢,是应用没读完缓冲区还是内核丢包。这个分层判断能力靠的就是多看内核协议栈的文档和实际抓包数据。

有一个值得养成的习惯:每次分析完一个网络问题,把当时的ss输出、tcpdump 抓包文件、内核参数快照保存下来,时间久了就是很好的排障参考素材。我自己维护了一个小型的案例仓库,遇到类似问题直接检索,效率高很多。

7.1 应用层代码和底层调优的配合

底层调优不是只改内核参数就完事,应用层代码必须配合才能生效。比如你设置了很大的接收缓冲区,但应用是单线程阻塞读取,处理不过来的话,缓冲区再大也只是延缓丢包时间。合理的架构是接收线程只负责把数据投递到队列,工作线程池来处理业务。业务处理慢时,队列要有背压机制,避免无限积压导致内存暴涨。

UDP 场景还有一个特有问题:包长度不固定,应用层必须自己处理报文边界。有些开发者直接用recvfrom()收到的长度来切分,但如果发送端每次发的消息不完整,接收端很容易出现拆包错位。好的做法还是在应用层协议里显式加上消息长度字段,收到数据后先解析长度再按长度读取完整消息。

7.2 抓包之外的新一代观测手段

tcpdump 能看包,但想看内核协议栈内部的行为,比如重传定时器触发、拥塞窗口变化,可以用ss -tin查看 TCP 连接的信息,里面会显示 cwnd、ssthresh、bytes_acked、重传次数等。对于更细粒度的内核追踪,bpftraceperf系列工具能直接挂到协议栈的函数上,比如跟踪tcp_retransmit_skb被调用时的参数。

对于不想碰 BPF 的开发者,用netstat -s看协议统计信息是最快的路径。它会输出 SYN 重传次数、丢包重传次数、连接重置次数、checksum 错误次数等聚合指标,比抓包更适合日常巡检。

8. 实践中的参数选择与性能验证

8.1 单机压测时要注意的无效调优

很多人调完参数就用 iperf3 本机回环测试,这里有个大坑:回环接口(lo)不经过物理网卡,也没有真实的网络延迟和丢包,测出的性能数据上线以后几乎没有参考价值。正确做法是准备两台物理机或两个 VM,用真实网卡互联测试,并且要用taskset把 iperf3 绑到特定的 CPU 核上,避免多核调度导致性能波动。

如果只能单机验证,也可以把 UDP 接收缓冲调大后,用假流工具往本机 UDP 口灌数据,观察ss -unp里的接收队列是否持续积压。这个方法能验证你的应用消费速度是否足够,但不能代表真实网络的带宽上限。

8.2 面向 C/S 场景的收发性能优化清单

我把平时做 C++ TCP 服务端优化的清单列出来,按优先级排序:

  1. 设置SO_REUSEADDR,避免重启端口冲突
  2. 调大listenbacklog 和 somaxconn,避免 accept 队列溢出
  3. 开启 TCP_NODELAY,禁用 Nagle 算法降低小包延迟
  4. 为长连接开启 TCP_KEEPALIVE,并设置合理的空闲时间和探测次数
  5. 调整收发缓冲区到合适大小,避免默认值太小导致吞吐受限
  6. 开启SO_REUSEPORT,让多个进程/线程可以共享监听端口,充分利用多核
  7. 大文件传输考虑启用TCP_CORKsendfile零拷贝减少用户态拷贝

其中第 6 条的SO_REUSEPORT要特别注意:它要求所有共享端口的 socket 都设置该选项,并且内核会按哈希把新连接分发到不同监听 socket,适合多进程模型的服务器。但如果你预期按连接保持会话状态,需要在应用层处理连接分配问题。

UDP 服务端也有对应的优化思路:使用SO_REUSEPORT开启多进程收包,每个进程独立recvfrom(),可以摊分内核锁竞争。实测中四进程收 UDP 流量比单进程吞吐提升明显,但前提是应用层能处理包乱序。

8.3 修改缓冲区之后如何验证生效

改了内核参数后不要只靠sysctl输出确认,直接在代码里打日志验证。getsockopt(fd, SOL_SOCKET, SO_RCVBUF, ...)能读到实际生效的接收缓冲区大小。这里有个小细节:内核会将你设置的值自动翻倍,因为内核要留出一半空间给协议的内部开销,所以你在代码里设 8MB,getsockopt读到的可能是 16MB 左右。

如果应用已经运行,又不能重启,临时调整内核参数是没用的,因为 socket 的缓冲区大小在创建时就已经参照当时的系统参数设置了。必须重启进程让新参数生效,这是很多人调参后发现没用的原因。

9. 给新手的一些心里话

学习 Linux 网络的时候,你不需要把内核源码全部读完,但你一定要会看数据流:数据从 send 调用开始,怎么一步步变成网络包,到了对端怎么一步步变成 read 的返回值。理解这个流程,你就理解了大半的底层知识。

网络问题排查里最怕的不是不懂协议,而是乱猜。我之前花过两天时间调一个 UDP 间歇性丢包问题,最后发现是测试用的两块网卡有一个协商到了 100Mbps 半双工,导致冲突丢包,和协议栈一点关系都没有。从那以后我就养成了先看网卡ethtool eth0确认速率和双工模式,再做其他排查的习惯。

另一个容易误导人的点是“iperf3 能打满带宽就说明服务没问题”。实际上,iperf3 默认用大包持续打流,跟真实业务的小包、突发流量特征完全不同。很多服务在高吞吐下没问题,但一遇到高并发小包就崩,就是因为包速率(pps)上去了,内核协议栈的处理能力成为瓶颈。验证包转发性能时,建议用iperf3 -u -l 64测小包 PPS,然后在发送侧用sar -n DEV 1观察每秒包量。

最后想说,任何调优都要在压测环境反复验证后再上生产。修改网络栈参数不像改业务配置那样能快速回滚,如果调大了缓冲区导致某个服务器内存吃紧,影响面会比想象中大得多。我见过因为有人顺手把net.core.rmem_max调成 512MB 然后忘记回收连接,最终文件服务器内存耗尽的事故。调参一时爽,运维火葬场,这句话是认真的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询