土法炼钢兴趣小组的算法知识备份

【网络工程】网络模型的工程视角:为什么你需要理解分层

文章导航

分类入口
network
标签入口
#network-model#OSI#TCP-IP#layering#abstraction-leak#troubleshooting

目录

线上一个 HTTP 接口突然变慢,P99 延迟从 20ms 飙到 500ms。你打开监控面板,应用层指标正常——Handler 处理耗时没变。数据库查询没变慢。GC 没抖动。日志里也没异常。

你怀疑是网络问题,但”网络问题”太笼统了。是 DNS 解析变慢了?是 TCP 握手耗时增加了?是中间某一跳丢包导致重传?还是服务端的接收缓冲区满了?

这时候你需要的不是”ping 一下看看通不通”,而是一个系统化的排查框架:从哪一层开始看?每一层能告诉你什么信息?每一层有哪些工具?

这就是网络分层模型的工程价值——它不是考试知识点,是排查问题的坐标系。

这篇文章做三件事:

  1. 把 OSI 七层和 TCP/IP 四层放在一起,讲清楚它们在工程中的真实用途和区别。
  2. 拆解每一层的核心工程关注点,包括你在每一层能用什么工具、能看到什么信息。
  3. 讲清楚分层抽象在哪些场景会”泄漏”——上层以为底层帮你搞定了,但其实没有。

一、两套模型的历史与工程定位

网络分层有两套模型:OSI 七层模型和 TCP/IP 四层模型。工程实践中几乎所有人都在用 TCP/IP 四层模型,但 OSI 的术语仍然到处出现——“L4 负载均衡”“L7 代理”里的 L4、L7 就来自 OSI 编号。搞清楚两套模型的关系和差异,才不会被术语搞晕。

OSI 七层:一个从未完整实现的参考框架

OSI(Open Systems Interconnection)模型由 ISO 在 1984 年发布,最初目标是成为全球网络的标准协议栈。它把网络通信切成七层:

层号 名称 核心职责
7 应用层(Application) 提供应用程序的网络接口
6 表示层(Presentation) 数据格式转换、加密、压缩
5 会话层(Session) 管理会话生命周期
4 传输层(Transport) 端到端可靠传输、流量控制
3 网络层(Network) 路由和寻址
2 数据链路层(Data Link) 帧封装、MAC 寻址、差错检测
1 物理层(Physical) 比特流在物理介质上的传输

OSI 模型的 5、6、7 层划分在实践中几乎没有独立实现。TLS 既做加密(表示层)又做会话管理(会话层),HTTP 同时涉及应用和表示——硬把它们塞进单独的层没有工程意义。

但 OSI 模型有一个持久的贡献:它建立了”层号”这套通用语言。 当你说”L4 负载均衡”,所有工程师都知道你在说传输层(TCP/UDP 级别)的负载均衡。当你说”L7 代理”,大家知道你在说能解析 HTTP 的代理。这套编号已经脱离了 OSI 协议本身,成为网络工程的公共词汇。

OSI 模型失败的技术原因也值得一提。当年 ISO 试图同时定义模型和协议实现(X.25、CLNP 等),开发周期漫长,规范复杂。而 TCP/IP 的策略完全相反——先写代码跑起来,再写 RFC 记录。到 1990 年代初,TCP/IP 已经是互联网事实标准,OSI 的协议实现被彻底边缘化。最终留下来的只有七层模型本身作为教学和沟通的参考框架。

TCP/IP 四层:互联网真正运行的模型

TCP/IP 模型来自实践而非标准委员会。它随 ARPANET 和后来的互联网一起演化,反映了互联网协议栈的真实结构:

TCP/IP 层 对应 OSI 层 核心协议 工程关注点
应用层 5-7 HTTP、DNS、TLS、gRPC 应用协议行为、序列化、加密
传输层 4 TCP、UDP、QUIC 连接管理、可靠性、拥塞控制
网络层 3 IP、ICMP 路由、寻址、分片
链路层 1-2 Ethernet、Wi-Fi 帧封装、MAC 寻址、MTU

TCP/IP 模型的务实之处在于:它不强求每一层的职责严格分离。TLS 在 TCP 之上运行但不独立成层;QUIC 在 UDP 之上同时做了传输和加密,打破了”传输层”和”应用层”的边界。这不是设计缺陷,而是工程权衡——协议设计者宁可打破分层也要解决实际问题(比如减少握手 RTT)。

RFC 1122 (Requirements for Internet Hosts) 在 Section 1.1.3 明确定义了这四层,并强调了一个重要原则:“分层是用来组织设计的,不是用来限制实现的”。实际的协议实现可以也经常会跨层访问信息——比如 TCP 需要知道 IP 头部的源/目标地址来计算校验和(伪首部)。

两套模型的对照与差异

下面这张图展示了两套模型的对应关系和每一层在实际工程中涉及的典型协议:

OSI 七层                TCP/IP 四层            代表协议与工具
┌─────────────┐
│ 7. 应用层    │
├─────────────┤        ┌─────────────┐
│ 6. 表示层    │───────→│  应用层      │        HTTP, DNS, TLS, gRPC, MQTT
├─────────────┤        │             │        curl, dig, openssl
│ 5. 会话层    │        └──────┬──────┘
├─────────────┤               │
│ 4. 传输层    │───────→┌──────┴──────┐        TCP, UDP, QUIC
├─────────────┤        │  传输层      │        ss, netstat, tcpdump
│ 3. 网络层    │───────→├─────────────┤        IP, ICMP
├─────────────┤        │  网络层      │        ip, traceroute, mtr, ping
│ 2. 数据链路层│───────→├─────────────┤        Ethernet, ARP, VLAN
├─────────────┤        │  链路层      │        ip link, ethtool, arp
│ 1. 物理层    │        └─────────────┘        网卡、线缆、光纤
└─────────────┘

