首页 / 资讯中心 / 文章详情

Linux协议栈详解:从TCP/IP到MTU的故障排查实战

Linux协议栈详解:从TCP/IP到MTU的故障排查实战 ★ FEATURED ARTICLE
1. 先搞清楚Linux协议栈是怎么一层层“拆信封”的做网络排查或者写网络程序的人迟早会遇到这么一个场景明明应用看起来没啥问题但服务就是慢、连接就是建不起来、流量就是上不去。这个时候如果你不懂Linux协议栈的工作原理就只能瞎猜。去年我帮某公司排查过一次线上问题现象是两台机器之间传输大文件速度只有预期的一半抓包一看全是TCP重传再往里拆发现是MTU设置导致IP分片丢包。那一刻我就觉得网络协议栈这东西不是面试背几个状态就行真得从底层把它吃透。正好借这篇内容把Linux网络这条链路上最核心的三层逐个拆开讲传输层的TCP/UDP、网络层的IP、数据链路层的MAC帧。它们之间的关系可以理解成寄快递MAC帧是快递车上跑的“运单标签”负责在相邻网点之间传送IP是包裹上的“收件人地址”决定包裹从哪个城市发往哪个城市TCP/UDP则是包裹里附的“交接确认单”保证收货方知道自己收到了什么、缺了什么、要不要补发。每一层只干自己那一层的事层与层之间通过协议头互相“对话”。这篇文章适合三类人一是刚开始学Linux网络、被各种状态机绕晕的新人二是写网络服务端程序、经常要跟socket和epoll打交道的开发三是做运维和SRE、需要快速定位线上网络故障的人。下面我按数据收发从底到顶的顺序来讲这样你脑子里能形成一条完整的链路——从网卡收包到内核协议栈处理再到用户态程序拿到数据每一步都说得清清楚楚。1.1 从网卡收包到应用读完数据中间发生了什么先走一遍整条链路。当网络数据帧到达物理网卡时网卡通过DMA把数据写入内核内存的环形缓冲区也就是ring buffer。这里没有CPU逐字节拷贝都是硬件直接搬运效率很高。随后网卡发起中断现在多是MSI-X多队列中断触发napi轮询机制内核开始处理这批包。接下来链路层先校验MAC帧的FCS字段也就是帧尾的循环冗余校验值看看帧在传输过程中有没有损坏。校验通过后内核摘掉以太网帧头14字节不含VLAN的话和帧尾的FCS4字节把里面的IPv4或IPv6报文交给网络层。IP层拿到报文后先看合法性和校验和再根据目的IP地址查路由表决定是本地接收还是转发。如果目的IP是本机地址IP层就往上传给传输层。传输层这里就该TCP/UDP上场了。TCP会拿报文的四元组——源IP、源端口、目的IP、目的端口——去查找对应的socket找到后做序列号连续性检查和拥塞窗口相关处理然后数据进入接收队列。如果队列里对端的数据到了但应用层迟迟不读你会看到接收缓冲区和Recv-Q持续堆积。最后用户态程序通过read或者recv系统调用把数据从内核缓冲区拷贝到用户态内存。这一步是内核态到用户态的切换也是很多高性能网络服务优化时关注的点比如包拷贝、系统调用次数、锁竞争。整条链路看下来数据从物理层到应用层像剥洋葱一样一层层去掉协议头。但每个方向的包在发送时刚好相反应用数据先封装成TCP或UDP段再包上IP头再包上MAC帧头最后经网卡发出去。理解这个双向过程你排查问题时就有了一条清晰的线索延迟高到底发生在哪一层。2. TCP/UDP传输层两个性格完全不同的“快递员”传输层提供了两个风格截然不同的服务TCP和UDP。用一句话概括TCP是“打了合同的快递”必须能追溯、能补货、能确认签收UDP是“普通快递员”把包裹丢到驿站就走能不能收到凭运气。很多应用选型时纠结用TCP还是UDP其实核心就一条你愿不愿意为了高可靠性牺牲实时性和握手延迟。2.1 TCP头里每个字段都不是白给的先看TCP报文头。基础TCP头是20字节不含选项。依次有源端口16位、目的端口16位、序列号32位、确认号32位、头部长度4位、保留位、标志位URG/ACK/PSH/RST/SYN/FIN、窗口大小16位、校验和16位、紧急指针16位。序列号和确认号是最容易让人发懵的两个字段。序列号表示这个包里第一个字节的编号确认号表示接收方期待收到的下一个字节编号同时它隐含了“序号之前的字节我都已经收好了”的意思。比如A发送数据包序列号1000、长度100字节B收到后会回复确认号1100。这个机制保证了数据可以有序、去重、不丢地传输。当初我看了几次抓包才彻底明白原来确认号不是“最后收到哪个字节”而是“下一个该收哪个字节”。标志位里SYN用于连接建立FIN用于正常关闭RST用于异常重置ACK表示确认号有效PSH提示对端尽快把数据交给应用URG和紧急指针现在已经很少用到。窗口大小字段是接收方告诉发送方“我缓冲区还剩多少”它构成了TCP流控的基础发送方不能无限制地往外喷数据必须看着对方的窗口大小来。2.2 三次握手和四次挥手别只背状态机TCP建立连接的三次握手很多人背得滚瓜烂熟但看tcpdump抓包时容易犯迷糊。一个典型的建连抓包是这样的1 0.000000 192.168.1.10.5000 192.168.1.20.8080: Flags [S], seq 1000 2 0.000056 192.168.1.20.8080 192.168.1.10.5000: Flags [S.], seq 8000, ack 1001 3 0.000060 192.168.1.10.5000 192.168.1.20.8080: Flags [.], ack 8001第一包客户端发SYN把自己初始序列号告诉服务端同时把窗口大小、MSS最大段大小、SACK这样的选项一并带过去。第二包服务端回应SYNACK它会带上自己的初始序列号并且确认客户端的序列号1000ack值1001表示“我期待你下一个字节是1001”。第三包客户端发送纯ACK确认服务端的序列号8000。这个包也携带用户数据属于经典优化称为TCP Fast Open的简化版思路。为什么要三次而不是两次假设只有两次服务端收到SYN就认为连接建立了如果这个SYN是个迟到重复的旧包服务端就会开一个没必要的连接浪费资源。三次握手让双方都确认对方收到了自己的序列号彼此达成一致才宣告连接可用。四次挥手对应的抓包1 FIN, seq 2000 2 ACK, ack 2001 3 FIN, seq 9000 4 ACK, ack 9001主动关闭方发FIN被动方回ACK然后被动方再发自己的FIN最后主动方回ACK关闭完成。注意中间第二第三步之间被动方可能还要发送剩余数据。收到主动方最后一个ACK后被动方进入CLOSED主动方则要进入TIME_WAIT状态等待2MSL最长报文段寿命后才彻底关闭。很多线上故障跟TIME_WAIT过多有关比如短连接密集创建的时候大量socket卡在TIME_WAIT。这种情况可以调整内核参数比如net.ipv4.tcp_tw_reuse但要理解它的机制它只能用在客户端场景并且依赖时间戳选项不能盲目开启。2.3 重传、拥塞控制与窗口TCP性能的关键TCP的可靠性不是空喊的丢包要靠超时重传和快速重传去弥补。超时重传如果按固定时间等待效率太低于是有了自适应算法根据RTT往返时间动态调整超时阈值。快速重传则是在收到三个重复ACK时不等超时立刻重传丢失的包。原因是每收到一个乱序包对端就会重复确认自己期待的序号比如丢了1001-1100后面收到的1101、1200等包都会让对端回ack 1001连续三个重复的ack基本能判断丢了包。还有一个常见坑是TCP的队头阻塞问题。TCP是字节流协议必须按顺序交付给上层如果中间丢了一个包即使后面重传之前已经收到更多数据缓冲区也会堵住不往上送。HTTP/2多路复用本来指望一个TCP连接并发传输多个请求结果一个丢包就让整条连接的所有请求跟着排队这就是为什么后来HTTP/3选择改用基于UDP的QUIC把流量控制移到多个独立的流上。拥塞控制方面传统算法有慢启动、拥塞避免、快速恢复等。表现为发送窗口不是瞬间拉满而是从一个初始窗口开始每收到一个ACK就指数增加直到达到慢启动阈值再改成线性增加。中间如果发生丢包阈值会一刀砍半。对业务方来说拥塞窗口决定了单条连接的吞吐上限——在带宽很高但延迟很长的链路上比如跨洋传输算一下带宽延迟积就会明白窗口太小根本没法利用满带宽需要调大缓冲区或者启用BBR这样的拥塞控制算法。2.4 UDP头就8个字节为什么还能干大事UDP头更简单一共四个16位字段源端口、目的端口、长度、校验和。没有序列号、没有确认、没有窗口、没有连接状态。这意味着UDP天然支持多对多通信广播、组播收发双方不用维护状态内核开销极小发送时延极低。但也意味着丢包谁都不知道顺序也可能乱。我见过不少开发在选UDP还是TCP上犯难。说实话如果只是传个小数据、丢了可以重传全部那UDP加应用层重传完全可行如果要传大数据且要求高可靠直接用TCP往往更省心。UDP真正发光的地方是实时音视频、网络游戏、DNS解析、以及需要极低额外开销的物联网上报场景。比如流媒体里偶尔丢一帧画面用户感知不强但要是用TCP的重传等机制延迟反而不可接受。2.5 通过ss和netstat快速判断连接状态排查问题时ss比netstat更快更方便因为它直接读取内核路由表信息。常用的几个查看方式ss -t # 查看TCP连接 ss -u # 查看UDP连接 ss -s # 查看汇总信息 ss -tnp # 显示进程有时候线上看到一堆SYN_RECV状态的连接这通常意味着服务端收到了SYN但没有完成握手可能是半连接队列满了也可能是SYN Flood攻击。调整内核参数之前先看当前队列占用情况ss -lnt # 可以配合比较 Recv-Q / Send-Q另一个我常看的指标是sockets的统计信息。如果看到大量连接处于TIME_WAIT结合业务类型决定是调整tcp_tw_reuse、tcp_max_tw_buckets还是从架构上改用长连接池彻底减少新建连接数量。总之传输层的状态机不是一个静态知识它直接反映在线上指标里学会读这些状态等于拿到了TCP的体检报告。3. IP层路由、分片、TTL都在IP头里IP层是把包从A主机送到B主机的“交通系统”。它不管包里装的是什么也不管两个主机中间隔了多少跳它只关心一件事按照路由表决定下一步应该把包交给谁。3.1 IPv4报文头读懂这些字段就能看懂包IPv4头标准长度20字节可选字段另计。版本号固定为4IHL告诉头部长度以4字节为单位TOS在现代Linux里一般转换为DSCP字段用。总长度是整个IP报文包含头的长度紧接着标识、标志位和片偏移这三兄弟就是用来做分片和重组的。TTL每经过一个路由器就减1减到0就丢弃是为了防止数据包在网络里死循环。协议号字段告诉上层是TCP6、UDP17还是ICMP1。头部校验和只校验IP头不校验数据部分因为数据部分的可靠性由上层负责。抓包时看到TTL值是个很常用的判断手段。比如ping百度TTL返回50多说明中间经过了很多路由器ping局域网内机器TTL是64因为默认初始TTL就是64。不同操作系统的初始TTL不同Linux是64Windows是128老版本Unix是255。单看初始值能根据TTL变化推测网络跳数排查跨网访问时很有用。3.2 路由查表每一跳都是独立决策发送IP包时Linux内核用目的IP匹配路由表找一条最优路由。各个路由项要参与比较匹配的优先级先匹配最具体的即最长前缀匹配如果有等价多路径则做负载均衡。ip route show # 输出类似 default via 192.168.1.1 dev eth0 192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.10上面第一条是默认路由所有没匹配到更具体路由的包都走这里。第二条是直连网段。添加静态路由最常用ip route add 10.0.0.0/8 via 192.168.1.1 dev eth0理解“每跳路由独立决策”很关键——数据包离开本地时你路由器只知道下一跳是谁不需要知道整条路径。每个中间路由器都做类似的查表动作逐跳把包送向目的地。排查路由问题时traceroute就是依赖这个特性通过不断递增TTL让沿途路由器返回超时ICMP报文从而还原整条路径。3.3 IP分片和路径MTU发现以及那些“经典”的故障IP层的分片逻辑是如果下一个出接口的MTU比IP报文要小就把包切成多个片每个片都带上自己的IP头在到达目的地后再组装。IPv6则取消中间路由器分片只有源端可以分片这也是IPv6改进之一。实际抓包中TCP对MSS的协商大大减少了IP分片的发生。TCP握手时双方会在SYN包里带MSS选项表示自己愿意接收的TCP数据段最大为多少。典型协商结果是用出接口MTU减掉IP头20字节和TCP头20字节。以太网MTU是1500所以MSS就协商为1460。但是别高兴太早如果路径中间有个MTU为1400的链路并且这条链路偷偷把超过1400的包丢弃那么TCP实际上还是会遇到分片或黑洞问题。这里就需要路径MTU发现PMTUD靠ICMP消息通知源端缩小报文大小很多防火墙会禁掉这种ICMP结果就是包发不出去典型症状是“短的包通大的包全断”。遇到这类问题有一个比较快的验证办法ping -M do -s 1400 192.168.1.20 # -M do 表示禁止分片 # -s 1400 表示ICMP负载1400字节加上IP头28字节总报文1428字节如果这条ping通但-s 1472通不过基本可以判断路径上有MTU瓶颈。3.4 ICMPIP层的好帮手ICMP就是IP层的“传话筒”。ping走了ICMP echo请求和应答traceroute走了ICMP超时路径MTU发现走了ICMP“需要分片”的消息。很多网络故障排查都离不开ICMP但也有安全设备习惯性丢弃ICMP导致ping不通但业务端口却是通的。所以排查时别只信ping的结果要配合TCP端口连通性和路由情况综合判断。4. MAC帧与链路层解决“最后一公里”的投递到了这一层我们终于站在物理连接的世界里了。所谓链路层就是负责在同一段物理网络内把数据帧从一台机器送到另一台机器。IP负责大方向MAC负责具体怎么走下一跳。4.1 以太网帧的结构14字节头的背后最常见的以太网帧结构目的MAC地址6字节、源MAC地址6字节、以太网类型2字节、载荷46~1500字节、FCS校验4字节。以太网类型字段如果是0x0800表示上面是IPv40x86DD是IPv60x0806是ARP。目的MAC地址是在帧头部所以网卡收到一包数据时首先就判断目的MAC是否是自己的、是否是多播地址或者是否置了混杂模式不匹配的帧直接被丢掉。有人会问明明有IP地址了为什么还需要MAC地址这是因为IP地址在物理网络中其实是个逻辑标识如果一个网络内要知道下一跳机器的具体网卡必须靠二层地址。就像你寄快递要填省市区和街道门牌号IP地址是区MAC地址是门牌号光知道到哪个区没用快递员需要精确到门牌。4.2 ARP怎么找到邻居的MAC地址当Linux要往同一网段的对端发包时查询邻居缓存表neighbor table也就是内核维护的ARP缓存。如果表中没有对应IP的MAC就会发出ARP请求全网广播“谁是这个IP把你的MAC告诉我”。对应主机回一个单播ARP应答双方把对应的IP-MAC映射写入缓存。排查ARP问题是链路层故障重头戏。有一回我排查某公司虚拟机网络丢包抓包发现网络上充斥着大量ARP请求原因是某台机器网卡工作不正常持续发出ARP风暴交换机CPU被拖垮正常数据包排队延误。定位方法很简单抓一段时间的ARP包统计谁发的多。运维经验里有个实用操作反复ping不通但ARP能解析多半是二层交换机问题ARP解析不了多半是IP配错或网卡故障。清ARP缓存可以用ip neigh flush all但更重要的是找到反复刷新的根源。4.3 MTU、VLAN和巨帧MTU决定了一个二层帧能承载的IP报文最大尺寸。以太网经典1500字节意味着IPTCPPayload一共不能超过1500。调大MTU到9000巨帧能减少帧数量、降低CPU开销但同时要求链路上所有设备都支持否则就是灾难。我曾经踩过坑把两台服务器的网卡和交换机端口都设成MTU 9000看着大文件传得飞快但某天经过另一个接入交换机时包全部被丢弃原因就是中间链路还是1500。VLAN则是给帧头增加4字节的802.1Q标签把物理局域网划分成多个虚拟局域网。这使得广播域缩小同时便于隔离和安全策略。很多玩软路由和高性能交换的人对VLAN不陌生它改变了二层网络的隔离方式这也意味着抓包时普通网卡默认会丢带VLAN标签的帧需要用工具设置VLAN offload否则抓不到数据。5. 一个完整的问题排查实战从协议栈角度抽丝剥茧理论知识讲再多不如真实走一遍排查过程。下面是一个基于真实场景的脱敏案例问题本身很典型排查路径也包含了前面讲到的各层核心知识。5.1 现象描述与初步定位某服务上云后客户反馈接口偶发超时平均延迟比之前高了10倍。我们用ping看网关延迟正常但访问业务端口时明显感觉有卡顿。先用tcpdump抓一下业务端口流量tcpdump -i eth0 tcp port 8080 -nn -c 1000一下子看到大量TCP DUP ACK和Retransmission。这说明链路本身有丢包或者路径上有设备对某些包处理不过来。但为什么同一路径上小包的ping是好的大包就有问题这大概率是MTU和分片相关。5.2 逐步验证从二层到四层排除第一步先确认链路MTU。用ip link show eth0看到MTU是1500。然后做禁止分片的ping测试ping -M do -s 1400 网关IP ping -M do -s 1472 网关IP发现1472字节通不过1400可以通过。说明从本机到网关这段链路或设备上路径MTU实际只有约1428字节。原因通常是中间隧道封装增加了开销比如VXLAN或者IPSec隧道把内层报文包了一层额外头部导致整包超过物理链路的1500字节。但应用TCP建的连接可能协商了1460的MSS于是超过了路径MTU触发了分片或黑洞。第二步查看路由和ARP。经过ip route get和arp -n确认下一跳的MAC地址正常链路层无异常。到这里就基本锁定了IP层的路径MTU问题。5.3 解决方案与验证解决路径有两个思路。一个是全局下调接口MTU比如把eth0的MTU改成1400。但这会影响整个主机上所有业务如果机器上有大量其他服务改动影响面大。另一个思路是针对应用侧改用小一点的MSS常见做法是在iptables中针对特定目标IP设置TCP MSS钳制iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1360或者直接调整应用socket的TCP_MAXSEG选项。改完后重新抓包确认不再有分片和重传延迟也降回正常水平。这个案例折扣了TCP、IP、MTU三层知识可见协议栈理解得越深层定位越准。6. 我常用的学习和排查工具以及几条保命经验光靠理论在真实工程里撑不住工具才是日常工作的刀。这里分享几个我常用的命令和要点都是偏实战的。tcpdump是排查网络问题的第一选择。它支持表达式过滤、离线抓包、按端口和协议抓取。线上排查时我习惯把输出精简同时带时间戳和绝对序号tcpdump -i eth0 tcp port 8080 -nn -tttt -vv抓包文件保存下来后用Wireshark看能直接看到TCP流、重传、乱序以及各层协议字段。ss命令查看连接状态netstat其实被ss取代了大半但有时候netstat -i可以看接口丢包统计ip -s link也能展示收发队列和错误计数。这些统计看多了慢慢就练出一双发现问题的眼睛。还有一个必须强调的经验不要在业务高峰期贸然重启网络服务或者清空邻居表可能触发大规模连接重连。我第一次清ARP缓存是在一个生产网关上几十个业务容器同时开始解析网关出口带宽瞬时间慢了好几倍被现场运维追着问了一下午。从那以后类似操我都是先确认影响范围、准备好回滚方案再最小化执行。另外做性能优化时别只看带宽。我在实际项目中见过很多工程师把网络带宽当成唯一瓶颈调完TCP缓冲区、换掉拥塞算法发现还是慢。因为数据经过的每一层都有队列和锁应用层读得慢内核接收缓冲堆满对端发送窗口收缩全链路就“融会贯通”地慢了。排查时要从上到下、从下到上一层层看网卡错误计数、IP统计、TCP重传、socket队列、应用处理时间每项都记录一下问题往往出现在你以为最不可能的那层。协议栈这些概念学的时候觉得抽象但每用上一次就清晰一分。遇到抓包里的现象多问几个“这层头部的哪个字段导致了这个问题”然后去翻RFC和内核源码注释慢慢就能建立起自己的排查直觉。我个人的体会是网络这东西没有捷径但只要你愿意把tcpdump和ss用熟把每一层协议的核心字段记牢大部分故障已经逃不出你的视野。
阅读完成 · 觉得有帮助?
咨询建站