机房里的排障现场经常是这样开始的:业务方反馈接口偶发超时,登录机器抓个包,tcpdump 跑起来一看,TCP 报文长度赫然写着几千字节,而抓包里三次握手的 MSS 明明是 1460。再往下翻,Wireshark 的校验和一栏一片红,Checksum: 0x0000 [incorrect],几乎是每个包都报错。第一反应通常是两个方向:要么网卡驱动有问题,要么被人打了。然后开始查防火墙、查 ACL、查是不是有中间设备在做手脚,折腾半天,业务侧的重传统计一点没变,连接成功率也一直正常。
这种时候最该怀疑的其实不是链路,也不是攻击,而是抓包这件事本身。你看到的这个"包",和线上真正在网线里跑的那个"包",很可能根本不是同一个东西。网卡卸载(offload)把一部分原本由内核协议栈完成的工作挪到了别的位置——有的往前挪,有的往后挪——于是抓包点看到的形态,和线路上真实传输的形态,从此不再一致。这不是故障,但如果你不知道这件事,基于抓包做出的每一个判断都可能跑偏。
所以这篇想讲清楚两件事:为什么抓出来的东西会"不像真的",以及到底哪些时候该把卸载关掉、哪些时候关掉是自找麻烦。结论先放在这儿——排障时先关卸载再抓,长期运行时为了吞吐与 CPU 占用应该保留,这是两套设置,不是二选一。
要理解观测失真,先要建立一个空间感:一次发送并不是"应用把数据交给网卡"这么简单的一步,而是一条有顺序的路径。发送方向大致是应用写入 → 套接字层 → TCP 处理 → IP 处理 → 邻居子系统 → 驱动 → 网卡;接收方向反过来,网卡 → 驱动 → 协议栈 → 套接字 → 应用。每个环节之间都存在一个"数据包此刻长什么样"的瞬间。
抓包工具挂在哪一层,看到的就是哪一层的形态。常见的情况是抓包点位于协议栈与驱动之间,也就是内核里那个被称为 skb 的缓冲区刚被交给驱动、或刚从驱动收上来的位置。这个位置在分段之前,也在合并之后。于是问题来了:如果分段还没发生,你看到的当然就是一个大包;如果合并已经发生,你看到的当然是合并后的结果。报文数量、报文长度、校验和状态,三项全部失真。
这就是为什么同一次通信,在发送端抓、在接收端抓、在中间的旁路设备抓,三者对不上是完全正常的。不是谁错了,是三个观测点本来就位于路径的不同位置。很多人第一次遇到会以为是自己 tcpdump 参数写错了,去调 snaplen、换接口、加 -vvv,其实方向全错。
还有一个容易踩的坑:抓包文件里的长度超过 MTU,不代表链路上真的传了一个超过 MTU 的帧。IP 层分片和 TCP 分段是两件事,网卡做的分段卸载发生在更靠后的位置,抓包点在它之前,看到超长报文是必然结果。把这个当作"网卡 MTU 配置错了"去改配置,属于典型的误判。
再补两个抓包习惯上的坑。一个是截断:默认快照长度会把报文尾部截掉,只看协议头的时候觉得无所谓,一旦要量长度、要看载荷,被截掉的部分就成了新的假象,而且无法事后补救。另一个是只抓一个点:单机环境问题不大,但只要涉及转发、桥接、容器或者隧道,单点抓包能支撑的结论非常有限。两个点同时抓、放在一起对照,能省掉的猜测比想象中多得多。
卸载这个词听着玄,本质就是一句话:把协议栈里某一步的活儿,交给网卡或者交给更晚的时刻去做。为什么值得这么做?因为按 MSS 切段、逐包算校验和这类工作,在高频收发下是一笔不小的开销,而且高度重复。把它交给专门做这件事的硬件,或者至少推迟到不得不做的时候再做,能明显省下 CPU。这个思路本身没错,代价就是观测。
发送方向的思路是"推迟"。分段本来应该在 TCP 层按 MSS 切成一段一段,卸载之后,协议栈先攒着一个大块往下传,直到真正要发出去之前才切。抓包点在切之前,于是看到的是完整的大块。校验和同理,本来应该由协议栈算好填进报文头,卸载之后这个字段留给网卡去填,抓包点看到的就是一个还没填的空位或者残留值。
接收方向的思路是"合并"。网卡连续收到一批属于同一条流、序号连续的小报文,与其一个个往上送,不如拼成一个大的再送。协议栈拿到的就是合并之后的 skb,抓包点自然也看到合并后的形态。你数出来的包数,比线路上真实传输的帧数要少;你看到的单包长度,比真实帧要大。
这两件事都不影响通信的正确性——对端收到的依然是正确的数据,连接状态机毫无察觉。但它们直接破坏了"抓包即真相"这个前提。而绝大多数排障动作,恰恰建立在这个前提上。
这里还要厘清一个容易混的概念:卸载省下来的是"每包处理次数",不是"总字节数"。按 MSS 切段意味着原本一个大数据块要走 N 次完整的协议栈处理和 N 次驱动交接,卸载之后这个 N 次被压到了一次,真正到网卡时才展开。合并方向同理,N 次收包处理被压成一次。理解了这一点,就能明白为什么关掉卸载之后 CPU 上升在小包密集的场景里格外明显——省掉的正是那 N 次重复劳动。
也正因为卸载改变的是"处理次数",它对统计口径的影响就不是线性的、可简单折算的。看到抓包里的包数想反推线路上的帧数,这条路走不通,因为合并的条件本身就取决于流的连续性和报文的到达节奏,没有固定比例可用。遇到必须精确计数的场景,正确做法是关掉合并重新抓,而不是拿一个经验系数去换算。
下面这张表按卸载项拆开,列出它挪走了什么、抓包会看到什么、排障时的处理,以及长期运行该不该保留。这是本文最实用的部分,建议对着自己的 ethtool -k 输出一行行看。
| 卸载项 | 它把什么事挪走了 | 抓包时会看到什么 | 排障时怎么办 | 长期运行要不要保留 |
|---|---|---|---|---|
| TSO(TCP 分段卸载) | 把 TCP 按 MSS 切段的工作从协议栈挪到网卡,协议栈往下传的是一大块 | 发送方向出现远大于 MSS 的报文长度,看起来像超长帧 | 凡是按报文长度做判断的排查都要先关掉,尤其是 MTU 与分片相关问题 | 保留。大流量发送方向省下的 CPU 很可观 |
| GSO(通用分段卸载) | TSO 的通用化,把分段推迟到更靠后的时刻,覆盖面不限于 TCP | 与 TSO 类似的超长报文,且可能在更多协议上出现,容易被误判成报文异常 | 排障时与 TSO 一起关,只关一个往往看不出变化 | 保留。关掉后分段回到 CPU,小包密集时开销上升 |
| GRO(通用接收合并) | 接收方向把同一条流的连续报文合并后再交给协议栈 | 包数变少、单包变大,时序被压扁,时间戳看起来挤在一起 | 要数包、要算 PPS、要看单包时序的排查必须关掉 | 保留。接收方向的省 CPU 主力,关掉代价明显 |
| LRO(大量接收合并) | 接收方向的早期合并实现,行为比 GRO 更激进 | 合并后的大包,个别实现下报文头字段可能被改写,转发场景尤为可疑 | 先关。作为桥接或转发节点时,这类合并容易干扰对报文头本身的判断 | 视角色而定。纯转发节点通常不建议开启 |
| 收发方向校验和卸载 | 校验和的计算与校验交给网卡,协议栈不再逐包处理 | 发送方向校验和字段可能是未计算的空值;接收方向可能显示不正确 | 想用校验和判断链路或中间件是否改包时,必须先关再抓 | 保留。校验和计算交给网卡几乎没有副作用 |
| 隧道与 VLAN 相关卸载 | 把外层封装处理、内层报文的分段与校验和一并交给网卡 | 内层报文的可见性变化,统计口径与裸报文不同,重传与长度的读法全变 | 排查 overlay 网络问题时,隧道口的卸载和物理口的卸载都要过一遍 | 保留。隧道叠加场景下这部分开销最值得卸载 |
| 分散聚合(scatter-gather) | 允许报文数据分散在非连续内存中,是上面几项卸载的依赖基础 | 单独看影响不大,但它是分段卸载能生效的前提 | 一般不用单独动它;关掉上层卸载时留意驱动的依赖提示 | 保留。关掉可能连带让其他卸载失效 |
表里有几个地方值得单独拎出来讲。第一个是 TSO 和 GSO 的关系——只关 TSO 不关 GSO,抓出来的包照样是长的,因为 GSO 会在更靠后的时刻接管分段。很多人关了一个发现没变化,就得出"卸载不影响抓包"的错误结论,其实是没关干净。第二个是 LRO 和 GRO 的区别,两者都在接收方向合并,但 LRO 的实现更激进,作为桥接或转发节点时,合并行为可能动到报文头本身的字段,这类节点上通常不建议开 LRO。第三个是依赖顺序,scatter-gather 是分段卸载的基础,动它的时候要留意驱动给出的提示,有些组合是关不掉的。
卸载只是让观测失真的一半原因,另一半是抓包点选错。这两件事经常叠加出现,所以单独拿出来说。
物理口是最直觉的选择,但未必是正确选择。物理口上看到的是封装完成、即将上线路的形态,如果你的问题出在 overlay 的内部寻址或者隧道内层,物理口抓出来的东西里,内层信息的可读性取决于隧道相关卸载怎么工作,未必能直接看明白。反过来,如果问题出在物理链路本身,只在隧道口抓又看不到外层。
容器环境里,veth 是一对设备,两端各有各的处理路径。在容器内的 eth0 抓、在宿主机侧的 veth 抓、在桥接口抓、在物理口抓,四个位置的形态可以各不相同。经验上比较稳妥的做法是先在容器内和宿主机侧各抓一份,确认问题发生在哪一段,再决定要不要往物理口延伸。一上来就抓物理口,最容易得到一堆看着正常但没用的数据。
桥接和隧道叠加的时候还要多想一层:报文可能不止一次经过分段与合并的处理。外层封装加一层、隧道口再处理一次,抓包点每挪动一格,看到的形态就变一次。这种环境下,与其纠结哪个点"最真实",不如明确自己要看的是哪一层的问题——看外层就抓物理口,看内层就抓隧道口,两边都看就都抓,然后对照。
最后一点:抓包点的选择要写进排查记录。同一个问题过两周再看,如果记录里只有一份 pcap 文件名,没人记得当时抓的是哪个接口、卸载开着还是关着,那份文件的参考价值就大打折扣。
判断标准其实很简单:只要你的结论依赖"报文长度、报文数量、报文时序、校验和"这四样中的任何一样,就先关卸载。依赖得越重,越不能偷懒。
第一类是按长度做判断的。路径 MTU 相关的问题首当其冲——你要看 DF 位、要看 ICMP 需要分片的消息、要看报文到底有没有被切开,卸载开着的话长度字段全是假的,什么都看不出来。MSS 协商异常、某些中间设备对超长报文的处理、应用侧设置了奇怪的发送缓冲,这些也都属于这一类。
第二类是按数量做判断的。要数重传、要算 PPS、要判断是否存在微突发、要看小包占比,接收方向的合并会把这些数字全部压扁。你数出来一秒一千个包,线路上可能传了几千个帧,基于这个数字做的容量判断从一开始就错了。
第三类是按时序做判断的。延迟分析、重传超时的定位、判断某个请求是不是卡在网络上,这些都要看单包的时间戳。合并之后,一批报文被压成了一两个 skb,时间戳也就挤到了一起,你看到的"延迟尖刺"可能只是合并造成的假象。这一条最容易被忽略,因为 Wireshark 里的时间列看着很正常。
第四类是想用校验和做判断的。怀疑链路层损坏、怀疑中间有设备改包、想通过校验和错误率定位物理问题,这些场景下卸载必须关——因为开着的时候,校验和字段本来就没算,红一片是必然。
第五类是容器与虚拟网络环境。veth、桥、overlay 隧道叠加在一起,分段与合并的行为会在多个层次上发生,抓包点的选择变得极其关键。在物理口抓和在 veth 上抓看到的形态可能完全不同,在隧道口抓又是另一套。容器网络出问题时,只在某一个点抓包就下结论,风险很高。
第六类是旁路采集与检测类设备。入侵检测、流量采集这一类系统的规则,往往是按报文长度、包数、分片特征写的。如果采集到的是合并或未分段的形态,规则匹配的结果就会和真实情况对不上。这类设备的部署前,务必要先确认采集点看到的是哪种形态。
还有一类不算排查、但同样需要先关卸载的:要做基线比对的时候。比如你要证明"优化前后包数有没有变化""某次变更有没有引入额外分片",这类结论必须建立在可比的观测条件下。前后两次抓包如果一次开着卸载一次关着,数字差异完全没有意义。做基线之前先把观测条件固定下来,这是一条硬规矩。
判断丢包的时候也一样,别只盯着抓包文件。ethtool -S 提供的网卡统计里有独立的收发计数、错误与丢弃计数,这是硬件层面的口径,不受卸载影响,和抓包结果互为补充。抓包看的是"经过协议栈的报文",网卡统计看的是"网卡实际处理了多少",两者对不上往往正说明中间有卸载或者缓冲在起作用,而不是数据凭空消失了。
说完该关的,再说不该关的。关掉卸载意味着这部分工作回到 CPU,代价是 CPU 占用上升、吞吐可能下降。这个代价在什么情况下不可接受,要心里有数。
长期运行的大流量业务,卸载是省 CPU 的重要手段。尤其是发送方向的分段卸载和接收方向的合并卸载,这两项对 CPU 的节省最为明显。把它们关掉跑在生产环境上,等于主动给自己的机器加负担。真要关,也只能是短时间的排查窗口,抓完立刻恢复。
作为网关、转发节点或者隧道端点的机器,接收方向的合并对吞吐帮助很大。这类机器本身就要处理双向流量,关掉合并之后 CPU 的上升会比较直观。在这类节点上做排查,更推荐的做法是在业务低峰开一个短窗口,或者换一个观测点,而不是直接在生产转发路径上长期关掉。
还有一种情况更隐蔽:关掉之后忘了开回来。这是排障里最常见的事故之一。抓完包、分析完、问题也定位了,没人记得把参数改回去,机器就这么带着关闭状态跑了几个月,直到某天流量上来了发现 CPU 不对劲,回过头查才发现。所以我的建议是,任何一次关闭操作都要配套一个明确的恢复动作,最好写进操作记录里。
把顺序固定下来,能省掉很多来回。我自己习惯按下面这套走,改动小、可回退、不留坑。
第一步,先确定抓包点。问自己一个问题:我要看的是哪个位置上的流量?物理口、veth、桥接口、隧道口,这四个抓出来的东西口径都不一样。容器环境下这一步尤其重要,很多人默认抓 eth0,结果问题出在 veth 上,白抓一场。
第二步,记录当前的卸载状态,用 ethtool -k 接口名 看一遍全量输出,保存一份。这一步纯粹是为了回退,别觉得多余——关的时候开了七八项,恢复的时候少开一项,问题就埋下了。
第三步,在相关接口上临时关闭。发送与接收方向的卸载都要关,常见的组合是 tso、gso、gro、lro,以及收发方向的校验和卸载。注意容器场景要同时考虑 veth 的两端和隧道口,只关物理口往往不够。
第四步,抓包。用 tcpdump -i 接口名 -s 0 -w 文件名 抓全包存文件,snaplen 别设小了,否则超长报文会被截断,又制造出新的假象。抓的时候尽量覆盖问题复现的完整时间段,宁可多抓一会儿。
第五步,抓完立刻恢复,并且用 ethtool -k 再核对一遍,确认和第二步记录的状态一致。如果发现某项关不下去或者恢复不了,多半是驱动层面的依赖限制,这在虚拟化网卡上挺常见。
第六步,如果判断需要长期保持关闭,就写进网络配置里——不管是 network scripts、netplan 还是 systemd-networkd,选择与你系统匹配的那一种。用命令行临时改的参数,重启之后就没了,这一点在排查报告里要写清楚,免得下次接手的人以为是永久配置。
需要抓包排障的时候,能自己登机器改网卡参数很重要——一万网络深耕 IDC 19 年(成立于 2007 年),这类内核态的调整在物理机上能自己做主。
采集类系统和普通业务的区别在于,它自己不做收发,只是看。所以它面临的取舍也完全不同:准确性的价值高于 CPU 的节省,但又不能不考虑开销。
实际部署里比较合理的做法是分角色处理。业务侧保持卸载开启,采集侧根据实际情况决定是否关闭。如果采集的检测规则高度依赖报文长度和包数,那就关掉合并相关的卸载,接受 CPU 上升;如果只看流级别的信息、看连接和字节数,开着也无妨。不要一刀切地在所有机器上都关掉。
要特别提醒的是口径问题:旁路设备通过镜像或分光看到的,是线路上真实传输的帧;主机内抓包看到的,是协议栈某个位置的形态。两者对不上是常态,不是故障。很多"采集设备和主机抓包结果不一致"的工单,查到最后发现根本不是问题,只是两个观测点的口径没对齐。排查这类工单的第一步,应该是先确认两边的观测位置,而不是急着找丢包。
还有一种部署思路是干脆换观测点——把采集放到能看到真实帧的位置,而不是在主机内部跟卸载较劲。这在有条件做镜像的环境里往往更省事,也不会给业务机带来额外开销。缺点是对基础设施有要求,不是谁都能做。
采集系统还有一条容易被忽略的要求:文档化。采集链路上到底关了哪些卸载、采集点位于哪一层、看到的是封装前还是封装后的形态,这些如果不写清楚,后面接手的人按自己的理解去解读数据,很容易得出相反的结论。我见过不少"采集数据对不上"的争论,最后发现双方根本不是在讨论同一个观测口径。
最后一个提醒:采集侧的任何卸载调整,都要当成一次生产变更来走。它不改变业务收发路径,但会改变 CPU 占用,在采集点本身就承担高 PPS 的情况下,这个变化并不小。改之前留基线,改之后看一段时间,确认没有副作用再固化到部署模板里。
抓包里校验和报错,是不是意味着有丢包?
不是。发送方向的校验和卸载会把计算留给网卡,抓包点上的字段本来就还没算,显示为不正确或者为零是正常现象。丢包要看的是重传、看的是序号空洞、看的是对端的确认情况,跟这一栏没关系。判断丢包请去 ethtool -S 看网卡统计里的丢包与错误计数,那才是正经口径。
关掉 offload 会不会影响正在跑的业务?
会有影响,主要是 CPU 占用上升,吞吐也可能下降,具体幅度取决于你的报文大小分布和 PPS 特征,不同业务差别很大。所以正确做法是开一个短窗口,抓完马上恢复,别让它长时间停留在关闭状态。真要在高峰期抓,建议先在非核心节点上试,确认影响可控再动生产。
容器里的 veth 要不要一起关?
要。而且这是容器网络排查里最容易漏的一步。虚拟网卡本身也有自己的卸载设置,只关物理口,veth 上的合并与分段行为照样在发生。抓的时候建议在 veth 两端和物理口各抓一份做对照,很多 overlay 网络的问题,是靠这种对照才看出来的。
采集设备看到的流量,和业务实际跑的是否一致?
要看采集点的位置。旁路设备看到的是线路上的真实帧,主机内抓包看到的是协议栈某个阶段的形态,两者本来就不该直接对比。如果采集设备的规则依赖包数或长度,建议在自己的采集链路上明确关掉合并类卸载,并且把这一点写进部署文档,否则换个环境部署就会出问题。
抓包看到的长度超过 MTU,是不是网卡配置错了?
大概率不是。分段卸载把切段推迟到了抓包点之后,你看到的就是还没切的大块。先查 ethtool -k 确认卸载状态,关掉再抓一次,如果长度恢复正常范围,那就只是观测问题。真要去动 MTU 配置之前,务必先排除这个可能。
为什么同一个连接,两端抓到的包大小不一样?
因为两端的卸载配置可能不同,抓包点也不同。一端关了 TSO 一端没关,看到的形态自然不一样。这不需要修,只需要知道。做两端对照分析时,先把两边的卸载状态拉平,否则对照出来的差异全是假的。
关掉之后忘了开回来,会有什么后果?
短期通常看不出来,长期是 CPU 占用持续偏高,吞吐上不去。而且这种问题很难排查,因为没人记得几个月前改过网卡参数。所以关闭操作一定要配一个明确的恢复动作,最好两个人交叉确认,或者干脆写进操作记录里留痕。
把整篇的结论压缩成一句话:卸载本身没有错,错的是把抓包结果当成线路真相。发送方向的分段被推迟,抓包点就会看到远大于 MSS 的包;接收方向做了合并,抓包看到的就是合并之后的结果;校验和由网卡计算,抓到的包校验和就可能是未计算的或者看起来错误的。这三件事都不代表业务出错,但都会直接影响你基于抓包做出的判断。
所以不要问"卸载到底该开还是该关",要问"现在是在排障还是在跑业务"。排障的时候,凡是依赖长度、数量、时序、校验和的判断,一律先关再抓,抓完立刻恢复;长期运行的时候,为了吞吐与 CPU 占用,卸载应该保留。这两套设置服务于两个不同的目标,混在一起讨论永远得不出结论。
给运维留一句实在话:把 ethtool -k 的输出存一份基线,把关闭和恢复写成一对固定动作,把抓包点写进每一次的排查记录。这三件事做到了,抓包这件事才重新变得可信。
本文内容为 Linux 网络栈与网卡驱动公开机制的梳理,涉及的分段卸载、接收合并、校验和卸载、隧道相关卸载等行为描述,均基于内核与驱动的通用工作机理,不包含任何特定环境下的测试数据,也不对应任何具体机型或驱动版本。文中提到的查看与调整方式(如 ethtool 的 -k、-K、-S 等用法)属于公开的标准工具能力,实际可开关的项目受网卡型号与驱动实现限制,以本机 ethtool -k 的实际输出为准。
服务器与网络相关的产品信息、配置与价格,请查询 https://www.idc10000.net/ 相关服务器产品页,具体以官网实时报价与合同为准。本文不构成任何性能承诺,也不作为配置建议的直接依据,实际部署请结合自身业务特征验证后再调整。
Copyright © 2013-2020 idc10000.net. All Rights Reserved. 一万网络 科技有限公司 版权所有 深圳市科技有限公司 粤ICP备07026347号
本网站的域名注册业务代理北京新网数码信息技术有限公司的产品