关键差异在于 OSI 的上三层。OSI 把应用相关的功能切成应用、表示、会话三层,但在真实的互联网协议中,这三层的职责经常混在一起。HTTP 既有应用语义(请求方法、状态码),也有表示层的内容协商(Content-Type、Content-Encoding),还有会话管理的意味(Cookie、Keep-Alive)。TLS 提供加密(表示层)和会话恢复(会话层)。强行把它们分开是理论上的整洁,工程上的负担。

工程中到底用哪个?

结论很直接:日常工程用 TCP/IP 四层模型思考问题,用 OSI 层号作为通用术语。

比如,你在排查网络延迟时,思路是沿着 TCP/IP 四层走的:

  1. 先看应用层——请求本身有没有问题?
  2. 再看传输层——TCP 有没有重传?窗口有没有收缩?
  3. 再看网络层——某一跳的延迟是不是异常?有没有丢包?
  4. 最后看链路层——MTU 有没有问题?有没有 CRC 错误?

但你描述问题时,会说”L4 负载均衡”“L7 路由”“L3 VPN”——用的是 OSI 的层号。

一个微妙的点:很多人说的”L2 交换”“L3 路由”严格来说混用了两套模型的术语——L2、L3 是 OSI 编号,但交换机和路由器的行为用 TCP/IP 模型的链路层和网络层来理解更自然。不过工程师之间已经约定俗成,不会有人纠正你。

二、每一层的工程关注点与工具

分层模型的工程价值不在于背诵每层的定义,而在于知道:出了问题,该去哪一层找线索,用什么工具。

下面是 Linux 上每一层对应的核心诊断工具速查表:

核心关注 主力工具 辅助工具
链路层 MTU、MAC、帧错误 ip linkethtool arpbridgetcpdump -e
网络层 路由、IP 分片、ICMP ip routemtrtraceroute pingnmap/proc/net/snmp
传输层 连接状态、重传、窗口 sstcpdump netstat/proc/net/snmpconntrack
应用层 HTTP、DNS、TLS curldigopenssl mitmproxyngrep、Chrome DevTools

链路层(L2):帧、MAC 与 MTU

链路层负责在直连的网络节点之间传递帧(frame)。对后端工程师来说,这一层平时几乎不需要关注——直到你遇到以下几类场景。

场景 1:MTU 不匹配导致的丢包。 以太网默认 MTU 是 1500 字节。如果路径上某一跳的 MTU 更小(比如某些隧道协议会加封装头,把有效 MTU 缩小到 1400),而发送端设置了 DF(Don’t Fragment)位,那这个包就会被直接丢弃。更糟糕的是,某些中间设备会静默丢弃而不返回 ICMP “Fragmentation Needed” 消息——这就是所谓的 PMTUD 黑洞(Path MTU Discovery black hole)。从发送端看,就是莫名其妙的丢包,TCP 会触发重传,延迟飙升。

各种隧道和封装对 MTU 的影响:

封装类型 额外头部开销 有效 MTU(基于 1500)
无封装 0 1500
VLAN (802.1Q) 4 字节 1496
GRE 24 字节 1476
IPsec (ESP+AH) 50-60 字节 1440-1450
VXLAN 50 字节 1450
WireGuard 60 字节 1440
Geneve 50+ 字节 1450 以下

在容器网络中 MTU 问题尤其常见。比如 Kubernetes 使用 VXLAN Overlay 网络时,Pod 看到的 MTU 是 1500,但实际路径的有效 MTU 只有 1450。如果 CNI 插件没有正确设置 Pod 网卡的 MTU,大包就会被丢弃。

关键命令:

# 查看网卡 MTU
ip link show eth0
# 输出示例:
# 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP ...

# 用 ping 测试路径 MTU(设置 DF 位,逐步减小包大小)
# ICMP 头 8 字节 + IP 头 20 字节,所以 -s 1472 对应总长度 1500
ping -M do -s 1472 -c 3 目标IP
# 如果收到 "Frag needed and DF set",说明路径 MTU 小于 1500

# 二分法找到精确路径 MTU
ping -M do -s 1400 -c 1 目标IP   # 通过 → MTU > 1428
ping -M do -s 1450 -c 1 目标IP   # 不通 → MTU 在 1428-1478 之间
ping -M do -s 1425 -c 1 目标IP   # 继续缩小范围

# 查看链路层统计(CRC 错误、丢帧)
ethtool -S eth0 | grep -iE "error|drop|crc|collision"

场景 2:ARP 问题导致的间歇性网络中断。 ARP(Address Resolution Protocol)负责把 IP 地址解析成 MAC 地址。ARP 缓存有老化时间(Linux 默认 base_reachable_time 30 秒,但实际老化时间是 15-45 秒之间的随机值)。当 ARP 缓存过期后重新解析时,如果目标设备没有及时回复 ARP Reply(比如负载高时 ARP 响应被延迟),就会出现短暂的网络中断。

