AI Infra 训练营
总览
  • Day 1 · 集群起步 + CNI
  • Day 2 · 控制面 + etcd
  • Day 3 · CRD + Operator + Webhook
  • Day 4 · 存储深度
  • Day 5 · 卷扩容 + 安全
  • Day 6 · 调度 + 可观测
  • Day 7 · Harbor + ArgoCD + Mesh
  • Day 8 · AI Infra
  • Day 9 · Triton + GPU
  • Day 10 · MIG + HPA + 量化
  • Day 11 · AI Agent 端到端
  • Day 12 · 灾备
  • Day 13 · Operator + 联邦 + Mesh + RAG
  • Day 14 · CKA / CKS + 总结
  • LLM 训练手册
  • RAG + Agent 手册
  • 推理优化手册
  • 上下文工程手册
  • Agent 开发手册
  • 面试深度复盘
  • 训练 v2 深度手册
  • 心智模型
  • 看懂命令输出
  • 容器网络底层
  • K8s 网络深入
  • DNS 全套
  • 故障排查方法论
  • 心智模型
  • 容器挂载完整指南
  • K8s Volumes 大全
  • PV/PVC/CSI 深入
  • NFS 深入
  • 分布式存储概览
  • 故障排查 runbook
HiHuo 主站
GitHub
总览
  • Day 1 · 集群起步 + CNI
  • Day 2 · 控制面 + etcd
  • Day 3 · CRD + Operator + Webhook
  • Day 4 · 存储深度
  • Day 5 · 卷扩容 + 安全
  • Day 6 · 调度 + 可观测
  • Day 7 · Harbor + ArgoCD + Mesh
  • Day 8 · AI Infra
  • Day 9 · Triton + GPU
  • Day 10 · MIG + HPA + 量化
  • Day 11 · AI Agent 端到端
  • Day 12 · 灾备
  • Day 13 · Operator + 联邦 + Mesh + RAG
  • Day 14 · CKA / CKS + 总结
  • LLM 训练手册
  • RAG + Agent 手册
  • 推理优化手册
  • 上下文工程手册
  • Agent 开发手册
  • 面试深度复盘
  • 训练 v2 深度手册
  • 心智模型
  • 看懂命令输出
  • 容器网络底层
  • K8s 网络深入
  • DNS 全套
  • 故障排查方法论
  • 心智模型
  • 容器挂载完整指南
  • K8s Volumes 大全
  • PV/PVC/CSI 深入
  • NFS 深入
  • 分布式存储概览
  • 故障排查 runbook
HiHuo 主站
GitHub
  • 网络专题

    • 网络心智模型:从一个包的"一生"讲起
    • 看懂网络命令输出 —— 把每一个数字讲清楚
    • 容器网络底层:netns / veth / bridge / VXLAN
    • K8s 网络深入:Service / NodePort / Ingress 流量端到端
    • DNS 深入:CoreDNS / resolv.conf / ndots / 解析坑
    • 生产网络故障排查方法论

网络心智模型:从一个包的"一生"讲起

不讲完整网络教科书。讲应用开发 / SRE 最需要的 30% 原理——学完后再看 ip route / iptables / tcpdump 输出,每个数字你都能告诉自己"它是啥、为啥这样"。

本篇配套使用:01-output-reading.md 看懂输出 / 02-namespaces.md 容器底层 / 05-troubleshooting.md 故障排查。

这篇要回答什么

基础
  1. 浏览器打开 https://example.com,到底发生了什么?
  2. ip route 输出每一行到底什么意思?
  3. 同网段为啥"自动通"、跨网段为啥要网关?
进阶
  1. NAT 是什么、SNAT / DNAT / MASQUERADE 三者怎么区分?
  2. 为什么 K8s ClusterIP 不能 ping 通、但 curl 通?
  3. CLOSE_WAIT 堆积是哪一端的 bug?
生产
  1. 跨节点 pod 偶发性丢包——从哪一层开始查?
  2. 应用一开始正常、跑半小时后慢——什么原因?
  3. 集群升级后部分节点 NotReady——网络层面查什么?

学完这篇,每一个问题你都有方向。


1. 一个包从浏览器到 example.com 的"一生"

完整流程(保存到脑子里):

sequenceDiagram
    autonumber
    participant U as 你 (192.168.1.5)
    participant DNS as DNS (8.8.8.8)
    participant ARP as ARP 模块
    participant GW as 家路由器 (192.168.1.1)<br>公网 IP: 1.2.3.4
    participant ISP as 运营商网关
    participant SRV as example.com<br>(93.184.215.14)

    Note over U: 浏览器: 我想访问 example.com
    U->>DNS: A 记录查询: example.com?
    DNS-->>U: 93.184.215.14
    Note over U: 路由决策: 93.184.215.14<br>不在我的子网 → 走默认网关
    U->>ARP: 192.168.1.1 的 MAC?
    ARP-->>U: aa:bb:cc:dd:ee:ff
    Note over U: 构造包: src=192.168.1.5<br>dst=93.184.215.14<br>L2: dst MAC = aa:bb:cc:..
    U->>GW: 发送
    Note over GW: SNAT: 改 src 为 1.2.3.4<br>记入 conntrack 表
    GW->>ISP: 转发
    ISP->>SRV: 经多跳路由
    SRV-->>ISP: 回包 src=93.184.. dst=1.2.3.4
    ISP-->>GW: 回包
    Note over GW: 查 conntrack:<br>1.2.3.4:54321 ↔ 192.168.1.5:54321
    Note over GW: 改 dst 回 192.168.1.5
    GW-->>U: 包到达

每一步都对应一个命令 + 一段配置。下面拆开每一步。

关键 take-away

包在你电脑这边:

  • IP 头不变(除非中间有 NAT 改写)
  • MAC 头每跳一次换一次(每个路由器重写)
  • 源 IP / 源 port 在 NAT 时被改写
  • conntrack 表记着"谁连出去过",回包能找路

这 4 句话理解了,下面所有内容都好懂。


2. MAC vs IP —— 最容易混的两个概念

