1. tcpdump 抓包的底层逻辑:内核里到底发生了什么
tcpdump大概是 Linux 上被用得最多、也被误解得最深的网络抓包工具之一。很多人对它的印象停留在“敲一行命令就能看到包里有什么”,但实际排障时你会发现,同一台机器上不同的人抓出来的东西完全不是一回事:有人抓不到包,有人抓到了但时间是错的,有人抓到的包校验和全是错的,还有人一抓包业务就抖。这些差异绝大多数不是命令敲错了,而是没搞清楚它到底在内核的哪一层动手、按什么规则筛选、把数据交给了谁。
先把定位说清楚:tcpdump是一个命令行抓包与解析工具,底层依赖libpcap库,在 Linux 上通过AF_PACKET套接字直接从网络设备层拿数据。它能做的事情包括:实时查看经过某块网卡的数据包、按表达式精准过滤、把原始数据流落盘成.pcap文件交给后续分析、以及作为脚本里的一环做长时间的抽样监控。适合的人群很宽:运维排障、后端排查接口异常、安全同学做流量审计、开发验证自己写的协议实现,甚至只是想弄明白“我这个请求到底发出去没有”的人,都用得上它。
但它的价值不在于“能看”,而在于“能看准”。一次有效抓包和一次无效抓包,区别往往在几个参数、一个过滤表达式、一次缓冲区设置上。下面我按从内核到命令行的顺序,把它拆开讲。
1.1 一份数据包的“影子副本”是怎么到手的
网络包从网卡进入内核,走的是驱动收包、软中断、协议栈逐层解析的路径。tcpdump并不在这条主路径的终点等着,而是在链路层就挂了一个钩子:AF_PACKET套接字注册到指定网络设备上,内核在把 skb 往上递之前,先复制一份(或者共享一份引用)投递给所有注册的抓包器。这就是为什么tcpdump能看到完整的以太网头,包括源 MAC、目的 MAC、VLAN 标签,而ss、netstat这类工具只能看到四层信息。
这个“复制”动作是整个抓包行为成本的核心来源。拷贝发生在软中断上下文,包越大、速率越高,拷贝的 CPU 开销越明显。所以你会看到一个很反直觉的现象:一条过滤表达式写得越精准,抓包对业务的影响越小,因为不匹配的包在内核里就被丢掉了,根本不会被复制到用户态。这也是我后面反复强调“先把过滤表达式写好再按回车”的原因。
另一个经常被忽略的点是 snaplen,也就是每个包最多抓多少字节。早期版本默认只抓 68 字节,头部是够了,但应用层内容基本看不到,很多人第一次抓 HTTP 请求体抓了个空,就是栽在这上面。现在的版本默认值已经提到 262144,基本等于全抓,但如果你要长期落盘,还是建议用-s手动控制到合理值,比如只看头部就-s 96,能省下大量磁盘。
理解了这一层,后面的所有参数就不再是死记硬背:-i决定钩子挂在哪个设备上,-s决定每次复制多少,-B决定内核给你留了多大的环形缓冲区,-w决定复制完往哪写。它们全是围绕“复制”这件事做文章。
1.2 混杂模式:开与不开,看到的世界不一样
默认情况下网卡只接收目的 MAC 是自己的帧,这是硬件层面的过滤。tcpdump启动时会尝试把网卡切到混杂模式(promiscuous),让网卡把总线上所有帧都收上来。这个行为在很多场景下是必要的:比如排查交换机端口镜像流量、分析 ARP 欺骗、看同一广播域里到底有谁在发包。
但混杂模式不是万能的,也不总是你想要的。在虚拟化环境和云主机上,网卡上游往往是一个虚拟交换机,你开了混杂模式也未必能看到别人的流量,因为上游压根不会把别人的帧转给你。而在一些安全策略比较严格的环境里,把网卡切到混杂模式可能触发监控告警,所以tcpdump提供了-p参数来关闭混杂模式——只抓本机收发的包。
我的习惯是:排障场景一律加-p。你要排查的是本机到对端的通信,本机没参与的流量对你没有意义,关掉混杂模式既减少噪音也避免不必要的麻烦。只有在明确要做流量审计、镜像分析的时候才打开。
这里还有一个坑值得单独说:用-i any抓包时,情况会变得不一样。any是一个伪设备,内核在链路层之上做了一次“伪链路层封装”,你抓到的包头是 Linux cooked capture 格式,不是标准的以太网头。表现就是:ether host这类基于 MAC 的过滤表达式在-i any下会失效或者报错,抓出来的包在 Wireshark 里看到的链路层也是Linux cooked。如果你需要分析 MAC、VLAN 或者精确的以太网帧结构,必须指定具体网卡名(比如eth0),不能用any。
1.3 BPF:把过滤放在内核里跑
过滤表达式是tcpdump最被低估的部分。很多人习惯先抓一大堆再慢慢看,实际生产环境里这么做风险很大:一方面是前面说的拷贝开销,另一方面是抓包文件动辄几个 G,后续分析反而更慢。
正确的姿势是理解过滤表达式是怎么工作的。你写的host 10.10.20.5 and port 443并不是tcpdump在用户态逐条比对,而是被libpcap编译成一段BPF 字节码,注入内核,由内核在抓包点上直接执行。这意味着过滤的代价极低,而且不匹配的包连复制这一步都省了。换句话说,过滤表达式写得越准,抓包越轻。
BPF 的能力边界也值得了解:它能访问链路层、网络层、传输层的固定偏移字段,能做算术和位运算,能组合逻辑条件,但不能做应用层字符串匹配(比如“找出所有 URL 里带/api/order的请求”)。所以tcpdump的定位是粗筛,精筛要交给落盘后的tshark或者 Wireshark 的显示过滤器。我通常的做法是:tcpdump只按 IP、端口、TCP 标志位这类字段筛,把范围压到几百个包,落盘之后再细看。
把这三层逻辑串起来就是:钩子挂在链路层(-i),复制多少自己定(-s),要不要别人的包自己定(-p),筛什么在内核里定(过滤表达式)。剩下的所有参数,都是在调整这四个决定带来的副作用。
2. 参数选型与速查:少踩坑的几种固定组合
tcpdump的参数有几十个,但真正日常用到的不到十五个。我把它们分成三类:影响抓什么的基础参数、影响存哪的文件参数、影响怎么显示的输出参数。分类的意义在于,你可以按需组合,而不是每次翻手册。
先给一张速查表,后面再逐条讲背后的原因。
| 参数 | 作用 | 使用建议 |
|---|---|---|
-i <iface> | 指定网卡 | 优先写具体网卡,慎用any |
-nn | 不解析主机名和端口名 | 几乎必加,避免反查拖慢 |
-p | 关闭混杂模式 | 排障默认加 |
-s <len> | 每个包抓多少字节 | 落盘时按需,-s 0表示全抓 |
-c <n> | 抓够 n 个包就停 | 交互式抓包建议加 |
-w <file> | 原始数据落盘 | 排障首选,便于复查 |
-C <MB> | 按大小切分文件 | 长时间抓必备 |
-G <sec> | 按时间切分文件 | 配合文件名时间格式 |
-B <KiB> | 内核缓冲区大小 | 丢包时调大 |
-Z <user> | 落盘后降权 | 生产环境建议开 |
-e | 打印链路层头 | 查 MAC、VLAN 时加 |
-A | 以 ASCII 显示载荷 | 看明文协议内容 |
-X | 十六进制加 ASCII | 看二进制协议 |
-tttt | 带日期的时间戳 | 跨天分析必备 |
-S | 显示绝对序列号 | 手工算偏移时需要 |
2.1 三个几乎每次都要带的基础参数
-i、-nn、-p这三个我称为“默认三件套”。-i不用多说,不指定的话tcpdump会自己挑一块网卡,挑错了你会在那儿纳闷“为什么一个包都没有”。用tcpdump -D可以列出所有可用网卡,配合ip -br a看清楚哪个是业务网卡、哪个是管理网卡,这一步花十秒钟,能省半小时。
-nn是很多人忽略的性能杀手。不加-n时,tcpdump每抓到一个包都会尝试做反向 DNS 解析,把 IP 变成域名。内网 DNS 不通或者响应慢的时候,你会看到输出一顿一顿的,甚至大量包被内核丢掉——因为用户态处理不过来,缓冲区满了。-n关掉主机名解析,-nn连端口名一起关掉(不加的话 443 会显示成https,看日志的时候反而不方便 grep)。所以只要不是在人面前演示,一律-nn。
-p前面讲过了,排障场景关掉混杂模式,减少无关流量。这一条在云主机上尤其明显,因为云环境的虚拟网卡经常能收到一堆 ARP、DHCP、健康检查的广播包,混杂模式开着,噪音能占到八九成。
组合起来就是我最常用的起手式:
tcpdump -i eth0 -nn -p -c 200 -s 0 -w /tmp/cap.pcap 'host 10.10.20.5 and port 8080'抓 200 个包就停,全量落盘,表达式限定到单机单端口。这条命令既快又轻,出了结果直接拖回本地用 Wireshark 打开。
2.2 落盘相关:-w、-s、-C、-G 与 -Z 的配合
-w是把原始二进制流写文件,这比在终端里看输出强太多。终端输出有两个致命问题:一是刷新太快,人眼跟不上,二是输出的是解析后的文本,很多信息(比如 TCP 选项、载荷内容)已经被截断或转换过了。落盘之后,同一个文件可以被反复用不同过滤条件读(-r file.pcap '表达式'),也可以交给tshark做统计。
-s和-w要一起考虑。-s 0表示不截断,全抓。如果你只关心 TCP 头(排查重传、握手、RST),-s 96就足够覆盖到 TCP 选项,文件能小一个数量级。我遇到过一次线上问题,同事抓了二十分钟全量包,文件 40 多个 G,传到跳板机上直接把磁盘写满了,最后还得上机器删。先想清楚要看哪一层,再决定 snaplen,这个习惯能救你很多次。
-C和-G是长时间抓包的安全阀。-C 100表示单个文件到 100MB 就切下一个,-G 600表示每 600 秒切一个。这两个参数经常和文件名里的时间格式一起用:
tcpdump -i eth0 -nn -p -s 96 -w /data/cap/%Y%m%d-%H%M%S.pcap -G 600 -C 100 'port 3306'注意文件名里的%Y%m%d-%H%M%S只有在配合-G时才会被strftime展开,单独写是不生效的,会老老实实生成一个名字里带百分号的文件。这个坑我踩过一次,第二天早上看到目录里躺着一个%Y%m%d-%H%M%S.pcap,还以为命令写错了。
-Z是安全相关的。tcpdump需要 root 权限才能抓包,但如果一直以 root 身份运行,一旦有解析漏洞就很危险。-Z tcpdump会在打开设备之后把进程权限降到指定用户。要留意的是:降权发生在打开输出文件之后还是之前,不同版本的实现细节有差异,如果目标目录只有 root 可写,可能会看到“Permission denied”。稳妥的做法是把输出目录的属主设成对应低权用户,或者干脆先写/tmp再挪走。
2.3 输出格式参数:让终端里的信息刚好够用
-e打印链路层头,排查 MAC 冲突、VLAN 标签、ARP 问题时必加。加了这个参数,输出前面会多出源 MAC 和目的 MAC,配合-nn一起用,一眼就能看出包到底是从哪个物理口进来的。
-A以 ASCII 显示载荷,适合 HTTP、Redis、MySQL 这类文本协议。抓 Redis 命令的时候我常用:
tcpdump -i eth0 -nn -A -s 0 'tcp port 6379 and host 10.10.20.5'输出里能直接看到*2\r\n$3\r\nGET\r\n...这样的内容,比翻十六进制快得多。-X则是十六进制加 ASCII 并排显示,适合看二进制协议或者排查编码问题。
时间戳参数容易被忽视,但对分析帮助极大。-tttt带完整日期,跨天抓包时必须加,不然你分不清是今天还是昨天的包。-ttt打印与上一个包的时间间隔,这个小功能在分析延迟问题时特别好用——你不需要导出到 Wireshark,直接在终端里就能看出两个包之间隔了 3 秒还是 3 毫秒。-ttttt则是相对第一个包的时间偏移。
还有-S,显示绝对序列号。默认tcpdump显示的是相对序列号(为了让你好读),但如果你要手工核对某个字节偏移、或者和另一端抓的包对比序列号,就必须用-S看原始值。两端同时抓包做对账时,两边都加-S,否则一边是相对值一边是绝对值,对不上。
3. 过滤表达式实战:把抓包范围压到最小
过滤表达式写得好不好,直接决定了这次抓包是“三分钟出结论”还是“抓了三个 G 还没头绪”。表达式语法本身不难,难的是在压力下快速写出既准确又不漏的条件。
3.1 三层基础过滤:主机、网段、端口
最基础的三类限定词是host、net、port。它们都可以加src或dst前缀来限定方向。方向限定在排查单向问题时非常关键:比如你怀疑请求发出去了但响应没回来,那就只需要抓dst host <本机IP>的入向包,出向包你已经知道长什么样了。
# 只看某台主机相关的流量 tcpdump -i eth0 -nn 'host 10.10.20.5' # 只看某网段,注意 net 后面跟的是网络地址 tcpdump -i eth0 -nn 'net 10.10.20.0/24' # 只看某个方向的包 tcpdump -i eth0 -nn 'src host 10.10.20.5 and dst port 8080' tcpdump -i eth0 -nn 'dst host 10.10.20.5 and src port 8080' # 端口范围 tcpdump -i eth0 -nn 'tcp portrange 8000-8100'关于net有个小陷阱:net 10.10.20.0/24和net 10.10.20在某些版本下行为不完全一致,写完整 CIDR 最保险。另外port会同时匹配源端口和目的端口,如果你只关心服务端端口,写dst port 8080更准。
还有一个实用的组合是排除法。有时候噪音来自某个固定来源(比如监控探针、健康检查),你可以在表达式里用not干掉它:
tcpdump -i eth0 -nn 'port 8080 and not host 10.10.20.99'这比抓完再筛选高效得多,因为排除掉的包在内核里就被丢了。
3.2 协议与字段级过滤:从“看端口”到“看内容”
基础过滤只能定位到连接,但很多问题需要在包级别判断。这时候就要用到字段级表达式,直接读包头里的字节。
最经典的一个是只抓带载荷的 TCP 包。抓 HTTP 明文内容时,如果不过滤,你会被大量的纯 ACK 包淹没,翻十页才看到一个真正的请求。这个表达式看起来复杂,但拆开很好理解:
tcpdump -i eth0 -nn -A -s 0 \ 'tcp port 80 and (((ip[2:2] - ((ip[0]&0xf)<<2)) - ((tcp[12]&0xf0)>>2)) != 0)'逐段解释:ip[2:2]取 IP 头里偏移 2 开始的 2 个字节,也就是 IP 总长度;(ip[0]&0xf)<<2取 IP 头第一个字节的低 4 位(首部长度,单位是 4 字节),左移 2 位换算成字节数;两者相减得到 IP 载荷长度。(tcp[12]&0xf0)>>2取 TCP 头第 13 个字节的高 4 位(数据偏移,单位也是 4 字节),换算成 TCP 头长度。再相减就是 TCP 载荷长度,不等于 0 说明这个包带着数据。
学会这个套路之后,很多字段都能这么读。比如排查路由环路时抓 TTL 异常小的包:
tcpdump -i eth0 -nn 'ip[8] < 5'ip[8]就是 TTL 字段。又比如只看 TCP 的 SYN 包,判断握手有没有发出去:
tcpdump -i eth0 -nn 'tcp[tcpflags] & tcp-syn != 0 and tcp[tcpflags] & tcp-ack == 0'tcp[tcpflags]是tcpdump提供的一个语法糖,等价于tcp[13],也就是 TCP 标志位字节。用&做位与,就能精确筛出某类标志组合。常用的几个:tcp-syn、tcp-ack、tcp-fin、tcp-rst、tcp-push。要注意的是,只看tcp-syn != 0会把 SYN+ACK 也带进来,所以判断“纯 SYN”必须再排除 ACK。
ICMP 也有类似的字段语法,比如icmp[icmptype] == icmp-echo只抓 ping 请求。这个在排查“ping 不通但端口通”的问题时很有用,能立刻区分是 ICMP 被拦了还是整条链路有问题。
3.3 逻辑组合与括号转义:那些一写就报错的表达式
and、or、not三个逻辑符组合起来能表达很复杂的条件,但括号的转义问题几乎每个人都踩过。原因很简单:括号在 shell 里有特殊含义,如果你不加引号,shell 会先把它解释成子 shell 语法,tcpdump收到的表达式就是残缺的,于是报syntax error。
正确写法是把整个表达式用单引号包起来:
# 正确 tcpdump -i eth0 -nn 'host 10.10.20.5 and (port 80 or port 443)' # 错误,括号会被 shell 吃掉 tcpdump -i eth0 -nn host 10.10.20.5 and (port 80 or port 443)还有一种情况是表达式里本身带单引号或者需要 shell 变量展开。前者用双引号包外层、单引号在内,后者用双引号并注意$的转义。我一般的规则是:表达式里没有变量就用单引号,有变量就用双引号并且把变量用${}明确括起来,减少歧义。
not的优先级也需要小心。not port 22 and host 10.10.20.5的含义是(not port 22) and (host 10.10.20.5),不是not (port 22 and host 10.10.20.5)。虽然在这个例子里结果一样,但在复杂表达式里差别会很大。只要用了not,就把后面的条件用括号明确框起来,这是最省心的做法。
写表达式的最后一条经验:先在本地用-c 5试跑。任何新写的复杂表达式,先加-c 5抓五个包看看输出对不对,确认无误再去掉限制正式抓。这个习惯能避免“抓了十分钟发现条件写反了”这种低级但极耗时间的失误。
4. 五个典型排障场景的抓包打法
工具本身不难,难的是遇到具体问题时知道抓什么、在哪抓、抓完怎么看。下面这五个场景是我这些年重复遇到最多的,每个都给出完整的命令和判读思路。
4.1 接口超时与 5xx:两端同时抓做对账
“用户说接口超时,服务端日志显示 200”这类扯皮,最好的终结方式就是两端同时抓包。服务端本机抓一份,客户端或者网关侧抓一份,然后按五元组和时间戳对齐。
服务端这边的起手命令:
tcpdump -i eth0 -nn -p -s 0 -w /tmp/server.pcap \ 'host 10.10.20.5 and tcp port 8080'客户端侧对应抓:
tcpdump -i eth0 -nn -p -s 0 -w /tmp/client.pcap \ 'host 10.10.30.8 and tcp port 8080'抓完先看几个关键时间点。用-ttt或者 Wireshark 的tcp.time_delta看:SYN 发出到 SYN+ACK 回来隔了多久(这是网络 RTT 加服务端 accept 排队时间)、请求最后一个字节发出到第一个响应字节回来隔了多久(这是服务端处理时间加网络时间)、响应发完到 FIN 隔了多久(这是连接关闭策略)。
如果服务端看到的请求到达时间比客户端发出时间晚了三秒,问题在网络或者在中间的负载均衡;如果服务端处理只花 50ms 但客户端收到响应花了三秒,那要看是不是响应包在回程被限速或者被中间设备排队。这种对账最有说服力的地方在于:双方看的是同一批字节,没有解释空间。
还有一个细节:如果响应体很大,注意看是不是卡在了某个窗口大小上。tcpdump输出里win后面的数字就是接收窗口,如果它一路降到很小然后停住,说明接收端应用读得慢,数据堵在缓冲区里。这个结论光看应用日志是得不出来的。
4.2 DNS 解析慢:到底是超时还是走了备用
“接口偶尔慢一下”最常见的元凶之一就是 DNS。应用层看到的是一次慢请求,实际可能是解析器先问了主 DNS 没回应,等超时之后才去问备用 DNS。
抓 DNS 非常简单,但要抓对位置。如果本机配了本地缓存(比如系统自带的解析服务),你在业务网卡上抓不到实际的递归查询,得抓环回口:
tcpdump -i lo -nn -p 'udp port 53'如果没有本地缓存,直接在业务网卡上抓:
tcpdump -i eth0 -nn -p -tttt 'udp port 53 or tcp port 53'判读的关键是时间间隔。正常情况下,一个查询发出到响应回来应该在几十毫秒内。如果你看到查询发出后整整 5 秒才有响应,或者压根没有响应、5 秒后又发了一次相同查询,那就是典型的重试超时。这时候配合-ttt看间隔最直观。
另一个常见情况是响应回来了但状态码不对。tcpdump会把 DNS 响应的 flags 打出来,[NXDOMAIN]表示域名不存在,[SERVFAIL]表示服务端处理失败。这两种情况下应用都会等解析器返回,而解析器可能还会去试别的后缀或者别的服务器,整体耗时就被拉长了。看到 NXDOMAIN 就要去查配置里的域名是不是拼错了或者少了搜索域,这种问题在容器环境里特别常见。
还有一点:DNS 用 UDP 时,如果响应超过 512 字节会被截断,解析器会改用 TCP 重查。你会在抓包里看到一次 UDP 查询后紧跟一次 TCP 连接。如果这个 TCP 连接本身有问题(比如被中间设备拦了),表现就是解析偶发失败。抓包时把tcp port 53也带上,就是这个原因。
4.3 TCP 重传与握手失败:从标志位看出链路质量
网络质量问题在应用层往往表现为“偶发慢”,在 TCP 层看就很清楚。判断重传主要看两个信号:重复 ACK和同一个序列号的数据被发了多次。
抓重传不需要特殊表达式,抓全量然后看就行。但如果流量大,可以先按标志位粗筛:
# 抓 RST 包,定位是谁主动断的连接 tcpdump -i eth0 -nn -tttt 'tcp[tcpflags] & tcp-rst != 0' # 抓纯 SYN,看握手请求有没有发出去 tcpdump -i eth0 -nn -tttt 'tcp[tcpflags] & tcp-syn != 0 and tcp[tcpflags] & tcp-ack == 0'RST 的方向特别有意义。如果是服务端发出的 RST,多半是端口没监听、连接队列满了、或者被应用主动关了;如果是客户端发的 RST,可能是客户端超时放弃或者连接池回收。抓到 RST 之后,用-X看一眼有没有带数据,有些服务会在 RST 之前塞一段错误信息。
握手失败的表现就更直白了:连续几个 SYN 发出去,没有任何 SYN+ACK 回来,等到超时后应用报连接失败。这时候要分两种情况:SYN 发出去了但没回应(网络不通、被安全策略拦、对端没监听),还是SYN 根本没发出去(本机路由问题、本地端口耗尽)。区分方法是在本机抓一次、在网关侧抓一次,看包有没有出网卡。如果本机抓到了但网关没抓到,问题就在本机到网关这一段。
重传的判读稍微复杂一点。你会看到同一个序列号的数据段出现两次甚至三次,间隔分别是几百毫秒、一秒多、三秒多(这就是 RTO 指数退避)。如果重传集中在某个时间段,说明那段时间链路有丢包;如果重传伴随大量重复 ACK,说明是快速重传,通常是单个包丢了触发的。重传率高但延迟正常的链路,比延迟高但零丢包的链路危害大得多,因为前者会拖慢所有大流量传输。
4.4 容器与网络命名空间内抓包
容器环境下最常见的困惑是“在宿主机上抓不到容器的流量”。原因很简单:容器的网络栈在自己的命名空间里,宿主机上的eth0看到的只是转发后的流量,而且经过了 NAT 之后源 IP 已经变了。
有三种打法,按推荐顺序排:
第一种是进命名空间抓。用nsenter进入容器的网络命名空间,这时候你看到的就是容器视角的完整网络栈:
# 先找到容器主进程的 PID nsenter -t <PID> -n tcpdump -i eth0 -nn -p -s 0 -w /tmp/container.pcap 'port 8080'这样抓出来的包,IP 是容器自己的 IP,抓到的网卡是容器内的eth0(实际是 veth 的一端)。这个视角最适合排查容器内部的应用问题。
第二种是在宿主机抓 veth 对。每个容器在宿主机上对应一个 veth 设备,名字类似veth3a1b2c。用ip link能看到这些设备,直接抓对应的 veth 就行。好处是不用进容器,坏处是要先搞清楚哪个 veth 对应哪个容器。
第三种是抓网桥。如果容器走的是 bridge 网络,抓docker0或者自定义网桥,能看到所有容器的流量混合。适合快速看整体情况,但定位到具体容器就要靠 IP 区分了。
这里有个必须注意的点:在容器里抓包需要额外的能力。默认的容器安全配置往往没有CAP_NET_RAW,tcpdump会直接报权限错误。稳妥做法是用nsenter从宿主机进入(宿主机上是 root),而不是在容器里装一个tcpdump再跑。另外,容器里的tcpdump版本可能很老,参数支持不全,这个也要留意。
还有一个隐蔽的坑:容器的网络命名空间里可能看不到宿主机层面的丢包。如果包在宿主机内核的 netfilter 或者流量控制环节被丢了,容器内的抓包是看不到的,必须在宿主机的对应 hook 点抓(比如抓网桥、抓物理网卡)。所以排查“包到底有没有到达宿主机”这类问题时,抓包位置必须逐层往上游推,一层一层确认。
4.5 抓包文件交接:怎么让接手的人少问三句话
抓包最终往往不是自己看,而是交给别人或者存档复盘。这时候文件本身的可读性就很重要。我一般遵守几条规则:
文件名带信息和时间。cap-20250115-1430-8080-timeout.pcap比1.pcap强太多,接手的人一眼知道这是什么。用-G自动切分时可以带时间格式,手工抓的时候自己起个好名字。
保留原始文件,不要只留过滤后的。有些人为了省空间,抓完直接tcpdump -r过滤一遍输出文本就删了原始 pcap。这是大忌,因为后续想换个条件分析就没了。原始 pcap 保存好,分析时用-r二次过滤:
tcpdump -r /tmp/server.pcap -nn -tttt 'tcp[tcpflags] & tcp-rst != 0'在交付说明里写清三件事:抓的是哪台机器哪块网卡、用的什么过滤表达式、抓的时间范围。这三条信息能省掉接手人百分之八十的追问。
如果要把文件从生产机传出来,注意大小和敏感信息。全量包可能包含明文凭据,传输和存储都要按内部规范处理。我一般会在抓包完成后立刻做一次-s 96的“瘦身版”用于传阅,原始版本留在原机短期保存。
5. 抓包自身的代价与翻车现场清单
tcpdump不是免费的。它在内核里复制数据、在用户态解析和写盘,这些开销在高流量机器上会真实地影响业务。同时,它看到的“真相”有时候是被网卡和内核加工过的假象。这一节讲的就是这两类问题。
5.1 丢包统计:为什么抓到的比实际少
每次tcpdump退出时都会打印三行统计:
1200 packets captured 1350 packets received by filter 150 packets dropped by kernel这三行的含义经常被误读。captured是真正写到文件或显示出来的数量,received by filter是通过了 BPF 过滤、被内核投递给tcpdump的数量,dropped by kernel是因为缓冲区满而被丢弃的数量。只要第三行不为零,这次抓包就是不完整的,基于它得出的结论可能出错。
丢包的原因几乎总是同一个:用户态处理不过来。解决方案按优先级排:
第一,把过滤表达式写得更严,减少进入用户态的包量。这是最有效的,因为没有拷贝就没有开销。
第二,加大内核缓冲区,用-B参数,单位是 KiB。默认值在较新版本是 2MiB 左右,高流量场景可以调到 32MiB 甚至更大:
tcpdump -i eth0 -nn -p -s 96 -B 32768 -w /tmp/cap.pcap 'port 8080'第三,减小 snaplen。每个包少复制几十字节,在高 PPS 下累积起来差别很大。
第四,直接落盘而不是打印到终端。终端输出涉及格式化和刷新,比写文件慢得多。远端的 SSH 会话更是如此,输出回传本身就要走网络。
还有一个容易被忽略的因素:磁盘 IO。如果你把 pcap 写在网络存储或者慢盘上,写盘速度跟不上抓包速度,同样会造成阻塞。抓包文件一律写本地盘,事后挪走。
最后一点经验:长时间抓包不要用-c,用-G和-C做滚动。因为-c到了数量就退出,如果期间丢了包你也不知道丢了哪段;滚动文件则能保证时间上的连续性,每个文件都能独立对应一个时间段。
5.2 校验和错误与 offload 假象
很多人第一次抓本机发出的包时会吓一跳:tcpdump输出里明明写着incorrect checksum,但实际上业务完全正常。这是网卡checksum offload造成的假象。
原理是这样:为了省 CPU,现代网卡支持让硬件计算校验和。操作系统在构造包的时候,把校验和字段先填一个占位值,标记“这个包需要硬件计算”,然后交给网卡。网卡算完才真正发到线上。而tcpdump是在交给网卡之前抓的包,所以看到的是那个还没算好的占位值,自然“错误”。
同样的问题也会出现在TSO/GSO上。大包分片的工作也被卸载给了网卡,所以你在本机抓包时会看到长度达到 65535 的“巨型 TCP 段”,这在物理链路上是不可能出现的。如果你据此判断“MTU 配置有问题”,那就是被误导了。
处理办法有几个:
第一,在接收端抓。接收端收到的是已经过网卡处理的包,校验和是正确的。所以排查协议问题时,优先在接收侧抓包,能避免一大类假象。
第二,临时关闭 offload,但这种操作会影响性能,只能在测试环境做,生产环境慎用:
ethtool -K eth0 tso off gso off gro off第三,在 Wireshark 里忽略校验和验证。打开首选项关掉 TCP/UDP checksum validation,就不会满屏红黑了。这是分析本机抓包文件时的标准做法。
我一般的选择是:如果只是看时序和标志位,校验和错误直接无视;如果确实需要验证数据完整性,就在对端抓包或者看应用层是否报错。绝大多数排障场景根本不需要关心校验和。
5.3 常见问题速查表
把上面这些零散的经验整理成一张表,遇到问题时可以直接对着查。
| 现象 | 最可能的原因 | 处理方式 |
|---|---|---|
| 一个包都抓不到 | 网卡选错 / 表达式太严 / 权限不足 | tcpdump -D确认网卡,去掉表达式试抓,检查是否需要 root |
| 抓到的包比预期少很多 | 内核丢包 | 看退出统计的 dropped 行,加-B、缩窄表达式、减小-s |
| 输出一顿一顿的 | 未加-n,反向解析阻塞 | 加-nn |
| 校验和全部错误 | 网卡 offload | 在对端抓包,或忽略校验和验证 |
| 看到超大 TCP 段 | TSO/GSO 卸载 | 属于正常现象,不要据此判断 MTU |
ether host报错 | 用了-i any | 换成具体网卡名 |
| 文件名里带百分号 | %格式未配合-G | 加-G或自己拼文件名 |
| 落盘报 Permission denied | -Z降权后无权写目标目录 | 调整目录属主或改写到可写路径 |
| 时间戳没有日期 | 未加-tttt | 跨天分析必须加 |
| 序列号对不上 | 一端用了默认相对值 | 两端都加-S |
| 容器内抓不到包 | 在宿主机抓的是转发后的流量 | 用nsenter进命名空间抓 |
| 抓包后业务抖动 | 拷贝和写盘开销过大 | 缩窄表达式、限制 snaplen、缩短抓包时长 |
最后分享一个我自己的固定习惯,算是这些年的肌肉记忆:任何一次抓包,先想清楚“我要回答什么问题”,再倒推需要抓哪一层、抓多久、抓多少。如果问题是“请求到底有没有到服务端”,那只需要抓 SYN 和第一个数据包,几十个包就够;如果问题是“为什么这个大文件传得慢”,那得全量抓并且关注窗口和重传。目的不同,命令完全不一样。我见过太多人上来就tcpdump -i any不带任何过滤,抓了两分钟,文件好几个 G,最后还是不知道要看什么——抓包这个事,准备工作比执行本身重要得多。