处理TCP问题,最怕的就是两眼一抹黑。应用报“连接超时”,数据库那边说“我没看到异常”,防火墙说“我没拦”,交换机说“我这边流量正常”——所有人都在踢皮球,但问题确实存在。这时候你手上最趁手的工具,就是tcpdump。它不关心你业务层的各种解释,只告诉你一个事实:数据包到底到没到,回了没有,哪一端先放弃的。
tcpdump是Linux下最经典的命令行抓包工具,零依赖、上手快,几乎所有发行版都自带或一行命令就能装。它特别适合两类人:一类是后端开发和运维,排查“连不上”“响应慢”这类高频问题;另一类是刚接触网络协议栈的初学者,用它把课本上的三次握手、重传、四次挥手真正在网络上复现一遍。这篇文章我不会给你堆参数手册,而是直接带你走一遍我实际排查TCP问题时的完整思路和抓包命令,看完你就能自己上手。
1. 动手之前:搞懂tcpdump到底在抓什么
1.1 tcpdump的工作方式与抓包位置的选择
tcpdump的原理并不神秘:它在内核里通过libpcap库把网卡上流经的数据包复制一份到用户态,然后按你给的过滤规则进行匹配和输出。注意“复制”这个词,tcpdump本身不修改、不拦截任何数据包,它只是一个被动的观察者。这一点很重要,因为很多人出了故障不敢抓包,怕抓包影响线上流量,实际上只要过滤规则得当,tcpdump的开销很小。
抓包位置的选择,决定了你诊断问题的有效程度。最简单的经验是:离问题点越近越好。客户端连不上服务端,就要在客户端抓“出去的包”和“回来的包”;服务端说没收到请求,就要在服务端抓“进来的包”。更复杂的情况是客户端和服务端都在你控制范围之外,中间隔着别人的网络,那你能做的只有在自己这一侧抓包,然后结合双端的时间戳和现象做推断。我曾经排过一个跨地域访问超时的问题,客户端抓包能看到SYN发出但始终没有SYN-ACK,服务端却说自己收到了连接请求也回了包。两边各执一词,最后才发现是中间链路设备丢弃了特定大小的包——这不抓包还真没法定位。
1.2 最常用的参数与过滤表达式,一条条说清楚
先把最常用的几个参数记住,日常排障90%的场景都靠它们。
tcpdump -i any -nn -s 0 -w /tmp/capture.pcap port 8080-i any:抓所有网卡,避免你搞不清流量走的是eth0还是bond0。-nn:不做域名和端口名解析。DNS解析在排查时非常干扰视线,而且如果解析超时还会拖慢抓包,所以一律关闭。-s 0:抓完整包。默认只抓前96字节,对于分析TCP载荷不够用,直接设0抓全量。-w:写文件。终端上滚动输出的包,事后分析效率极低,先落盘再慢慢看。port 8080:端口过滤,最常用的过滤维度。
过滤表达式是tcpdump的灵魂,但初学者很容易被复杂的BPF语法吓到。其实日常需要记住的就这几个:
| 需求 | 表达式 |
|---|---|
| 只看某台主机的流量 | host 10.1.2.3 |
| 只看某个端口的流量 | port 8080 |
| 只看某对地址和端口 | host 10.1.2.3 and port 8080 |
| 只看TCP协议 | tcp |
| 只看SYN包 | tcp[tcpflags] & tcp-syn != 0 |
| 只看RST包 | tcp[tcpflags] & tcp-rst != 0 |
为什么后面三个过滤要写成tcp[tcpflags] & tcp-syn这种形式?这是BPF语法里对TCP头部标志位的位运算写法。TCP头里每一个标志位占一个bit,SYN是00000010(bit 1),ACK是00010000(bit 4)。tcp[tcpflags]取出整个标志位字节,然后和目标标志位做按位与,结果不为0就说明该标志位被置位了。这里有个容易踩的坑:tcp[tcpflags] & tcp-syn != 0匹配到的不仅仅是纯SYN包,SYN-ACK也会被命中,因为SYN-ACK里SYN位也是1。如果你只想看“新建连接请求”的纯SYN,需要排除ACK位,写成tcp[tcpflags] & (tcp-syn|tcp-ack) == tcp-syn。这个细节在分析握手超时时特别容易混淆,我在后面实战部分会再强调。
还有一个参数值得单独说说:-c。指定抓多少个包后自动停止,比如tcpdump -c 10。不要小看它,在生产环境排查时,你往往只需要确认前几个握手包和重传包长什么样,-c能避免你按Ctrl+C之前抓入一堆无用流量。配合timeout命令使用效果更佳:timeout 10 tcpdump -i eth0 -nn port 8080 -w /tmp/a.pcap,10秒后自动结束,不用干等。
2. TCP问题诊断的思路框架:先明确你想验证什么
2.1 连接建立失败,怎么看三次握手
TCP连接建立失败,是排查对象中最常见的。应用日志里通常表现为“Connection timed out”或者“Connection refused”,两者对应的数据包现象完全不同。
“Connection timed out”说明SYN发出去了,但一直没收到SYN-ACK。这时抓包要看三个层面:第一,本机是不是真的把SYN发出去了?第二,对端有没有回SYN-ACK?第三,如果对端回了,这个回包有没有到达本机网卡?三层对应三个不同的故障域:本机路由或防火墙、对端主机状态或防火墙、中间链路。判断方法很简单:在本机抓包,看有没有SYN重传,再看有没有SYN-ACK乱序到达。如果SYN发了一堆,一个回应都没有,问题大概率不在本机,在对端或链路。
“Connection refused”则说明你收到了RST包。RST是TCP协议里“硬拒绝”的信号,收到它意味着对端确实在线,但端口上没有监听或者内核策略拒绝了你。分析这个问题时抓包目的不是看“有没有包”,而是确认谁发的RST、发的时机,这比单纯看日志要准确得多。
有个Linux参数你必须先知道:net.ipv4.tcp_syn_retries。它控制内核SYN重试的次数,默认值是6。也就是说第一次SYN发出后,大概1秒没有回应会重试,之后以约指数退避的节奏继续重试,总共约127秒后才彻底放弃。这就是为什么很多“连接超时”的应用层错误要等很久才报出来——理论上一分钟多,实际体验非常煎熬。排查时看到多个SYN重传包,时间间隔分别是1秒、2秒、4秒、8秒这样退避,就说明是本机的TCP栈在正常重试,问题在网络路径或对端。
2.2 连接慢、响应慢,重点抓重传和ACK
如果说连接失败是“看得见的病”,那重传就是“慢性病”,它让你觉得系统“卡”,但又没到完全不可用的程度。TCP的可靠性靠的是确认机制:发送方发了数据,要等接收方回ACK才放心;超过重传超时(RTO)没等到ACK,就重新发一遍。重传本身是设计的一部分,偶尔一两个重传不稀奇,但如果重传比例偏高,就说明网络上存在丢包,或者接收方处理不过来。
抓包时,重传包在tcpdump的终端输出里通常会有tcp retransmission的注释(写文件后用tcpdump -r查看也有),在Wireshark里则是一个单独的标记。正常流程中,数据包的时间应该是平滑递增的,一旦你看到大量数据包的时间戳“倒退”到之前的值,同时伴随“tcp retransmission”,丢包问题就实锤了。
除了重传,还有一个现象叫DUP ACK(重复确认)。接收方发现丢了一个包,但后续的包还在继续到达,它会不停地发送“我期待的是第N号包”的ACK,这就是DUP ACK。发送方连续收到3个DUP ACK就触发快速重传,不等RTO超时。看到DUP ACK,能进一步缩小范围:丢包发生在中间某个具体的位置,而且极可能是单方向的路径问题。
2.3 连接被重置,RST包意味着什么
RST包是TCP协议里最“粗暴”的终止信号,收到它意味着连接立刻死亡,缓冲区里所有未确认的数据直接作废。这和FIN不一样,FIN是“好聚好散”,大家优雅地挥手告别;RST是“话不投机半句多”,直接断。抓包时看到RST,首先要做的是分清到底是谁发的、发RST那一刻连接处于什么状态。
产生RST的场景非常多,但常见的就是这么几类:目的端口没有进程在监听(内核直接回RST);连接请求被防火墙策略拦截后模拟TCP栈发RST(比如某些云安全组);对端进程崩溃重启,连接状态丢失;本机TCP超时清理半开连接;应用层主动调用SO_LINGER强制关闭。排查RST时,我通常会在两端同时抓包做时间对齐,看谁先发RST。谁先发,谁就是“主动翻脸”的一方,问题大概率就在它身上。然后再结合那台机器上的进程状态和内核日志缩小范围。
3. 实战复盘:一次三次握手超时的完整定位
3.1 先收集信息再动手,别上来就抓
很多人的习惯是接到工单就立刻打开tcpdump,然后发现抓了一堆不知道是啥的包,分析起来更懵。我的习惯是先花两分钟收集信息:本机IP是什么,对端IP和端口是什么,当前这条连接在内核里的状态是什么样的。用到的命令很简单:
# 查看当前与目标地址相关的连接状态 ss -tn state established | grep 10.0.0.5 ss -tn state syn-sent | grep 10.0.0.5 # 查看本机路由,确认数据从哪张网卡出去 ip route get 10.0.0.5ip route get这条命令的价值很大,它不光告诉你走哪张网卡,还能告诉你源地址选的是哪个。如果你机器上有多个IP,而应用的源地址是另一个,路由策略会直接影响抓包的过滤条件。信息收集完毕,再决定在哪里抓、抓什么。
3.2 抓包命令怎么设计,一次讲透
假设要排查本机(192.168.1.10)访问对端(10.0.0.5)的8080端口超时,我的抓包命令是这样的:
tcpdump -i any -nn -s 0 'host 10.0.0.5 and port 8080 and tcp' -w /tmp/syn_timeout.pcap注意我把过滤条件用单引号包起来,里面用了tcp关键字来限定协议。如果你不写tcp,那么DNS、UDP等其他协议的流量只要满足host和port条件都会被抓进来。端口8080如果恰好是HTTP服务的,你可能会抓到大量TLS握手和应用数据,噪音很大。分析连接建立问题时,可以再缩窄到只抓带有SYN标志的包:
tcpdump -i any -nn -s 0 'tcp[13] & 2 != 0 and host 10.0.0.5 and port 8080' -w /tmp/syn_timeout.pcap这里tcp[13]是取TCP头第14个字节(偏移13,从0开始算)——也就是标志位所在的那个字节。& 2是对SYN位做按位与。这里要特别提醒:这个过滤会同时抓到SYN和SYN-ACK,但没关系,我们就是要同时看这两个方向的包才能判断问题在哪一侧。
抓完包,用如下命令回放查看:
tcpdump -nn -r /tmp/syn_timeout.pcap -tttt加-tttt是为了显示完整的日期时间,配合抓包时长能判断重传节奏。如果你的包里有这样的输出序列:
21:00:01.000001 IP 192.168.1.10.52001 > 10.0.0.5.8080: Flags [S], seq 1000 21:00:02.000001 IP 192.168.1.10.52001 > 10.0.0.5.8080: Flags [S], seq 1000 21:00:04.000001 IP 192.168.1.10.52001 > 10.0.0.5.8080: Flags [S], seq 1000三次SYN间隔约1秒、2秒,指数退避,但没有任何一个SYN-ACK回来。这基本可以确定:SYN出得去,但回包进不来,要么是中间链路单向丢包,要么是对端防火墙拦了入方向的SYN没有回包,或者对端根本没这个服务但防火墙没有回RST而是直接丢弃。下一步动作就是去对端抓包验证,如果对端也抓不到SYN,那问题就在更深层的位置。
3.3 从握手包推断RTT和丢包位置
如果SYN-ACK能回来,但ACK发出去后连接还是建立不起来,情况就更有意思了。这种问题往往表现为:SYN和SYN-ACK都成功交换了,但应用层迟迟没有数据,然后某个时刻收到RST。我在一次跨机房联调中就遇到过类似的问题:本机三个端口分别连三台服务器,其中两台正常,一台总是连接建立后立刻断。抓包看三次握手完全正常,问题出在第四次包——客户端发完ACK后,服务端立刻回了RST。
遇到这种情况,千万不要在客户端抓包上死磕。直接去服务端抓,会发现服务端的内核状态根本没有这个连接,回RST是因为服务端进程根本没收到连接。当时进一步查,发现服务端配置的内部负载均衡只在后端节点上放了90端口,而我们的应用连接的是8080,服务端进程监听的是另一个端口,连接当然被内核拒绝。这就是典型的“客户端看得见握手,服务端看不见连接”的两层认知差,只有双端对比抓包才能快速定位。
4. 实战复盘:重传与响应缓慢问题的定位技巧
4.1 用tcpdump直接观察重传节奏
响应慢、接口超时这类问题,很多情况下不是代码逻辑复杂,而是网络在反复重传。判断方法很简单:抓包时加上-tttt观察数据包时间,如果你看到某个应用数据包发送后,隔了一段时间又发了一遍相同序列号的包,就说明发生了重传。tcpdump在读取抓包文件时如果识别到重传,还会在包注释里直接标出来。
重传的关键不是“看到它”,而是“看到它之前发生了什么”。所以我在抓包时通常会把TCP头部选项也抓全,用-vv参数增加输出细节。这样能看到接收方通告的窗口大小(Window)以及时间戳选项。窗口突然变成0,说明接收方缓存满了,发送方再发数据就是徒劳,只能等窗口更新。时间戳选项则能精确计算数据包单向延迟,如果时间戳显示一个包的发送时间和接收时间差了几百毫秒,而你确认走的是内网,那基本是链路质量出了问题。
4.2 重传问题的常见根因排序
根据我的经验,重传问题的根因按出现频率排大约是:物理链路丢包、中间防火墙安全策略丢包、TCP缓冲区太小、接收方处理不过来、网卡异常或驱动bug。物理链路丢包最好查,看对端交换机端口的CRC错误计数就行;防火墙策略丢包最气人,因为它不做回应,数据包就像进了黑洞,只能靠两端对比抓包定位。
比较隐蔽的是MTU问题导致的重传。TCP在握手时会协商MSS(最大报文段大小),如果中间链路MTU比两端协商出来的MSS小,大包就会被丢弃。表现出来就是小包通信正常,一传大数据就断,然后重传。查这个问题我习惯抓包时指定-s 0抓全包,然后在Wireshark里看[Packet size limited during capture]之类的标记。用tcpdump也能看:如果同一份payload你看到一个包发了两次,第二次报长度明显不同,大概率是分片和重组的问题。
# 查看系统当前的TCP重传统计(实际是全局累计值) netstat -s | grep -i retrans这个命令输出的重传计数是系统开机以来的汇总值。排查时先记一个基准值,过几分钟再看一次,如果增长很快,就说明系统整体在经历丢包,而不是某一个连接的问题。诊断范围一下子就打开了。
4.3 感受一下:把抓包结果和系统状态放在一起看
单纯抓包能告诉你“网络丢了包”,但解释不了“为什么丢包”。所以我的习惯是抓包的同时记录系统的网络栈状态,用到的命令不多:
# 队列长度和丢包统计 ss -s netstat -iss -s输出里有TCP的当前连接数和各种计时器信息。netstat -i能直接看到网卡层的收发包错误数、丢包数和溢出数。如果一块网卡的RX-ERR或RX-DRP列长期大于0,问题就不在网络而在本机网卡或中断处理上。我遇到过一次高并发场景下大量TCP重传,抓包怎么看都是对端没回ACK,但查来查去发现是本机网卡多队列没有开启,CPU软中断全部堆在一个核上,处理不过来了,导致收包延迟和丢包。这个案例当时如果不看netstat -i,光分析包可能要浪费很多时间。
5. 实战复盘:RST连接重置问题的排查实录
5.1 用过滤快速锁定RST包
RST包的出现通常是“突然死亡”,而且它本身往往只占用一个包的数据量,混在大量正常流量里非常容易漏看。所以我排查RST时,第一件事就是把RST包单独过滤出来:
tcpdump -i any -nn 'tcp[13] & 4 != 0' -w /tmp/rst.pcaptcp[13] & 4就是RST标志位。抓到后查看来源IP和端口,基本就能判断出是谁在“主动发难”。这里有个小细节:TCP头发送方在设置RST时,往往也会带上ACK标志(RST-ACK),所以不能简单靠“只有RST没有ACK”来认定,而是要综合连接当时的序列号和确认号来分析。
收到RST的一瞬间,连接里已经传输的数据全部作废,所以应用层可能同时报“Connection reset by peer”。这句话网上很多人解读为“对端主动断开”,其实不准确。它只说明你收到了RST,但谁发的、为什么发,必须结合抓包和两端状态才能确定。
5.2 双端抓包,确认谁先翻脸
排查RST的黄金法则是双端同时抓。只有看到时间线,你才能回答“谁的RST先出现”这个问题。如果是服务端先发RST,而客户端根本没做什么异常操作,那就是服务端的问题。如果客户端先发RST,那就得回头看客户端应用逻辑。
这里有一个很典型的坑,就是半连接回收问题。一台服务器上可能有海量短连接关闭,内核为了回收资源,会定期清理TCP半开连接。如果清理的时候恰好有客户端在这个连接上发了新数据,内核就直接回一个RST。这类问题在抓包上表现非常诡异:发送方认为自己还在发送数据,接收方已经忘了这个连接。解决方向通常不是抓包能搞定的,而是要看应用是否用了连接池复用空闲连接,或者调整内核的tcp_keepalive_time参数。
5.3 RST问题的几个典型场景速查
分享一下我工作中排查RST故障的频率清单,供你参考:
| 场景 | 抓包特征 | 常见根因 |
|---|---|---|
| 端口未监听 | SYN进来,立刻回RST,没有SYN-ACK | 进程没启动,或监听在别的端口 |
| 防火墙拦截后回RST | SYN进来后RST,来源看起来像对端 | 安全组/防火墙策略,伪装内核行为 |
| 连接空闲超时被回收 | 长时间无数据后,来一个数据触发RST | keepalive配置不当或负载均衡空闲超时 |
| 服务端进程崩溃 | 已有连接上收到RST,时间点和进程崩溃一致 | 进程异常退出,内核清理连接 |
| TIME_WAIT过多导致新建连接失败 | 新SYN收到RST,老连接TIME_WAIT堆积 | 四元组复用冲突,或tcp_tw_reuse配置不当 |
每次我遇到RST,第一件事都是看时间点是否和某个操作吻合:是不是刚发布过代码?是不是负载均衡刚做过健康检查?是不是防火墙刚改了策略?RST不像超时那样有模糊的等待空间,它是精确到毫秒的,所以时间点是破案的第一线索。
6. tcpdump使用中的常见坑与提效技巧
6.1 抓包时的性能顾虑,怎么把影响降到最低
生产环境抓包,大部分人的顾虑是“会不会增加延迟”。tcpdump本身只复制包,理论上单包占用的CPU很低,但前提是你过滤得当。我有一次在流量很大的网关上抓包,因为没有加port限制,直接抓了整个网卡的所有流量,结果tcpdump进程CPU直接飙到200%。后来加了精确的host+port过滤,CPU降到个位数。
另外记住一个大原则:线上抓包别追求“全”,追求“准”。宁可用-w落盘、用-c限制包数量,也不要让终端刷屏——终端输出本身比抓包更费CPU。如果必须长时间抓包,建议配合strace确认tcpdump进程没有异常行为,或者干脆用snaplen只抓包头(比如-s 96),分析连接级问题足够了,占用的IO会小很多。
6.2 抓包文件的保存和分析习惯
tcpdump写文件时默认是不带缓冲的,这意味着如果你用Ctrl+C中断,文件末尾可能缺几个包。这不算bug,但会影响分析。解决方法是抓包结束后等一两秒再中断,或者用timeout命令自然终止。还有一个实用技巧:抓包文件最好用pcap格式,因为无论后续用Wireshark、tcpdump还是tshark去读都可以,格式兼容性最好。
分析阶段我推荐一个习惯:先用tcpdump -r快速过一遍,确认包的数量、标志位方向、是否有重传标注,再用Wireshark做图形化深挖。Wireshark里可以直接看TCP流图、计算RTT、看Expert Information提示,很多tcpdump终端上不容易看出来的问题,图形界面里一目了然。但千万不要本末倒置——图形工具只能帮你确认现象,定位根因还是要回到系统状态和业务日志上。
6.3 更精准的过滤:指定标志位组合
过滤表达式里最容易出错的场景已经提过了:纯SYN和SYN-ACK的区分。我平时有一个习惯,对于需要精确匹配标志位的场景,宁可多敲几个字符,也要写成完整的位掩码比较。比如:
# 只看纯SYN包(SYN=1 且 ACK=0) tcpdump -nn 'tcp[13] & (tcp-syn|tcp-ack) == tcp-syn' # 只看纯ACK包(ACK=1 且 SYN=0 且 FIN=0 且 RST=0) tcpdump -nn 'tcp[13] & (tcp-syn|tcp-fin|tcp-rst) == tcp-ack'这里tcp-syn、tcp-ack都是tcpdump内置的宏,对应标志位的数值。用括号把多个标志位包起来做位运算,再跟目标值比较,能精确匹配组合。这种方法比单独& tcp-syn != 0更可靠,因为在分析建连异常时,把SYN-ACK误当成SYN会让你得出完全错误的结论。
7. 写在最后:把抓包能力沉淀成自己的诊断习惯
按照我个人的经验,学会tcpdump语法只是一星期的事,真正值钱的,是在一次次排障中积累的“抓包直觉”。你看到一堆包的时候,能不能最快反应出这是哪一层的谁在做什么、正常流程应该是什么样、现在偏离了什么——这个直觉没法靠参数手册培养,只能靠多抓、多看、多对比双端状态。
最后分享一个我一直沿用的工作习惯:每次抓包排障,我都会把系统当时的连接状态(ss -s)、TCP统计(netstat -s)、网卡错误统计(netstat -i)和抓包文件放在一起,作为一个完整的“故障现场”保存下来。等故障修复之后,再回来翻一遍,对比正常状态下的抓包有什么不同。几次下来,你对TCP协议的理解就会从“课本上的三次握手”变成“能摸到心跳的真实网络”。希望你也能在下次遇到网络怪问题时,先想到打开tcpdump看一眼——它不会说话,但数据包从来不说谎。