TCP三次握手实战:用抓包定位连接超时与半连接队列问题

发布时间:2026/8/29 22:51:54
TCP三次握手实战:用抓包定位连接超时与半连接队列问题 TCP 三次握手这块内容基本是每个做网络、做后端、做嵌入式开发的都背过的八股文。但说句实话能把三次握手真正用来排查线上问题的人十个里未必有两个。我印象很深的一次线上事故业务方反馈服务端口时而通、时而不通应用日志里满是连接超时开发翻了一整天代码也没找到原因。后来我们同时在客户端和服务器两端抓包发现三次握手压根没走完——客户端只发了 SYN服务器一直没回 SYN-ACKSYN 就按 1 秒、2 秒、4 秒、8 秒的节奏反复重传。那一刻我意识到八股文背得再熟不如会看一张真实抓包。这篇内容会围绕 TCP 三次握手展开把握手原理、协议栈现场、线上事故复盘和排查命令串成一条线。适合被线上连接问题折磨过的后端开发、运维、嵌入式工程师也适合准备面试但想更进一步理解 TCP 的读者。1. 三次握手不是背出来的是看出来的1.1 真实网络上的三次握手长什么样三次握手的流程教材里写得明明白白客户端先发 SYN同步报文服务器回 SYNACK同步确认客户端再回 ACK确认。用 seq/ack 来精确描述就是客户端 - 服务器SYNseqx服务器 - 客户端SYNACKseqyackx1客户端 - 服务器ACKseqx1acky1为什么必须是三次而不是两次简单理解SYN 是“我在线你能听到吗”SYNACK 是“我能听到你你也能听到我吗”ACK 是“我能听到你我们开始说话吧”。只有三次交互双方才能各自确认自己的发送能力和对方的接收能力都正常。这个类比不完美但足够解释基本问题。线上看三次握手最常用的手段就是 tcpdump。抓包命令可以是tcpdump -i eth0 -nn tcp port 8080 -w /tmp/handshake.pcap抓下来的关键报文大概是这样的12:00:01.123456 IP 10.0.0.1.52341 10.0.0.2.8080: Flags [S], seq 1000, win 64240 12:00:01.123789 IP 10.0.0.2.8080 10.0.0.1.52341: Flags [S.], seq 2000, ack 1001, win 64240 12:00:01.123812 IP 10.0.0.1.52341 10.0.0.2.8080: Flags [.], ack 2001, win 64240这里 Flags [S] 是 SYN[S.] 是 SYNACK[.] 是纯 ACK。看包的时候我会特别关注两个信息一是握手有没有完成二是握手花了多长时间。如果 SYN 到 SYNACK 的间隔远大于同机房正常延迟说明中间有丢包重传或者服务器的处理链路出了问题这时候就已经可以判断问题在 TCP 层之下或之上而不是应用代码。这里要强调一个很容易被忽略的细节三次握手里任何一步丢了应用层的表现都是同一个——连接超时。但背后的原因是完全不同的可能是防火墙悄悄丢弃 SYN可能是服务器的半连接队列满了也可能是服务器的 accept 逻辑卡住。只看应用日志永远定位不到必须看抓包。“连接超时”这四个字的背后至少对应着五种完全不同的排障路径。1.2 四次挥手和序列号机制细节都在报文里经常被一起问的还有三次握手和四次挥手的区别。挥手之所以是四次核心原因是 TCP 连接是双向的每一方向的关闭都需要独立的 FINACK。A 发 FIN 表示“我没有数据要发了”B 回 ACK 表示“我收到了”但 B 此时可能还有数据没发完所以 B 要等数据发完后再发自己的 FINA 再回 ACK。这个“等数据发完”导致多出一个步骤于是就成了四次。三次握手和四次挥手在线上都是事故高发区但侧重点完全不同。握手阶段出问题连接根本建立不起来挥手阶段出问题往往是连接释放不干净表现为大量 TIME_WAIT、端口耗尽或者连接被 RST 强行断开。这里不展开太多后面第四部分我会给一张问题速查表。三次握手里 seq 和 ack 的关系是整个 TCP 可靠性的地基。客户端第一次发的 seqx服务器回的 ackx1表示“我已经准备好接收你下一个字节”这里的 1 是因为 SYN 占一个序列号。真实抓包里看到的 seq 不是从 0 开始而是随机的初始序列号 ISN这是为了防止伪造连接。理解这一点之后再看 Wireshark 里的相对序列号显示就不会被一串大数字吓到了。TCP 的 ACK 机制也不是逐包确认而是累积确认ack 号表示“这个序号之前的字节我都收到了”。基于这个机制才能理解为什么乱序时会出现重复 ACK进而触发快重传。这里顺带说一个 Wireshark 里偶尔看到的提示acked unseen segment意思是 ACK 号大于本地已经抓到的数据范围。我第一次见也以为出了问题后来发现多数情况是网络中存在乱序、中间设备有缓存或者抓包点不在完整链路上对端确实已经收到了数据。线上遇到这个标识别慌结合重传统计和丢包曲线一起判断即可。2. 三次握手背后是整个协议栈的现场2.1 半连接队列和全连接队列握手最容易翻车的地方三次握手能不能完成不完全取决于网络通不通很大程度取决于服务器内核里两个队列的状态半连接队列SYN 队列和全连接队列accept 队列。半连接队列存的是“收到 SYN 但还没完成握手”的连接。客户端 SYN 到达后如果半连接队列满了服务器直接丢弃这个 SYN客户端就只能等超时重传。全连接队列存的是“三次握手已完成、等待应用调用 accept”的连接。注意全连接队列满并不意味着客户端不能完成握手而是握手完成后的连接会被按策略丢弃或重置客户端表现为“握手看着成功了但连接马上不可用”。Linux 下我一般用 ss 来看这两个队列ss -lnt输出中 Recv-Q 和 Send-Q 两列的含义要分情况看。对于 LISTEN 状态的 socketRecv-Q 表示当前全连接队列里排队的连接数量Send-Q 表示全连接队列的最大长度。如果 Recv-Q 长期等于 Send-Q说明队列已经打满。内核参数角度重点关心这几个net.ipv4.tcp_max_syn_backlog限制半连接队列的大小net.core.somaxconn限制 accept 队列的最大长度应用层 listen 函数的 backlog 参数会被 somaxconn 封顶net.ipv4.tcp_syncookies半连接队列满时用 SYN cookies 缓解真实场景里我遇到过最经典的一次某 Java 服务高峰期偶尔连接超时监控里 CPU 不高、内存正常但 ss 显示某个端口 Recv-Q 一直顶到上限。定位下来是 accept 线程池被慢调用拖住线程全部阻塞在外部接口上来不及 accept 新连接。修复方向不是调大 somaxconn而是要去处理慢调用本身。加队列只能缓冲解决不了根因。这也解释了为什么只调内核参数经常治标不治本。2.2 握手时协商的那些参数才是吞吐量的关键很多人以为三次握手就是打招呼其实握手报文里还顺带协商了一堆影响传输性能的参数。其中最重要的三个MSS最大段大小、窗口缩放因子Window Scale、SACK 和时间戳选项。MSS 相当于“一节车厢能装多少货”。握手时双方会把自己的 MSS 告诉对方通常 MTU 1500 减去 IP 头 20 字节和 TCP 头 20 字节得到 1460。如果中间网络设备的 MTU 更小而握手时又没协商出合适值就可能出现“连接是通的但传大包时卡住”的怪问题。热词里提到的 iptables 的 tcpmss 选项就是用来钳制 SYN 报文里的 MSS 值避免 MTU 黑洞iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu窗口缩放因子则是影响吞吐上限的关键。TCP 头的 window 字段只有 16 位最大 65535 字节如果握手时不协商缩放因子一条 TCP 连接在延迟较大的链路上吞吐会非常有限。握手时双方会在 SYN/SYN-ACK 里带上 Window Scale 选项实际可用窗口等于窗口字段值乘以 2 的缩放因子次方。想要跑满高带宽长链路这个参数必须正常协商。流量控制层面的滑动窗口是接收方通过 ACK 里的 window 字段告诉发送方“你最多还能发多少字节”。如果接收方应用不读数据窗口会逐渐减小甚至变成 0这就是零窗口状态。线上表现为连接还在、底层网络也通但数据就是发不过去。很多人这时候会怀疑网络故障其实只是接收方应用卡住了。这也顺便回答了一个热词里的疑问TCP 比 UDP 更“省流”吗严格说 TCP 不是省流而是发送行为受到流量控制和拥塞控制的约束不会像 UDP 那样无脑灌数据。2.3 慢启动与拥塞控制握手完成后数据怎么跑三次握手完成连接建立数据开始飞。但 TCP 不会一开始就全速发而是用慢启动逐渐探测网络容量。初始拥塞窗口cwnd很小每收到一轮 ACK 就翻一倍直到达到慢启动阈值ssthresh或发生丢包。拥塞控制的几个阶段和线上事故直接相关慢启动连接刚建立时 cwnd 从初始值开始指数增长拥塞避免到达 ssthresh 后线性增长避免莽撞快重传收到三个重复 ACK立即重传疑似丢失的包不等待超时快恢复快重传后把 ssthresh 降为当前 cwnd 的一半再进入拥塞避免线上最典型的现象是网络有轻微丢包时TCP 一旦触发重传和拥塞控制cwnd 会锐减吞吐可能从几百 Mbps 掉到几 Mbps。用户感知就是“网卡了”但链路本身没断ping 也通只有看 TCP 统计才能发现问题。我遇过不少“应用无响应”的事件最后结论都是底层交换机端口有少量丢包TCP 拥塞控制把吞吐压垮了应用层的超时时间又定得太短。这里顺带提一个实用工具思路排查拥塞问题时除了抓包看重传还可以用 ss -ti 看单个 socket 的 cwnd、ssthresh、rtt 等信息ss -ti dport :8080输出里的 cwnd 和 rtt 能直接反映这条连接的传输状态。这个命令比 tcpdump 门槛低很多适合第一轮快速排查。数据从网卡到 socket 接收队列中间还要经过硬中断、软中断、IP 层、TCP 层如果系统层面出现软中断堆积即使三层四层都正常应用同样感知不到数据这条链路在排查时也应该纳入视野。3. 用三次握手视角复盘一次线上事故3.1 先别急着看代码把现象分好类说一个我自己处理过的典型案例。某内部 API 服务客户端不定时报连接超时重试几次后能恢复但持续了大半天。初步排查过程是这样的先 ping 网关通再用 telnet 测试端口时通时不通应用 CPU 和内存都正常日志里只有“连接超时”四个字。第一反应不要是去查业务代码而是先确认“通”和“不通”分别对应 TCP 层的哪个阶段。最简单的做法就是客户端抓包看请求到达哪一步如果连第一发报文都没有可能是客户端网络配置或 DNS 问题如果 SYN 发了但收不到 SYNACK问题在中间链路或服务器如果三次握手完成但应用没响应问题在服务端应用或全连接队列我们那次抓包一看客户端只发了 SYN服务器一直没回。再接服务器端抓包发现服务器网卡上根本没有收到 SYN。链路到服务器之前就断了。最终定位到负载均衡设备的后端健康检查策略把部分源 IP 的流量丢了节点本身没动静但设备悄悄把那些源 IP 拉进了黑名单。这个定位过程不算复杂但如果不是用三次握手的视角去拆解很容易在业务代码里绕一天。这里分享一个我自己养成的习惯任何连接异常优先用“哪一步没走完”来归类而不是直接用“网络不好”来定性。三次握手的三段报文天然把排查范围切成了三段——客户端出发段、中间链路段、服务器处理段每一段对应的证据完全不一样。顺带说一句ping 通只能代表 ICMP 可达它和 TCP 走的是不同协议栈路径不能拿它证明四层连通。3.2 全连接队列溢出的典型现场另一种比较常见的事故是握手能完成但连接马上被重置。抓包会看到客户端发完 ACK 后服务器直接回 RST或者服务器端的 SYNACK 反复重发。这种情况要重点怀疑全连接队列溢出。之前处理过一个有意思的现场业务团队反馈每隔几分钟服务就“卡死”一次重启能好但很快复发。我们做了两个动作一是抓包二是看 ss -lnt。抓包显示有大量握手完成后立刻 RST 的记录ss 显示 Recv-Q 在流量高峰前就已经接近 Send-Q 上限。根本原因是应用框架里连接监听 backlog 设置得很大但框架背后某个线程池被数据库慢查询拖死accept 停止运转队列一满内核就开始丢连接。调优方向通常有两个层面。第一层是内核参数把 net.core.somaxconn 调大以及确认应用 listen backlog 设置合理第二层才是真正的根因——处理线程不能阻塞。一个值得记住的组合是sysctl -w net.core.somaxconn1024 sysctl -w net.ipv4.tcp_max_syn_backlog1024但请注意这只解决“队列不够大”这一层解决不了“应用不去 accept”这一层。把 somaxconn 调得再大accept 慢的问题只会被推迟暴露不会消失。我向来建议团队在监控里加一台“全连接队列占用率”的指标一旦逼近 100%就说明应用处理能力已经到瓶颈而不是等到用户报障。3.3 嵌入式场景连接只能保持一分钟是怎么回事这个是我在排查工业设备时经常碰到的一类问题设备重启之后能连上一小会儿大约一分钟左右连接就断掉再次连接必须重启设备或者等很久才能连上。很多人第一反应是三次握手有问题但抓包一看三次握手明明是成功的问题出在建立连接之后。这种情况十有八九是“连接建立后再无数据导致中间设备清理会话”。典型路径是这样设备跟服务器之间有一个 NAT 网关或者工业防火墙这类设备会对 TCP 会话做老化通常一两分钟内没有数据就会把会话表记录删掉。客户端和服务端各自认为连接还活着但中间设备的会话已经没了后面数据一到就被丢弃表现为连接“假死”。解决办法通常有两个方向。一是开启 TCP keepalive让内核自动往连接里注入探测报文sysctl -w net.ipv4.tcp_keepalive_time30 sysctl -w net.ipv4.tcp_keepalive_intvl10 sysctl -w net.ipv4.tcp_keepalive_probes3二是应用层做心跳比如 Modbus TCP 这类工业协议尽量在应用层维持短间隔的读写请求让连接一直有流量避免被老化。所以排查这类问题时要牢记三次握手成功不等于连接链路长期可用中间设备的会话老化、NAT 超时、对端半开都会在握手完成之后把连接“暗杀”掉。这也顺便解释了一个很常见的现象Modbus TCP 设备能 ping 通但用 Modscan 之类的工具连不上。ping 走的是 ICMP和 TCP 完全是两条路径ICMP 能通只说明三层可达TCP 连不上还得继续排查端口、服务进程、防火墙规则以及从站配置。三层通、四层不通、应用不通是三个完全不同的问题。别以为只有后端才搞 TCPC#、Qt 里写 TCP 通信一样要面对这些状态问题排查思路完全通用。3.4 几种一眼就能定位的 TCP 相关报错最后列几个我平时经常见到的和 TCP 直接相关的报错属于看一眼就能缩小范围的那种Docker 启动容器时报 ports are not available: exposing port TCP 0.0.0.0通常是宿主机端口被占或端口范围不足跟连接本身无关但本质也是 TCP 端口资源分配失败。日志里出现 tcp: sendmsg failed due to socket memory overlimit说明发送缓冲区内存超限多数是 tcp_wmem 配置偏小或者系统内存压力大严重的确实可能导致 ssh 等依赖长连接的程序异常退出。Wireshark 里成片 TCP Retransmission 且间隔固定基本可以断定路径上有丢包如果重传伴随乱序优先怀疑中间链路设备开启了某项加速策略。这些报错本身并不代表 TCP 协议出了问题但都发生在 TCP 这条链路上处理思路也一样先看是哪一层没走通再看具体参数和策略最后回看应用逻辑。4. 线上排查 TCP 连接问题的命令和速查表4.1 常用的命令组合排查 TCP 连接问题我自己的工具箱里基本就这几样。首先是 tcpdump用于拿到最底层证据# 抓指定端口的所有 TCP 包 tcpdump -i eth0 -nn tcp port 8080 -s 0 -w /tmp/tcp.pcap # 只抓 SYN 相关 tcpdump -i eth0 -nn tcp[tcpflags] tcp-syn ! 0其次是 ss看当前连接和队列状态这个比 netstat 快且信息更全# 查看监听端口的队列情况 ss -lnt # 查看到某端口的连接细节包括 cwnd、rtt ss -ti dport :8080然后是系统统计用 netstat -s 或 nstat 看 SYN 重传、丢包、RST 等累积计数netstat -s | grep -i -E retrans|reset|overflow最后是确认内核参数。不要凭感觉调先把当前值记下来sysctl net.ipv4.tcp_syn_retries net.ipv4.tcp_max_syn_backlog net.core.somaxconn \ net.ipv4.tcp_keepalive_time net.ipv4.tcp_keepalive_intvl net.ipv4.tcp_keepalive_probes \ net.ipv4.tcp_rmem net.ipv4.tcp_wmem net.ipv4.tcp_abort_on_overflow排障顺序我一般固定为先抓包定性哪一段丢了再看系统统计定量丢了多少最后用 ss 定位到具体连接。这个顺序能避免一上来就被海量信息淹没。4.2 常见现象、原因与排查方向速查表结合前面几部分的内容我整理了一张速查表覆盖绝大多数线上 TCP 故障场景现象可能原因优先排查方向SYN 一直重传无 SYNACK中间链路丢包、防火墙丢 SYN、半连接队列满两端同时抓包看 SYN 是否到达服务器SYNACK 发出但客户端没继续客户端丢弃、NAT 表没有映射客户端抓包检查本地防火墙三次握手完成但请求无响应accept 队列满、应用线程阻塞ss -lnt 看 Recv-Q 是否顶满连接建立后数据发不出去接收方窗口为零、中间设备丢包ss -ti 看 cwnd抓包看 window 字段连接一段时间后自动断开keepalive 未开启、NAT 会话老化开启 keepalive应用层心跳大量 TIME_WAIT 导致新连接失败短连接太多、端口被占用看 ip_local_port_range考虑长连接大量 TCP Retransmission网络丢包、对端处理慢抓包统计重传间隔和乱序情况端口 bind 失败端口占用或端口范围不足lsof 查占用检查启用端口范围容器端口映射失败端口已被占用检查宿主机端口占用调整映射端口这张表算是我这几年处理线上问题的浓缩版大部分场景都能在这里找到入口。每次排查的时候先对着表确认最接近的一行再做详细抓包不会乱。4.3 排障思路和个人教训最后说几个我认为比较重要的排障理念。第一永远先看证据再看日志。应用日志里的“连接超时”只是一个症状不代表问题在应用抓包能够直接把问题定位到网络层还是传输层这一步不能省。第二抓包要两端同时抓只看一端很容易误判。比如客户端只发 SYN 没收到 SYNACK如果服务器网卡上根本没收到 SYN那问题就不在服务器如果服务器收到了 SYN 但没回包那才轮到服务器内核和 socket 层。第三查参数之前先看默认值改完参数之后一定要验证别把“调大 somaxconn”当成万能药。我踩过最大的坑是早年遇到一个莫名其妙的连接超时排查了半天后来发现是某个防火墙设备把每五分钟一次的健康检查报文误判成攻击直接把服务器 IP 拉黑了。那时候要是能第一时间用三次握手的角度去定位至少能省半天时间。从那以后我处理所有连接问题都坚持一个原则先定位是哪一段报文断的再去追原因。我个人现在带团队不管是不是网络组的人都要先会看懂 tcpdump 里的三次握手报文。TCP 三次握手是面试题但它同时也是每一次 TCP 真实连接建立过程的“现场”。下次线上再遇到连接超时别急着甩锅给网络先抓包看一眼 SYN、SYN-ACK、ACK 到底走到哪一步。八股文能不能解决线上事故就看你有没有能力把它翻译成现场证据。另外再分享一个小习惯排障结束后我会把抓包文件和当时的 ss 输出存档写上结论和解决措施。几个月后回头看很多“新问题”其实都有旧影子。TCP 这套协议几十年没变变的是我们对它的理解深度。