在虚拟化和容器环境中,ARP 还有另一个常见问题:虚拟 IP 漂移后,旧 MAC 地址的 ARP 缓存没有及时更新。这就是为什么高可用方案(如 Keepalived)在 VIP 漂移时要主动发送 Gratuitous ARP 来刷新同网段设备的 ARP 缓存。

# 查看 ARP 缓存(ip neigh 比 arp 命令更现代)
ip neigh show
# 输出示例:
# 192.168.1.1 dev eth0 lladdr 00:11:22:33:44:55 REACHABLE
# 192.168.1.2 dev eth0 lladdr 00:11:22:33:44:66 STALE

# 各状态含义:
# REACHABLE - 最近确认过可达
# STALE     - 超过老化时间,下次使用时会重新验证
# DELAY     - 正在等待确认探测的响应
# PROBE     - 正在主动发送确认探测
# FAILED    - 解析失败,对端不可达
# INCOMPLETE - ARP 请求已发送,等待响应

# 手动清除特定 ARP 条目(排查时有用)
ip neigh del 192.168.1.1 dev eth0

# 查看 ARP 缓存超时设置
cat /proc/sys/net/ipv4/neigh/eth0/base_reachable_time_ms

网络层(L3):IP、路由与 ICMP

网络层负责跨网络的包转发和路由。后端工程师最常在这一层排查的问题是:路由是否正确、某一跳是否丢包、延迟在哪一段飙高。

路由表是网络层的核心数据结构。 Linux 的路由表决定了每一个出站数据包的下一跳(next hop)。路由查找使用最长前缀匹配(Longest Prefix Match)——如果有多条路由都匹配目标 IP,内核选择掩码最长(最具体)的那条。

# 查看主路由表
ip route show
# 输出示例:
# default via 10.0.0.1 dev eth0 proto dhcp src 10.0.0.100 metric 100
# 10.0.0.0/24 dev eth0 proto kernel scope link src 10.0.0.100
# 172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1

# 查看到目标的精确路由(包括使用哪个网卡、源 IP)
ip route get 8.8.8.8
# 输出示例:
# 8.8.8.8 via 10.0.0.1 dev eth0 src 10.0.0.100 uid 0
#     cache

# 查看策略路由规则(如果有多张路由表)
ip rule show

traceroute 和 mtr 是路径诊断的核心工具。 traceroute 通过逐步增加 TTL 来发现路径上的每一跳。每经过一个路由器,TTL 减 1;减到 0 时路由器会丢弃数据包并返回 ICMP Time Exceeded 消息,traceroute 就知道了这一跳的 IP 地址。

# traceroute 三种模式:
# ICMP 模式(-I)— 有些路由器不回复 ICMP Echo
traceroute -I 目标IP

# UDP 模式(默认)— 有些防火墙拦截高端口 UDP
traceroute 目标IP

# TCP 模式(-T)— 用 TCP SYN,最不容易被拦截
traceroute -T -p 443 目标IP

# mtr 是更好的选择:持续监测,统计丢包率和延迟抖动
mtr --report --report-cycles 100 目标IP
# 输出会显示每一跳的 Loss%、Avg、Best、Wrst、StDev

读 mtr 输出的关键技巧:中间某一跳显示丢包不一定是问题——很多路由器对 ICMP 做限速(rate limiting),导致探测包被丢弃,但实际转发正常。只有当丢包从某一跳开始持续到目标时,才说明该跳确实有问题。 如果只有中间某一跳丢包,但后续跳和目标的丢包率正常,通常只是路由器的 ICMP 限速。

# 查看 IP 层统计(分片、重组、丢包)
cat /proc/net/snmp | grep Ip
# 关注字段:
# InDiscards  - 入站丢弃
# OutDiscards - 出站丢弃(通常是路由表查找失败)
# FragFails   - 分片失败(DF 位设置但包太大)

传输层(L4):TCP/UDP 的连接与流控

传输层是后端工程师打交道最多的一层。TCP 的连接状态、重传率、窗口大小、拥塞控制算法——这些直接影响你的服务性能。本系列后续有 8 篇文章专门讲 TCP 工程(06-13 篇),这里列出最关键的诊断入口。

ss(Socket Statistics)是 netstat 的现代替代品。它直接读取内核的 socket 信息,比 netstat(解析 /proc/net 文件)快得多,尤其是在连接数很大的服务器上。

# 查看所有 TCP 连接状态统计
ss -s
# 输出示例:
# TCP:   347 (estab 289, closed 12, orphaned 0, timewait 46)

# 查看特定端口的连接详情(-t TCP, -i 内部信息)
ss -ti dst :443
# 输出示例(关键字段解读):
# ESTAB 0 0 10.0.0.100:55432 93.184.216.34:443
#   cubic wscale:7,7 rto:204 rtt:1.234/0.567 ato:40
#   mss:1460 pmtu:1500 rcvmss:1460 advmss:1460
#   cwnd:10 ssthresh:7 bytes_sent:1234 bytes_acked:1234
#   bytes_received:5678 send 94.7Mbps
#   retrans:0/0 rcv_rtt:1 rcv_space:29200
#
# 关键字段:
# cubic     - 拥塞控制算法
# rto:204   - 当前重传超时 204ms
# rtt:1.234/0.567 - 平滑 RTT 1.234ms / RTT 偏差 0.567ms
# cwnd:10   - 拥塞窗口 10 个 MSS
# retrans:0/0 - 当前未确认重传数 / 累计重传次数