MAC 地址(第 2 层 / 链路层 / L2)

  • 网卡硬件烧死的地址:aa:bb:cc:dd:ee:ff(48 位 / 12 hex)
  • 只在同一个广播域内有意义(一个交换机连的所有口、一个 VLAN)
  • 跨网段时被每跳路由器重写
$ ip link show eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq state UP mode DEFAULT
    link/ether aa:bb:cc:dd:ee:ff brd ff:ff:ff:ff:ff:ff
#               ^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^
#               (1) 本机 MAC      (2) 广播地址 (全 F)

字段意义:

标记含义
(1)你的网卡 MAC,IEEE OUI 厂商号 + 序列号
(2)这个网段的广播 MAC,发到这个 MAC = 整个 L2 域都收到

IP 地址(第 3 层 / 网络层 / L3)

  • 软件分配的逻辑地址:192.168.1.5、10.244.0.10
  • 跨网段有意义——全网唯一(或私网内唯一)
  • 路由就是基于 IP 决定的
$ ip addr show eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq state UP
    link/ether aa:bb:cc:dd:ee:ff brd ff:ff:ff:ff:ff:ff
    inet 192.168.1.5/24 brd 192.168.1.255 scope global dynamic noprefixroute eth0
#         ^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^ ^^^^^ ^^^^^^^ ^^^^^^^ ^^^^^^^^^^^^^^
#         (1)             (2)                (3)   (4)     (5)     (6)
       valid_lft 84321sec preferred_lft 84321sec
#      ^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^
#      (7)                (8)

逐字段解读:

标记含义
(1)IP/CIDR:本机 IP + 子网掩码 24 位
(2)广播地址:子网的"全 1"地址(192.168.1.255)
(3)scope:global = 跨子网可达;link = 仅本子网;host = 仅本机回环
(4)dynamic:DHCP 分配的(static 就是静态配的)
(5)noprefixroute:内核不自动加 prefix 路由(NetworkManager 风格)
(6)网卡名
(7)valid_lft:DHCP 租约总寿命(秒)
(8)preferred_lft:DHCP 优先使用期

ARP —— L2 和 L3 的桥

数据包要发出去时,应用知道目标 IP、网卡知道目标 MAC。谁来翻译 IP → MAC?

ARP(Address Resolution Protocol):

sequenceDiagram
    participant A as A: 192.168.1.5
    participant B as 全网段广播
    participant C as B: 192.168.1.10
    participant D as 其它机器

    Note over A: 要发包到 192.168.1.10<br>但只知道 IP、不知道 MAC
    A->>B: ARP Request:<br>"谁是 192.168.1.10?"<br>(广播 dst MAC=ff:ff:...)
    Note over D: 我不是、不响应
    C->>A: ARP Reply:<br>"我是 192.168.1.10<br>MAC=11:22:33:44:55:66"
    Note over A: 写入 ARP 缓存表
    A->>C: 数据包(知道 MAC 了)

看本机 ARP 表:

$ ip neigh
192.168.1.1 dev eth0 lladdr aa:bb:cc:dd:ee:00 REACHABLE
192.168.1.10 dev eth0 lladdr 11:22:33:44:55:66 STALE
192.168.1.99 dev eth0 INCOMPLETE
#                          ^^^^^^^^^^^
#                          (1) (2)

字段:

标记含义状态机
REACHABLE最近通信验证过、缓存有效健康
STALE缓存还在但旧、下次用时验证健康
DELAY等几秒验证短暂
PROBE正在主动验证短暂
INCOMPLETE发 ARP request 没回应网络问题!
FAILED多次失败目标不通

关键关系:包"穿过"网络的样子

应用层    "去访问 example.com"           ← 域名
DNS              ↓
                 ↓ 解析
                 ↓
网络层    "去访问 93.184.215.14"          ← IP 不变,端到端
路由             ↓ 查路由表
                 ↓ 下一跳: 192.168.1.1
ARP              ↓ 192.168.1.1 的 MAC?
                 ↓
链路层    包发到 aa:bb:cc:...              ← MAC 每跳一次重写
物理层    电信号 / 光信号

反直觉的关键点

每一跳路由器都改 MAC、不改 IP(除非中间有 NAT)。源/目的 IP 是"端到端"的、源/目的 MAC 只在"这一跳"有意义。

例子:你 → 家路由器 → 运营商 → ... → example.com

节点src IPdst IPsrc MACdst MAC
你出发时192.168.1.593.184...你的 MAC路由器 MAC
路由器转发后192.168.1.5 (没改)93.184...路由器 WAN MACISP 设备 MAC
运营商转发后192.168.1.5 (没改)93.184...ISP MAC下一跳 MAC

但 NAT 后 src IP 会被改成路由器的公网 IP。这是 NAT 的特殊行为,不是路由的常态。

进阶:Proxy ARP / Gratuitous ARP

Proxy ARP:路由器代替远程主机回 ARP,让两个不同子网的机器以为"同子网"。K8s 部分 CNI(如 calico)默认开启。

sysctl net.ipv4.conf.all.proxy_arp
# net.ipv4.conf.all.proxy_arp = 1

Gratuitous ARP:主动广播自己的 IP↔MAC 映射,常用于:

  • VIP 漂移(kube-vip / keepalived 切换 leader 时,新 leader 主动广播让网络更新)
  • IP 冲突检测
  • 网卡新接入
arping -A -I eth0 192.168.1.5    # 主动通告

排错信号:节点切换后流量没切换 = gratuitous ARP 没发 / 被丢。


3. 子网 / CIDR / 网关 —— 二进制的真相

CIDR 表示法

192.168.1.5/24
└──┬──┘ │ └┬┘
   │    │  └── 子网掩码 = 高 24 位
   │    │
   │    └── 主机 IP
   │
   └── IPv4 地址

二进制:

192.168.1.5  = 11000000.10101000.00000001.00000101
掩码 /24     = 11111111.11111111.11111111.00000000
                                       ↑ 这里分界
                
AND 操作得网络号:
192.168.1.0  = 11000000.10101000.00000001.00000000

子网范围: 192.168.1.0 ~ 192.168.1.255
   - 192.168.1.0  = 网络号(不能给设备用)
   - 192.168.1.255 = 广播地址(不能给设备用)
   - 192.168.1.1 ~ 192.168.1.254  → 254 个可用 IP

