记一次综合型流量分析 | 添柴不加火
接到反馈时我正盯着监控面板发呆:某个核心业务平台,入口带宽半小时内从稳定期的 200Mbps 左右一路爬到 400Mbps 上下,几乎翻了一倍;负载均衡上的并发连接数从几千涨到上万,每分钟的新建连接数也在同步飙升。但有意思的是,平台的核心业务处理量,也就是真正完成的请求数,纹丝不动。说句大白话就是:柴添了一大把,火苗连晃都没晃一下。
那段时间团队正准备按惯例扩容,但直觉告诉我这事没那么简单。带宽涨、连接数涨,业务量却不涨,说明有大量流量在“空转”。带着“流量到底去哪儿了”的问题,我做了一轮从网络采集、数据包分析到会话级还原的综合型流量分析。这轮排查前前后后折腾了两天半,最后定位到的问题很有意思:不是某个单点故障,而是客户端连接复用策略、服务端健康检查机制和日志落盘策略三件事叠加在一起,制造了一堆看起来像请求、实际上毫无业务价值的“幻影连接”。这篇文章就是把完整排查链路复盘一遍,重点讲清楚每个环节我是怎么看数据的、为什么那么看,以及最终怎么把证据串成完整链条。如果你也遇到过“流量涨了、业务不涨”的情况,这文章应该能省你不少时间。
1. 异常现象:“柴添了一大把,火苗纹丝不动”
1.1 从监控面板上看到的诡异剪刀差
那天下午三点左右,值班群里开始有人喊“带宽告警”。我习惯先看两组对比数据:入口带宽和核心业务请求量放同一个时间轴,然后叠加新建连接数。正常情况下,这三条曲线应该是基本同步的,业务请求涨一点,带宽和连接数跟着动一点,顶多有几分钟的偏移。
但这次我看到的是一条典型的“剪刀差”:带宽曲线从 200Mbps 一路冲上 400Mbps,新建连接数从每分钟几千涨到两万多,可核心业务处理量却像被按了暂停键,在一千多 QPS 附近拉成了一条直线。更反常的是,用户侧反馈的接口响应时间并没有大幅恶化,只是偶尔出现几秒的卡顿,但并没有大面积超时。换句话说,系统嘴上说“我很忙”,实际干的活却没有变多。
这种剪刀差说明一个问题:流量成分里出现了大量“非业务性质”的请求。它们占了带宽、占了连接数、占了负载均衡和业务节点的处理名额,但没产生实际业务结果。这种情况下,如果直接按原计划扩容,一定是最亏的操作——你给炉子添了一堆柴,结果发现火不够旺是因为炉子本身结构有问题,柴再多也没用。
1.2 先定义问题:这不是扩容能解决的
很多团队遇到带宽上涨的第一反应是扩容或者限流。但我在这个项目里的习惯是,先花几分钟把问题定义清楚,再决定动哪里。当时的原始问题是:为什么流量和连接数上涨,而业务量不涨?要给这个问题一个负责任的答案,只靠监控面板不够,得看流量里到底是什么内容。
监控面板上的“带宽”是用整机网卡统计聚合出来的,它只能告诉你“有这么多的字节经过”,但没法告诉你这些字节属于谁、是什么协议、有没有完成业务交互。要回答这些问题,只能靠流量分析:在关键点位抓取原始数据包,然后从数据链路层一路解码到应用层,把每一条连接的行为还原出来。
我当时在团队里说了一句话:这次要做的是“综合型流量分析”,不光是看带宽和包量,而是要回答四个问题——流量从哪里来、由什么构成、经过哪些路径、在什么环节断掉。这四个问题对应的正好是流量分析的四个层级:数据链路层、网络层、传输层、应用层。后面所有的排查动作,都是围绕这四个问题展开的。
2. 综合型流量分析的采集设计与工具准备
2.1 观察点位的选择:为什么不能只看边界带宽
流量分析的第一步不是打开抓包工具,而是想清楚在哪里抓。带宽监控告诉你“入口有 400Mbps”,但入口是一个汇聚点,所有流量混在一起,根本分不清是来自外部的用户请求,还是内部服务之间的调用,或者是监控系统自己的探测流量。
我的做法是在三个位置同时观察:核心交换机出口镜像、负载均衡前端口镜像、以及两个典型业务节点的接入交换机镜像。端口镜像(SPAN)在这里比物理分光器(TAP)更合适,因为它是只读操作,不会在链路上引入故障点,配置也灵活。需要注意的是一台交换机的镜像端口带宽有限,如果被镜像端口流量超过镜像口能力会丢包,所以我们必须分两批采集,而不是把所有镜像流量灌到一个口上。
点位选择背后的逻辑是分层定位:核心交换机出口负责确认“流量确实进来了”;负载均衡前端口负责确认“流量进了入口就是进了集群”;业务节点接入交换机负责确认“流量有没有真正到达后端”。如果某个环节流量对不上,问题就能直接定位到那一层。这比拿着一堆数据盲猜要高效得多。
2.2 抓包时长、文件大小与工具链
采集方案上,我选的是在高峰时段抓 60 分钟全量包。为什么一小时?太短了只能看到瞬时脉冲,看不出趋势;太长了对磁盘和后续分析都是负担。我们当时预估入口 400Mbps 的流量,60 分钟下来,PCAP 文件大约在 100GB 到 120GB 之间。这个体积对后续分析和处理非常不友好,所以还要在采集端开一个简单的滚动抓包策略:单文件 1GB,写满就滚动,保留最近 30 个文件。
工具链上,我用了 tcpdump 做采集,tshark 做离线解析,配合自写的一组 Python 小脚本做统计聚合。图形界面工具 Wireshark 也装了,主要用于深入追溯个别可疑连接的时候做可视化查看,不适合直接打开 100GB 的抓包文件,会卡到怀疑人生。实际工作中,绕过 GUI、直接上手 tshark 命令是必备技能,也是这次能快速推进的关键。
一个小提醒:抓包文件的元数据非常容易忽视,但很关键。每份文件都要标注采集时间、采集点位、交换机端口,否则后面分析到一半,根本想不起来这段流量是从哪里抓的。我用文件名做了编码:20240115-1500-core-eth3.pcap这种格式,同时在分析前先用capinfos检查文件完整性,确认没有采集断点。
3. 第一轮带宽拆解:流量去了哪里
3.1 四维拆解:IP、端口、协议、包长
拿到第一批抓包文件后,我没有急着看具体内容,而是先做了一轮“四维拆解”:按源 IP、目的 IP、源端口、目的端口、协议类型和包长区间,把流量做一个聚合统计。这一步的目的是快速回答“流量从哪里来、由什么构成”这两个问题。
拆解结果出来之后,有两个发现比较有意思。第一,入口流量的双边分布并不对称:下载方向(从服务端返回客户端的响应方向)带宽占了总带宽的 85%,而上行请求方向的字节量只有正常水平的两倍左右。也就是说,网络里充满了大量响应报文,但请求数量并没有成比例地增加到那个程度。第二,按端口一看,90% 以上的流量都集中在 443 端口(HTTPS)。这排除了有大文件下载、视频流等异常大流量应用的影响,也说明流量看起来像是“正常业务流量”,不能靠“是不是被盗用了”这种思路去排查。
包长分布进一步暴露了问题:正常业务请求,尤其是 HTTPS 请求,数据包长度分布应该是混合的,有少量大包传输响应体,有大量中小包交替。但这次抓包里,响应方向出现了异常多的“短包”:64 到 128 字节之间的小包占比超过了 60%。这些小包体积小、数量大,带宽就是这么被撑起来的,而且它们的协议语义非常可疑——正常业务响应很少会大部分都是几十字节的空包。
3.2 连接数与包量之间的严重背离
四维拆解看的是流量成分,接着我做了传输层的初步统计,核心是观察连接数和包量之间的关系。这里有两种典型异常:如果连接数上升而每连接包数下降,说明连接在“反复建连但没干实事”;如果连接数不变而每连接包数上升,说明是单条连接内流量异常。
我用了 tshark 的-z conv,tcp会话统计功能,把每个 TCP 流的双向包数、字节数和持续时间导出来,然后写了个小脚本按“每连接总包数”做分桶统计。正常业务下,一条 HTTP 连接至少要经历 TCP 三次握手、几个请求往返、几个响应返回,总的包数在 10 到 30 包之间是比较常见的。但这批抓包里,有接近 40% 的 TCP 流只有 2 到 4 个包。
2 到 4 个包是什么概念?大概率是三次握手完成后,一方立刻发送了一个 RST 或者 FIN 关闭连接,中间没有任何数据交换;或者最多有一次数据请求,但响应还没来得及完成就断了。这种连接对业务没有任何贡献,却在连接表、CPU 调度、日志写入上消耗大量资源。重新看新建连接数的曲线,就知道这批“空连接”已经把系统的处理能力吃掉了一大半。
4. 会话级还原:一半连接是“幻影连接”
4.1 TCP 握手阶段的反复重建
有了“大量短连接”这个方向,我开始逐层深入,第一步看 TCP 握手阶段的异常。从抓包里提取 SYN、SYN+ACK、ACK 三类包的计数,然后计算握手成功率,以及有没有出现大量的重复 SYN。
统计结果让我确认了第一层异常:SYN 包总数比正常基线高了约 4 倍,正常时期每分钟大约 1.2 万个 SYN,这次涨到了每分钟 5.8 万个。如果这些 SYN 都成功建立了连接并且完成了业务,那业务 QPS 应该早就爆表了。但实际业务量没涨,所以这 5.8 万个 SYN 里,绝大部分连接根本没有走到“业务请求”这一步。
更关键的是握手的完成率。我筛选了每个 TCP 流的第一个包和最后一个包,发现只有约 55% 的流完成了三次握手,剩下的要么只有 SYN 没有响应,要么是 SYN+ACK 发出后客户端没有回最终 ACK。这类“半截连接”几乎可以肯定是客户端在服务端没有响应或响应很慢时主动放弃了建连,然后立刻发起重试。重试机制本身是正常的,但如果重试频率过高、连接池又不复用,就会形成这种 SYN 风暴式的流量特征。
这种模式和“火苗不动”之间的关系已经呼之欲出:系统根本没到业务处理环节,光是建连和放弃连接就把资源消耗差不多了。
4.2 建连后没有业务数据交换
接下来是第二步:重点分析那些完成了三次握手、但“没有业务数据交换”的连接。方法是在 tshark 里过滤出 TCP 流,统计每一条流的载荷字节数——注意这里统计的是 TCP payload,不是包的链路层大小,这样才能过滤掉握手包本身。
我用tshark -r capture.pcap -q -z conv,tcp导出每条流的bytes字段,然后写脚本判断:如果一条流的双向 payload 都小于 100 字节,就标记为“疑似空连接”。结果有点吓人:所有完成了握手的连接里,大约有 45% 的空连接,协商完加密握手后只发了一个几十字节的探测请求,然后就没下文了。
这类连接的特征非常统一:建立后大约 1 到 3 秒内关闭,关闭方式通常是从客户端主动发 FIN;连接里唯一的应用层数据是一个很小的 HTTP 请求,响应体通常不超过几十字节,内容几乎都是 200 状态码。它们很像“心跳”,但又比正常的心跳频繁得多——一个业务节点每秒钟几乎要收到十几个这种探测。这种连接确实能拿到 200 响应,但不产生任何核心业务副作用,纯粹就是把系统的资源烧在了无意义的事情上。
4.3 “幻影连接”的行为画像
到了这一步,“幻影连接”的行为画像基本清晰了:高频短连接、小报文、完成握手、来回一两个小请求、迅速关闭。它们不报错、不超时,正常监控看每个请求的响应时间也都正常,所以从业务监控指标上很难发现问题。但它们实实在在占了三样东西:连接数、日志写入量、以及每次建连和断连时操作系统协议栈的 CPU 开销。
而这三样东西里,最容易被忽略的是日志写入量。我后续检查服务端应用日志时发现,每条连接无论有没有业务,都会产生连接建立日志和连接关闭日志,有请求的还会额外打一行 access log。4 万个幻影连接一拥而入,等于日志系统每秒要处理接近一百行无意义记录,把日志 IO 打到了接近饱和。日志 IO 一旦饱和,又会拖慢正常请求的响应,引发客户端重试,重试又产生更多连接,形成一个恶性循环。
这个阶段的核心结论已经出来了:流量分析里,真正要紧的往往不是“带宽有多大”,而是“有效负载占带宽的比例有多高”。把连接生命周期拆开看,比盯着带宽曲线有效得多。
5. 顺着连接特征挖到的配置根因
5.1 客户端连接池参数与空闲超时的错位
“幻影连接”行为画像清楚了,接下来要回答的核心问题就一个:谁在制造这些连接?我先把抓包文件按源 IP 聚合并排序,结果发现制造幻影连接的源 IP 高度集中,排名前五位全部是平台自己机房里的网关节点和若干后台服务,根本没有真实终端用户。
这个发现直接推翻了我第一反应里“外部攻击或爬虫”的判断。问题出在我们自己的服务调用链路上:内部服务之间的 HTTP 调用才是元凶。
顺着源 IP 去翻客户端代码和配置,找到了一个很典型的配置错位。其中一个基础服务使用的 HTTP 客户端连接池,参数里设置了空闲连接存活时间为 30 秒;而服务端的 Keep-Alive 超时参数是 60 秒。表面上看客户端比服务端短,应该不会出问题,但真实行为不是这么线性的。服务端会在连接空闲满 60 秒时主动断开;而客户端连接池每 30 秒做一次空闲清理,清理之前会先发出一个无业务语义的探测请求,确认连接是否可用。
于是在服务端视角看,就会出现这种模式:客户端建连,空闲 29 秒,发一个几十字节的探测请求,然后又进入空闲期。服务端每 60 秒回收一批连接,客户端每 30 秒探测一轮,两条时间线的错位导致连接始终处于“建了探测、探测完闲置、闲置后重建”的循环里。说白了,客户端在拿一个健康检查动作去“养”连接池里的一堆空闲连接,而这些连接对业务没有任何增量价值。
5.2 服务端的健康检查探测与日志写放大
但只靠这个还不能解释带宽为什么能翻一倍。继续深挖之后,第二个元凶浮出水面:负载均衡对后端节点的健康检查频率配得太激进了。
我们的负载均衡器默认每 5 秒对每个后端节点发起一次 HTTP HEAD/GET 探测,路径是固定的/healthz。这次排查时有一个新上线的接入层,后端节点数量翻了几倍,再加上每条 VIP 都配了多个探测源,累计起来,健康检查请求在入口流量里的占比一下子就上去了。
这类健康检查请求的特征和幻影连接完全一致:短连接、小报文、固定路径、无业务逻辑。而且健康检查探测是“自产自收”的,既不是用户发起的,也不产生任何业务结果。从源 IP 聚合结果看,排名第一的源 IP 几乎全是负载均衡器地址,它们每分钟发出的探测请求数超过 4 万个。
健康检查配置本身不算错,错的是它和日志落盘策略叠加后的放大效应。服务端接入了全量访问日志,无论请求来自真实用户还是健康检查,只要产生一次 HTTP 交互,就会写一条结构化日志;连接建立和关闭还会额外写生命周期日志。高峰时段,这一层的日志写入量已经超出了磁盘的持续写入能力,导致 IO 等待时间显著上升。IO 一慢,处理新请求的速度跟着变慢,客户端重试又增加,最终呈现出“什么都涨、业务不涨”的全局现象。
5.3 证据链闭环:抓包特征与配置项的对应
到了这一步,排查已经从“猜测”走到了“证实”,剩下最关键的是把抓包特征和配置项一一对上,形成完整证据链。我把所有线索列成了一个表:
| 流量特征 | 对应配置/行为 | 证据来源 |
|---|---|---|
| 大量 2-4 包的短 TCP 流 | 客户端连接池探测请求 | 抓包流长度统计 |
| 每 30 秒一次的周期性探测模式 | 连接池空闲清理周期 30s | 时间戳间隔分析 |
固定路径/healthz的高频请求 | 负载均衡健康检查 | HTTP 层 URL 统计 |
| 日志写入量随连接数暴涨 | 全量访问日志 + 连接生命周期日志 | 服务端日志 IO 指标与代码 |
| 吞吐不升反降 | 日志 IO 饱和导致处理速度下降 | 系统 CPU/IO 监控 |
证据链里最漂亮的一环是“时间戳间隔分析”:我把幻影连接的第一个请求时间和前一个连接的关闭时间做差,发现间隔呈现明显的 30 秒和 60 秒两个波峰。30 秒对应客户端连接池的清理周期,60 秒对应服务端的空闲超时。这不是巧合,是两条独立配置共同作用下的必然结果。
这种“从抓包特征反推配置项”的思路,恰恰是综合型流量分析里最核心的部分。抓包只告诉你“发生了什么”,配置项告诉你“为什么发生”,两者对上,结论就铁了。
6. 调整、复测与复盘心得
6.1 做了哪些改动
优化方案分三层落下去。第一层是客户端连接池参数调整:把连接池的空闲清理周期从 30 秒改成和服务端对齐的 75 秒,同时关闭无业务语义的探测请求,改为在取连接时做真正的超时重连,而不是定时“诈尸”。这一层改动动的是根因,直接影响幻影连接的数量。
第二层是健康检查策略收敛:将负载均衡的健康检查周期从 5 秒调整为 15 秒,同时把探测路径从接入层转发到后端的逻辑改为在 LB 本地直接终结,不再穿透到业务节点。健康检查的目的是确认节点进程活着,而不是模拟完整业务链路,不需要也不应该打到业务代码里。
第三层是日志落盘策略优化:全量访问日志改为采样记录,抽样比例设为 10%;连接建立和关闭日志直接从业务日志里移出,改为独立审计渠道按需记录。与此同时,给日志盘做了 IO 调度优先级调整,避免日志写入挤占业务请求的处理资源。
这三层改动没有一个是“拍脑袋”的,全部是从抓包结论倒推出来的:第一层解决“谁在制造幻影连接”,第二层解决“谁在放大高频小报文”,第三层解决“资源被什么吃掉”。
6.2 复测结果
改动上线后,我按同样的采集方案,又在同一时间段各抓了一小时流量做对比。结果非常直观:入口带宽从 400Mbps 回落到了 230Mbps,比异常之前略高但已经在合理范围内;新建连接数从每分钟 2 万多降回到五千左右;SYN 包数量回到正常基线。
最有说服力的数据是这两个:重传率从峰值 8.7% 降回了 0.4%,说明协议栈层面的拥堵解除了;核心业务处理量从原来拉直的一千多 QPS,开始跟随真实用户请求波动,高峰冲到两千六,相当于处理能力直接翻倍。也就是说,没有新增一台机器,只是把空转的流量清掉,业务量自己就上去了。这正应了那句话:你不是火力不够,是柴火全堆在了炉子外面。
复测之后我又观察了两天,没有再出现流量和业务量背离的现象。
6.3 流量分析的经验体会
这轮排查下来,我自己的体会主要有三点。第一,流量分析不要只盯带宽和包量,那只是“症状”不是“病因”。真正有价值的是连接生命周期分析:连接是怎么建立的、存活多久、有没有业务数据交互、怎么关闭的。把每个连接当成一个独立对象去追踪,才能看到系统里那些“看似正常、实为空转”的流量。
第二,面对“流量涨了业务不涨”的问题,不要急着扩容。扩容解决的是“资源不够”,但如果问题出在“资源被无效流量占用”,扩容只是把浪费放大,成本高还不解决根因。先搞清楚流量构成里有效部分和无意义部分的比例,再做决策都不迟。
第三,健康检查、连接池探测、日志记录这三类“隐形流量”,在节点少的时候影响可以忽略,但一旦规模上来,它们会悄悄吃掉大量资源。平时做容量评估时要记得把这些算进去,就像给炉子添柴时,先数清楚有多少柴是湿的、点不着的。如果设计方案时有意识地统一各层的超时参数,后续这种“添柴不加火”的妖蛾子能少一大半。
最后再分享一个小技巧:这轮分析里最有用的一个命令是用 tshark 按 TCP 流做会话统计,配合一个十几行的 Python 脚本做分桶,快速找出“只有两三个包”的流。这套组合拳在几乎所有和流量相关的疑难杂症里都能用得上。如果你手头正好遇到类似的问题,先别急着加机器,抓一小时包,把连接生命周期拆开看看,答案往往就藏在那些不起眼的三两个包里。