# 查看 TCP 层统计(重传、RST、SYN 等)
cat /proc/net/snmp | grep Tcp
# 关键字段:RetransSegs(重传段数)、AttemptFails(连接失败数)

# 按状态统计连接数(快速发现 TIME_WAIT/CLOSE_WAIT 堆积)
ss -tan | awk '{print $1}' | sort | uniq -c | sort -rn

传输层最常见的问题模式:

现象 可能原因 排查命令 深入方向
连接建立慢 SYN 丢包或 SYN 队列满 ss -ltn 查 Recv-Q 06-tcp-connection
吞吐量低 接收窗口或拥塞窗口过小 ss -ti 看 cwnd/rwnd 08-tcp-flow-control
间歇性超时 丢包触发 RTO 退避 ss -ti 看 retrans/rto 12-tcp-diagnostics
TIME_WAIT 堆积 短连接频繁建立关闭 ss -tan \| grep TIME-WAIT \| wc -l 06-tcp-connection
CLOSE_WAIT 堆积 应用没有调用 close() ss -tnp \| grep CLOSE-WAIT 应用层 bug

应用层(L5-L7):HTTP、DNS、TLS

应用层涵盖了 OSI 的 5-7 层。对后端工程师来说,这一层的排查最直接——你能看到请求内容、响应状态码、TLS 握手细节。

curl -w 的时间分解是排查 HTTP 延迟的第一步。 它把总延迟拆成多个阶段,你可以立刻看到延迟集中在哪个环节:

# HTTP 请求全阶段计时
curl -w "\n\
---时间分解---\n\
DNS 解析:    %{time_namelookup}s\n\
TCP 连接:    %{time_connect}s\n\
TLS 握手:    %{time_appconnect}s\n\
首字节(TTFB): %{time_starttransfer}s\n\
总耗时:      %{time_total}s\n\
---连接信息---\n\
远端 IP:     %{remote_ip}\n\
本地 IP:     %{local_ip}\n\
HTTP 状态:   %{http_code}\n\
下载大小:    %{size_download} bytes\n" \
-o /dev/null -s https://example.com

# 输出示例:
# ---时间分解---
# DNS 解析:    0.004123s
# TCP 连接:    0.025456s    ← TCP 握手耗时 = 0.025 - 0.004 = 21ms
# TLS 握手:    0.052789s    ← TLS 握手耗时 = 0.053 - 0.025 = 28ms
# 首字节(TTFB): 0.089012s   ← 服务端处理耗时 = 0.089 - 0.053 = 36ms
# 总耗时:      0.091234s    ← 数据传输耗时 = 0.091 - 0.089 = 2ms

注意这些时间是累计值(cumulative),不是每个阶段的独立耗时。各阶段的真实耗时需要做减法。

# DNS 解析调试(追踪完整解析链路)
dig example.com +trace
# 这会显示从根 DNS 到权威 DNS 的每一步查询

# TLS 握手调试(显示协商的协议版本和密码套件)
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null | head -20
# 关注:Protocol version、Cipher suite、Server certificate chain

# 查看 TLS 证书到期时间(批量检查证书过期)
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -dates
# 输出:
# notBefore=Jan  1 00:00:00 2025 GMT
# notAfter=Dec 31 23:59:59 2025 GMT

三、数据包的封装与解封装

理解分层的一个关键视角是:数据在每经过一层时会被加上一层头部(header),到达对端后再逐层剥掉。 这个过程叫封装(encapsulation)和解封装(decapsulation)。

封装过程

一个 HTTP GET 请求从应用发出到变成线缆上的比特流,经历的封装过程:

graph TB
    subgraph 应用层
        A["HTTP 请求<br/>GET / HTTP/1.1<br/>Host: example.com"]
    end
    subgraph 传输层
        B["TCP Header (20B)<br/>Src Port: 55432<br/>Dst Port: 80<br/>Seq: 1000<br/>Flags: PSH,ACK"]
        C[HTTP Payload]
    end
    subgraph 网络层
        D["IP Header (20B)<br/>Src: 10.0.0.100<br/>Dst: 93.184.216.34<br/>TTL: 64<br/>Proto: TCP(6)"]
        E[TCP Header]
        F[HTTP Payload]
    end
    subgraph 链路层
        G["Eth Header (14B)<br/>Src MAC: aa:bb:cc:...<br/>Dst MAC: 11:22:33:..."]
        H[IP Header]
        I[TCP Header]
        J[HTTP Payload]
        K["FCS (4B)"]
    end
    A --> B
    B --> D
    D --> G

每一层的头部都携带该层需要的控制信息:

头部开销的工程影响

一个关键的工程含义:每一层的头部都在消耗有效载荷空间。

以太网帧(最大 1518 字节,不含前导码和 IFG)
├── Ethernet Header: 14 字节
├── IP Payload(最大 1500 字节 = MTU)
│   ├── IP Header: 20 字节
│   ├── TCP Payload(最大 1460 字节 = MSS)
│   │   ├── TCP Header: 20 字节
│   │   └── Application Data: 最大 1460 字节
│   │       └── 如果有 TCP 选项(时间戳 12 字节),MSS 降至 1448 字节
│   └── TCP Header
└── FCS (Frame Check Sequence): 4 字节