常见 CIDR 速查

CIDR总 IP可用主机典型用途
/3210单 IP / loopback
/3122点对点(RFC 3021)
/3042点对点链路
/2986小段
/281614小段
/24256254家庭 / K8s pod 单节点
/2210241022楼层网段
/1665,53665,534企业 / K8s 集群 service
/12~100 万~100 万私网 10.0.0.0/8 切段
/81670 万1670 万私网整段
/0全部-"0.0.0.0/0" 默认路由

私网网段(RFC 1918)

10.0.0.0/8         10.x.x.x        (1670 万 IP)  - 数据中心 / K8s
172.16.0.0/12      172.16-31.x.x   (100 万 IP)   - 中型企业
192.168.0.0/16     192.168.x.x     (6.5 万 IP)   - 家庭 / 小办公
100.64.0.0/10      100.64-127.x.x  (400 万 IP)   - **CGNAT (运营商级 NAT)**
169.254.0.0/16     169.254.x.x     - link-local (无 DHCP 时 / AWS metadata 169.254.169.254)

K8s 默认:

段默认 CIDR
Pod CIDR10.244.0.0/16(flannel)/ 10.0.0.0/16(cilium)
Service CIDR10.96.0.0/12(kubeadm 默认)
Cluster DNS10.96.0.10

网关:为什么"同网段不需要、跨网段需要"

设你 = 192.168.1.5/24。对端:

对端 IP是否同子网怎么发包
192.168.1.10✅ 同 192.168.1.0/24直接 ARP 拿 MAC、L2 送达
192.168.2.10❌ 不同子网找网关、网关转发
8.8.8.8❌ 公网找网关、一路路由

判断逻辑(内核做的):

对端 IP AND 我的掩码 == 我的网络号 ?
192.168.1.10 AND /24 = 192.168.1.0 ✅  → 同子网
192.168.2.10 AND /24 = 192.168.2.0 ❌  → 跨子网

网关必须和你同子网(否则你怎么直连它)。所以家里:

你 192.168.1.5/24
网关 192.168.1.1/24      ← 必须同段

反面教材

错误:以为 IP 离得近就同子网

你 = 10.244.0.5/32
对端 = 10.244.0.6/32

ping 10.244.0.6
# 不通!

/32 = 单 IP 子网,子网里只有"自己"。对端虽然 IP 看起来近,对内核来说 = 跨网段、要走路由。

K8s pod 默认用 /32 配置——pod 间通信全靠节点路由表 / 一个特殊的"node bridge 路由",不是同子网直连。详见 02-namespaces.md。


4. 路由表 —— 包"往哪走"的决策

路由表的工作就是回答:"给定一个目的 IP、下一跳走谁、走哪个网卡?"

RIB vs FIB(进阶但重要)

概念含义
RIB (Routing Information Base)控制面的所有候选路由(BGP / OSPF / static / direct)
FIB (Forwarding Information Base)数据面实际使用的路由(RIB 选优后下发)

对单机 Linux:

ip route                              # 默认看 main 表(接近 FIB)
ip route show table all               # 看所有路由表
ip rule show                          # 看路由规则(决定走哪个表)

多路由表场景(策略路由):

$ ip rule show
0:	from all lookup local
32766:	from all lookup main
32767:	from all lookup default

local 表是内核自动维护的(本机各 IP 的回环路由)。main 是你平时改的。default 是兜底。

看路由表 + 逐行解读

$ ip route
default via 192.168.1.1 dev eth0 proto dhcp src 192.168.1.5 metric 100
#       ↑      ↑           ↑       ↑         ↑              ↑
#      (1)    (2)         (3)     (4)       (5)            (6)
192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.5
#              ↑       ↑              ↑          ↑
#             (7)     (8)            (9)        (10)
10.244.0.0/24 via 10.0.24.31 dev eth0
10.244.1.0/24 via 10.0.24.32 dev eth0

字段解读:

标记含义
(1)默认路由(匹配所有不被前面规则匹配的目的 IP)
(2)via = "通过下一跳"
(3)走 eth0 这个网卡
(4)这条路由是 DHCP 学到的(其它取值:static / kernel / boot / dhcp / bgp)
(5)用这个 IP 作为发包时的源 IP
(6)metric:多条路由竞争时的优先级(越小越优先)
(7)这是个直连路由(没 via)—— 192.168.1.0/24 这个网段是直连
(8)scope link = 二层直连可达(不通过路由器)
(9)proto kernel = 内核自动加的(IP 配上 /24 时自动加)
(10)同 (5)

最长前缀匹配(路由的根本规则)

多条路由都匹配时,前缀长度最长的赢。

例子:

$ ip route
default via 192.168.1.1                # /0 → 匹配 0 位
10.0.0.0/8 via 10.1.1.1                # /8 → 匹配 8 位
10.244.0.0/16 via 10.1.2.1             # /16 → 匹配 16 位
10.244.1.0/24 via 10.1.3.1             # /24 → 匹配 24 位

$ ip route get 10.244.1.5
10.244.1.5 via 10.1.3.1 dev eth0       # ← /24 最长前缀,赢

10.244.1.5 同时匹配 default、/8、/16、/24 四条,取最长前缀的那条。

flowchart TD
    Start[目的 IP: 10.244.1.5] --> Match{有匹配的路由吗?}
    Match -->|匹配多条| Pick[挑前缀最长的]
    Match -->|只一条| Use[使用它]
    Match -->|0 条| Default[使用 default 路由]
    Pick --> Use
    Use --> Decision{需要 ARP?}
    Decision -->|同子网| ARP[ARP 拿 MAC]
    Decision -->|跨子网| Gateway[查下一跳的 MAC]
    Gateway --> ARP
    ARP --> Send[发包]

ip route get —— 必学排错命令

$ ip route get 8.8.8.8
8.8.8.8 via 192.168.1.1 dev eth0 src 192.168.1.5 uid 0
#       ↑          ↑         ↑      ↑               ↑
#       下一跳    出网卡    出网卡   实际用的源 IP   发包用户的 UID

怀疑路由错就跑这条——给你"如果发到这个 IP、会怎么走"的权威答案。

