不知道你有没有半夜被叫起来处理"网络时好时坏"的经历。明明服务没重启、密码没改、机房也没断电,但客户就是反馈卡顿、超时、掉线。这类问题我处理过太多次了,到最后你会发现一个残酷的事实:几乎所有网络故障,本质都是某个数据包没有被正确送达,或者送到后没被正确处理。而要搞清楚这个问题,你必须把一条链路彻底弄明白——从设备怎么拿到IP(DHCP),到拿到的是公网还是私网地址,再到这个数据包如何被封装、路由、转发,最后到达网卡之后是怎么被收进来的,以及为什么会被无声无息地丢掉。
这篇文章我就围绕这条主线,把DHCP的工作原理、公网与私网IP的区别、一次网络请求的数据包完整流程、网卡内部的工作机制,以及最让人头疼的网卡丢包问题,完整串一遍。适合刚入行的运维、网络工程师,也适合那些想系统搞懂"数据包到底怎么走"的开发者和学生。内容偏原理,但每一段我都会结合实际排障经验讲,不是那种看完就忘的理论书。
1. DHCP的四个阶段:从无人理睬到拿到地址的完整握手
1.1 为什么需要DHCP,它到底分配了什么
先说一个最基础的场景:你把一台崭新的电脑插到办公室网口上,没做任何配置,几十秒后就能上网了。这中间发生了什么?答案是DHCP(动态主机配置协议)替你完成了IP配置。
如果回到二十年前,那时候管理员是手工给每台机器填IP地址的。你想想那画面:300台电脑,一台台记IP、填子网掩码、填网关,如果哪天改了一次网段切换,就得重新跑一圈。这还不算最恐怖的,最恐怖的是有人乱填,把别人的IP占用了,冲突的机器直接断网。
DHCP解决的问题,就是把这套繁琐的动态配置过程自动化。它分配的远不止一个IP地址,还包括:
- IP地址
- 子网掩码
- 默认网关
- DNS服务器地址
- 租期(Lease Time)
- 以及一些可选参数,比如域后缀、NTP服务器等
从协议栈的位置来看,DHCP属于应用层协议,它基于UDP,客户端用68端口,服务器用67端口。但有意思的是,在客户端还没有IP地址的情况下,它是怎么和服务器通信的呢?靠的就是广播。
1.2 Discover、Offer、Request、Ack四步还不够,加上Release才完整
DHCP最经典的交互过程是四步握手,抓包经常能看到以下几个包:
第一步:DHCP Discover。客户端在拿到IP之前,源地址只能是0.0.0.0,目标地址是255.255.255.255,也就是广播地址。它向整个广播域喊了一嗓子:"有没有DHCP服务器?我需要一个IP地址。"注意,这里它连子网掩码都还不知道呢,所以只能拿全网络的广播地址来喊。
第二步:DHCP Offer。服务器收到Discover后,会从地址池里挑一个IP,通过广播(或者单播,取决于客户端在Discover里的标志位设置)回复一个Offer包:"我这里有这个IP,带上掩码、网关、租期,你要不要?"
第三步:DHCP Request。客户端收到Offer后,会广播一个Request包。这里有个关键点:为什么客户端的接受回复要用广播而不直接单播给那个服务器?因为可能同一网段有多台DHCP服务器都回了Offer,客户端选择了其中一台,但需要告诉其他服务器"我不用你们的IP了,请释放"。如果单播只告诉被选中的服务器,那些落选的服务器还会傻乎乎等着客户端来续租,造成地址浪费。
第四步:DHCP Ack。被选中的服务器收到Request后,确认这个IP正式分配给客户端,发一个Ack。客户端拿到Ack,就完成了配置。
除了这四步,还有一个包值得注意:Release。当客户端主动释放IP(比如你拔网线、dhclient -r命令)时会发送。另外,如果客户端后来发现IP被占用了(比如自己ping不通网关,或者有人手工配了相同IP),客户端会发一个DHCP Decline,告诉服务器"这个IP我不要了"。
1.3 租期续租机制和两个要命的阈值
拿到IP只是开始,这个IP不是永久归你的,它有一个租期。默认情况下,很多路由器里的租期是12小时或者24小时。客户端会在两个时间点尝试续租:
- T1,一般是租期的50%。到点后客户端直接单播给当时分配IP的那台DHCP服务器,说"IP我还用着,续一下"。如果成功了,就继续安静上网。
- 如果T1没续上,等到T2(租期的87.5%),客户端会重新回到广播方式发送Discover,请求任意一台可用的服务器来分配IP。
这里有个常见的坑:如果你改了DHCP地址池的网段配置,然后重新启动DHCP服务,而客户端还在旧租期内,它会继续用旧IP直到T1时间点,你可能发现机器半小时上不了网。我处理过好几次"改完路由器配置后有些手机连不上Wi-Fi"的问题,最后发现就是这个租期没到期的原因。所以现在我在改地址池时,会顺手把租期临时调成两分钟,等设备全部重新拿到新地址后再调回来。
跨网段的时候,DHCP请求就没法靠广播转发了,需要在三层设备上配置DHCP中继(IP Helper)。它的原理很简单:把客户端的广播包转成单播发给指定的DHCP服务器,服务器回复后再由中继单播返给客户端。很多人在配置跨网段DHCP时发现客户端一直拿不到IP,十有八九就是忘了在中继设备上配置接口地址和服务器地址。
2. 公网IP和私网IP:NAT背后的地址分配逻辑与运营商级陷阱
2.1 只有43亿个IPv4地址,为什么你家几十个设备都能上网
拿到IP之后,紧接着就要搞清楚一个概念:这个IP到底是公网的还是私网的。
IPv4的地址总量是2的32次方,大约43亿个。全球几十亿人,人手一个都不够,更别提一个人还有电脑、手机、平板、智能家电、服务器,一台设备就需要一个IP。所以大家想了一个办法:把IP地址划分成公网可路由的地址和私网内部使用的地址。
私网地址段是RFC 1918规定的,说白了就是三块"预留的自留地":
| 网段 | 范围 | 典型用途 |
|---|---|---|
| 10.0.0.0/8 | 10.0.0.0 ~ 10.255.255.255 | 大型企业内部网络 |
| 172.16.0.0/12 | 172.16.0.0 ~ 172.31.255.255 | 中型网络,云计算VPC常使用 |
| 192.168.0.0/16 | 192.168.0.0 ~ 192.168.255.255 | 家用路由器和中小公司最常用 |
这三段地址有个共同特点:它们在公网上不会被路由。也就是说,你拿着一个192.168.1.100的源IP去访问公网服务器,到了运营商核心网就会被丢掉,因为公网路由表里根本没有这些网段的路由条目。
那家里几十个设备怎么上网?答案是NAT(网络地址转换)。所有私网设备把数据包发给家用路由器,路由器把源IP统一改写成自己唯一的公网IP,再加上端口号来区分是哪个内部设备发起的会话。这种多对一的转换也叫NAPT(网络地址端口转换)。
过程可以简单理解为:内网一台设备用192.168.1.100:50000的源发送请求,路由器收到后,把它改成203.0.113.10:65000发出。服务器回包给203.0.113.10:65000,路由器查自己的转换表,知道这个端口对应内网192.168.1.100:50000,于是把包改回去扔给内网设备。
这里就衍生出一个所有运维都耳熟能详的结论:公网服务器没法主动连接内网设备。原因不是技术上行不通,而是NAT转换表里没有对应的映射。外部的包到了路由器,路由器根本不知道该转给谁。所以那些想在公司内网暴露服务到外网的人,要么做端口映射(手动添加转换规则),要么用一些隧道类工具打洞,原理都是让内网主动建立一条会话,再复用这条会话做反向数据传递。
2.2 家用宽带背后的运营商级NAT,你踩过的坑
现在不少家庭宽带光猫的WAN口地址,既不是公网IP,也不是我们熟悉的192.168这类私网IP,而是100.64.0.0/10这个范围内的地址。这段地址是RFC 6598规定的运营商级NAT专用地址(CGNAT)。
为什么运营商要搞这么一出?因为公网IPv4地址早就枯竭了,运营商手里的公网地址不够给每个宽带用户分配一个。他们把大量用户塞在同一台运营商NAT设备后面,让所有用户共享少量公网IP出口。
这台设备造成的实际影响,做过网络的人应该深有体会:
- 你在家里部署的服务,无论怎么设置路由器端口映射都访问不了,因为运营商那层NAT把你的公网流量又转了一道,映射关系根本不归你控制。
- P2P下载、视频通话、远程桌面直连经常失败,因为打洞打不过运营商那层NAT。
- 排查方法特别简单:登录光猫看WAN口IP,如果在100.64.0.0/10范围,基本就是中了CGNAT的招。打电话找运营商申请公网IPv4,大部分地区都能申请下来,只有极少数地区会拒绝。
IPv6曾经被视为解决地址短缺问题的终极方案,公网IPv6地址数量大到浪费也没事,每台设备都能分到独立公网地址。但这么多年过去了,很多应用和运维体系还是对IPv6适配不够上心,NAT这套IPv4时代的老办法依旧统治着日常网络。这个现实短期内不会改变。
3. 一次网页请求的数据包之旅:封装、路由与逐跳改写的全过程
3.1 从浏览器地址栏到服务器网卡,数据包经历了什么
地址问题搞清楚了,现在来看一个完整的数据包是怎么从你浏览器出发,跨过互联网,到达服务器的。
假设你在浏览器输入了一个域名,整个过程分这么几层:
第一,DNS解析。浏览器先要知道这个域名对应的IP地址。它会向配置好的DNS服务器发起查询,拿到IP。DNS查询本身也是一个完整的UDP数据包流程,这里先不展开。
第二,TCP三次握手。浏览器拿到的IP,和目的服务器的80或443端口建立起TCP连接。SYN、SYN-ACK、ACK,三次交互后,连接建立。
第三,HTTP请求发送。浏览器把HTTP请求封装成TCP数据段,加上源端口(随机高端口)和目的端口(80或443)。
第四,IP封装。操作系统把这个TCP段交给IP层,加上源IP(你的内网IP)和目的IP(服务器IP),为它找到一条出去的路径。
第五,以太网封装。在真正发出网卡之前,还要加上源MAC地址和目的MAC地址。注意,这里其实涉及一个关键判断:目的IP和你在不在同一个网段?
- 如果在同一网段,直接ARP查找目的IP的MAC地址。
- 如果不在同一网段,数据包必须先发给网关处理,此时目的MAC地址是网关的MAC,而不是最终服务器的MAC。
很多人刚开始抓包时会困惑:我明明在访问一台远程服务器,为什么以太网帧里的目的MAC是我家路由器的?答案就在这里——MAC地址是逐跳变化的,但IP地址在到目标之前保持不变。
3.2 中间每一跳的路由器到底做了什么
数据包从家用路由器出去后,会一路经过多台运营商路由器,每一跳做的事高度一致:
- 从物理端口收到以太网帧。
- 剥掉链路层的帧头和帧尾,取出IP包。
- 查路由表,依据目的IP做最长前缀匹配,决定下一跳IP和出接口。
- 重新封装一个新的以太网帧,目的MAC改成下一跳设备的MAC,源MAC改成自己出接口的MAC,从出接口发出去。
这个过程的本质,就是源IP和目的IP全程不变,但每一段的MAC地址都在改写。我在给新人讲这个的时候喜欢用快递做类比:IP地址是写在快递单上的收件人地址,从发货到收货全程不变;但每个中转站贴的转运条码每次都换成下一站的信息,不同中转站之间的"交接凭证"不一样。
整个过程中还有一个容易被忽略的机制:TTL(Time To Live)。IPv4包里有一个TTL字段,每经过一个路由器就减1,减到0就丢弃并给发送方回一个ICMP超时消息。这个机制有两个用途:一是防止数据包在环路上无限打转,二是被traceroute命令利用来探测路径上的每一跳设备。
关于MTU(最大传输单元),也值得说几句。大部分以太网链路MTU是1500字节,如果你的IP包总长度超过这个值,且包头的DF(禁止分片)标志没有设置,路由器就会把它拆成多个小片继续发。但很多安全设备、隧道设备会设置DF位,导致大包直接发不出去。这类问题排查的时候用ping命令加大包体并设置DF位来测,就能快速定位"能通则通,大包不通"的MTU故障。
3.3 数据包从入口到应用进程的最后几步
到了目标服务器,数据包也不是直接被应用拿到的,中间还有好几道关卡。
先是服务器的网卡根据目的MAC地址把帧收进来,剥掉以太网帧头,把IP包交给IP层。IP层检查目的IP是不是本机,是的话交给TCP/UDP层。TCP层通过目的端口号,找到正在监听这个端口的socket,把数据放到接收缓冲区。应用进程调用read()把它读走。
这个过程看着简单,但每一步都藏着几个"经典丢包点"。TCP层有个半连接队列和全连接队列,分别对应握手未完成和已完成但未被应用accept的连接。如果应用处理太慢,全连接队列满了,服务器就会直接丢弃新来的SYN包。你在客户端看到的症状是TCP连接超时,但在服务器上抓包却看不到明显的错误,因为包在协议栈里被静默丢掉了。
在排查这类问题时,我常用的一个思路是:先确定故障在哪一层,再用排除法逐步定位。而排除法的基础,是对这条完整链路的每一步心中有数。
4. 网卡内部是怎么工作的:环形队列、中断与卸载特性的底层逻辑
4.1 网卡绝不只是一个插网线的口
说到数据包到服务器的最后一米,就绕不开网卡。我接触过不少运维,对网卡的理解还停留在"一个RJ45接口"。实际上,网卡本身就是一个微型计算机,它承担了大量与CPU的协同工作。
网卡的核心职责可以拆成几条:
- 物理层收发:把电缆或光纤上的模拟电流/光信号,解码成二进制的数据帧。
- 帧校验:每个以太网帧末尾都带一个FCS(帧校验序列),网卡用CRC算法校验,如果发现FCS错误,直接丢弃这个帧,因为数据已经不完整了。
- 地址过滤:网卡检查帧头里的目的MAC,如果不是本机MAC、不是广播MAC、也不是组播MAC(在开启组播过滤的情况下),直接丢弃,不惊动CPU。
- DMA传输:数据帧校验通过后,网卡通过DMA(直接内存访问)把数据写到内存中的环形缓冲区,不用CPU逐拷。
为什么网卡要支持DMA?想象一下,如果每个收到的包都要CPU亲自从网卡寄存器里一个一个搬进内存,高速网络下CPU会直接被中断和拷贝操作打满,根本没有余力处理应用逻辑。DMA本质上是让硬件设备直接访问内存,只在完成一批传输后打断CPU。
4.2 环形队列和中断机制:数据进内存前的最后一道门
刚才提到的环形缓冲区(Ring Buffer),是理解和排查网卡丢包的关键。
它的工作原理像一条环形传送带:网卡把收到的帧放到Ring Buffer里,然后给CPU发一个中断信号,告诉它"有数据来了,快来处理"。CPU收到中断后,从Ring Buffer里取出数据,交给网络协议栈。
Ring Buffer是有容量的,默认在256或512个描述符左右(具体看网卡驱动)。如果CPU处理速度跟不上数据包到达的速度,Ring Buffer会被填满。满了之后会发生什么?新到的数据帧无处安放,直接被网卡丢弃。这个丢弃行为,会在网卡统计计数器里留下痕迹。这也是下一章要重点展开的问题。
还有个概念叫中断合并(Interrupt Coalescing)和NAPI。早期网卡是收到一个包就中断一次CPU,高并发下CPU会陷入中断风暴,干不了正事。现代驱动普遍使用NAPI机制:数据包大量到达时,网卡的中断响应转为轮询模式,CPU持续从Ring Buffer里批量取包,而不是被打断几千次。中断合并则是让网卡攒一批包再统一通知CPU,牺牲少量延迟换取高吞吐。在延迟敏感场景下,反而需要调低合并阈值。
4.3 卸载特性:为什么抓包看到的MTU超过了1500
现在的高性能网卡还有各种硬件卸载特性,比如校验和卸载和分段卸载。
Checksum Offload的意思是,TCP/UDP包的校验和计算交给网卡硬件完成,CPU不用算了。TSO(TCP Segmentation Offload)更狠,应用和驱动可以把一个几MB大小的TCP段直接交给网卡,网卡硬件自己按照MTU把它切成一个个符合标准的以太网帧再发出去。
这带来一个抓包时的常见迷惑现象:在服务器上用tcpdump抓包,经常看到超过MTU的"超大包"。这不是出了bug,而是抓包点位于驱动层之上或者在硬件队列中,看到的是TSO切分前的大段数据。排查真实MTU问题时,如果开着TSO,抓包结果很容易误导你,这时候要先用ethtool把TSO/GRO/GSO临时关掉再测试。
多队列(RSS,Receive Side Scaling)也是现代网卡的标配。网卡内部有多个Queue,每个Queue可以绑定不同的CPU核,通过哈希算法把不同连接的数据流分散到不同队列,实现多核并行处理收包。如果没有多队列,或者驱动没正确启用,所有收到的包都会压到一个CPU核上,那个核的softirq(软中断)使用率直接飙到100%,而其他核闲着。这是很多高流量服务器性能上不去的隐形原因。
5. 实测网卡丢包排查:计数器里的真相与一次断流问题复盘
5.1 一次"网络时好时坏"的完整复盘
前四年份的铺垫,都是为了这一刻:真刀真枪地排查一次网卡丢包。我这里说的"丢包",是真正意义上的沉默故障,网络层的ping可能正常,但应用层就是时快时慢。
之前帮某客户处理过一次特别典型的问题。现象是:服务器上跑着一个接口服务,客户端反馈时好时坏,高峰期延迟从正常的5ms飙到300ms,偶尔直接超时。服务器CPU、内存、磁盘都很健康,完全没有高负载迹象。
我的排查链路是这样的:
第一步,ping测试。从客户端ping服务器,发现平均延迟还行,但max值比平时高了一个量级。再ping网关,延迟稳定,说明物理链路没大问题,问题大概率出在服务器收包后的某个环节。
第二步,检查网卡统计计数器。这是最关键的一步。ethtool -S 输出里有个叫做rx_missed_errors的参数,一直在持续增长。这说明网卡内部FIFO或者Ring Buffer发生过溢出,有数据包在还没进入协议栈之前就被硬件丢弃了。
第三步,确认Ring Buffer大小。ethtool -g 显示当前RX Ring大小是256,而当时这台机器上的业务流量峰值已经把队列冲爆了。CPU把包从Ring Buffer取走的速度,赶不上网卡把包塞进来的速度。
第四步,调整Ring Buffer。ethtool -G 把RX Ring从256调到4096。为什么调大有效?因为Ring Buffer实际上是一块网卡和驱动共享的内存描述符队列,调大之后,相当于给网卡到CPU之间加了更长的缓冲带,短时间的流量尖峰不会被直接丢包。
第五步,观察效果。调整后,rx_missed_errors基本恒定,延迟恢复正常。当然我不建议一上来就无脑调大Ring Buffer。如果CPU处理能力本身就不足,调大Ring只是把包堆积延后,并且消耗更多内存,根本问题并没有解决。
5.2 两类"被隐瞒"的丢包:如果你只盯着ifconfig就错了
网卡丢包问题最坑人的地方,不是丢包本身,而是丢包了你看不出来。
很多人排查丢包,第一反应是看ifconfig。输入命令后看到RX dropped为0,就觉得网卡没问题。但你要知道,ifconfig和netstat -i里的dropped计数,统计的是协议栈层面的丢弃——比如IP层发现目的地址不是本机、ARP处理失败、内存分配失败的包。而网卡硬件层面因为Ring满了丢掉的包,很多网卡驱动并不会同步到这个计数里,给你看到的只是ethtool -S里那些带missed、over、fifo字眼的计数器。
所以我的经验是:不要只用ifconfig判断网卡是否丢包,必须结合ethtool -S的数据。
下面这张表可以帮你快速定位丢包方向:
| 计数器或现象 | 可能原因 | 优先排查方向 |
|---|---|---|
| rx_crc_errors / rx_fcs_errors 增大 | 物理链路问题,电缆或光模块故障 | 换线、换光模块、检查协商速率 |
| rx_missed_errors / rx_no_buffer 增大 | 网卡Ring Buffer过小或溢出 | 调大ethtool -G的rx参数 |
| RX-DRP增高(netstat -i) | 协议栈处理缓慢或畸形包 | 检查内核dmesg、防火墙规则、nf_conntrack表 |
| tx_dropped增高 | 发送队列拥堵或流量控制 | 检查Ring Buffer的TX方向、流量整形配置 |
| 单核softirq占用100% | 多队列/中断绑定未生效 | ethtool -L启用多队列,绑定中断到多个CPU |
| dmesg出现nf_conntrack: table full | 连接跟踪表满,新连接被丢弃 | 调大nf_conntrack_max或者清理无效连接 |
5.3 用两端抓包稳住问题的证据链
计数器的证据能缩小范围,但要最终确认丢包到底发生在哪一段,我强烈建议在两端同时抓包。
具体操作是这样的:假设客户端访问服务器超时。在服务器的网卡上抓包,同时也在客户端出口抓包。
如果客户端的TCP栈已经发出了SYN包,但服务器端抓不到这个SYN,说明包在中间链路丢了,或者被中间设备过滤了。如果服务器抓到了SYN,但在服务器上却从未发出SYN-ACK,说明问题出在服务器的协议栈处理上,可能性包括半连接队列满了、防火墙drop规则拦截了入站连接、应用进程根本没有listen。
另一种常见情况:服务器把SYN-ACK发出去了,但客户端收不到。这时候要在客户端抓包,判断是不是客户端防火墙把回包拦了,或者是NAT映射异常导致回包被转给了错误的内网主机。
可以说,一次完整的网络排障,本质上是"沿着数据包的足迹,逐层验证"。从DHCP拿到的地址是不是正确,到路由路径对不对,再到网卡收包有没有损耗,每走一步都是在用证据接近真相。
我在实际工作时有个习惯:拿到一台新服务器,第一件事就是把网卡的基准计数器、Ring Buffer大小、多队列配置、关键内核网络参数记录下来。这样等出问题时,我可以第一时间对比哪些指标偏离了基线,而无须现查历史。这个习惯帮我省了很多半夜排查的时间。也建议你从今天开始,做个网络基线台账,哪怕只是一张表格,出问题时你真的会感谢当时的自己。