以太网帧的最大载荷(MTU)是 1500 字节,IP 头部 20 字节,TCP 头部 20 字节(不含选项),留给应用数据的空间是 1460 字节——这就是 TCP 的 MSS(Maximum Segment Size)默认值的由来。

如果加上 TCP 时间戳选项(12 字节,几乎所有现代系统都开启),实际 MSS 降到 1448 字节。再加上 TLS 记录层头部(5 字节)+ TLS 加密开销(AEAD 通常 16 字节 tag + 可能的 padding),实际能传的应用数据还要更少。

如果再加上隧道封装(比如 VXLAN 加 50 字节),有效载荷进一步缩小。这就是为什么在容器网络中,MTU 问题尤其容易出现——底层的隧道封装”偷走”了一部分有效空间,如果上层不知道,就会触发分片或丢包。

用 tcpdump 观察封装结构

tcpdump 可以直接看到各层头部:

# 抓包并显示各层头部(-e 显示链路层,-vv 详细显示 IP 和 TCP 头部字段)
tcpdump -i eth0 -nn -e -vv -c 5 port 80

# 输出示例(一个 TCP SYN 包):
# 10:23:45.123456 aa:bb:cc:dd:ee:ff > 11:22:33:44:55:66, ethertype IPv4 (0x0800), length 74:
#   (tos 0x0, ttl 64, id 12345, offset 0, flags [DF], proto TCP (6), length 60)
#   10.0.0.100.55432 > 93.184.216.34.80: Flags [S], cksum 0xabcd (correct),
#   seq 1234567890, win 64240, options [mss 1460,sackOK,TS val 123456 ecr 0,
#   nop,wscale 7], length 0

这段输出包含了三层信息: - 链路层:源 MAC → 目标 MAC,ethertype 0x0800(IPv4) - 网络层:TTL 64,flags [DF](不允许分片),proto TCP (6) - 传输层:Flags [S](SYN),MSS 1460,Window Scale 7(窗口乘数 128)

每一层都像一个”信封”:链路层写了”从这个 MAC 到那个 MAC”,网络层写了”从这个 IP 到那个 IP,最多经过 64 个路由器”,传输层写了”从这个端口到那个端口,这是连接建立的第一个包”。

四、分层抽象的泄漏

分层的核心承诺是:上层不需要关心下层的实现细节。 HTTP 不需要知道 TCP 怎么重传,TCP 不需要知道 IP 怎么路由,IP 不需要知道以太网怎么发帧。

但在生产环境中,这个承诺经常被打破。Joel Spolsky 在 2002 年提出”抽象泄漏定律”(The Law of Leaky Abstractions):所有非平凡的抽象在某种程度上都是泄漏的。网络分层是这条定律最典型的例证之一。

以下是五种最常见的抽象泄漏,以及每一种的排查方法。

泄漏 1:TCP 保证可靠传输,但不保证延迟

TCP 的重传机制保证数据最终会到达对端。但重传需要等待 RTO(Retransmission Timeout),Linux 的初始 RTO 是 1 秒(由 net.ipv4.tcp_syn_retries 和初始 RTO 计算决定,RFC 6298 建议初始值 1 秒)。首次重传等 1 秒,如果还没收到 ACK,第二次等 2 秒(指数退避),第三次等 4 秒,第四次 8 秒……

对应用层来说,你调用 send() 把数据交给 TCP,TCP 说”好的收到了”。但这个”收到了”只是放进了发送缓冲区,不代表对端已经收到。如果底层丢包,TCP 会重传,你的应用可能会感受到突然的延迟毛刺——但应用层的 API 不会告诉你”刚才重传了三次”。

实际影响有多大? 假设丢包率 0.1%(一个很常见的值),TCP 的单次重传延迟至少增加一个 RTO(通常 200ms-1s)。如果你的服务有 10 个串行 TCP 调用(比如一个请求链路涉及 10 个微服务),每个调用触发重传的概率约 0.1%,那整条链路至少触发一次重传的概率约 1%。也就是 P99 延迟会受到 TCP 重传的显著影响。

# 查看 TCP 重传相关指标
ss -ti dst :8080
# 输出中关注:
# rto:204 rtt:1.5/0.5 ... retrans:0/3
# rto = 当前重传超时(ms)
# rtt = 平滑 RTT / RTT 偏差(ms)
# retrans = 当前未确认重传 / 累计重传次数

# 查看全局重传统计
cat /proc/net/snmp | grep Tcp | tail -1 | awk '{print "RetransSegs:", $13, "OutSegs:", $12}'
# RetransSegs / OutSegs = 重传率
# 经验值:< 0.01% 正常,0.01%-0.1% 需要关注,> 0.1% 有明确问题

泄漏 2:IP 路由对应用透明,但路径变化影响性能

应用层不需要知道数据包走了哪条路。但如果 BGP 路由收敛导致路径切换,新路径的延迟可能比旧路径高很多。应用层看到的就是”延迟突然变大了”,但不知道原因在网络层。

一个真实场景:你的服务部署在北京机房,用户请求通过 CDN 回源到你的服务器。某天凌晨运营商做路由调整,CDN 边缘节点到你机房的路径从”北京直连”变成”绕道上海再回北京”。RTT 从 5ms 变成 30ms,你的所有 API 延迟增加 25ms。应用层指标看不出任何代码问题——这是一个纯网络层的变化。