$ ip route get 10.244.1.5
10.244.1.5 via 10.0.24.32 dev eth0 src 10.0.24.28
# 路由器认为 10.244.1.5 在 worker 节点 m5 (10.0.24.32) 上

K8s 跨节点 pod 通信不通时,在节点上跑 ip route get ⟨pod-ip⟩ 立刻看出"包应该走哪个节点"——和实际跑的对照。

反面教材:以为路由不通 = ping 不通

$ ip route get 10.244.1.5
10.244.1.5 via 10.0.24.32 dev eth0    # 路由 OK
$ ping 10.244.1.5
# 不通

路由表说"该走 m5"≠ 包真能到 m5。可能:

  • 中间防火墙挡(节点之间安全组、iptables)
  • m5 上的 pod 已死(路由还在但 endpoint 没了)
  • CNI 异常

路由 OK 只是说"路径已知",不保证"链路通"。


5. NAT —— 让私网通公网(理解 K8s 出网必备)

NAT 的三种类型

graph LR
    subgraph SNAT[Source NAT / 出网场景]
        A1[私网 192.168.1.5] -->|改 src| B1[网关公网 1.2.3.4]
        B1 --> C1[公网]
    end

    subgraph DNAT[Destination NAT / 入网场景]
        A2[外部访问 公网:8080] -->|改 dst| B2[内网 10.0.0.5:80]
    end

    subgraph MASQ[MASQUERADE / 动态 SNAT]
        A3[多个内部客户端] -->|出口 IP 自动选| B3[公网]
    end

    style SNAT fill:#e1f5ff
    style DNAT fill:#ffe1f5
    style MASQ fill:#f5ffe1

SNAT (Source NAT) —— 你家路由器在做的事

你 → 路由器 → 公网

原始包:    src=192.168.1.5:54321  dst=93.184.215.14:443
经路由器:  src=1.2.3.4:54321      dst=93.184.215.14:443
           ^^^^^^^^^^^^^^^^^
           src 被改成路由器公网 IP

回包:      src=93.184.215.14:443  dst=1.2.3.4:54321
路由器查 conntrack 表:"这个 54321 端口给的是 192.168.1.5"
改回:      src=93.184.215.14:443  dst=192.168.1.5:54321

DNAT (Destination NAT) —— 端口转发 / NodePort

外部 → 路由器:8080 → 内部:80

原始包: src=外部 IP  dst=路由器公网:8080
DNAT:   src=外部 IP  dst=内部 10.0.0.5:80
        ^^^^^^^^^^   ^^^^^^^^^^^^^^^^^^^
        没改         dst 改成内部 IP/port

K8s NodePort 就是 DNAT:

外部 → 节点 IP:30080 → DNAT → pod:8080

MASQUERADE —— 动态 SNAT(K8s 标配)

iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
# 所有从 eth0 出去的包、src 改成 eth0 当时的 IP

跟静态 SNAT 的区别:

静态 SNATMASQUERADE
配置指定改成哪个 IP动态选出口 IP
用途多 IP 选一个固定的出口 IP 可能变化(如 DHCP)
K8s 用例-默认

K8s pod 出公网用的就是 MASQUERADE。pod IP(10.244.x.x)→ 节点 eth0 IP → 公网。

conntrack —— NAT 的"记忆"

NAT 不是无状态的。每个连接都被 conntrack 记录,回包才能找到正确的客户端。

$ cat /proc/net/nf_conntrack | head -3
ipv4     2 tcp      6 86399 ESTABLISHED src=192.168.1.5 dst=93.184.215.14 sport=54321 dport=443 \
                              src=93.184.215.14 dst=1.2.3.4 sport=443 dport=54321 [ASSURED] mark=0 use=2
#                                                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
#                                                         (1) 正向 tuple        (2) 反向 tuple

# 用 conntrack 命令更友好
$ conntrack -L 2>/dev/null
tcp 6 86399 ESTABLISHED src=192.168.1.5 dst=93.184.215.14 sport=54321 dport=443 \
                       src=93.184.215.14 dst=1.2.3.4 sport=443 dport=54321 ...

每个 conntrack entry 含两个 tuple:

  • 正向:客户端 → 服务端的视角
  • 反向:服务端 → 客户端的视角

NAT 用正向 tuple 改包,回包来时用反向 tuple 查 → 还原回客户端 IP。

conntrack 表满 —— 生产事故 #1

# dmesg 出现:
nf_conntrack: nf_conntrack: table full, dropping packet

# 看当前
$ sysctl net.netfilter.nf_conntrack_count
net.netfilter.nf_conntrack_count = 524286
$ sysctl net.netfilter.nf_conntrack_max
net.netfilter.nf_conntrack_max = 524288
#                                ^^^^^^^^
#                                 满了

症状:业务偶发性超时、丢包、连接被拒。90% 应用 SRE 第一次见这个的反应是"是不是网络问题"——是网络问题,但根因是 conntrack 表。

修:

sysctl -w net.netfilter.nf_conntrack_max=1048576

# 持久化(详见 commands/sysctl.md)
echo 'net.netfilter.nf_conntrack_max = 1048576' >> /etc/sysctl.d/99-conntrack.conf
sysctl --system

或者降低 TIME_WAIT 类 entry 超时:

sysctl -w net.netfilter.nf_conntrack_tcp_timeout_time_wait=10
# 默认 120 秒、调小让 entry 早点回收

NAT 的副作用 —— 不是免费的

  1. 额外 CPU 开销:每个包过 NAT 表都要查 conntrack
  2. 状态有限:conntrack 表大小是上限
  3. 某些协议跑不动:FTP / SIP 等内嵌 IP 的协议 NAT 透明性差(要 helper / ALG)
  4. 端到端原则破坏:客户端的真实 IP 在 NAT 后不可见(要 X-Forwarded-For 等头)

K8s service mesh 的"绕开 NAT"(直接 pod-to-pod 通信)就是为了避开这些。


6. TCP 三次握手 + 状态机(看懂 ss 输出)

三次握手(为什么是 3 次)

