网卡出问题最烦人的地方,不是它彻底罢工,而是它"半死不活"——链路状态一切正常,ping 通、握手成功、监控大盘绿油油,但业务侧就是慢、卡、偶发超时。这类问题的根子往往不在你的应用代码里,也不在交换机上,而是埋在网卡驱动某个极少被走到的分支里。我这次遇到的,就是这么一个典型的网卡驱动 BUG:它不报错、不崩溃,只在一个很窄的流量特征窗口里悄悄丢掉一个接收队列的中断,然后让哈希到那个队列上的几条长连接慢到让人怀疑人生。下面我把这三天两夜的完整排错链路摊开讲,包括我怎么排除掉物理层、怎么从驱动统计计数器里抠出证据、怎么用单变量实验把嫌疑范围从"整机"缩到"一个函数",以及最后怎么在不影响业务的前提下先止血、再打补丁、再做回归验证。适合正在被网络性能问题折磨的后端、运维、内核方向的同行参考,哪怕你对驱动内部不熟,前半段的取证方法论也能直接抄来用。
1. 先别急着换网卡:这台机器的"慢"有明确指纹
那天晚上十一点多,值班同事把一个告警截图甩到群里,说某台做数据同步的机器"网络高血压"。我第一反应是这词用得挺形象——CPU 不高、内存不高、磁盘不忙,唯独网络这条线忽高忽低,像血压一样一阵一阵往上窜。
1.1 三条曲线拼出来的异常画像
我没有急着登机器,而是先把三个东西拉出来并排看:业务侧的应用层耗时曲线、网卡收发字节的速率曲线、以及 TCP 重传率的曲线。这三条曲线放在同一个时间轴上,信息量比任何单一指标都大。
看完我心里大概有数了。应用层耗时呈现明显的"脉冲式尖刺",每隔十几秒就有一簇请求耗时从 20 毫秒跳到 3 秒以上;而带宽速率曲线在这段时间里,总吞吐量其实是够的,只是上行和下行出现了一种很别扭的比例失调。最关键的第三条:重传率一直在 0.3% 到 2% 之间抖动,峰值能到 5%,但重传的分布极不均匀——大部分连接干干净净,少数几条连接几乎所有包都在重传。
这个"少数连接全军覆没、多数连接毫发无伤"的特征,直接把排查方向从"整块网卡坏了"推向"某条通道坏了"。因为如果是对外链路问题、光模块老化、交换机端口拥塞,受影响的应该是全机所有连接,呈现一种均匀的劣化。而现在这种高度选择性,只能来自网卡内部做了分流——也就是多队列网卡把不同的连接哈希到了不同的硬件接收队列上,其中某一个队列出了问题。
这一步判断的价值极高。它让我省掉了至少半天在交换机侧和线缆上瞎折腾的时间,也避免了那种"先换个网卡试试"的粗暴做法——换卡如果碰巧让机器重启、队列重新初始化,问题会短暂消失,于是你以为修好了,过两天它又回来找你,而且这次你连上次做了什么都不敢确定。
1.2 为什么"能通但慢"比"彻底不通"更难查
彻底不通的问题,排查路径是收敛的:链路起没起来、IP 配没配对、路由通不通、防火墙拦没拦,一层层砍下去,总能砍到某个明确的断点。而"能通但慢"的问题,每一层都是"部分正常",每一层都能找到一点可疑数据,但每一层的数据又都不足以定罪。
更麻烦的是,这类问题有极强的环境敏感性。同一台机器,你在测试环境复现,它是好的;你在业务低峰期复现,它是好的;你甚至怀疑是不是自己看错了。我在第二天上午就经历了这个阶段:连续两小时监控曲线平得像一条直线,我一度以为问题被某个自动更新悄悄修好了,结果下午两点业务量一上来,尖刺又准时出现。
所以我在动手之前给自己定了一条规矩:在没拿到确凿证据之前,不重启、不重装、不改任何可能影响现场状态的配置。现场一旦被破坏,那些统计计数器的累积值会被清零,日志会被覆盖,你就失去了唯一能证明"这里真的发生过什么"的物证。
提醒一句:网卡驱动的绝大多数异常计数器和日志,都是累积型的或者只在异常发生瞬间打印一次。重启一次,等于把所有证据冲进下水道。
2. 四层假设与排除:从物理链路一路砍到驱动
我习惯把网络问题拆成四层去看:物理与链路层、硬件与固件层、驱动与内核层、协议栈与应用层。拆层不是为了走流程,而是为了给每一层设计一个"能证伪它"的实验——注意,是证伪,不是证实。找问题的时候,"证明它没问题"永远比"怀疑它有问题"更高效,因为前者能被一个干净的实验快速终结。
2.1 物理层与链路协商:先花十分钟排掉 80% 的嫌疑
物理层排查的核心只有两件事:链路有没有以预期速率和双工模式协商成功,以及有没有物理层的错误累积。
我做的第一件事是查链路状态。重点看三个字段:协商出来的速率、双工模式、以及链路是否处于全双工。这一步听起来很基础,但它能排掉一类非常隐蔽的坑——某些情况下网卡和交换机端口会协商成一个降级的速率,比如万兆口协商成百兆口,链路状态依然是"up",一切看起来都很正常,但带宽被砍了一百倍。这次的输出很干净,速率和双工都符合预期。
第二件事是查链路层错误计数。这里我关注的是几类:CRC 校验错误、帧对齐错误、以及丢弃计数。这三类计数如果持续增长,基本可以判定问题在物理介质或者光模块一侧。我连续采了三次样本,间隔十分钟,发现这些计数虽然有零星增长,但增速极低,而且和我观察到的问题时间点对不上。
第三件事是看链路翻转历史。我翻了内核日志里所有和链路状态相关的事件,确认在问题发生的时间窗口内,没有任何一次链路 down/up 记录。这一条很关键,因为如果链路反复翻转,那大量的问题现象都能被解释成"链路抖动导致的瞬时丢包",而链路不抖,就意味着问题发生在链路之上的某一层。
三件事做完,大概花了十分钟,物理层的嫌疑基本排除。我特意把这几项的输出截图存档,因为后面如果问题定位到驱动,我需要用这些数据证明"不是物理层的锅",否则跟厂商沟通的时候,对方第一句话一定是让你先排除线缆和光模块。
2.2 硬件统计计数器:驱动留给你的第一手证词
排除完物理层,我转向网卡自己的统计计数器。这里要区分两类计数器:一类是标准的网络接口统计,另一类是驱动私有的硬件统计。后者才是真正的富矿。
标准统计里我只看到零星的接收丢包和发送丢包,数量不大,乍一看像是正常的缓冲抖动。但驱动私有统计里的几个字段,让我一下子坐直了:
| 计数器类别 | 观测值特征 | 我的判断 |
|---|---|---|
| 接收缓冲不足类计数 | 数值随时间阶梯式增长,集中在问题时段 | 硬件收到包但没有可用描述符,包被直接丢弃 |
| 接收 FIFO 溢出类计数 | 有增长但幅度小 | 短暂突发导致,不是主因 |
| 队列复位类计数 | 从 0 跳到非零,且跳变时刻与业务尖刺吻合 | 硬件队列发生过复位,这是强信号 |
| 校验和与分片相关错误 | 基本为零 | 报文本身没问题,排除对端异常 |
那张表里最扎眼的是第三行。队列复位计数从零跳到非零,意味着硬件层面执行过一次队列重启动作。硬件不会无缘无故重启队列,通常是驱动在检测到某个队列长时间没有进展之后,主动下发的恢复动作。也就是说,驱动已经"发现"了异常,并且做了自愈——但自愈的代价,是那个队列在这段时间里彻底停止工作,上面挂着的连接自然就卡住了。
这个发现把问题的性质彻底改变了。之前的假设是"某个地方在丢包",现在的假设变成"某个队列会周期性卡死,然后被驱动重启"。丢包是结果,队列卡死才是原因。
2.3 换机换口换内核:正交实验怎么设计才不白折腾
拿到这个假设之后,我设计了一组单变量实验。这里的原则是:每次只改一个变量,并且每次改动之后必须有明确的观测窗口和判定标准,不能"改了看看"。
第一步是换端口。这台机器是双口网卡,我把业务流量切到另一个物理口上。如果问题跟着物理口走,那说明是硬件通道问题;如果问题不跟着走,而是依然表现为"某个队列异常",那嫌疑就上移到驱动。切换之后,问题依然出现,而且出现时异常的队列编号变了——注意,是编号变了,但"有且仅有一个队列异常"这个模式没变。这一步基本排除了单个物理通道的硬件故障。
第二步是换机器。我找了一台配置相同、但内核版本不同的机器做对照,把同样的业务流量引过去。这台机器跑了两天,一条尖刺都没有。这是整个排查过程中最有价值的一次对照实验,因为它把嫌疑范围从"硬件型号"缩到了"软件栈版本"。
第三步是版本比对。我把两台机器的内核版本、网卡驱动模块版本、固件版本列成一张表逐一比对。结果很清楚:出问题的机器内核版本较新,驱动模块也跟着换了新版;正常的那台机器内核较老,驱动是旧版。硬件型号完全一致,固件版本也一致。
到这里,嫌疑范围已经窄到"新版本驱动或者新版本内核的网络子系统改动"这一个点上。但"窄"不等于"准",我还需要拿到运行时的直接证据,证明这个队列到底是怎么卡死的。
3. 关键证据链:把嫌疑锁定到驱动与固件的交界处
前面所有的推断都建立在统计计数器和对照实验上,属于间接证据。间接证据能缩小范围,但不能定案。要定案,必须抓到异常发生瞬间的现场。
3.1 中断分布图上的那道"缺口"
多队列网卡的每个接收队列都对应一个独立的中断号。正常情况下,这些中断号的计数会随着流量均匀增长。我写了个一秒钟采一次的小脚本,把每个队列的中断计数记下来算增量,连续跑了半小时,然后把数据画成一张按时间排列的分布图。
异常一目了然:在大部分时间里,各队列的中断增量大致均衡,只有轻微的偏斜(这是 RSS 哈希的正常现象);但在问题发生的时段,某一个队列的中断增量直接归零,而其他队列的增量明显上升。归零的那个队列,编号正好和前面统计里出现复位计数的队列一致。
这个现象非常说明问题。中断归零意味着两种可能:要么这个队列上根本没有包进来,要么包进来了但中断没有被触发或者被屏蔽了。结合前面"接收缓冲不足计数在增长"的事实——包是进来了的,只是没被处理。那就是中断没有触发。
我进一步核对了这个队列对应的中断亲和性设置,确认它绑定在固定的 CPU 核上,而且那个 CPU 核的负载很低,不存在因为 CPU 忙不过来导致中断被延迟的可能。同时我看了软中断的统计数据,那个核上的网络软中断处理时间在异常时段也是零——彻底没活干。
一条清晰的因果链成型了:队列收到包 → 硬件写描述符 → DMA 把数据搬进内存 → 但中断没有上报 → 驱动不知道有包要处理 → 包在环形缓冲区里越积越多 → 缓冲区满 → 后续到达的包被硬件直接丢弃 → 驱动侧的超时检测机制发现了这个"长时间无进展"的队列 → 触发队列复位 → 中断重新使能 → 短暂恢复 → 循环往复。
3.2 watchdog 与 reset 日志的时间对齐
有了因果链,下一步是找日志佐证。内核的网络子系统里有一个看门狗机制,专门检测发送方向的队列超时;接收方向虽然没有完全对称的看门狗,但驱动自身通常会带一个检测逻辑,在轮询或者定时器里判断队列是否有进展。
我在内核日志里翻到了相关的记录,时间戳精确到毫秒。把这些记录和我采集的中断增量数据对齐之后,吻合度非常高:中断停止上报之后,大约经过几十秒,出现了队列复位的日志;复位之后,中断计数恢复增长;再过一段时间,又停止上报。
这里有个细节值得说:复位日志本身并没有报"错误",它报的是一个信息级别的事件。如果我只看日志级别过滤,这条记录会被直接过滤掉。这也是我第一次排查时漏掉它的原因——我一开始只关注了错误和警告级别。后来我改成不加级别过滤,把整个时间窗口的日志全部导出,才找到它。
经验:排这类问题时,不要用日志级别做过滤。驱动的自愈动作通常以信息级别输出,因为对驱动来说那是"我处理好了",对你来说那是"出过事"。
3.3 offload 与中断合并:两个最容易被误伤的"嫌疑人"
在锁定驱动之前,我认真排查了两个"惯犯":硬件卸载功能和中断合并。
硬件卸载这块,我逐个关闭了分片卸载、校验和卸载、大包聚合等特性做对照。关闭之后问题依然存在,只是表现形态略有变化。这一步的意义在于排除"卸载功能与特定报文组合触发硬件异常"这种可能。实测下来,关闭大包聚合会让问题出现得更频繁一些,这说明它只是改变了流量形态,不是根因。
中断合并是更值得怀疑的对象。现代网卡普遍支持自适应中断调节:流量大的时候减少中断次数以提高吞吐,流量小的时候增加中断次数以降低延迟。这套逻辑由硬件和驱动协同完成,而协同的地方恰恰是最容易出竞态的地方。
我用工具把这个网卡的中断合并参数读出来,发现自适应模式是开启的。然后我做了个实验:把自适应关掉,固定成一组保守的静态参数,再跑同样的业务流量。结果是——问题出现的频率大幅下降,但没有完全消失。这个结果非常关键:它说明中断合并确实是触发器之一,但不是唯一的必要条件。换句话说,关掉它只是把触发窗口变窄了,没有消除那个窗口。
到这一步,我已经可以确定问题出在驱动处理"中断使能与中断屏蔽"这对状态切换的逻辑上,而且这个切换跟中断合并的调节动作存在时序上的交叉。但具体是哪个函数、什么条件,还需要看代码。
4. 根因剖析:环形缓冲区回收路径上的竞态是怎么发生的
我把新旧两个版本的驱动源码拉下来做了比对。这个驱动的规模不小,直接全文 diff 会淹没在噪音里,所以我的做法是先定位到和中断控制、队列复位相关的几个函数,只比对这几块。
4.1 收包路径全景:从 DMA 到协议栈
为了说清楚问题,得先把收包这条路径捋一遍。虽然看起来复杂,但核心就四步。
第一步,硬件收到包,通过 DMA 把数据写进预先分配好的缓冲区,同时把对应的描述符标记为"已完成"。这个缓冲区来自一个环形的描述符数组,通常叫接收环。环的大小是可配的,比如 1024、2048、4096 个描述符。
第二步,硬件触发中断,通知 CPU"有活干了"。为了减少中断次数,现代网卡普遍采用中断合并:不是每来一个包就中断一次,而是攒一批,或者等一小段时间再中断。
第三步,驱动在中断处理函数里做最少的事——屏蔽该队列的中断,然后把这个队列挂到软中断的轮询列表里,由软中断去做实际的收包工作。之所以要屏蔽中断,是因为接下来驱动要一次性把环里所有已完成的描述符都处理掉,如果这时候中断还在不停触发,会白白消耗 CPU。
第四步,驱动处理完一批描述符之后,重新填充缓冲区、更新描述符状态,然后把中断重新使能,等待下一批。
关键就在第三和第四步之间那个"屏蔽—处理—重新使能"的窗口。这是一段典型的临界区,窗口内外的状态必须严格同步,否则中断就可能永久丢失。
4.2 卡死那一刻,描述符表发生了什么
比对代码之后,我找到了改动点。新版本在这个驱动的轮询函数尾部,增加了一段"按需决策是否重新使能中断"的逻辑,本意是优化:如果这一轮处理完之后环里还有积压的包,那就不急着开中断,直接继续轮询,省掉一次中断的往返开销。
思路本身没问题,问题出在这个决策依赖的一个状态变量上。这个变量同时会被中断合并的调节逻辑修改——中断合并的调节是由一个定时器驱动的,它会在运行时动态算出新的合并参数,然后写进硬件寄存器,同时顺手把这个状态变量标记成"参数已变更"。
于是就有了这样一个时序:轮询函数判断"环里没有积压,可以开中断了",读取状态变量得到"未变更";就在这条指令和后面真正去写中断使能寄存器之间,定时器抢了进来,把状态变量改成"已变更",并且因为参数变更而短暂地屏蔽了一次中断;定时器退出;轮询函数拿着刚才读到的旧值,执行了"不需要开中断"的分支。
结果就是:中断被定时器屏蔽了,而轮询函数以为自己已经开过了(或者说以为自己不需要开),两边谁都没去真正打开它。中断就此静默,直到驱动自己的健康检测逻辑在几十秒后发现队列长时间无进展,才强制复位恢复。
这个竞态窗口有多窄?按我的估算,在正常的 CPU 频率下大概只有几百纳秒。但恰恰是因为它窄,才极难复现——它需要中断合并的调节定时器正好落在这个窗口里。这也解释了为什么问题呈现出强烈的随机性和低速率相关性。
4.3 为什么只在低速率长连接场景出现
这里有个反直觉的地方值得展开:问题为什么在流量小的时候更容易出现,而不是在高负载时?
原因在于中断合并的自适应逻辑。流量大的时候,合并参数基本稳定在一个高合并值上,调节定时器判定"参数不需要变",根本不写寄存器,竞态窗口自然不会被触发。而流量小的时候,尤其是那种"长连接、低频次、小报文"的场景,合并参数会在几个档位之间反复跳变,定时器频繁动作,撞上窗口的概率就上去了。
我们这台机器跑的是数据同步,恰好就是这种形态:连接数不多,但每条连接都长期存在,报文小而稀疏。这解释了为什么我一开始在压力测试下怎么都复现不出来——我拿压测工具打满带宽,反而一切正常。后来我调整了测试方法,用少量连接、低速率、长时长的方式去压,复现概率立刻上来了。
这个教训我记了很久:复现一个线上问题,要复现的是它的流量形态,不是它的流量大小。很多人压测只知道拉高带宽,结果把最该触发的那个分支给绕过去了。
顺带算一笔资源账,说明为什么这类参数不能随便调大。接收环的大小直接吃内存:假设每个队列 4096 个描述符,每个描述符对应一个约 2KB 的缓冲区,单个队列就是 8MB;一台机器 16 个接收队列,光接收缓冲就是 128MB。如果为了缓解丢包把环调到 8192,这个数字翻倍。而且环越大,队列卡死之后积压的报文越多,恢复时的瞬时冲击也越大。所以"把缓冲区调大"从来不是解法,只是把症状推后。
5. 从规避到修复:参数、补丁与验证的三段式
定位到根因之后,剩下的工作是工程问题:怎么在不影响业务的前提下止血,怎么把修复落地,怎么证明它真的好了。这三件事的顺序不能乱,先止血再修复,先修复再验证,每一步都要有可回退的方案。
5.1 先止血:几个可以立刻生效的运行时参数
线上的业务不能等我慢慢编译验证,所以我先做了一组可以热生效的调整。这些调整不解决根因,但能把触发概率压到足够低,给后续的修复争取时间。
第一项,关闭中断合并的自适应模式,改用固定的静态参数。这一步直接掐掉了那个定时器的动作——参数不变,定时器就不会去写寄存器,竞态窗口基本不会被撞上。代价是失去了自适应带来的性能弹性,在高负载时可能会有更多中断开销,但对这台以低速率长连接为主的机器来说,这个代价可以接受。
第二项,收敛接收队列的数量。原来开了 16 个队列,实际业务连接数不多,大量队列其实是空闲的,但它们各自都有独立的中断合并调节定时器在跑,定时器越多,撞窗口的概率越高。我把队列数降到 4 个,配合中断亲和性重新绑定,既降低了竞态概率,也没影响吞吐。
第三项,把接收环的大小调到一个偏保守的值。这里的目的不是防丢包,而是缩短"队列卡死到被检测到"这段时间里积压的报文数量,减少恢复瞬间的冲击。同时我把驱动的健康检测周期调短了一些,让卡死能被更快发现和恢复。
这三项调整上线之后,连续观察了 72 小时,业务侧的耗时尖刺从每小时十几次降到零。但我很清楚这只是暂时的,因为根因还在。
| 调整项 | 目的 | 副作用 | 是否长期保留 |
|---|---|---|---|
| 固定中断合并参数 | 消除定时器与轮询的竞态触发条件 | 失去自适应的性能弹性 | 修复后恢复自适应 |
| 减少接收队列数 | 减少并发定时器数量 | 极端高并发下分流粒度变粗 | 视业务保留 |
| 缩小接收环 | 降低卡死后积压量 | 突发流量下更容易丢包 | 修复后调回 |
| 缩短健康检测周期 | 加快自愈速度 | 略增后台开销 | 保留 |
5.2 再修复:版本比对与最小复现用例
止血的同时,我在测试环境搭了一套最小复现环境。这套环境的关键设计是:不用压测工具,而是写一个小脚本,开 8 条长连接,每条连接每秒发几十个小包,持续跑。这个流量形态跟线上高度接近。
有了稳定的复现手段,验证补丁就有了依据。我做了三组对照:
第一组,旧版本驱动 + 最小复现流量,跑 12 小时,统计队列复位次数。作为基线。
第二组,新版本驱动(有问题的那个)+ 最小复现流量,跑 12 小时。这一步是为了证明复现环境确实能复现,如果这组跑不出问题,说明我的复现环境是无效的,后面的验证都没意义。
第三组,打了修复补丁的版本 + 最小复现流量,跑 12 小时。这一组是验证组。
修复本身的思路很直接:把那个状态变量的读取和中断使能的写入,放进同一个原子操作里,或者说,让"是否重新使能中断"这个决策不再依赖一个可能被并发修改的变量,而是每次都在临界区内重新判断。另外一个更稳妥的做法是在轮询函数退出前,无条件地做一次"重新检查并尝试使能",即使前面判断过不需要——这一次无条件检查的成本极低,但能把窗口彻底焊死。
我在测试环境里把这两种改法都试了。第一种改动小,但需要仔细梳理所有访问这个变量的路径,漏一个就白搭。第二种改动更粗暴,但更安全,而且几乎不影响性能——反正这个函数本来就在热路径上,多一次判断的开销可以忽略。
顺带说一句版本管理上的坑:我做版本比对的时候,一开始只对比了驱动模块自身的版本号,结果发现两个环境的驱动版本号是一样的,差点以为方向错了。后来才发现,驱动是以内核模块形式随内核一起编译的,版本号相同不代表代码相同,真正要比对的是代码本身和编译时间。这个细节坑了我小半天。
5.3 回归验证:怎么证明它真的好了
"问题没再出现"和"问题被修好了"是两回事。尤其这种低概率的竞态问题,你观察三天没事,不代表它真的没了,可能只是运气好。
我的回归验证分成三层。
第一层是复现环境的长时间浸泡。打了补丁的版本在最小复现环境下连续跑了两周,期间定时采集队列复位计数和中断增量的方差。为什么看方差?因为即使没有完全卡死,只要中断分布出现异常偏斜,也说明那条路径上有异常。两周下来,数据平稳。
第二层是灰度上线。先切一台机器,观察一周;再切三台,观察一周;最后全量。每一阶段都保留回退路径,一旦出现异常,一键切回旧版本。
第三层是加监控。这是我觉得最有价值的一步。原来的监控只看总的收发速率和总丢包数,这些指标对这种"单队列卡死"的问题几乎无感——一个队列卡死了,其他队列会顶上,总速率看起来毫无异常。我推动加了三类指标:按队列拆分的中断计数增量、按队列拆分的丢包计数、以及队列复位事件的计数。这三个指标加上之后,这类问题的发现时间从"业务侧抱怨"提前到了"异常发生后一分钟内"。
监控指标的粒度,决定了你能不能看见问题。聚合指标天生就是用来掩盖局部异常的。
6. 事后复盘:把这次排错沉淀成方法论
这次排错前后花了三天多,真正定位到根因只用了不到一天,剩下两天全花在验证和沟通上。回过头看,有些做法帮了大忙,有些习惯差点把我带沟里。
6.1 一套可复用的网卡故障取证清单
我现在处理这类问题,基本按下面这个顺序走,每一步都有明确的产出物,不做没有产出的事。
第一步,锁定异常画像。把应用耗时、带宽速率、重传率三条曲线对齐看,判断异常是"全局均匀劣化"还是"局部选择性劣化"。这一步决定后面所有方向。
第二步,排除物理层。看链路协商速率和双工、看链路层错误计数趋势、翻链路状态翻转记录。产出是一份"物理层正常"的证据。
第三步,读驱动私有统计。重点找接收缓冲不足、队列复位、硬件错误这几类计数,记录它们在时间轴上的跳变点。
第四步,看中断分布。按队列拆分中断计数,算增量,找归零或者严重偏斜的队列。这一步是定位到"哪个通道出问题"的关键。
第五步,对齐日志。不加级别过滤导出异常窗口的全部内核日志,和前面的时间点对齐。
第六步,设计对照实验。换端口、换机器、换版本,每次只改一个变量,每次都有明确的观测窗口和判定标准。
第七步,构造匹配的复现流量。复现的是流量形态,不是流量大小。低速率、少连接、长时长,这种形态往往比压满带宽更容易触发问题。
6.2 我在这次事故里改掉的几个坏习惯
第一个坏习惯是"先重启试试"。我以前遇到说不清的网络问题,第一反应是重启服务甚至重启机器,因为大部分问题重启就好了。但重启会把统计计数器和日志现场全部清空,对于这种偶发的、难以复现的问题,等于自断线索。现在我改成:先取证,存好数据,再决定要不要重启。
第二个坏习惯是只看聚合指标。总带宽、总丢包率、总连接数,这些指标看起来舒服,但它们天然会掩盖局部的、单通道的异常。一个 16 队列的网卡,坏掉一个队列,总吞吐只掉 6%,监控上根本看不出来。现在我更愿意花时间看按维度拆分的细粒度数据。
第三个坏习惯是压测只加压。以前验证网络性能,打开压测工具打满带宽,跑通了就认为没问题。这次的教训是,很多缺陷只在特定的流量形态下才会暴露,而压测工具生成的流量形态恰恰是最"标准"的那种——大包、满速、短连接。真实业务往往是反过来的。
最后一个,也算是个提醒:在这次排查的过程中,我跟硬件侧、内核侧的同行来回沟通了好几轮,每次沟通最大的障碍不是技术,而是"证据的形式"。同样一组数据,整理成表格和整理成一段描述,对方的响应速度完全不一样。后来我养成了习惯,每次沟通前先把数据整理成"现象—数据—结论"三段式,并且标注清楚哪些是实测、哪些是推断。这个习惯帮我省下的时间,比任何一次技术突破都多。