# mtr 持续监测路径上每一跳的延迟和丢包率
mtr --report-cycles 60 目标IP
# 连续运行 60 轮探测,输出每一跳的统计信息

# 如果你怀疑路由变化导致延迟增加,对比两个时间点的 traceroute 结果:
traceroute -I 目标IP > /tmp/trace_before.txt
# ... 等问题复现 ...
traceroute -I 目标IP > /tmp/trace_after.txt
diff /tmp/trace_before.txt /tmp/trace_after.txt

泄漏 3:MTU 对传输层透明,但影响吞吐量

前面讲过 PMTUD 黑洞的问题。TCP 以为自己可以发 MSS=1460 字节的段,但路径上某个设备的 MTU 只有 1400(比如 VXLAN 隧道),IP 层发现包超过 MTU 且设置了 DF 位,就丢弃并发回 ICMP “Fragmentation Needed”。但如果中间防火墙屏蔽了 ICMP,TCP 永远收不到这个通知——包发出去了,ACK 永远不来,只能超时重传。

更隐蔽的情况是:MTU 问题只影响大包。如果你的服务大部分是小请求(几百字节),可能一切正常;但偶尔有一个大响应(比如返回一个 1MB 的 JSON),TCP 会把它切成多个 MSS 大小的段来发送——这些段每一个都会触发 MTU 问题。表现为”偶尔超时,而且只有大响应超时”。

排查关键:如果你看到只有大包丢失、小包正常,首先怀疑 MTU 问题。

# 测试路径 MTU(逐步减小 -s 的值直到 ping 通)
for size in 1472 1450 1400 1350 1300; do
    echo -n "Size $size: "
    ping -M do -s $size -c 1 -W 2 目标IP 2>&1 | grep -oE "(bytes from|Frag needed)"
done

泄漏 4:Nagle 算法与 Delayed ACK 的交互

这是传输层内部的一个经典抽象泄漏。Nagle 算法(RFC 896)的目的是合并小包——如果已经有未确认的数据在路上,就把后续小数据攒起来,等收到 ACK 后一起发。Delayed ACK(RFC 1122)的目的是减少纯 ACK 包的数量——收到数据后不立即回 ACK,等 40ms 看看有没有数据可以捎带。

两个优化各自都是合理的,但组合在一起会导致最多 40ms 的额外延迟:

时间线:

发送端                              接收端
  │                                    │
  │──── 数据包 1(小包)──────────────→│
  │                                    │ ← 收到数据包 1
  │                                    │ ← Delayed ACK:等 40ms
  │ ← Nagle:有未确认数据,           │
  │   攒着数据包 2 不发               │
  │                                    │
  │        (双方互相等待 40ms)        │
  │                                    │
  │←────────── ACK ───────────────────│ ← 40ms 到期,发送 ACK
  │                                    │
  │──── 数据包 2 ──────────────────→│ ← 收到 ACK 后才发送

应用层只知道”小消息发得慢”,不知道是传输层两个优化策略互相等待导致的。

解决方法:对延迟敏感的应用设置 TCP_NODELAY 关闭 Nagle 算法。几乎所有 RPC 框架(gRPC、Thrift)和数据库客户端驱动都默认开启 TCP_NODELAY

// C 语言
int flag = 1;
setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag));
// Go 语言(net.TCPConn 默认就开启了 TCP_NODELAY)
conn.SetNoDelay(true)
# Python
import socket
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)

注意:TCP_NODELAY 会增加小包数量,理论上增加网络开销。但在现代网络中,带宽充裕,40ms 的延迟远比多发几个小包的开销严重。除了极少数场景(比如通过低带宽卫星链路传输大量小消息),都应该开启 TCP_NODELAY

泄漏 5:DNS 缓存对应用透明,但缓存失效影响可用性

应用层调用 getaddrinfo() 做域名解析,以为这是一个瞬时操作。但 DNS 解析涉及多层缓存,任何一层出问题都会影响应用:

应用进程的 DNS 解析路径:

应用代码
  └→ glibc getaddrinfo()
      └→ nsswitch.conf 决定解析顺序
          ├→ /etc/hosts(本地文件,无缓存问题)
          └→ DNS resolver(/etc/resolv.conf 中的 nameserver)
              └→ 本地 DNS 缓存(systemd-resolved / dnsmasq)
                  └→ 递归 DNS 服务器(ISP / 8.8.8.8 / 114.114.114.114)
                      └→ 权威 DNS 服务器

每一层都有缓存和 TTL。常见问题包括:

  1. DNS TTL 过长:你的服务通过域名连接另一个服务,DNS 的 TTL 是 600 秒(10 分钟)。对端 IP 变了(比如做了扩缩容或故障切换),你的服务在 TTL 过期前一直连接旧 IP——请求失败 10 分钟。

  2. DNS 服务器故障:如果 /etc/resolv.conf 中只配了一个 nameserver,而那个服务器挂了,所有 DNS 解析都会超时。glibc 的默认 DNS 超时是 5 秒,重试 2 次,每次超时翻倍——一次解析最多等 15 秒。

  3. glibc DNS 无缓存:glibc 的 getaddrinfo() 本身不做缓存。如果没有本地 DNS 缓存(systemd-resolved 等),每次调用都会发 DNS 查询。高 QPS 的服务可能把 DNS 服务器打满。

# 查看当前使用的 DNS 服务器
cat /etc/resolv.conf