sequenceDiagram
    autonumber
    participant C as Client
    participant S as Server

    Note over S: socket() + bind() + listen()<br>状态: LISTEN
    Note over C: socket() + connect()
    C->>S: SYN seq=x
    Note over C: 状态: SYN-SENT
    Note over S: 状态: SYN-RECV<br>(写入半连接队列)
    S->>C: SYN-ACK seq=y, ack=x+1
    Note over C: 状态: ESTABLISHED
    C->>S: ACK ack=y+1
    Note over S: 半连接队列出 → 全连接队列<br>accept() 取出<br>状态: ESTABLISHED

为什么必须 3 次:

  • 1 次:client 不能确认 server 收到、server 不能确认 client 能收
  • 2 次:server 不能确认 client 是否真要建连(旧 SYN 可能误触)
  • 3 次:双向都确认对方"能收 + 能发"

状态机(所有 ss 输出的 State 列)

stateDiagram-v2
    [*] --> CLOSED
    CLOSED --> LISTEN: listen()
    LISTEN --> SYN_RECV: 收 SYN, 发 SYN-ACK
    SYN_RECV --> ESTABLISHED: 收 ACK
    CLOSED --> SYN_SENT: connect()<br>发 SYN
    SYN_SENT --> ESTABLISHED: 收 SYN-ACK<br>发 ACK
    ESTABLISHED --> FIN_WAIT_1: close()<br>发 FIN
    FIN_WAIT_1 --> FIN_WAIT_2: 收 ACK
    FIN_WAIT_2 --> TIME_WAIT: 收 FIN
    TIME_WAIT --> CLOSED: 等 2*MSL
    ESTABLISHED --> CLOSE_WAIT: 对端 close()<br>收 FIN
    CLOSE_WAIT --> LAST_ACK: 本端 close()<br>发 FIN
    LAST_ACK --> CLOSED: 收 ACK

状态详解 + 生产意义

$ ss -tn
State        Recv-Q  Send-Q  Local Address:Port   Peer Address:Port
ESTAB        0       0       10.0.24.28:43210     10.0.24.29:6443
TIME-WAIT    0       0       10.0.24.28:42345     10.0.24.29:6443
CLOSE-WAIT   100     0       10.0.24.28:54321     10.0.24.30:8080
SYN-SENT     0       1       10.0.24.28:60000     10.0.24.99:443
状态意义生产看到时排查
LISTENserver 在等正常ss -lntp
ESTAB通信中正常-
TIME-WAIT主动关闭方等 2*MSL(60-120s)几千个正常 / 几万要查:短连接太多 / 没用连接池net.ipv4.tcp_tw_reuse=1 / tw_recycle(4.12+ 已删)
CLOSE-WAIT对端 FIN 了、本端没 close应用 bug:没正确 close() 或 defer close() 漏看应用代码 / 重启临时缓解
SYN-SENTclient 等 SYN-ACK目标没监听 / 防火墙挡 / SYN floodnc -zv target port / tcpdump
SYN-RECVserver 等 ACK短暂 / 堆积 = SYN flood 攻击sysctl net.ipv4.tcp_syncookies=1
FIN-WAIT-1/2关闭中短暂-
LAST-ACK等最后 ACK短暂-

TIME_WAIT 的真相

为什么要等 2*MSL (120 秒)?

1. **保证最后一个 ACK 真的到了对端**
   如果丢了、对端会重传 FIN,需要本端还能回 ACK

2. **让网络中"漂着"的旧包消失**
   防止新连接(用同样的 4-tuple)收到旧连接的延迟包

短连接场景(K8s pod 频繁起停 + HTTP 不复用)会让 TIME_WAIT 堆积:

$ ss -tn state time-wait | wc -l
50000

修法(按效果排序):

  1. 应用层用长连接 / 连接池(根本)
  2. net.ipv4.tcp_tw_reuse = 1(让 TIME_WAIT 端口可被新连接复用,安全)
  3. tcp_tw_recycle = 1(已被 4.12+ 内核删除——NAT 场景不安全)
  4. net.ipv4.ip_local_port_range = 1024 65535(扩大端口池)

CLOSE_WAIT 堆积 —— 应用 bug

$ ss -tn state close-wait | wc -l
500

这是应用代码 bug——server 端收到 FIN(client 想关连接),本端代码没调用 close() 或 socket.close() 漏写。

经典场景:

# ❌ 异常路径漏 close
try:
    data = sock.recv(1024)
    process(data)        # 这里抛异常
    sock.close()         # 永远到不了
except Exception:
    pass                 # 静默吞异常、socket 没关
# → CLOSE_WAIT 堆积

# ✅ defer / finally / context manager
with socket.socket() as sock:    # Python 自动管
    ...
// ❌ 漏 defer
conn, _ := net.Dial(...)
process(conn)
conn.Close()             // 上面 panic 会跳过这里

// ✅ defer
conn, _ := net.Dial(...)
defer conn.Close()
process(conn)

Recv-Q / Send-Q 列

State        Recv-Q  Send-Q  Local:Port     Peer:Port
LISTEN       0       128     0.0.0.0:80
ESTAB        500     1000    1.2.3.4:80     5.6.7.8:54321
状态Recv-QSend-Q
LISTEN半连接队列当前长度backlog 上限
ESTAB收到但应用没 read 的字节数发了但对端没 ACK 的字节数

ESTAB 时 Recv-Q 持续大:应用消费慢、reader 卡了。Send-Q 大:网络丢包重传 / 对端处理慢。

LISTEN 状态下 Recv-Q 接近 Send-Q 上限:accept() 跟不上、半连接快堆满 → server 处理能力不足。


7. K8s Pod 网络底层 —— 看清楚才能排错

单节点视角

flowchart TB
    subgraph PodA[Pod A: netns A]
        EthA[eth0<br>10.244.0.5/32]
    end
    subgraph PodB[Pod B: netns B]
        EthB[eth0<br>10.244.0.6/32]
    end
    subgraph HostNs[节点 host netns]
        VethA[vethXXXX<br>peer]
        VethB[vethYYYY<br>peer]
        CNI[cni0 bridge<br>10.244.0.1/24]
        HostEth[eth0<br>10.0.24.28]
        Iptables[iptables NAT/Filter]
        RT[路由表]
    end

    EthA -.veth pair.-> VethA
    EthB -.veth pair.-> VethB
    VethA --> CNI
    VethB --> CNI
    CNI --> Iptables
    Iptables --> RT
    RT --> HostEth
    HostEth -->|MASQUERADE| Extern[外部]

