1. 把 UDP/TCP 放回它们该在的位置先讲一个我遇到过的真实场景一个内部服务上线后监控上偶发出现几十毫秒的延迟尖刺运维第一反应是网络抖动网络那边查了一圈说没问题最后定位到的是服务端在写小包时开了 Nagle而客户端又开了延迟确认两个机制一撞40 毫秒就凭空冒出来了。这件事给我的教训很直接——UDP 和 TCP 这两份协议如果你只记住一个可靠一个不可靠那你在遇到问题时基本只能靠猜。它们决定了你的程序在内核里被怎么排队、怎么切包、怎么重传、怎么算超时这些细节全都会以偶发卡顿连接数异常带宽上不去的形式出现在你的监控面板上。这篇内容想做的事是把 UDP/TCP 从课本上的表格里拽出来放到实际编码、抓包、调参、排障的语境里重新讲一遍。它适合三类人写过 socket 但没深挖过内核行为的开发者正在做协议选型或自研传输的工程师以及被连接数、丢包率、TIME_WAIT 折腾过的运维同学。哪怕你目前只会写一个最简单的 TCP 回显服务看完之后也应该能回答几个具体问题为什么我的连接一多就卡、为什么 UDP 明明发了但收不到、为什么带宽跑不满、为什么短连接压测一上来就报端口耗尽。我不会按TCP 三次握手、四次挥手、UDP 头部八个字节这种教材顺序平铺而是按设计取舍—内核账本—实证抓包—线上排障这条线走。原因很实际协议本身是死的真正难的是判断在什么条件下应该选哪条路以及出了问题该往哪一层看。这两件事教材给不了只能在踩坑里积累。1.1 一次偶发丢包引出的分层视角很多人排查网络问题时习惯从网络两个字开始找原因其实第一步应该是确认自己站在哪一层。以太网帧、IP 包、TCP 段或 UDP 数据报这三个东西在抓包里长得很像但它们的边界和语义完全不同搞混了就会得出错误结论。举个具体例子。以太网的标准 MTU 是 1500 字节这是链路层能承载的最大载荷。IP 头占 20 字节不带选项剩下的 1480 字节交给传输层。TCP 头又占 20 字节不带选项所以MSS 的经典值就是 1460 字节而 UDP 头只有 8 字节载荷理论上能到 1472 字节。这个 1480/1460/1472 的数字差异是后面所有分片问题的起点。为什么强调这个因为当你在应用层一次性 write 了 3000 字节TCP 会帮你切成多个段分开发接收端按序号重组你永远看不到中间过程而 UDP 如果一次 send 了 3000 字节IP 层会把它切成两个分片发出去只要有一个分片在路上丢了整个数据报在接收端就会被直接丢弃内核连个错误都不一定报给你。同样一条链路同样的丢包率TCP 只是慢一点UDP 可能是数据直接消失。所以分层视角的实际价值是它告诉你故障的责任边界在哪。链路层看 MTU 和双工模式IP 层看路由和分片传输层看连接状态和缓冲队列应用层看读写逻辑。排查永远是从上往下确认现象、从下往上找根因两条线在中途对上答案就出来了。跳过任何一层去猜基本都在浪费时间。1.2 设计取舍可靠性和实时性不能同时最大化TCP 和 UDP 的差别本质不是功能多少的差别而是对不确定性的处理策略不同。TCP 选择把不确定性全部吃掉丢包就重传乱序就重排拥塞就降速代价是延迟不可预测、头部开销更大、连接状态要占内存。UDP 选择把不确定性原样交给上层发出去就不管收到什么算什么代价是可靠性、顺序、流量控制全部得自己实现。这个取舍在什么场景下会翻转我总结成三个判断维度。第一是延迟下限是否比平均延迟更重要。语音和视频通话里一个 200ms 前丢失的音频帧即使重传成功也毫无意义用户只听到卡顿。这类场景里宁可丢也不要等是硬需求UDP 是唯一选择。第二是连接数规模。TCP 每条连接在内核里都要占一份 struct sock、收发缓冲区、定时器十万连接就是实打实的内存和 CPU。UDP 是无状态的一个 socket 就能接几万个对端所以大规模广播、服务发现、监控上报这类场景用 UDP 或者UDP 应用层会话表更划算。第三是是否需要多播或广播。TCP 是点对点的物理上就不支持一对多UDP 天然支持组播这是做局域网发现、集群心跳广播时的唯一选项。反过来什么场景必须用 TCP凡是数据完整性不可妥协且延迟可以容忍的比如配置文件下发、数据库连接、文件传输、HTTP 请求。这些场景里少一个字节都是事故重传带来的几百毫秒完全可以接受。提示不要用TCP 慢、UDP 快这种说法做决策依据。在链路质量好的内网两者的吞吐差距远小于很多人想象在链路质量差的公网UDP 的裸吞吐反而可能更低因为丢包后没有任何退让机制会和别人的流量硬碰硬。1.3 四种典型场景的第一判断我把常见场景粗略归成四类每类的第一判断列在下表里。这张表不是让你背而是给你一个起点——真正落地时还要继续往下追问细节。场景类型典型例子第一判断关键理由请求-响应、数据不可丢配置拉取、数据库访问TCP完整性优先延迟可容忍实时流媒体音视频通话、直播UDP延迟下限优先旧数据无价值大规模短消息监控上报、日志采集UDP 或应用层批量 TCP连接开销 vs 可靠性需权衡一对多分发局域网发现、集群广播UDP 组播TCP 物理上不支持拿第三类展开说因为它最容易选错。假设你要做一件事几千个节点每隔几秒上报一次心跳。用 TCP每个节点一条长连接服务端要维护几千个 socket还要处理断线重连的惊群用 UDP服务端一个 socket 收所有包节点掉线靠多久没收到来判断逻辑简单得多代价是丢几个心跳无所谓——反正下一次还会来。但如果上报的是计费日志丢一条就是钱的问题那 UDP 就不能用了得走 TCP 长连接或者应用层批量确认。同一个业务里心跳和日志的正确选择可能完全相反这就是为什么不能一刀切地说我们全部用 UDP。2. TCP 的内核账本那些参数不是随便填的TCP 之所以让人头疼是因为它有大量状态和参数散落在内核里而这些参数平时看不见出问题时才冒出来。我习惯把 TCP 理解成一本内核账本连接是账户缓冲区是余额窗口是信用额度拥塞窗口是风控上限。你要调优本质上是在调这本账本的规则。不理解账本逻辑就照抄网上的 sysctl 配置大概率是负优化。2.1 状态机到底在防什么TCP 的状态机有十来个状态很多人背过就忘。我建议换个角度记每个状态存在的意义都是为了解决一个具体的消息可能丢失问题。三次握手解决的是双方都确认自己和对方的收发能力正常。客户端发 SYN服务端回 SYNACK客户端再回 ACK。为什么不能两次因为两次握手时服务端无法确认客户端能收到自己的包而且历史上滞留的重复 SYN 会让服务端白白建立连接。三次握手的最后那个 ACK 就是让服务端确认对方确实收到了我的 SYN。四次挥手解决的是双向关闭。TCP 是全双工的A 不想发了不代表 B 也不想发所以要分两次 FIN 各自关闭一个方向。主动关闭方进入 FIN_WAIT_1收到对方 ACK 后进 FIN_WAIT_2收到对方 FIN 后回 ACK 进 TIME_WAIT等 2MSL 后进 CLOSED。TIME_WAIT 是最容易被误解的状态。它存在的两个理由一是保证最后那个 ACK 能到达对方如果 ACK 丢了对方会重发 FIN此时主动关闭方还在 TIME_WAIT 就能重发 ACK二是让这条连接上迷路的旧报文在网络中自然消亡避免它们被新连接误收。所以 TIME_WAIT 不是 bug是保护机制。但保护是有代价的。Linux 上 2MSL 默认是 60 秒意味着一条主动关闭的连接要占着一个本地端口 60 秒。如果你的服务是短连接模式每秒几千次请求端口就会迅速耗尽报出 Cannot assign requested address。这时候的解法不是简单调小tcp_fin_timeout那个参数只影响 FIN_WAIT_2而是要么改成长连接要么调大ip_local_port_range要么让服务端做主动关闭方把 TIME_WAIT 压到服务端去。还有一个被忽视的状态是 CLOSE_WAIT。它表示对方已经关了我还没关。如果 CLOSE_WAIT 大量堆积百分之百是应用层代码没调用 close不是内核问题调任何 sysctl 都没用。这一点我见过太多次误判。2.2 窗口与拥塞控制带宽跑不满的真正原因带宽跑不满最常见的三个原因是接收窗口太小、拥塞窗口上不去、单连接受限于 RTT。这三个要先分清不然会乱调。先算一笔账这是每个做传输的人都该会的计算。带宽时延积BDP 链路带宽 × 往返时间。假设一条链路是 200MbpsRTT 是 60ms那么 BDP 200 × 10^6 / 8 × 0.06 1.5MB。这个数字意味着要让这条链路跑满发送方在收到第一个 ACK 之前必须能连续发出 1.5MB 的数据。如果接收窗口只有 64KB那实际吞吐上限就是 64KB / 0.06s ≈ 1.07MB/s ≈ 8.5Mbps无论带宽多大都只有这么点。这就是为什么跨地域传输时带宽明明够但速度上不去是常态。解决办法有两个一是开窗口缩放TCP Window Scale 选项把 16 位窗口字段左移最多 14 位最大支持 1GB二是调大内核的收发缓冲区net.ipv4.tcp_rmem和tcp_wmem。拥塞控制这一侧cwnd从慢启动开始指数增长到ssthresh后转线性增长遇到丢包就下降。经典算法是 CUBIC现在很多场景用 BBR。CUBIC 靠丢包判断拥塞在有随机丢包的链路上会误判、过度降速BBR 靠测量带宽和 RTT 建模在有一定丢包率的长肥链路上表现更好。选哪个取决于你的链路特征不是越新越好纯内网低丢包环境下两者差别不大高丢包的跨境链路上 BBR 往往优势明显。注意开 BBR 需要内核支持且要确认中间设备不会因为非 CUBIC 的拥塞行为把它当成异常流量。上线前一定在灰度环境实测不要直接全量切。2.3 常用内核参数与它们的真实代价网上流传的 TCP 调优清单一大半是过时的或者有副作用的。我把真正常用、且我确认过效果与代价的几条列出来。参数作用调大的收益潜在代价net.core.somaxconnaccept 队列上限高并发下减少握手被丢弃需应用 listen 的 backlog 同步调大net.ipv4.tcp_max_syn_backlogSYN 队列上限抗突发握手洪峰内存占用略增net.ipv4.ip_local_port_range本地端口范围缓解短连接端口耗尽与目标端口冲突概率略升net.ipv4.tcp_rmem接收缓冲区提高大 BDP 链路吞吐内存占用显著上升net.ipv4.tcp_wmem发送缓冲区同上同上net.ipv4.tcp_congestion_control拥塞控制算法长肥链路吞吐提升需实测可能与他人不公平关于somaxconn有个经典坑很多人只改内核参数忘了应用层的 listen backlog。以 Python 为例s.listen(128)这个 128 会被内核取min(128, somaxconn)你内核调到 65535 但应用还写 128等于白调。Nginx 的backlog配置同理。缓冲区这条也要说清楚。tcp_rmem和tcp_wmem都是三元组最小、默认、最大内核会在最大值的范围内自动调节。直接把最大值拉到几百 MB 是危险的每条连接都可能吃到上限几千连接就能把内存吃穿。稳妥做法是先算 BDP把最大值设成单连接最坏情况下需要的两倍然后观察ss -m里的实际占用再微调。至于tcp_tw_reuse和已被移除的tcp_tw_recycle我的态度很明确前者在客户端侧可以谨慎开启后者在任何场景下都不要碰。recycle 依赖时间戳在 NAT 环境下会造成大量连接被误丢早就在新内核里删掉了看到还在推荐它的文章可以直接跳过。3. UDP 的不作为才是它的特性很多人对 UDP 的印象停留在不可靠、会丢包、不保证顺序然后就没了。这个认知太浅。UDP 真正的价值是它把控制权完整地交回给了应用层让你可以按业务需求定制每一条策略。你在 TCP 上没法决定这个包丢了不用重传在 UDP 上可以你在 TCP 上没法做到晚到的包直接扔掉不阻塞后续在 UDP 上可以。理解了这一点你才会明白为什么 HTTP/3 要跑到 QUIC 上为什么实时音视频宁可自己写重传逻辑也不用 TCP。3.1 八个字节里藏着的坑UDP 头部只有四个字段源端口、目的端口、长度、校验和各 16 位。看起来简单但有两个坑值得单独说。第一个坑是校验和字段可以是 0。在 IPv4 下发送方把校验和填 0 表示我不计算校验和接收方看到 0 就直接放行。这意味着 UDP 在 IPv4 下可以完全没有完整性校验。IPv6 下这个行为被禁止了校验和是强制的。所以如果你的服务同时跑在 v4 和 v6 上别指望 UDP 的校验和一定生效。还有一个细节UDP 的校验和计算会用到一个伪首部把源 IP、目的 IP、协议号、UDP 长度都算进去。这意味着如果 NAT 设备改写了 IP 地址却没重算校验和接收端会算出错误并丢包。这是某些网络环境下 UDP 莫名不通的原因之一排查时容易被忽略。第二个坑是长度字段只有 16 位。65535 是理论上限减去 IP 头 20 字节和 UDP 头 8 字节载荷最大 65507 字节。但这个数字在实际网络里毫无意义因为超过 MTU 就会被 IP 层分片。3.2 MTU 分片UDP 丢包最隐蔽的来源这是我最想强调的一节。以太网 MTU 1500IP 头 20UDP 头 8所以不分片的最大载荷是 1472 字节。你如果 send 了 2000 字节IP 层会把它拆成两个分片。接收端要等两个分片都到齐才能重组任何一个丢了整个 2000 字节全部作废。问题在于大多数 UDP socket 默认不会告诉你这件事。丢包发生在 IP 层重组阶段应用的 recvfrom 只是没收到没有任何错误码。你会在业务层看到偶发数据缺失但抓包一看发送端确实发出去了。更麻烦的是路径 MTU 发现PMTUD。有些链路的中间设备会丢弃带 DF 标志的大包并且不回 ICMP导致 PMTUD 失效出现小包能通、大包不通的黑洞现象。这种情况下表现非常诡异ping 通小请求通稍大一点的请求就超时。实际工程里的做法是保守取值。QUIC 把初始包限制在 1200 字节就是为了避开绝大多数链路的 MTU 黑洞。你自己设计 UDP 协议时也可以照这个思路应用层主动把单包控制在 1200 字节以内超出的数据在应用层自己切分和重组不要指望 IP 分片。提示想测某条路径的实际 MTU可以用带 DF 标志、逐级增大包长的探测方式。但注意很多环境会屏蔽这类探测结果只能作为参考最终还是要靠实测业务流量来确认。3.3 在 UDP 上重建可靠性一份取舍清单如果你决定用 UDP 并且需要一定可靠性那实际上是在重新实现 TCP 的一部分。这时候最关键的不是怎么实现而是哪些要实现、哪些可以不实现。我的取舍清单如下。能力是否必做说明序号 重排序必做否则乱序数据无法使用超时重传必做但超时时间要基于 RTT 动态计算快速重传建议靠后续包触发减少等待拥塞控制建议公网场景必做否则会挤垮别人也害自己流量控制可选对端处理能力差异大时需要校验和建议应用层自己加不全信底层重传策略是最容易做错的一块。一个常见错误是固定超时时间比如统统 200ms。链路 RTT 波动时固定值要么重传太频繁造成拥塞要么太慢导致卡顿。正确做法是维护一个 RTT 的滑动估计类似 TCP 的 SRTT 和 RTTVAR 思路超时时间取SRTT 4 × RTTVAR并设一个下限避免过早重传。另一个容易忽略的是重传放大效应。丢包率 5% 时你重传一次能解决大部分问题但如果重传的包也丢了再重传一次就变成 3 份流量。在拥塞时这会让情况更糟。所以重传次数必须有上限且重传时要降低发送速率——这也是为什么自研协议里拥塞控制不能省。4. 抓包实证把协议从课本里拽出来光看文字记不住协议行为我在带人的时候一律要求先抓一次完整的包。抓包的价值在于把抽象的握手重传窗口更新变成具体的时间戳和字节你能亲眼看到 40ms 的延迟尖刺长什么样看到重传是怎么被触发的。这一节给的是可以直接抄的最小实验环境。4.1 最小复现环境与抓包命令先准备一个最简服务端和客户端。TCP 版本我加了TCP_NODELAY因为默认开着 Nagle 会干扰对时序的观察。# tcp_server.py import socket srv socket.socket(socket.AF_INET, socket.SOCK_STREAM) srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) srv.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) srv.bind((0.0.0.0, 9000)) srv.listen(128) print(listening on 9000) conn, addr srv.accept() print(accepted from, addr) while True: data conn.recv(4096) if not data: break conn.sendall(becho: data) conn.close()# tcp_client.py import socket c socket.socket(socket.AF_INET, socket.SOCK_STREAM) c.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) c.connect((127.0.0.1, 9000)) for i in range(3): c.sendall(bhello-%d % i) print(c.recv(4096)) c.shutdown(socket.SHUT_WR) c.close()UDP 版本更简单但要注意SO_RCVBUF的调整——默认接收缓冲区往往只有一百多 KB突发流量下会在内核里直接丢包。# udp_server.py import socket u socket.socket(socket.AF_INET, socket.SOCK_DGRAM) u.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 4 * 1024 * 1024) u.bind((0.0.0.0, 9001)) print(udp listening on 9001) while True: data, addr u.recvfrom(65535) print(recv, len(data), from, addr)抓包用 tcpdump几个参数要在意-nn不做名称解析否则 DNS 查询会污染结果、-s 0抓完整包老版本默认只抓 96 字节会截断 payload、-w写文件留给后续分析。# 抓 TCP 9000 端口的完整报文 sudo tcpdump -i lo -nn -s 0 tcp port 9000 -w tcp.pcap # 抓 UDP 9001并统计每秒包数 sudo tcpdump -i lo -nn -s 0 udp port 9001 -w udp.pcap # 实时看连接状态分布 ss -tan | awk NR1 {print $1} | sort | uniq -c | sort -rn4.2 握手与挥手的逐包解读抓完包用任意图形工具打开你会看到这样一串。我按顺序讲每一包在说什么。第一包客户端 SYN标志位是[S]序列号是一个随机初始值ISN。ISN 随机化是安全设计不是随便取的避免被预测序列号注入数据。第二包服务端 SYNACK标志位[S.]带上自己的 ISN 和 ACK客户端 ISN1。注意这一包里通常还会携带 MSS、窗口缩放因子、SACK 允许这些选项这是双方协商参数的唯一机会——窗口缩放因子一旦定了整条连接都不能改这也是为什么用 tcpdump 看握手包时一定要展开 TCP 选项。第三包客户端 ACK连接建立。从这一包之后双方就可以互发数据了。再看挥手。假设是客户端主动关闭第一包客户端 FIN标志位[F.]进入 FIN_WAIT_1。 第二包服务端 ACK客户端进入 FIN_WAIT_2。 第三包服务端 FIN同时带上 ACK客户端进入 TIME_WAIT回最后一个 ACK。抓包里你会注意到第二次和第三次挥手之间常常有明显间隔。这不是协议慢而是服务端的应用层还没调 close——数据可能还在缓冲区里没发完或者代码里忘了关。这个间隔在实际排障里就是服务端处理慢的直接证据。还有个小细节值得看TIME_WAIT 那一端在 60 秒内如果再抓到同一个四元组的包就是旧连接的残留报文。这验证了 2MSL 的作用也让你明白为什么快速重连同一个四元组有时会收到奇怪的 RST。4.3 分片与缓冲溢出的现场证据UDP 这一侧最值得抓的是分片。你在客户端 send 一个 3000 字节的包tcpdump 里应该能看到两个 IP 分片第二个的 frag 标志位会显示偏移量。如果只看到第一个分片而接收端什么都没收到那就是分片丢失导致整包被丢。# 触发分片的发送端 import socket s socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.sendto(bx * 3000, (127.0.0.1, 9001))另一个现场证据是接收缓冲区溢出。当发送速率超过接收端处理速度内核会在netstat -su的RcvbufErrors里累加计数。这个计数是排查 UDP 丢包的第一站比在应用层猜有用得多。# 看 UDP 层的统计 netstat -su | grep -A 5 Udp # 关键指标InErrors / RcvbufErrors / SndbufErrors # RcvbufErrors 持续增长 应用读得太慢需要调大 SO_RCVBUF 或加消费线程还有一个容易漏的点网卡层的 ring buffer。如果ethtool -S eth0里的rx_discards在涨说明包还没进内核就已经被网卡丢了这时候调应用和内核参数都没用得加大 ring buffer 或者上多队列。排查顺序应该是网卡丢包 → 内核 socket 缓冲区丢包 → 应用读取不及时一层层往下排。5. 线上问题排查实录协议知识最终要落到排障上。这一节我把常见的几类症状整理成可以直接对照的路径再挑几个典型场景做复盘。这些经验大部分是从错误判断里换来的写出来是希望你能少走一遍。5.1 症状对照表先定位再动手症状优先排查方向常用命令连接建立慢或超时握手丢包、backlog 满ss -lnt、netstat -s | grep -i listen连接数异常多CLOSE_WAIT 堆积应用未关ss -tan state close-wait | wc -l报端口耗尽短连接过多、TIME_WAIT 占用ss -tan state time-wait | wc -l带宽跑不满窗口太小、BDP 不足ss -m、ss -ti偶发几十毫秒延迟Nagle 与延迟确认叠加抓包看 ACK 间隔UDP 偶发丢数据RcvbufErrors、分片丢失netstat -su、抓包大包不通小包通路径 MTU 黑洞缩小包长验证这张表的使用要点是先用命令确认现象再对照方向不要反过来。我见过有人一上来就改 sysctl改完发现问题是应用层连接泄漏白折腾一场还引了新问题。5.2 三个典型故障的判断过程第一个CLOSE_WAIT 堆积。现象是服务端连接数缓慢上涨几天后耗尽。ss -tan state close-wait | wc -l显示几千条。这时候要看的是代码而不是内核——CLOSE_WAIT 意味着对端发了 FIN本端收到了但没调 close。常见原因是异常分支里return前没关连接、用了连接池但归还逻辑漏了、或者某个阻塞读永远不返回。修法是加资源管理比如 Python 的with、Go 的defer同时在池化层加超时回收兜底。第二个吞吐上不去但 CPU 和带宽都闲着。用ss -ti看单条连接的cwnd和rtt。如果 cwnd 停在几十 KB 不再增长同时retrans有少量增长多半是随机丢包触发了 CUBIC 降速。如果 cwnd 很大但吞吐还是低那就是接收窗口卡住了看ss -m里的接收缓冲占用必要时调大tcp_rmem。这两个方向完全不同别混着调。第三个UDP 服务在压力下大量丢包。先在服务端看netstat -su的RcvbufErrors。如果这个数在涨说明应用消费不过来解法是加大SO_RCVBUF注意 Linux 上有net.core.rmem_max上限setsockopt会被截断到该值并增加消费线程。如果这个数不涨但业务还是丢那就往上一层看网卡rx_discards或者往下一层看分片——把单包控制在 1200 字节以内再压一遍往往立刻见效。这个自上而下确认、自下而上定位的顺序我在前面提过一次这里再强调能用一个命令区分开的两种情况千万不要凭经验合并判断。5.3 踩过的坑与独家清单最后几条是我个人反复验证过、但一般文档里不太写的东西。关于 Nagle 和延迟确认。服务端连续写小包、客户端开了延迟确认时会出现最多 40ms 才回 ACK的现象。原因是 Nagle 在等前一个包的 ACK而延迟确认在等更多数据一起确认两边互相等。解法是业务侧对小包敏感的连接开TCP_NODELAY或者把多次小写合并成一次大写。这件事在本地测试完全看不出来跨机房一压就现原形。关于 TIME_WAIT 的数量阈值。不要看到几千条就慌这个数和你的 QPS 直接相关短连接场景下TIME_WAIT 数量大致等于 60 秒内的连接数。真正该关注的是端口消耗速度用ss -s看总连接数趋势比看单个状态有用。关于 UDP 的 socket 复用。多个进程同时绑同一个 UDP 端口时默认行为是谁最后绑谁收其他进程收不到。要用SO_REUSEPORT才能让内核做负载均衡。这个坑在做多进程 UDP 服务时几乎必踩表现是起了 8 个进程只有 1 个在干活。关于抓包本身的影响。在高流量机器上开全量抓包tcpdump自己会成为瓶颈导致丢包并且污染观测结果。正确做法是先按端口和协议过滤必要时用-c限制包数、用-w落盘再离线分析不要在线上机器上做实时全量解析。关于压测环境与生产环境的差异。压测机上往往没有真实网络设备路径 MTU、NAT 行为、中间设备的连接跟踪表都和生产不同。凡是涉及 MTU、长连接保持、NAT 超时的结论必须在接近生产的网络路径上验证过才能上线这一点我在两套环境结论相反的例子上栽过。关于参数调整的纪律。每次只改一个参数改完在灰度环境跑够至少一个完整的流量周期比如覆盖一次高峰记录前后指标再决定是否全量。一次改十个参数出了问题是没法归因的最后只能全部回滚重来。我在实际工作中形成的习惯是遇到网络问题先花五分钟把现象、范围、时间点、最近变更四件事写清楚再动手。这五分钟往往能省掉几个小时的瞎试——因为大部分所谓网络抖动最后都指向某次配置变更或者某个没关的连接。协议本身是稳定的不稳定的永远是使用它的方式。
阅读完成 · 觉得有帮助?