# 测试 DNS 解析耗时
time dig example.com

# 查看 systemd-resolved 的缓存统计
resolvectl statistics

# 清除本地 DNS 缓存
resolvectl flush-caches

五、分层排查的实战框架

把前面的内容整合成一个可操作的排查框架。当你面对”网络好像有问题”的场景时,按这个流程走。

排查策略:自上而下

一般来说,排查网络问题应该从应用层开始,逐层向下。原因很简单:大部分”网络问题”其实不是网络的问题,而是应用层的问题(配置错误、超时设置不合理、连接泄漏等)。如果你一上来就抓包,大概率是在浪费时间。

反过来,如果应用层确认没问题,再往下查传输层、网络层,最后才考虑链路层。

第一步:应用层快速定位

# 1. HTTP 各阶段耗时(一条命令定位延迟集中在哪个阶段)
curl -w "\nDNS:%{time_namelookup} TCP:%{time_connect} TLS:%{time_appconnect} TTFB:%{time_starttransfer} Total:%{time_total}\n" \
  -o /dev/null -s https://目标

# 解读:
# DNS 阶段 > 100ms → DNS 问题,查 dig
# TCP - DNS > 100ms → TCP 握手慢,查 ss / 路由
# TLS - TCP > 200ms → TLS 握手慢,查证书链 / 密码套件
# TTFB - TLS > 500ms → 服务端处理慢,查应用日志
# Total - TTFB 很大 → 传输慢,查带宽 / 拥塞

第二步:传输层检查

# 2. TCP 连接状态和重传(单行搞定)
ss -ti dst 目标IP:端口
# 看 retrans(重传)、rto(重传超时)、cwnd(拥塞窗口)

# 快速统计连接状态分布
ss -tan | awk 'NR>1{print $1}' | sort | uniq -c | sort -rn
# 异常信号:大量 TIME_WAIT(> 10000)或 CLOSE_WAIT(> 100)

# 查看监听 socket 的队列溢出
ss -ltn
# Recv-Q > 0 表示 accept 队列有积压
# 如果 Recv-Q 持续接近 Send-Q(即 backlog),说明应用 accept 太慢

第三步:网络层检查

# 3. 路径延迟和丢包
mtr --report --report-cycles 30 目标IP
# 逐跳检查 Loss% 和 Avg 延迟
# 注意:中间跳丢包但目标不丢包 → 路由器 ICMP 限速,通常不是问题
# 中间跳开始丢包且持续到目标 → 该跳确实有问题

# 检查路由是否正确
ip route get 目标IP

第四步:链路层检查

# 4. 网卡错误统计(通常最后才查)
ethtool -S eth0 | grep -iE "error|drop|crc|collision|fifo"
# rx_errors > 0 → 入站帧有问题(线缆/网卡/交换机端口)
# tx_errors > 0 → 出站帧有问题
# rx_dropped > 0 → 内核网络栈来不及处理,丢弃了帧

第五步:抓包确认

如果前面的步骤定位到了怀疑的层次但还不确定根因,用 tcpdump 抓包做最终确认:

# 抓特定端口的包,保存到文件供 Wireshark 分析
# -s 0 捕获完整包、-c 限制包数量防止文件过大
tcpdump -i eth0 -nn -s 0 -w /tmp/capture.pcap 'port 443' -c 10000

# 轻量级快速查看(不保存文件)
tcpdump -i eth0 -nn -c 20 'host 目标IP and port 443'

排查决策树

"网络好像有问题"
    │
    ├→ ping 目标通吗?
    │   ├→ 不通 → traceroute 看哪一跳断了
    │   │        → 检查防火墙规则(iptables -L / nft list ruleset)
    │   └→ 通 → 不是连通性问题,继续
    │
    ├→ 是延迟问题?
    │   ├→ curl -w 分解延迟阶段
    │   │   ├→ DNS 慢 → dig +trace 追踪解析链路
    │   │   ├→ TCP 慢 → ss -ti 查 rtt / retrans
    │   │   ├→ TLS 慢 → openssl s_client 查证书链
    │   │   └→ TTFB 慢 → 服务端问题,不是网络
    │   └→ mtr 查路径每一跳延迟
    │
    ├→ 是丢包/超时问题?
    │   ├→ 只有大包超时 → MTU 问题,ping -M do -s 测路径 MTU
    │   ├→ 大小包都丢 → mtr 查路径丢包
    │   └→ 间歇性丢包 → tcpdump 抓包分析 TCP 重传模式
    │
    └→ 是吞吐量问题?
        ├→ ss -ti 查 cwnd / rwnd
        ├→ 窗口太小 → 检查 sysctl 缓冲区设置
        └→ cwnd 上不去 → 可能是拥塞控制问题,查丢包率

六、分层模型的边界与局限

分层模型是一个强大的思维工具,但它有几个固有的局限。理解这些局限,才能知道什么时候分层模型帮不了你。

跨层优化越来越普遍

现代协议设计的一个明确趋势是有意打破分层来换取性能。

QUIC 是最典型的例子。传统的 HTTPS 需要 TCP 三次握手(1 RTT)+ TLS 1.3 握手(1 RTT)= 2 RTT 才能开始传输数据。QUIC 把传输层(可靠传输、拥塞控制、流多路复用)和加密层(TLS 1.3)融合在一起,只需 1 RTT(甚至 0-RTT 恢复连接)就能开始传输。代价是:QUIC 无法使用现有的 L4 负载均衡器(因为负载均衡器不理解 QUIC 的连接 ID 机制),中间网络设备无法观察到 QUIC 的传输层行为(因为传输头部是加密的)。

