TCP高频面试题知识地图:握手挥手、重传窗口与拥塞控制调优
做后端开发这些年被问得最多的网络问题基本都绕不开 TCP。不管是校招一面还是工作五六年后的技术终面面试官翻开简历看到熟悉网络编程下一句大概率就是说说三次握手。很多人背得滚瓜烂熟真遇到线上大量的 CLOSE_WAIT、连接建立慢、吞吐上不去这类情况还是两眼一抹黑。这篇东西我想换个角度写把 TCP 高频面试题当成一张知识地图从面试官真正想考察的点出发一路拆到内核参数和生产环境里的排查手法。适合正在准备面试的同学也适合工作几年但一直没系统梳理过传输层的朋友看完至少能做到面试时答得有层次、线上出问题时知道往哪儿看。1. 先把问题拆开TCP 面试到底在考什么1.1 面试官的三层考察意图大部分人准备 TCP 面试的方式是背结论三次握手是为了防止失效连接请求突然到达服务端。这句话本身没错但只答到这一层面试官往往会追问一句还有别的原因吗然后就卡住了。我观察下来TCP 相关的提问基本分三层。第一层是事实层考你知不知道这件事比如 TIME_WAIT 的时长是 2MSL、快速重传的阈值是 3 个重复 ACK。这一层靠记忆就能过也是最容易被刷掉的地方——不是记不住而是记得零碎一追问就露馅。第二层是机制层考你知不知道这个设计解决了什么问题。比如为什么握手是三次而不是两次本质上是双方都要完成发送自己的初始序列号和确认对方的初始序列号两个动作任何一方少做一次序列号同步就不完整。答到这一层面试官会觉得你理解了协议而不是背了书。第三层是工程层考你能不能把协议知识用到实际场景里。典型问法线上服务端出现几万个 CLOSE_WAIT你怎么排查、跨机房传输只有 200Mbps但带宽是 1Gbps可能是什么原因。这一层最能区分候选人的真实水平因为它要求你把协议行为、内核实现、应用代码三者串起来看。1.2 为什么 TCP 是面试的固定保留节目我在实际带人和被面的过程中发现TCP 之所以常考是因为它天然具备三个特点。一是可验证。你答得对不对面试官很容易判断不像架构设计题那样见仁见智。二是有梯度。同一道题从三次握手是哪三次到SYN 队列满了会怎样可以问出五六个深度能精确测出候选人的水位。三是强关联。TCP 和并发模型、连接池、超时设置、负载均衡、微服务调用链都直接相关一个对 TCP 没概念的人写出来的 RPC 客户端大概率会在超时重试和连接复用上踩坑。所以准备这部分内容正确的姿势不是刷题库而是把 TCP 当成一个带状态的可靠字节流协议来理解把状态机、序号空间、窗口机制这三根支柱想透。后面的所有细节都是这三根支柱长出来的枝叶。1.3 一条主线串起所有考点如果只能记一条主线我建议记这个TCP 用序号解决顺序和去重用确认和重传解决丢包用窗口解决发太快或收太慢用握手挥手解决从什么状态开始、到什么状态结束。四次挥手对应结束三次握手对应开始中间传数据的过程全靠序号 确认 窗口在跑。你在面试里遇到的绝大部分问题都可以挂到这条主线上。比如为什么需要 TIME_WAIT挂到结束上滑动窗口和拥塞控制的区别挂到窗口上——一个是接收方能力一个是网络能力。有了这条主线回答的时候就不容易东一榔头西一棒子。提示面试里遇到不熟悉的 TCP 问题先想它属于开始 / 传输 / 结束哪一段再从对应的机制去找答案比硬想结论靠谱得多。2. 连接建立与断开握手挥手里藏着的细节2.1 三次握手每一步究竟在协商什么先把报文摆清楚这里的每个字段面试官都可能追问。步骤方向关键字段语义第一次客户端 → 服务端SYN1, seqx我想建连接我的初始序列号是 x第二次服务端 → 客户端SYN1, ACK1, seqy, ackx1收到你的 x我确认我的初始序列号是 y第三次客户端 → 服务端ACK1, seqx1, acky1收到你的 y我确认注意ackx1这个加一很多人只会说确认号是 x 加 1但说不清为什么加一。原因是SYN 报文虽然不带数据但要消耗一个序号所以对端下一个期望收到的字节序号就是 x1。同理 FIN 报文也消耗一个序号这就是四次挥手最后 ACK 是ackw1的原因。这条如果答得出来面试官基本会认为你确实理解序号空间而不是背答案。另一个容易被问的点是第三次握手的 ACK 能不能带数据。答案是可以。因为此时客户端已经知道自己的发送能力正常、服务端的接收能力正常携带数据不会造成混乱。而前两次握手不行第一次的 SYN 如果带数据服务端在没确认的情况下不知道这些数据该不该收第二次的 SYNACK 带数据客户端同样处于半信半疑的状态。这一问本质考的仍然是什么时候双方对彼此的收发能力都有把握了。2.2 为什么必须是三次两次和四次差在哪两次握手行不行是最高频的追问。标准答案是防止历史失效连接的请求突然到达服务端导致服务端白白建立连接、浪费资源。这个场景可以讲得更具体一点客户端发了一个 SYN因为网络拥塞在某个节点滞留了很久客户端早就超时重发并完成了传输、关闭了连接。结果这个迟到的 SYN 送到了服务端如果只靠两次握手就直接进入 ESTABLISHED就会分配资源等一个永远不会来的数据一直挂到超时。这个解释是对的但只说了一半。更完整的回答应该包含另一层三次握手才能完成双方初始序列号的可靠同步。序列号不是随便选的它是后续所有确认、去重、重排的基准。如果只有两次服务端发出了自己的 ISNy但无法确认客户端是否收到如果客户端因为某些原因没收到后续所有acky1都是错的。第三次 ACK 的存在就是让服务端确信客户端的序号我收到了我的序号客户端也收到了。至于四次握手行不行理论上当然可以多一次确认不会有错误但没必要——第二次的 SYN 和 ACK 合并发送已经足够再拆开只是多一个 RTT对建立延迟没有任何收益。这也是理解 TCP 设计哲学的一个切口够用就好多余确认不加。2.3 半连接队列、全连接队列与 SYN Flood这组概念在实际面试中出现频率极高因为它是你懂不懂内核实现的分水岭。服务端收到 SYN 之后连接并不会立刻交给应用而是先进半连接队列SYN 队列此时连接处于 SYN_RECV 状态。等第三次握手的 ACK 到达连接被移入全连接队列Accept 队列状态变成 ESTABLISHED等待应用调用accept()取走。两个队列的容量由不同参数控制# 半连接队列上限实际值还受 tcp_max_syn_backlog 和 somaxconn 共同影响 net.ipv4.tcp_max_syn_backlog 8192 # 全连接队列上限listen(fd, backlog) 的 backlog 会被这个值截断 net.core.somaxconn 4096 # SYN Flood 防护 net.ipv4.tcp_syncookies 1一个非常实用的问题如果全连接队列满了新的握手会怎样。在开启tcp_abort_on_overflow0默认的情况下服务端会丢弃第三次握手的 ACK客户端会认为连接已建立并开始发数据但数据到达后被丢弃或回 RST表现出来就是连接建好了但请求超时。把tcp_abort_on_overflow设成 1 则直接回 RST客户端立刻报连接重置反而更容易定位问题。我在一次压测里就踩过这个坑QPS 上不去客户端日志全是超时服务端 CPU 和带宽都很闲。用netstat -s | grep -i listen一看times the listen queue of a socket overflowed在飞涨才知道是somaxconn太小、Accept 队列溢出。改完参数重启服务QPS 直接翻了几倍。这个排查思路值得记住服务端不忙但请求超时先查队列溢出计数。半连接队列溢出则是另一种表现。SYN Flood 攻击下攻击方只发 SYN 不回 ACK半连接队列迅速占满正常用户的 SYN 被丢弃表现出来是连接建立阶段就超时。这时候看netstat -s | grep -i synSYNs to LISTEN sockets dropped会大幅增长。2.4 四次挥手的完整状态流转挥手之所以是四次是因为 TCP 支持半关闭一方发完数据了可以先关掉自己的发送方向但仍然能继续接收对方的数据。这个特性在实际场景里很有用比如客户端把请求发完就shutdown(SHUT_WR)等着服务端把响应流式返回。状态谁含义FIN_WAIT_1主动关闭方发了 FIN等对方 ACKCLOSE_WAIT被动关闭方收到 FIN 并回了 ACK等应用调用 closeFIN_WAIT_2主动关闭方收到对方 ACK等对方的 FINLAST_ACK被动关闭方发了 FIN等最后一个 ACKTIME_WAIT主动关闭方最后一个 ACK 已发等待 2MSLCLOSED双方连接彻底结束这张表真正有用的地方在于用状态定位问题。CLOSE_WAIT堆积几乎只有一个原因应用代码收到了 FIN但没有调用close()。可能是逻辑里漏了 finally 块可能是连接被放进了池子但没做健康检查也可能是某个回调里异常抛出导致关闭逻辑没执行。这时候翻内核参数是没用的只能翻代码。我见过一个典型例子某团队用连接池访问下游下游做灰度发布时主动断开连接但因为异常路径没有正确归还和关闭池子里的死连接越积越多ss -s里 CLOSE_WAIT 一路涨到几千。FIN_WAIT_2堆积则说明对方收到了 FIN 却迟迟不发自己的 FIN通常是对方应用层没有关闭连接。这里有个内核参数决定 FIN_WAIT_2 的存活时间# 单位秒默认 60意味着孤儿连接在 FIN_WAIT_2 最多待这么久 net.ipv4.tcp_fin_timeout 60注意tcp_fin_timeout只影响孤儿连接应用已关闭对应用尚未关闭的连接不生效。很多人以为调小它就能解决 FIN_WAIT_2 堆积其实是无效操作。2.5 TIME_WAIT 到底在等什么TIME_WAIT 是面试里最容易被反复纠缠的话题想答出层次得说清楚它存在的两个理由。理由一保证最后一个 ACK 能到达对方。假设主动关闭方发出的最后一个 ACK 丢了被动方会超时重发 FIN。如果主动方此时已经进入 CLOSED收到这个 FIN 就会回一个 RST被动方会认为连接异常终止数据可能丢失。TIME_WAIT 状态下重发的 FIN 能被正常接收并再次回 ACK。理由二让本次连接产生的所有报文在网络中自然消亡。报文在网络里可能迷路最长存活时间叫 MSL。等待 2MSL一个来回就能确保双向的旧报文都过期了新连接相同四元组不会被旧报文污染。这也解释了为什么 TIME_WAIT 出现在主动关闭方。TIME_WAIT 多了会不会有危害要分情况。它本身不占文件描述符只占内核里的一小块内存和四元组资源。真正的风险是端口耗尽——如果本机作为客户端高频短连接打向同一目标源端口只有 6 万多个可能不够用。常见的应对手段我列在下面但要强调大部分时候正确做法是改成长连接或连接池而不是调内核参数。# 允许将 TIME_WAIT 状态的连接复用于新的出向连接需开启时间戳 net.ipv4.tcp_tw_reuse 1 # 时间戳tw_reuse 的前置条件 net.ipv4.tcp_timestamps 1 # TIME_WAIT 套接字数量上限超过直接销毁并打日志慎用 net.ipv4.tcp_max_tw_buckets 262144tcp_tw_reuse只对客户端出向连接生效服务端被动关闭的场景帮不上忙它依赖时间戳递增来判断旧报文是否可以安全丢弃。至于早年常见的tcp_tw_recycle在 NAT 环境下会误杀正常连接新版内核已经移除面试里如果有人问起能说清它为什么被删掉反而是加分项。3. 可靠性是怎么堆出来的序号、重传与窗口3.1 序号、确认与字节流语义TCP 最容易被误解的一点是它不保证消息可靠它保证的是字节流可靠。协议本身没有消息这个概念应用写进去的可能是 100 字节对端读出来的可能是 30 70也可能等两次写50 50一起读出来 100 字节。这就是粘包问题的根源后面会专门展开。序号是按字节编号的不是按报文。假设客户端发了一个 1000 字节的段seq1那么下一个段的 seq 就是 1001接收方回的 ack 是 1001 表示1 到 1000 我都收到了下次从 1001 开始。这种按字节编号的设计是 TCP 能够灵活重组、部分确认、SACK 的基础。确认分两种。累积确认的意思是 ackN 表示 N 之前的所有字节都收到了N 本身还没收到。所以如果收到 1-1000 和 2001-3000但 1001-2000 丢了接收方只会回 ack1001即使它手里已经握着 2001-3000。这是很多初学者困惑的地方明明收到了后面为什么说是前面的。答案是累积确认只表达连续收到哪里。选择确认SACK就是对累积确认的补丁。接收方在 TCP 选项里告诉发送方我额外收到了 2001-3000 这段发送方就只用重传 1001-2000。SACK 在高丢包链路上能显著减少重传量现代内核默认开启。还有 D-SACK 用于判断是不是自己重传了对方已经收到的数据主要用来区分包丢了和包乱序了避免不必要的拥塞窗口收缩。3.2 四种重传触发方式与 RTO 的计算丢包了怎么办内核有四套机制按生效顺序大概是这样。超时重传RTO是兜底手段。发送方为每个段启动定时器超过 RTO 还没收到确认就重传。RTO 不是固定值它根据 RTT 动态计算用的是加权移动平均SRTT (1 - α) * SRTT α * RTT # α 通常取 1/8 RTTVAR (1 - β) * RTTVAR β * |SRTT - RTT| # β 通常取 1/4 RTO SRTT 4 * RTTVAR这个公式面试里被问到的概率不低因为它体现了用统计量估计网络抖动的思路。加分点是能说出 RTO 有上下界Linux 里最小 200ms最大 120s以及指数退避连续重传时 RTO 翻倍避免在网络拥塞时火上浇油。快速重传是主力。发送方收到 3 个重复 ACK也就是同一个 ack 号重复三次就认为该段丢了不等超时立刻重传。为什么是 3 而不是 1因为网络存在轻微乱序1 个重复 ACK 可能只是乱序导致3 个基本能排除巧合。这个阈值可调但一般不建议动。SACK 驱动的重传收到 SACK 块后就能精确知道丢了哪些段重传效率最高。RACK / TLP是较新的机制。TLPTail Loss Probe专门对付尾包丢失——一批数据发完最后几个包丢了没有后续包来触发重复 ACK只能干等 RTO。TLP 会在接近 RTO 时主动发一个探测包逼出重复 ACK 走快速重传。RACK 则用时间维度判断丢包谁的发送时间早于最近被确认的包且还未被确认就认为丢了对乱序场景更鲁棒。# 重传相关观测 netstat -s | grep -E retrans|timeout|SACK ss -ti # 单个连接的 cwnd、rtt、retrans、sacked 都能看到ss -ti是我最推荐的工具输出里几个字段值得记住cwnd是拥塞窗口rtt是平滑后的往返时间retrans是当前连接的重传次数bytes_retrans是重传字节数。线上排查吞吐问题时先ss -ti看一眼 cwnd 有没有涨起来、retrans 有没有异常比抓包快得多。3.3 滑动窗口、流量控制与死锁风险滑动窗口解决的是发太快的问题但这里的太快指的是接收方来不及处理。接收方在 ACK 里带一个 window 字段告诉发送方我还能收多少字节。发送方的发送窗口取两者较小值发送窗口 min(拥塞窗口 cwnd, 接收窗口 rwnd)这里有两个经典坑也是面试常考点。第一个是零窗口。接收方缓冲区满了通告 window0发送方就停止发送。如果后续的窗口更新报文丢了双方就会僵死——发送方等窗口更新接收方等数据。TCP 用零窗口探测解决发送方启动一个定时器定期发 1 字节的探测包接收方收到后必须回当前窗口值。这个探测是带退避的间隔逐渐拉长。第二个是糊涂窗口综合征SWS。如果接收方每次只腾出几十字节就急着通告发送方就会发一堆小包报文头占比极高网络效率极差。标准的规避手段是接收方等到窗口能空出一半或者达到 MSS 才通告更新发送方这边则用Nagle 算法合并小包。Nagle 算法规则很简单如果还有未确认的数据小包就先攒着等凑够一个 MSS 或收到 ACK 再发。它和延迟 ACK接收方等 40ms 或收到两个段再回 ACK撞在一起就会产生著名的 40ms 延迟问题在请求-响应型的小包场景下特别明显。// 对延迟敏感的 RPC 客户端通常会关掉 Nagle int flag 1; setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, flag, sizeof(flag));我的实操建议是请求-响应型、小包、低延迟场景关掉 Nagle大批量顺序传输的场景保留默认。不要无脑全局关闭那会让大文件传输产生大量小包。4. 拥塞控制与生产环境调优4.1 慢启动到拥塞避免cwnd 的一生拥塞窗口 cwnd 是发送方自己维护的一个我对网络承载能力的估计它管的是网络而不是接收方。经典算法分四个阶段。慢启动连接刚建立时 cwnd 从很小开始Linux 现代版本是 10 个 MSS每收到一个 ACK 就加 1 个 MSS效果是每个 RTT 翻一倍指数增长。名字叫慢启动其实不慢叫慢是相对于一上来就发满窗口而言。拥塞避免cwnd 涨到慢启动阈值 ssthresh 后改为线性增长每个 RTT 加 1 个 MSS。这一步是为了小心试探网络上限。快重传 快恢复收到 3 个重复 ACK 时说明网络还有传输能力对方还在回包所以不必回到起点。做法是把 ssthresh 设为当前 cwnd 的一半cwnd 也设为一半左右直接进入拥塞避免。这就是快恢复。而超时重传是更严重的信号会把 cwnd 直接打回 1 个 MSS 重新慢启动。这解释了一个常见的性能现象偶发丢包对吞吐的打击远大于持续丢包。因为一次超时就让 cwnd 归零重新爬坡要花好几个 RTT。4.2 从 CUBIC 到 BBR换算法的判断标准Linux 默认拥塞控制算法是CUBIC它用三次函数曲线代替了传统的线性增长在长肥管道高带宽高延迟链路上恢复速度更快。但它本质仍是基于丢包的算法——把丢包当成拥塞信号。问题在于现代网络里丢包不一定意味着拥塞。无线链路误码、中间设备缓冲区太小都会丢包CUBIC 一看到丢包就降速导致在弱网和高丢包链路上吞吐惨不忍睹。BBR换了思路它不把丢包当信号而是主动测量两个量瓶颈带宽和最小 RTT用两者的乘积BDP来估计应该发多快。所以 BBR 在有丢包的链路上也能保持较高吞吐。代价是它对公平性不太友好与其他算法共存时可能抢占带宽而且对深队列敏感。# 看当前可用算法和正在使用的 sysctl net.ipv4.tcp_available_congestion_control sysctl net.ipv4.tcp_congestion_control # 切换到 BBR需要内核支持通常 4.9 sysctl -w net.ipv4.tcp_congestion_controlbbr实际选型我的经验是公网跨区域传输、有明显丢包的链路BBR 收益显著内网低延迟环境CUBIC 和 BBR 差距不大不必折腾。切换前一定要做 A/B 对比别拍脑袋改生产。4.3 带宽时延积窗口到底要开多大这是我认为最实用的一个计算很多人调优失败就是因为没算过这个数。BDP带宽时延积 带宽 × RTT代表在途数据量也就是管道里能装多少数据。发送窗口必须不小于 BDP才能把带宽跑满。举个例子一条 1Gbps、RTT 40ms 的链路。BDP 1 Gbps × 40 ms 1000 Mbps × 0.04 s 40 Mbit 5 MB也就是说接收窗口至少要 5MB 才能跑满 1Gbps。而内核默认的tcp_rmem最大值往往只有 6MB 左右不同发行版差异较大如果对端通告的窗口只有 200KB那么实际吞吐上限就是吞吐上限 窗口 / RTT 200 KB / 0.04 s 5 MB/s 40 Mbps差了 25 倍。这就是带宽充足但传输很慢最常见的原因。对应参数# 三段分别是 min / default / max单位字节 net.ipv4.tcp_rmem 4096 131072 16777216 net.ipv4.tcp_wmem 4096 65536 16777216 # 窗口缩放因子让窗口字段能表达超过 64KB 的值必须开启 net.ipv4.tcp_window_scaling 1 # 允许应用直接指定接收缓冲区大小用于自定义协议栈 net.core.rmem_max 16777216 net.core.wmem_max 16777216有一点要注意tcp_rmem的 max 只是上限内核会根据实际需要自动调整。如果对端一直不发满缓冲区不会立刻扩到 max。另外应用层如果自己调了setsockopt(SO_RCVBUF)会覆盖内核的自动调整反而可能把窗口锁死在一个小值上。我见过有人为了优化性能手动设了个 64KB 的缓冲区结果整条链路吞吐被卡死排查了半天。4.4 长连接的保活与超时设置长连接不是建好就一劳永逸。中间设备防火墙、NAT 网关、负载均衡都有空闲超时可能在你不知情的情况下把连接表项清掉客户端还以为连接可用一发包就超时。TCP 有内置的 keepalive但默认参数非常保守# 空闲多久开始探测默认 7200 秒也就是两小时 net.ipv4.tcp_keepalive_time 600 # 探测间隔 net.ipv4.tcp_keepalive_intvl 30 # 探测失败几次判定连接死亡 net.ipv4.tcp_keepalive_probes 3两小时的空闲阈值对大多数服务都太长了。更关键的是TCP keepalive 只能探测连接是否还活着不能判断对端应用是否健康。所以生产上的通行做法是应用层心跳 合理超时客户端每隔 N 秒发一个轻量心跳超过 M 秒没响应就主动断开重连。心跳间隔要小于中间设备的最短空闲超时这个值得跟运维确认清楚。提示心跳间隔不是越短越好。太短会带来额外的包量和唤醒开销手机端还会显著增加耗电。一般取中间设备超时的三分之一到二分之一比较稳妥。5. 高频陷阱题与线上排查实录5.1 粘包一个被叫错名字的问题TCP 粘包这个说法其实不准确TCP 是字节流协议本来就没有包的概念谈不上粘。真正的问题是应用层没有定义消息边界。UDP 会不会有类似问题严格说不会UDP 保留消息边界一次 sendto 对应一次 recvfrom只要缓冲区够大。但如果你在应用层用一个大缓冲区循环 recvfrom也会读出错位的数据那是应用层的事。解决办法就三种我按推荐度排序。第一种长度字段前缀也是绝大多数二进制协议的做法。包头里固定位置放一个长度字段先读包头拿到长度再按长度读 body。import struct def pack(payload: bytes) - bytes: # 4 字节大端无符号整数表示 body 长度 return struct.pack(I, len(payload)) payload def unpack(buf: bytearray): if len(buf) 4: return None, buf (length,) struct.unpack(I, buf[:4]) if len(buf) 4 length: return None, buf body bytes(buf[4:4 length]) return body, buf[4 length:]注意unpack返回未消费的剩余缓冲区调用方要在一个循环里反复取直到取不出完整消息为止。这个半包处理是新手最容易写错的地方——只处理了一次就返回剩下半个包留在缓冲区里下次读又从头解析直接错位。第二种分隔符。适合文本协议比如每行一条 JSON 用\n分隔。实现简单但要求业务数据里不能出现分隔符或者做转义容易出问题。第三种固定长度。实现最简单但浪费带宽只适合定长消息。不管用哪种缓冲区一定要做成可增长的并设置上限防止恶意超长包把内存打爆。我见过一个服务因为没限制长度字段被构造了一个声明 4GB 的包直接 OOM。5.2 一张表搞定常见现象与排查路径线上遇到 TCP 相关问题很多人第一反应是抓包其实先看统计计数器效率高得多。下面这张表是我这些年攒下来的对照表。现象优先查看常见原因处理方向连接建立阶段超时netstat -s | grep -i syn半连接队列满、SYN Flood、backlog 太小调tcp_max_syn_backlog、开 syncookies连接建立成功但请求超时netstat -s | grep -i listenAccept 队列溢出应用 accept 太慢调somaxconn、加并发 accept大量 CLOSE_WAITss -s、应用代码收到 FIN 未调 close检查异常路径、连接池回收逻辑大量 TIME_WAITss -tan state time-wait | wc -l高频短连接、主动关闭方改长连接/连接池必要时tcp_tw_reuse吞吐远低于带宽ss -ti看 cwnd 和 rtt窗口太小、BDP 不足调大tcp_rmem/wmem、确认窗口缩放开启偶发卡顿数百毫秒抓包看重传RTO 超时重传、尾包丢失开 TLP/SACK考虑换 BBR小请求固定延迟 40ms抓包看 ACK 间隔Nagle 延迟 ACK开TCP_NODELAY长连接莫名断开抓包看 RST中间设备空闲超时清理加应用层心跳表格里最后一列的调参数其实应该放到最后。先定位是应用逻辑问题还是网络问题再决定动不动内核这个顺序不能反。我见过太多团队一出问题就批量改 sysctl改完连原来的问题是什么都忘了。5.3 一次真实的吞吐排查过程说一个我印象比较深的案例过程比结论更有参考价值。某系统做跨区域数据同步源端和目的端之间标称 500Mbps 带宽实际用iperf测单流只有 60Mbps 左右波动很大。第一反应是带宽不够但同机多流并发能跑到 400Mbps说明带宽没问题是单流受限。第一步算 BDP。用 ping 测 RTT 大约 50ms那么单流跑满 500Mbps 需要的窗口是500 Mbps × 0.05 s 25 Mbit ≈ 3.1 MB第二步ss -ti看实际窗口。输出里cwnd长期停在 60 左右rtt稳定在 50ms 上下retrans在涨。60 个 MSS 按 1460 字节算约 87KB对应的吞吐上限是87KB / 0.05s ≈ 1.7MB/s ≈ 14Mbps和实际观察对不上但方向一致——窗口严重不足。第三步查内核参数发现tcp_wmem的 max 是默认的小值接收端tcp_rmem也没放开。同时tcp_window_scaling虽然开着但由于默认缓冲小缩放因子也没起作用。第四步改参数并验证sysctl -w net.ipv4.tcp_rmem4096 131072 33554432 sysctl -w net.ipv4.tcp_wmem4096 65536 33554432 sysctl -w net.core.rmem_max33554432 sysctl -w net.core.wmem_max33554432改完复测单流跑到 380Mbps 左右接近理论上限。剩下的差距来自重传和协议开销属于正常范围。这个案例里最关键的不是最后那几个参数而是第二步用 ss 量化了窗口不足。如果一上来就抄一份网络优化参数大全贴进去可能碰巧解决了但你不知道是哪一项起了作用下次换个环境还得重新试。5.4 几个容易被问倒的细节最后整理几个面试里常被追问、但答错率很高的点。TCP 一定可靠吗。协议层面它保证有序、去重、不丢在连接存活期间但它不保证应用层语义正确。比如连接被 RST 时缓冲区里还没发出去的数据会丢比如重传可能导致对端收到重复数据业务必须自己做幂等。正确回答是TCP 提供的是连接级的有序可靠字节流不是端到端的消息投递保证。这也是为什么很多 MQ 要在 TCP 之上再做一层确认和去重。为什么服务端也可能是 TIME_WAIT 多的一方。因为 TIME_WAIT 出现在主动关闭方跟客户端服务端角色无关。如果服务端配置了短连接且不保持或者是服务端先关闭那就是服务端攒 TIME_WAIT。RST 什么时候发。常见几种连一个没监听的端口往已关闭的连接发数据全连接队列溢出且tcp_abort_on_overflow1应用调用了SO_LINGER且超时为 0 时 close。RST 有个特性是会直接清空发送缓冲区未发数据全部丢弃所以SO_LINGER0的 close 要慎用。滑动窗口和拥塞窗口的区别。滑动窗口rwnd反映接收方的处理能力由接收方通告拥塞窗口cwnd反映发送方对网络的估计发送方本地维护不传输。实际发送量取两者较小值。这个区分必须一口答清楚含糊了会被认为概念不清。长连接怎么检测对端已经挂了。分三层TCP keepalive 只能探测链路周期太长应用层心跳能探测进程但需要协议支持业务请求超时是最后一道防线。三者要配合用不能只依赖其中一个。写到这里我觉得准备 TCP 面试最有效的方式其实是把协议和自己写过的代码对上号。你调过一次TCP_NODELAY就永远记得 Nagle 和延迟 ACK 的组合你排查过一次 Accept 队列溢出就再也不会把somaxconn和tcp_max_syn_backlog搞混。纯背答案应付一面可以往深了走还是得让这些机制在实际场景里跑一遍。