ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

TCP/IP协议栈深度解析:从分层原理到实战排障

TCP/IP协议栈深度解析:从分层原理到实战排障 干了几年网络相关的工作后我越来越觉得一个现象很普遍大家背得出TCP三次握手是SYN、SYN-ACK、ACK也大概知道IP协议负责寻址但真到线上出现“连接超时”“偶尔失败”“传输很慢”这类问题时不少人还是习惯先在应用层翻日志翻不出结果就开始重启。真正愿意静下心把TCP/IP协议栈完整过一遍从链路层看到应用层并把每一层和实际故障对应起来的人其实很少。这篇内容是我给自己梳理的一份深度解析提纲也按这份大纲把核心知识点和实战排查经验串了起来。它不会只讲概念每一层都会落到底层机制和线上问题适合刚入门想建立体系的后端开发者也适合有点经验但总被网络问题卡住的运维和研发。读完你至少能建立一张“网络排障地图”以后看到现象能快速定位到协议栈的哪一层、哪个机制出了问题。1. 为什么值得从头啃一遍TCP/IP协议栈1.1 协议栈基础决定排障天花板先说一个我印象很深的案例某个内部系统经常出现接口偶发超时每次持续几秒又恢复正常。应用层看日志没有异常报错数据库慢查询也没有代码 review 了好几轮找不到问题。后来实在没办法在客户端和服务端同时抓包才看到真相客户端一直在发TCP重传服务端这边却什么都没收到。继续往下查发现客户端的网卡MTU是1500而中间一台网络设备的MTU被配成了1400导致超过1400字节的IP包被直接丢弃。因为TCP MSS协商机制在按理说TCP不会发超过对端接受能力的大包但中间设备MTU不一致时恰好绕过了MSS协商的保护于是大包就在中间被静默丢掉客户端只能反复重传表现就是偶发超时。这个案例里用到的基础知识其实就是IP分片、MTU、TCP重传这三件事。如果对协议栈只有模糊的概念遇到这种问题基本只能靠猜。协议栈基础决定了排障天花板懂分层你才知道该在哪一层抓包懂TCP状态机你才知道连接卡在哪一步懂窗口和重传你才不会把网络抖动误判成应用问题。TCP/IP协议栈不是考试内容是真正能救场的底层地图。1.2 分层模型与数据流转的基本逻辑TCP/IP协议栈通常分成四层链路层、网络层、传输层、应用层。数据发送时从上往下逐层封装接收时从下往上逐层解封装。我用寄快递来类比应用层数据是你实际要寄出去的物品传输层TCP/UDP头是快递单上的联系方式和单号用来保证寄件人和收件人能对上网络层IP头是收件地址用来决定这个包裹往哪个城市哪个区域走链路层以太网头是快递员手里的配送明细记录着这一站从哪个网点送到哪个网点。每一层只关心自己该关心的信息不需要理解上下层的全部内容。比如路由器只关心IP头里的目的地址交换机只关心MAC帧里的目的MACTCP只关心端口和序号应用只关心业务数据。这种分层设计让协议栈各层可以独立演进也让排查问题时可以逐层剥开。拿到一个网络故障先确认链路层有没有通再看IP层能不能路由然后看TCP要不要重传最后才回到应用层看数据对不对。顺序对了效率就高了。2. 链路层与网络层先打好地基2.1 链路层核心MAC、ARP与MTU很多人觉得链路层跟开发没什么关系但链路层的三个概念——MAC、ARP、MTU——每一个都能在线上引发事故。以太网帧结构不算复杂目的MAC、源MAC、长度/类型、上层数据、帧校验。在一个二层网络里设备之间通信靠的是MAC地址而不是IP地址。那IP地址怎么变成MAC地址靠ARP协议。主机想要发送数据时先在本地ARP缓存表里找目的IP对应的MAC找不到就发一个ARP广播让目标IP的设备回自己的MAC。这个缓存不是永久的一般几十秒到几分钟就老化所以大量短连接场景下ARP请求本身也可能成为CPU消耗的来源。MTU是链路层最容易被忽略又最容易出问题的参数。标准以太网帧去掉14字节头部和4字节校验留给上层IP报文的最大长度是1500字节这就是最常见的MTU值。IP头默认20字节TCP头默认20字节所以TCP单段最多能带1460字节数据这个值叫MSS。TCP建连时会通过MSS协商让双方都按较小的值发送目的就是为了避免IP分片。但UDP没有这个机制应用层一次性发一个1600字节的数据IP层就必须分片。片和片之间是独立的只要有一片在传输中丢了整个数据报就无法重组表现出来就是“UDP丢包”。更隐蔽的是中间设备MTU不一致造成的黑洞大包被丢弃按照设计应该返回ICMP分片错误报文但很多网络设备为了安全会屏蔽ICMP发送端根本不知道包被丢了只能干等超时。出现“小请求正常、大请求超时”的现象优先检查MTU链路一致性。2.2 网络层IP寻址、路由与分片网络层解决的核心问题是“这个包该往哪送”。IPv4地址加子网掩码决定了网络范围比如 /24 表示前24位是网络位后8位是主机位可用地址是254个切到 /23主机位变成9位可用地址是510个。子网划得越大广播域越大排查时流量也越难隔离。实践中我见过不少团队随意分配IP、子网掩码写得乱七八糟最后故障定位时连路由走向都看不清。路由器转发IP包的核心规则是最长前缀匹配。路由表里可能有 /24 的明细路由也可能有 /0 的默认路由收到一个目的IP时路由器会选掩码最长的那一条。这个逻辑决定了路由表的组织方式也解释了为什么缺省路由总放在最后兜底。还有一个细节是TTL每经过一个路由器TTL减1减到0就丢弃并返回ICMP超时。traceroute 利用的就是这个机制通过递增TTL探测每一跳路径。如果链路里有环路TTL能保证包不会永远转圈。再展开说NAT。内网设备使用私有IP出公网前在边界设备上把源IP改写为公网IP同时记录四元组映射关系回包时再反向改写回来。这个机制让大量内网设备共享少量公网IP但也带来一个代价映射表项有上限并发连接特别多时端口号会耗尽新的连接就建立不起来。所以在高并发场景设计长连接、控制连接总量不单是应用层的事也是在给NAT设备减轻压力。3. 传输层深度拆解TCP的连接、可靠性与拥塞控制3.1 三次握手与四次挥手的状态机TCP连接建立的三次握手本质是让双方确认彼此的收发能力都正常。客户端发SYN带上初始序号x服务端回SYNACK带上自己的序号y同时确认收到x并期待x1客户端再回ACK确认收到y并期待y1。为什么一定要第三次ACK可以想象两台对讲机A说“能听到吗”B回“能听到你能听到我吗”A再回一句“能听到”。只有A确认“我能听到你”B才敢放心把数据发给A也就确认了双向通路。服务端处理连接时有两个队列值得关注。收到SYN后连接会进入半连接队列这个队列有长度限制默认值跟listen的backlog参数和内核somaxconn有关。半连接队列满了新SYN会被丢弃客户端表现就是连接超时。三次握手完成后连接进入全连接队列等应用调用accept取走。全连接队列如果一直没被取走队列溢出后新连接可能被对端直接RST表现是建立连接时偶尔报错。排查时用ss -ant看状态出现大量SYN_RECV优先怀疑半连接队列溢出和SYN泛洪出现大量ESTABLISHED但应用无响应优先怀疑全连接队列没及时accept。四次挥手的过程是主动关闭方发FIN被动方回ACK被动方随后发FIN主动方回最后的ACK。这里最值得说的是TIME_WAIT状态。主动关闭方在发出最后一个ACK后不会立刻关闭而是要等2MSL协议上的最长报文段寿命通常是1到4分钟。为什么等这么久一是担心最后一个ACK丢了对方重发FIN时还能再回ACK二是担心旧连接里的重复报文在网络中残存如果复用相同四元组新连接可能收到旧包。所以TIME_WAIT本身是协议的保护机制不是故障。短连接多的服务出现大量TIME_WAIT是正常现象不到万不得已不建议粗暴改参数。3.2 可靠传输的核心序号、重传与窗口TCP是面向字节流的协议每个字节都有自己的序号。确认号表示“我期望收到的下一个字节序号”所以收到ackn说明序号n之前的所有字节都收到了。这个机制是可靠传输的基础。实际传输时还有两个经常放在一起讲的算法延迟确认和Nagle算法。接收方不会收到一个包立刻回一个ACK而是等一小段时间合并确认减少ACK包数量Nagle算法则是发送方把小包攒起来一起发减少网络中微小报文。问题是当Nagle算法遇到延迟确认时可能出现一条数据发出去后迟迟等不到ACK后续小包又一直被Nagle攒着交互式应用就会感觉延迟很高。所以很多对延迟敏感的业务会关闭Nagle也就是设置TCP_NODELAY。重传机制从超时重传进化到快速重传加上SACK已经把效率提升了很多。超时重传等待的是RTO计时器到期RTO根据历史RTT动态计算太短会造成大量无用重传太长会放大丢包影响。快速重传的思路是接收方收到乱序包时会重复确认最后一个连续收到的序号发送方收到3个重复ACK就判断这个包丢了不用等超时。SACK允许接收方告诉发送方“我收到了哪些不连续的部分”发送方只重传真正丢掉的段避免了旧协议里从丢失点开始全部重传的浪费。窗口机制有流量控制和拥塞控制两个维度。接收方通告窗口rwnd表示自己的接收缓冲区还能收多少这是流量控制防止发送过快撑爆接收方。拥塞窗口cwnd是发送方自己控制的一个状态表示当前网络允许发送多少数据这个由拥塞控制算法维护。慢启动阶段cwnd从1开始指数增长达到阈值进入拥塞避免阶段线性增长发生丢包时通过快重传和快恢复降速。很多人面试时把四个算法背得很熟但实际调优时更重要是一个估算公式带宽时延积BDP 带宽 × RTT。比如链路带宽是100MbpsRTT是100msBDP大约是1.25MB。要让TCP跑满带宽拥塞窗口至少要达到1.25MB如果接收缓冲区小于这个值窗口再大也没用。这个公式决定了你要不要调大socket缓冲区。3.3 UDP与TCP的选型边界UDP的头部只有源端口、目的端口、长度、校验和四个字段没有序号、没有确认、没有窗口。所以UDP没有连接状态延迟低但也意味着它不保证不丢、不保证有序、不保证不重复。选型时并不存在“UDP一定比TCP好”的说法而是看业务能不能容忍这些不确定性。DNS查询、DHCP、实时音视频、游戏同步这些场景要么是短小请求要么是对延迟极其敏感UDP很合适需要可靠传输和顺序保证的业务老老实实用TCP。最近几年QUIC协议把UDP的价值重新拉高了。QUIC基于UDP但自己在应用层实现了类似TCP的可靠传输、拥塞控制、TLS加密和连接迁移能力。它的优势在于连接标识不再依赖四元组手机从Wi-Fi切到移动网络连接还能继续用多路复用时一条流丢包不影响其他流。HTTP/3选择QUIC本质就是对TCP队头阻塞问题的一种规避。研究协议栈走到这里你会发现TCP不再是一切的终点但理解TCP为什么会有这些问题依然是理解QUIC设计的必经之路。4. 应用层常见协议背后的协议栈视角4.1 HTTP的每一次演进都在跟TCP较劲HTTP是TCP协议栈最典型的应用。HTTP/1.1时代一个TCP连接同一时间只能处理一个请求后一个请求必须等前一个响应结束。keep-alive解决了重复建连的开销但没有解决队头阻塞前面的请求慢后面的请求只能排队。HTTP/2引入了二进制分帧和多路复用多个请求可以交错使用同一条TCP连接看起来解决了HTTP层面的队头阻塞但实际上TCP本身还有一个更大的队头阻塞如果这条TCP连接上发生了一个包丢失TCP要重传并保证有序交付所有复用的流都得等这个包回来。所以HTTP/2在丢包率稍高的链路上性能可能还不如HTTP/1.1的多连接方案。HTTP/3把传输层从TCP换成了QUIC本质上是把可靠性逻辑从内核里挪到了应用层这样一条流丢包时其他流的数据还可以继续交付。从HTTP的演进能清楚看到一个趋势应用层为了突破TCP的限制做了很多努力但协议栈的优化往往不是在原协议上打补丁而是另起炉灶。做技术选型时理解这个背景比单纯记住“HTTP/3更快”有价值得多。4.2 DNS解析流程与协议栈的交点DNS解析是整个网络访问的第一跳。浏览器查本地缓存没命中查hosts文件再没命中就向本地DNS服务器发起递归查询。本地DNS服务器如果没有缓存会代替客户端去根DNS服务器、顶级域服务器、权威服务器逐级查询。整个链路里任何一个环节缓存过期时间很长都会影响解析结果。排障时用 dig 带上 trace 参数可以看完整递归过程加上 norecurse 可以只看某一级服务器返回的结果判断是权威数据存在问题还是缓存数据存在问题。容易被忽略的是DNS的传输方式。标准DNS查询走UDP 53端口但当响应数据超过一定大小比如包含很多DNSSEC记录时会切换到TCP 53继续传输。不少网络安全策略只放行了UDP 53没有放行TCP 53结果就是部分域名解析异常而且解析行为时好时坏很迷惑。排查域名解析问题时要同时确认UDP和TCP两个方向的放行情况。TTL也要重点看修改了一条DNS记录但客户端仍然访问旧IP十有八九是链路里某一级缓存的TTL还没到期。这时候直接看各缓存服务器返回的TTL剩余值就能定位是哪一级在捣乱。5. 实战排查从现象到协议栈根因5.1 抓包分析三板斧握手、重传、丢包排查网络协议栈问题我习惯用三板斧先看握手有没有完成再看传输中有没有重传最后确认有没有丢包迹象。抓包工具用tcpdump在服务器上抓命令行可以写成tcpdump -i eth0 -nn -s 0 tcp port 80 -w /tmp/http_trace.pcap这条命令监听eth0网卡不做域名解析抓取TCP端口80的流量保存到文件。抓完用图形化的分析工具打开比如Wireshark第一步先过滤TCP握手包看三次握手是否完整出现。过滤条件可以用tcp.flags.syn 1 tcp.flags.syn 1 tcp.flags.ack 1 tcp.flags.ack 1如果只有客户端的SYN没有服务端的SYN-ACK说明SYN包根本没到服务端或者服务端没有回包这时候要查中间链路和服务端的半连接队列。如果SYN-ACK有了但没有最后的ACK问题很可能在客户端到服务端的回程方向。第二步看重传。分析工具里的Expert Info会列出Retransmission、Dup ACK、Out-of-Order这些事件。少量重传是正常现象但大量集中重传就要警惕。注意结合时间戳看是某个瞬间集中爆发还是持续不断集中爆发可能是链路抖动或网络设备重新收敛持续不断则可能是MTU不一致或对端处理能力不足。第三步看有没有TCP零窗口、窗口缩小等信号这类情况往往说明接收方的应用层没及时读走数据socket缓冲区被占满问题就回到了应用本身。5.2 高频故障速查表我自己整理了一张速查表遇到线上网络问题先对照一遍能节省大量时间。故障现象可能原因第一排查动作连接建立超时SYN被安全策略丢弃、半连接队列满抓包看是否有SYN-ACKss -ant看SYN_RECV数量连接被RST端口未监听、应用主动拒绝确认服务健康状态查看应用层日志大量TIME_WAIT短连接创建太频繁统计连接生命周期评估是否该复用连接大量TCP重传链路丢包、对端处理不过来同时抓客户端和服务端包对比哪侧没收到UDP丢包严重MTU分片、接收缓冲区过小用大包测试链路MTU检查rmem设置握手成功但业务超时后端连接池耗尽、七层设备异常看七层日志和TCP连接建立后的行为这里想多说一句这张表不是拿来直接照着改参数的而是用来定位方向的。比如大量重传如果客户端发出的包服务端根本没收到那问题在网络链路如果服务端收到了但因为处理慢来不及确认问题在服务端应用。同样是重传根因可能完全不同。5.3 内核参数调优的正确姿势内核网络参数调优是一把双刃剑。调好了性能提升明显调错了可能把整个服务的稳定性搭进去。我遵循三个原则先测量再调整一次只改一个参数改完必须对比压测。几个实践中有正向价值的参数可以优先了解。全连接队列上限对应net.core.somaxconn应用层listen时的backlog参数不能超过它如果accept速度跟不上调大这个值才有实际意义。空闲连接的保活探测时间net.ipv4.tcp_keepalive_time默认是7200秒对长连接服务来说有点长可以适当调到600秒让内核更早发现死连接并回收。高带宽长距离链路可以调大net.core.rmem_max和net.core.wmem_max再配合socket层的SO_RCVBUF让TCP窗口能匹配带宽时延积。不建议动的是tcp_tw_reuse这一类参数。tcp_tw_reuse在NAT环境下可能引起连接异常因为对端无法正确判断序号是否有效tcp_tw_recycle在很多新内核里已经直接移除了因为它会让同一个NAT出口后面的机器出现严重的丢包问题。与其靠这些参数回收TIME_WAIT不如在架构上减少连接创建次数。只要连接是复用的TIME_WAIT数量自然就降下来了。另外还要提醒socket缓冲区不是越大越好缓冲区超过BDP之后再增加只意味着内存浪费还可能在链路抖动时放大延迟。所有参数都要在真实业务流量下图个前后的指标对比再决定保留还是回滚。最后分享一个我自己养成的习惯每次线上网络故障先别急着怀疑应用按“包有没有发出、对端有没有收到、回包有没有抵达”这三步走配合抓包工具把每层拆开看一遍。大多数诡异问题到最后落点都是MTU、握手队列、重传、窗口这几个基础机制。把这几个基础吃透协议栈再复杂也能顺着一条连接从头讲到尾。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进