XDP 和 eBPF 让应用逻辑直接在网卡驱动层执行,跳过了整个内核协议栈。Facebook 的 Katran(基于 XDP 的 L4 负载均衡器)处理每秒数百万个包,延迟只有微秒级——如果走完整的 TCP/IP 协议栈,延迟会高一个数量级。代价是:你需要用 eBPF 的受限编程模型来写逻辑,且只能处理比较简单的包转发决策。

io_uring 的网络零拷贝 让数据直接从用户空间缓冲区传输到网卡的 DMA 缓冲区,跳过了内核的 socket 缓冲区拷贝。代价是:编程复杂度更高,且不是所有场景都能从中受益(小包场景中拷贝开销本身就很小)。

中间设备打破分层假设

理论上,L3 设备(路由器)只看 IP 头部,L4 设备只看到传输层端口。但实际的网络设备经常跨层操作:

理解中间设备的跨层行为对排查问题很重要。比如:你的服务突然收到大量 RST 包,应用日志只看到”连接被重置”。如果你只在传输层排查,可能找不到原因——实际原因可能是中间的 IDS(入侵检测系统)在 L7 做了内容检查,发现可疑流量后伪造了 RST 包来中断连接。

容器网络增加了隐藏层

在 Kubernetes 环境中,一个 Pod 发出的数据包可能经过以下路径:

Pod eth0 (veth pair) → cni0 bridge → iptables/nftables rules
  → 可能的 VXLAN/Geneve 封装 → 宿主机 eth0 → 物理网络
  → 对端宿主机 eth0 → VXLAN/Geneve 解封装
  → iptables/nftables rules → cni0 bridge → 目标 Pod eth0

这些”隐藏层”不在传统的四层模型中,但它们的行为(封装开销、SNAT/DNAT、iptables 规则链遍历)直接影响网络性能。

一个常见的容器网络问题:Pod 内的应用看到的 RTT 是 0.5ms,但宿主机上抓包看到的 RTT 是 0.1ms。多出来的 0.4ms 花在了 veth pair 的转发 + iptables 规则匹配上。如果 iptables 规则数量很多(比如有上千条 Service 规则),这个开销可能更大。

排查容器网络问题时,需要在 Pod 内部和宿主机上同时抓包,对比两处看到的包延迟差异,才能定位问题在容器网络的”隐藏层”还是在物理网络上。

# 在 Pod 内抓包
kubectl exec -it pod名 -- tcpdump -i eth0 -nn -c 20 port 80

# 在宿主机上抓包(找到 Pod 对应的 veth)
# 先找到 Pod 的 veth 设备名
ip link show | grep -A1 "veth"
# 在对应的 veth 上抓包
tcpdump -i vethXXXXXX -nn -c 20 port 80

七、结论

网络分层模型的工程价值归结为三点:

  1. 排查坐标系:出了问题,你知道该从哪一层开始看,每一层有什么工具可以用。本文给出了一套完整的四步排查框架(应用层 curl -w → 传输层 ss -ti → 网络层 mtr → 链路层 ethtool),覆盖了绝大多数线上网络问题的初步定位。

  2. 沟通语言:L4、L7 这些术语让工程师之间可以精确地描述问题所在的层次。当你说”这是一个 L4 的问题”,团队里的人立刻知道该看 TCP 连接状态而不是 HTTP 状态码。

  3. 理解代价:每一层的抽象都有泄漏的风险。TCP 保证可靠但不保证延迟,IP 路由对应用透明但路径变化影响性能,MTU 对传输层隐藏但影响吞吐量,DNS 缓存对应用隐藏但缓存失效影响可用性。知道这些泄漏发生在哪里,才能在问题出现时快速定位,而不是在层与层之间来回猜。

实际工程中不需要教条地遵守分层边界——QUIC 跨层设计是对的,eBPF 绕过协议栈也是对的。但你需要理解分层,才能理解这些设计为什么要”破坏”分层、破坏后带来了什么好处和什么新的复杂性。

分层模型也给本系列提供了一个清晰的组织框架:

后续文章将从链路层的以太网工程开始,逐层深入每一层的工程实践。


上一篇:(系列第一篇) 下一篇:以太网工程:帧结构、MTU 与 Jumbo Frame

参考资料

规范与标准

书籍

技术文章与工具文档

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-04-14 · network

【网络工程】ICMP 与网络诊断:ping 和 traceroute 的工程本质

ping 超时不代表'网络不通',traceroute 的星号不代表'那一跳有问题'。这篇文章拆解 ICMP 协议的工程本质——每种类型和代码的含义、ping 和 traceroute 的三种实现方式、ICMP 限速与防火墙行为对诊断的干扰,以及如何用 ICMP 构建系统化的网络诊断方法论。

2025-08-01 · network

【网络工程】DNS 故障排查实战:从超时到劫持

DNS 故障是最常见也最难排查的网络问题之一。本文系统性地覆盖 DNS 超时、NXDOMAIN、SERVFAIL、劫持四大故障类型的诊断方法,详解 dig、nslookup、drill 的高级用法,提供 DNS 故障的 SRE 应急手册和常见配置错误汇总。


By .