每个组件:

  • netns:网络命名空间,隔离的 IP / 路由 / iptables / socket。Pod 看到的是自己的 netns。
  • veth pair:一对"虚拟网线"——一头在 pod netns、一头在 host netns。两头是连通的,写一头另一头收到。
  • cni0 / bridge:虚拟交换机,把所有同节点 pod 的 veth 头串起来 + 给 pod 一个 default gateway。
  • 节点 iptables:kube-proxy 生成的规则 + CNI 自己的规则。

跨节点视角(Overlay 模式)

sequenceDiagram
    participant PA as Pod A<br>(10.244.0.5)
    participant CN1 as 节点 m1<br>cni0
    participant E1 as m1 eth0<br>(10.0.24.28)
    participant E2 as m2 eth0<br>(10.0.24.29)
    participant CN2 as 节点 m2<br>cni0
    participant PB as Pod B<br>(10.244.1.6)

    PA->>CN1: 发包 src=10.244.0.5 dst=10.244.1.6
    Note over CN1: 查路由表:<br>10.244.1.0/24 走 m2 (10.0.24.29)
    CN1->>E1: 转发
    Note over E1: 封装 VXLAN:<br>外层 src=10.0.24.28 dst=10.0.24.29<br>内层 src=10.244.0.5 dst=10.244.1.6
    E1->>E2: UDP 4789<br>(VXLAN 标准端口)
    Note over E2: 解封装
    E2->>CN2: 内层包
    CN2->>PB: src=10.244.0.5 dst=10.244.1.6

VXLAN = pod 流量被包成 UDP 包、外层用节点 IP、内层是 pod IP。

Pod 的实际路由表

# 在某个 pod 里
$ ip route
default via 10.244.0.1 dev eth0
10.244.0.0/24 dev eth0 proto kernel scope link src 10.244.0.5

简单:所有非 10.244.0.0/24(自己同节点的 pod)的流量 → 走 default gateway 10.244.0.1(cni0 / bridge IP)。

# 在节点 host netns
$ ip route
default via 10.0.24.1 dev eth0
10.0.24.0/24 dev eth0 proto kernel scope link src 10.0.24.28
10.244.0.0/24 dev cni0 proto kernel scope link src 10.244.0.1   # 本节点 pod
10.244.1.0/24 via 10.0.24.32 dev eth0                           # m2 节点 pod
10.244.2.0/24 via 10.0.24.30 dev eth0                           # m3 节点 pod

每个 worker pod CIDR 一条路由、指向对应节点。这是 CNI controller 写进来的——节点加入 / 退出集群时自动更新。

看 pod IP 怎么走(排错神器)

# 在节点上
$ ip route get 10.244.1.5
10.244.1.5 via 10.0.24.32 dev eth0 src 10.0.24.28
# 解读: 这个 IP 应该经节点 m5 (10.0.24.32) 路由
# 如果 m5 上没有 pod 10.244.1.5 → 路由对、但 endpoint 错
# 如果路由不存在 → CNI / 集群路由同步问题

详细排查见 02-namespaces.md 和 05-troubleshooting.md。


8. Service 的本质 —— 它不是个进程

K8s 让你迷惑的点:Service 不监听任何端口。它只是一组 iptables 规则 + DNS 记录。

ClusterIP 流量怎么走(详解)

sequenceDiagram
    autonumber
    participant App as Pod A 应用
    participant Resolver as Pod 内 DNS
    participant CoreDNS
    participant Kernel as Pod 内 iptables / kernel
    participant Backend as Pod B (10.244.1.5)

    App->>Resolver: getaddrinfo("my-svc")
    Resolver->>CoreDNS: my-svc.default.svc.cluster.local
    CoreDNS-->>Resolver: 10.96.1.5 (ClusterIP)
    Resolver-->>App: 10.96.1.5
    App->>Kernel: connect(10.96.1.5:80)
    Note over Kernel: 经 iptables KUBE-SERVICES 链<br>DNAT: 10.96.1.5:80 → 10.244.1.5:8080
    Kernel->>Backend: src=10.244.0.5 dst=10.244.1.5:8080
    Backend-->>App: 回包(DNAT 反向)

10.96.1.5 没人监听——它只是 iptables DNAT 规则里的"标记"。

看实际生效的 iptables 规则:

$ iptables -t nat -L KUBE-SERVICES -n | grep my-svc
KUBE-SVC-XXXX  tcp  --  0.0.0.0/0   10.96.1.5   tcp dpt:80 /* default/my-svc */

$ iptables -t nat -L KUBE-SVC-XXXX -n
KUBE-SEP-AAA  /* probability 0.5 */
KUBE-SEP-BBB  /* */

$ iptables -t nat -L KUBE-SEP-AAA -n
DNAT  tcp  ...  to:10.244.1.5:8080

3 个 endpoint 就 3 条 KUBE-SEP 链、按 statistic probability 分流。

四种 Service 类型对比

类型暴露范围实现
ClusterIP集群内iptables DNAT 到 pod
NodePort节点 IP:30000-32767iptables DNAT 到 pod(每个节点都开)
LoadBalancer公网 LB IP云 LB → NodePort → DNAT
ExternalNameDNS CNAMECoreDNS 配置(不经 iptables)

反面教材:试图 ping ClusterIP

$ ping 10.96.1.5
PING 10.96.1.5: 56 data bytes
^C
--- 10.96.1.5 ping statistics ---
5 packets transmitted, 0 packets received, 100% packet loss

这是正常的。ClusterIP 没人响应 ICMP(没人监听任何东西、它是 iptables 规则)。

要测 Service 通不通:

$ nc -zv 10.96.1.5 80
Connection to 10.96.1.5 80 port [tcp/*] succeeded!

# 或者
$ curl http://10.96.1.5:80

这个误解让很多人 K8s 网络排错时走错方向。

Headless Service —— 例外

spec:
  clusterIP: None    # ← Headless
  selector: {...}

Headless Service 没有 ClusterIP——DNS 直接返回所有 endpoint pod IP(A 记录列表)。 适合需要"自己做负载均衡"的场景(StatefulSet / Cassandra / Kafka 客户端等)。

$ dig my-headless.default.svc.cluster.local +short
10.244.0.5
10.244.1.6
10.244.2.7

9. OSI 七层 —— 排错分层用

flowchart TD
    L7[L7 应用层<br>HTTP / DNS / SSH] --> L6[L6 表示层<br>TLS / 编码]
    L6 --> L5[L5 会话层<br>TCP session]
    L5 --> L4[L4 传输层<br>TCP / UDP / 端口]
    L4 --> L3[L3 网络层<br>IP / ICMP / 路由]
    L3 --> L2[L2 链路层<br>MAC / VLAN / ARP]
    L2 --> L1[L1 物理层<br>网线 / 光纤 / 信号]

每层对应的排错命令

OSI 层排错命令看啥
L7 应用curl -v, 应用日志HTTP 状态码 / 应用报错
L6 表示openssl s_clientTLS 握手 / 证书
L5/4 会话/传输ss -tn, nc -zvsocket 状态 / 端口连通
L3 网络ping, mtr, ip route, tracerouteIP 可达 / 路径 / 路由
L2 链路ip link, ip neigh, ethtool网卡 UP / ARP / link 状态
L1 物理ethtool eth0速率 / 双工 / 错误计数

包穿过每一层的样子

应用产生数据: "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"
              ↓
L6 加 TLS:    [TLS record header] + 加密数据
              ↓
L5/4 加 TCP:  [TCP header: src port, dst port, seq, ack] + [TLS payload]
              ↓
L3 加 IP:     [IP header: src IP, dst IP, TTL] + [TCP segment]
              ↓
L2 加 Eth:    [Ethernet: src MAC, dst MAC] + [IP packet] + [FCS 校验]
              ↓
L1:           010101010110...

每跳路由器剥到 L3、改 MAC、再封 L2 发。 每跳防火墙可以看到 L4(端口、状态)甚至 L7(HTTP / DNS 解析)。

排错从底层往上(反直觉但有效)

flowchart TD
    Start[业务报错] --> L1{L1: 网卡 UP?}
    L1 -->|否| Fix1[ethtool 看 link / 换网线]
    L1 -->|是| L2{L2: MAC/ARP OK?}
    L2 -->|否| Fix2[ip neigh / 看 ARP 表]
    L2 -->|是| L3{L3: ping 通对端?}
    L3 -->|否| Fix3[ip route / mtr / 防火墙]
    L3 -->|是| L4{L4: 端口监听?}
    L4 -->|否| Fix4[ss -lntp / 服务挂了]
    L4 -->|是| L7{L7: 应用响应?}
    L7 -->|否| Fix5[curl -v / 应用日志]
    L7 -->|是| OK[✓ 没问题]

90% 的问题在 L3-L4。极少数到 L7。永远从下往上查。

反例:从上往下查的坑

"用户说 API 502"
  → 看应用日志(L7):没异常
  → 重启应用:还是 502
  → 半天后才发现 nginx upstream 配错了节点 IP(L3)

如果从下往上:

"502 → nginx upstream 是哪些"
  → curl nginx upstream IP 直接测:失败
  → ping 那个 IP:失败 → 节点根本不通
  → 改 upstream → 修复

5 分钟 vs 半天。


10. 反面教材合集(避坑指南)

❌ 误解 1:ping 通 = 没问题

$ ping example.com
PING example.com (93.184.215.14): 56 data bytes
64 bytes from 93.184.215.14: ... time=15.234 ms

$ nc -zv example.com 443
nc: connect to example.com port 443 (tcp) failed: Connection refused

ICMP(ping)和 TCP 走的是不同协议。很多防火墙放行 ICMP 但挡 TCP。

正确:测端口连通用 nc -zv / curl,不要单靠 ping。

❌ 误解 2:MTU 1500 是固定的

$ ip link show eth0 | grep mtu
mtu 1500
$ ip link show flannel.1 | grep mtu
mtu 1450   # ← VXLAN 占了 50 字节

不同链路 MTU 不同。VXLAN / IPIP 封装后 MTU 必须减小。

症状:跨节点 pod 大包(> 1450)丢、小包正常 → 应用偶发性卡顿、特征是"小请求 OK、大请求挂"。

修法:

# CNI 配置里设 MTU = 物理 MTU - 封装开销
# 例如:物理 1500 - VXLAN 50 = 1450

❌ 误解 3:K8s ClusterIP 能 ping

见上面 §8。ClusterIP 不响应 ICMP——这是正常的。用 curl / nc -zv。

❌ 误解 4:公网 IP 全球唯一

只在"路由可达"意义上唯一。CGNAT(运营商级 NAT)让多用户共享一个公网 IP。

# 在家里
$ curl ifconfig.me
1.2.3.4
$ curl https://api.ipify.org
1.2.3.4

# 在公司
$ curl ifconfig.me  
1.2.3.4   # ← 不同设备、可能看到同一个公网 IP(CGNAT)

业务做"IP 限流"时容易误杀同 NAT 网关后的合法用户。

❌ 误解 5:"netstat 看到 ESTABLISHED 就是连接好的"

$ ss -tn state established
ESTAB 0 0 1.2.3.4:443 5.6.7.8:54321

ESTABLISHED 是 TCP 状态。双方是否还活着是另一回事——可能客户端断网了、server 还以为连接好的。这就是为什么需要 TCP keepalive:

sysctl net.ipv4.tcp_keepalive_time      # 默认 7200 秒 = 2 小时
sysctl net.ipv4.tcp_keepalive_intvl     # 默认 75 秒
sysctl net.ipv4.tcp_keepalive_probes    # 默认 9 次

K8s 节点上长连接 + 中间 NAT 设备(防火墙超时 5 分钟)→ 连接半死、状态 ESTAB → 应用调用偶发性 timeout。

修:把 tcp_keepalive_time 调小:

sysctl -w net.ipv4.tcp_keepalive_time=60

或者应用层加 keep-alive 心跳。

❌ 误解 6:DNS 解析永远走 /etc/resolv.conf

$ cat /etc/resolv.conf
nameserver 10.96.0.10
search ns.svc.cluster.local svc.cluster.local cluster.local
options ndots:5

Go 程序、glibc、Node.js 各有自己的 resolver 实现 + cache。某些应用绕开 nsswitch 直接调远程 DNS。

详见 04-dns-deep.md。

❌ 误解 7:服务端口冲突就报错

# 进程 A bind 0.0.0.0:8080 成功
# 进程 B bind 127.0.0.1:8080 也成功??

Linux 允许"更具体的 bind 覆盖更宽的"。0.0.0.0 是 wildcard、127.0.0.1 更具体。 排查"为啥 curl localhost:8080 走到进程 A 不是进程 B"会迷惑。

$ ss -lntp '( sport = :8080 )'
LISTEN 0 128 0.0.0.0:8080  process A
LISTEN 0 128 127.0.0.1:8080 process B

最具体的 bind 优先。

❌ 误解 8:iptables -A 立即生效

-A 是追加到链尾。规则按顺序匹配、第一个匹配的赢。如果前面有 -j DROP 的全匹配规则,你的 -A ACCEPT 永远到不了。

iptables -A INPUT -j DROP                # 先 DROP 一切
iptables -A INPUT -p tcp --dport 80 -j ACCEPT   # 永远不会被命中

要插到前面用 -I(insert):

iptables -I INPUT 1 -p tcp --dport 80 -j ACCEPT

❌ 误解 9:/proc/net/tcp 列出的就是所有连接

$ cat /proc/net/tcp | wc -l
1000
$ ss -tn | wc -l
5000

/proc/net/tcp 只列 IPv4。IPv6 在 /proc/net/tcp6。容器里看 /proc/net/* 是当前 netns 视角、不是全节点。

K8s pod 里跑 ss 看到的只是 pod 自己 netns 的 socket。要看节点级用 nsenter -t 1 -n ss。

❌ 误解 10:包是按发送顺序到的

应用顺序发:包 1, 2, 3, 4, 5
对端可能收到顺序:1, 3, 2, 5, 4

IP 网络不保证顺序。TCP 在上层用 seq 重组、所以应用看到的是有序的。但UDP 不重组——如果你的应用基于 UDP(DNS / 自研协议),收包顺序是乱的。

业务排错时如果发现"应该按时间顺序的事件错乱",先想是不是中间网络乱序了。


11. 排查 cheatsheet(面对真实故障时的速查)

"连不上服务" 黄金 5 步

flowchart TD
    A[业务报错: 连不上 X] --> B[1. ping X<br>L3 通吗?]
    B -->|不通| B1[ip route get X<br>看路由]
    B -->|通| C[2. nc -zv X port<br>L4 端口通吗?]
    C -->|不通| C1[对端 ss -lntp<br>看监听]
    C -->|通| D[3. curl -v X<br>L7 服务对吗?]
    D -->|404/5xx| D1[看应用日志]
    D -->|连接断| D2[mtr X<br>看路径丢包]
    D -->|TLS 错| D3[openssl s_client]
    D -->|超时| D4[tcpdump 抓包]
    D -->|OK| E[✓ 没问题]

"节点 NotReady" 排查

# 1. 看 kubelet 状态
ssh m4 'systemctl status kubelet; journalctl -u kubelet --since "5 min ago" | tail -50'

# 2. 看到 cni / network plugin 错?
ssh m4 'ls /etc/cni/net.d/'                  # CNI 配置在不在
ssh m4 'crictl ps | grep cni'                 # CNI pod 跑没

# 3. 看 sysctl 关键参数
ssh m4 'sysctl net.bridge.bridge-nf-call-iptables net.ipv4.ip_forward'
# 都该是 1

# 4. 看 conntrack 满了没
ssh m4 'sysctl net.netfilter.nf_conntrack_count net.netfilter.nf_conntrack_max'

# 5. 看路由
ssh m4 'ip route'                             # 应该有指向其它节点 pod CIDR 的路由

"Pod 偶发性 timeout" 排查

# 1. 网络层抖动
ssh m4 'dmesg -T | grep -iE "drop|nf_conntrack|tcp"'      # 内核报错
ssh m4 'mtr -rn -c 30 -T -P <port> <peer>'                 # 持续测路径

# 2. conntrack 表满
ssh m4 'sysctl net.netfilter.nf_conntrack_count nf_conntrack_max'

# 3. TIME_WAIT / CLOSE_WAIT 堆积
ssh m4 'ss -s'                                              # 总览
ssh m4 'ss -tn state close-wait | wc -l'                    # 异常堆积?

# 4. 看应用日志找时间戳关联

"外部访问 NodePort 慢" 排查

# 1. 测节点本机
ssh m4 'curl -v http://localhost:30080'

# 2. 测节点 IP(外部模拟)
curl -v http://⟨m4-IP⟩:30080

# 3. 看 iptables KUBE-NODEPORTS 链是否有规则
ssh m4 'iptables -t nat -L KUBE-NODEPORTS -n -v | head'

# 4. 看 endpoint
kubectl get endpoints ⟨svc-name⟩

# 5. 抓包对比
ssh m4 'tcpdump -i any -nn host ⟨client-IP⟩ and port 30080'

12. 这篇之后

马上有用:

  • 看 ip addr / ip route 输出能逐行读懂了
  • 知道为啥 ClusterIP 不能 ping
  • 看到 CLOSE_WAIT 堆积知道是哪一端 bug
  • 排错时知道从 L3-L4 开始

继续学习:

文档学什么
01-output-reading.md把 ss / iptables / tcpdump 输出逐行讲透
02-namespaces.mdnetns / veth / bridge 心智模型实操
03-k8s-network-deep.mdService / NodePort / Ingress 完整流量走向
04-dns-deep.mdCoreDNS / resolv.conf / ndots:5 那个经典坑
05-troubleshooting.md把这篇的排查 cheatsheet 展开到 case study 级

命令文档

每个命令的语法 / 参数 / 踩坑独立查阅:../commands/。

本系列把它们串起来用。

在 GitHub 上编辑此页
Next
看懂网络命令输出 —— 把每一个数字讲清楚