看懂网络命令输出 —— 把每一个数字讲清楚
你跑
ip addr/ss -lntp/iptables -L -n -v/tcpdump—— 屏幕上一堆数字。这篇把它们逐字段拆开讲。读完之后看任何网络命令输出,每个数字 / 字段你都能告诉自己"它是啥、生产上看到该想啥"。
这篇怎么用
- 没场景时通读一遍 —— 建立各个命令的输出心智模型
- 真正排错时用
Ctrl-F搜对应字段 —— 把它当字典用
每个命令的"全套用法 / 参数"在 ../commands/ 里有独立文档。这篇只讲输出怎么读。
1. ip addr —— 看 IP 和网卡
$ ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq state UP group default qlen 1000
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
valid_lft 84321sec preferred_lft 84321sec
inet6 fe80::cafe:beef:1234:5678/64 scope link noprefixroute
valid_lft forever preferred_lft forever
3: cni0: <BROADCAST,MULTICAST,PROMISC,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP
link/ether 0a:58:0a:f4:00:01 brd ff:ff:ff:ff:ff:ff
inet 10.244.0.1/24 brd 10.244.0.255 scope global cni0
$ ip -c -br addr
lo UNKNOWN 127.0.0.1/8 ::1/128
eth0 UP 192.168.1.5/24 fe80::cafe:beef:1234:5678/64
cni0 UP 10.244.0.1/24
-br 一行一个网卡,生产巡检最爱。 -c 加颜色(终端可读性 +++)。
第一行逐字段拆解
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq state UP group default qlen 1000
↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑
(1) (2) (3) (4) (5) (6) (7) (8)
| 标记 | 含义 | 生产意义 |
|---|---|---|
| (1) | 接口序号(ifindex),内核 ID | veth pair 的 eth0@if<N> 那个 N 就是这个 |
| (2) | 接口名 | 实际网卡 / 虚拟设备名 |
| (3) | 接口 flags:BROADCAST = 支持广播;MULTICAST = 支持组播;UP = 管理员启用;LOWER_UP = 物理链路通 | UP 但缺 LOWER_UP = 网卡启用了但没插网线 / 对端 down |
| (4) | MTU(最大传输单元)字节 | 物理网卡通常 1500;VXLAN tunnel 1450;jumbo frame 9000 |
| (5) | qdisc:发包队列类型(fq / fq_codel / pfifo_fast / mq) | 流量调度 / QoS 相关 |
| (6) | 接口状态:UP / DOWN / UNKNOWN | UP = 工作中;DOWN = 没启用 |
| (7) | group:接口组(IPv6 / 路由组用) | 一般忽略 |
| (8) | qlen:发包队列长度 | 高速网卡可能调大 |
第二行 link/ether
link/ether aa:bb:cc:dd:ee:ff brd ff:ff:ff:ff:ff:ff
^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^
(1) 本网卡 MAC (2) 广播 MAC(永远是 ff:ff:..)
注意几种特殊 MAC:
| MAC 前缀 | 含义 |
|---|---|
00:00:00:00:00:00 | loopback 网卡的"伪 MAC" |
52:54:00:.. | KVM / QEMU 虚拟机网卡(厂商 OUI) |
00:50:56:.. | VMware 虚拟机 |
02:42:.. | Docker bridge(locally administered,第 2 位为 1) |
0a:58:.. | flannel cni0 默认(locally administered) |
00:1c:42:.. | Parallels |
aa:bb:cc:.. | 通常是手动设的(不在 IEEE OUI 列表) |
排查"为什么 MAC 不是预期的"
云主机有时 MAC 变化(DHCP 续约 / 实例迁移)。如果某依赖 MAC 绑定的策略(如 license / 防火墙规则)报错,先 ip link show eth0 对比。
inet 行(IPv4 地址)
inet 192.168.1.5/24 brd 192.168.1.255 scope global dynamic noprefixroute eth0
^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^ ^^^^^^^ ^^^^^^^^^^^^^^
(1) (2) (3) (4) (5)
valid_lft 84321sec preferred_lft 84321sec
^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^
(6) (7)
| 标记 | 含义 | 看到时该想啥 |
|---|---|---|
| (1) | IP + 子网掩码 | /24 = 254 个可用 IP |
| (2) | 广播地址(子网"全 1"地址) | 一般不用关心 |
| (3) | scope:global(跨子网可达)/ link(仅本网段)/ host(仅本机) | K8s pod IP 通常 global;loopback host |
| (4) | dynamic = DHCP;无此字段 = 静态配置 | 排查 IP 变化 |
| (5) | noprefixroute = 不自动加 prefix 路由(NetworkManager 风格) | 一般忽略 |
| (6) | DHCP 租约剩余有效时间 | forever = 静态 IP |
| (7) | DHCP 优先使用时间 | 一般等于 valid_lft |
几种 scope 的区别(容易混)
$ ip addr | grep inet
inet 127.0.0.1/8 scope host lo
inet 192.168.1.5/24 scope global eth0
inet 169.254.1.5/16 scope link eth0 # link-local(没 DHCP 时自动)
| scope | 范围 |
|---|---|
host | 仅本机能用(loopback 类) |
link | 仅同一 L2 网段(不出网卡) |
global | 可路由 / 可跨子网 |
K8s pod IP 永远是 global。
反面教材
误判"网卡 UP = 网络通"
$ ip link show eth0
2: eth0: <BROADCAST,MULTICAST,UP> mtu 1500 ...
只有 UP、缺 LOWER_UP —— 管理员启用了网卡、但物理链路没通(网线没插 / 交换机端口 down / 对端 NIC 关)。
排查:
$ ethtool eth0 | grep -i "link detected"
Link detected: no # ← 物理层没通
虚拟机里通常永远 LOWER_UP。物理机看到只 UP 没 LOWER_UP,第一时间检查网线 / 交换机端口。
2. ip route —— 看路由表
$ ip route
default via 192.168.1.1 dev eth0 proto dhcp src 192.168.1.5 metric 100
192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.5
10.244.0.0/24 dev cni0 proto kernel scope link src 10.244.0.1
10.244.1.0/24 via 10.0.24.32 dev eth0
10.244.2.0/24 via 10.0.24.33 dev eth0 mtu lock 1450
默认路由(第一行)
default via 192.168.1.1 dev eth0 proto dhcp src 192.168.1.5 metric 100
↑ ↑ ↑ ↑ ↑ ↑ ↑
(1) (2) (3) (4) (5) (6) (7)
| 标记 | 含义 | 解读 |
|---|---|---|
| (1) | "default"(=0.0.0.0/0)匹配所有不被其它规则匹配的目的 IP | 路由的"兜底" |
| (2) | via = 通过下一跳 | 直连路由没有 via |
| (3) | 下一跳 IP(必须和本机同子网,否则路由不可达) | 网关 IP |
| (4) | 出网卡 | 包从哪个网卡出去 |
| (5) | proto = 这条路由怎么来的:dhcp / static / kernel / boot / bgp / ospf | 用来排查"谁加的路由" |
| (6) | 发包时用作源 IP | 多 IP 网卡时决定哪个作 src |
| (7) | metric = 优先级(数字小优先) | 多 default 路由时决定走哪个 |
直连路由
192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.5
↑ ↑ ↑ ↑
(1) (2) (3) (4)
| 标记 | 含义 |
|---|---|
| (1) | dev = 出网卡(没 via,直接二层送达) |
| (2) | proto kernel = 内核自动加(IP 配 /24 时同步加) |
| (3) | scope link = 同一 L2 域可达 |
| (4) | src = 这个子网内本机的 IP |
K8s 节点的典型路由
10.244.1.0/24 via 10.0.24.32 dev eth0
10.244.2.0/24 via 10.0.24.33 dev eth0 mtu lock 1450
^^^^^^^^^^^^
(1) 锁定 MTU
| 标记 | 含义 |
|---|---|
| (1) | mtu lock 1450 = 这条路径的 MTU 锁定为 1450(VXLAN 封装余量) |
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
↑ ↑ ↑ ↑ ↑
(1) (2) (3) (4) (5)
cache
| 标记 | 含义 |
|---|---|
| (1) | 你查的目的 IP |
| (2) | 实际选的下一跳 |
| (3) | 实际走哪个网卡 |
| (4) | 实际用的源 IP(多 IP 机器关键) |
| (5) | 发包用户的 UID(用户级路由策略) |
| cache | 输出末尾有时显示这条路由被加入了内核 cache |
排错 "为什么 IP 包没走我想的路径"
$ ip route get 10.244.1.5
10.244.1.5 via 10.0.24.32 dev eth0 src 10.0.24.28 uid 0
如果跟你期望的不一样(比如以为应该走 cni0 → bridge 给本地 pod、实际走 eth0 → 别的节点),看:
- 路由表第一条匹配的规则
- 检查最长前缀匹配(短前缀的优先级会被长前缀压)
ip rule show是否有策略路由把你导走了
3. ss -lntp —— 看监听端口
最常用一条:
$ ss -lntp
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=1234,fd=3))
LISTEN 0 128 127.0.0.53%lo:53 0.0.0.0:* users:(("systemd-resolve",pid=890,fd=14))
LISTEN 0 128 [::]:22 [::]:* users:(("sshd",pid=1234,fd=4))
LISTEN 0 4096 127.0.0.1:6443 0.0.0.0:* users:(("kube-apiserver",pid=5678,fd=12))
flag 含义:
| flag | 含义 |
|---|---|
-l | listening 只看监听 |
-n | numeric 不解析名字(必加) |
-t | TCP |
-p | process 显示进程(要 root 才能看完整) |
列逐字段拆解
LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=1234,fd=3))
↑ ↑ ↑ ↑ ↑ ↑
(1) (2) (3) (4) (5) (6)
| 标记 | 含义 | 生产意义 |
|---|---|---|
| (1) | TCP 状态 | LISTEN / ESTAB / TIME-WAIT ... |
| (2) | Recv-Q(LISTEN 状态:半连接队列当前长度) | 持续 > 0 = 应用 accept() 慢 / SYN flood |
| (3) | Send-Q(LISTEN 状态:backlog 上限) | 应用 listen() 时设的 |
| (4) | Local Address:Port — 监听地址 | 0.0.0.0 所有 IPv4 / 127.0.0.1 仅本机 / [::] 所有 IPv6 / 具体 IP 只监听那个 |
| (5) | Peer Address:Port(LISTEN 状态固定 0.0.0.0:*,因为没"对端") | ESTABLISHED 时才是真客户端 |
| (6) | 进程信息(要 root) | pid=N,fd=M 表示进程 PID 和它的文件描述符 |
LISTEN 状态的 Recv-Q / Send-Q 含义(特殊)
State Recv-Q Send-Q Local Address:Port
LISTEN 0 128 0.0.0.0:22 ← 半连接 0、上限 128
LISTEN 5 128 0.0.0.0:80 ← 半连接 5 个还没 accept
LISTEN 128 128 0.0.0.0:443 ← 队列满!应用处理不过来
- LISTEN 状态:Recv-Q = 当前半连接 + 全连接队列未被 accept 的数量;Send-Q = backlog 上限
- 持续 Recv-Q > 0 → 应用 accept 慢
- Recv-Q = Send-Q(满了)→ 新连接被丢 →
SYN flood或应用挂
LOCAL ADDRESS 的几种形态
0.0.0.0:22 # IPv4 所有地址的 22 端口(最常见)
127.0.0.1:6443 # 仅本机回环(apiserver 只允许本机访问场景)
192.168.1.5:80 # 仅这个具体 IP(多 IP 机器选 binding)
[::]:22 # IPv6 所有地址
[::1]:6443 # IPv6 回环
127.0.0.53%lo:53 # %lo 表示绑定到 lo 接口(systemd-resolved)
*:54321 # 旧式写法,等价 0.0.0.0
ESTABLISHED 状态看连接
$ 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
ESTAB 100 0 10.0.24.28:50001 10.244.1.5:8080
ESTAB 0 50000 10.0.24.28:55555 10.0.24.30:9090
TIME-WAIT 0 0 10.0.24.28:42345 10.0.24.29:6443
CLOSE-WAIT 200 0 10.0.24.28:60000 10.0.24.31:80
| 状态 | Recv-Q | Send-Q | 解读 |
|---|---|---|---|
| ESTAB | 0 | 0 | 正常通信中 |
| ESTAB | 100 | 0 | 收到 100 字节、应用没 read → 消费者慢 |
| ESTAB | 0 | 50000 | 5 万字节发了但没收到 ACK → 网络丢包 / 对端处理慢 |
| TIME-WAIT | 0 | 0 | 等 2*MSL(正常) |
| CLOSE-WAIT | 200 | 0 | 应用 bug:对端 FIN 了、本端没 close(),还有 200 字节没读 |
排查 CLOSE_WAIT 堆积
# 1. 数有多少
$ ss -tn state close-wait | wc -l
500
# 2. 看是哪个进程
$ ss -tnp state close-wait | head
ESTAB ... users:(("my-app",pid=1234,fd=23))
↑
定位到具体进程
定位后看 my-app 代码——某条路径没正确 close socket。
4. iptables -L -n -v —— 看防火墙 + NAT 规则
K8s 节点上 iptables 规则可能上千条。永远加 -n -v --line-numbers 才能看清。
$ iptables -L INPUT -n -v --line-numbers
Chain INPUT (policy ACCEPT 1234 packets, 56789 bytes)
num pkts bytes target prot opt in out source destination
1 12345 1M KUBE-FIREWALL all -- * * 0.0.0.0/0 0.0.0.0/0
2 5000 500K KUBE-PROXY-FIREWALL all -- * * 0.0.0.0/0 0.0.0.0/0 ctstate NEW
3 100 10K ACCEPT tcp -- * * 192.168.1.0/24 0.0.0.0/0 tcp dpt:22
$ iptables -t nat -L KUBE-SERVICES -n -v | head
Chain KUBE-SERVICES (2 references)
pkts bytes target prot opt in out source destination
45 2700 KUBE-SVC-NPX46M4PTM tcp -- * * 0.0.0.0/0 10.96.0.1 /* default/kubernetes:https cluster IP */ tcp dpt:443
100 6000 KUBE-SVC-TCOU7JCQXE udp -- * * 0.0.0.0/0 10.96.0.10 /* kube-system/kube-dns:dns cluster IP */ udp dpt:53
Chain header 的 policy
Chain INPUT (policy ACCEPT 1234 packets, 56789 bytes)
^^^^^^ ^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^
(1) (2) (3)
| 标记 | 含义 |
|---|---|
| (1) | 默认 policy:没匹配上任何规则时的兜底动作(ACCEPT / DROP / REJECT) |
| (2) | 这条 chain 命中默认 policy 的包数 |
| (3) | 这条 chain 命中默认 policy 的字节数 |
规则行字段
1 12345 1M KUBE-FIREWALL all -- * * 0.0.0.0/0 0.0.0.0/0 ctstate NEW
↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑
(1) (2) (3) (4) (5) (6) (7) (8) (9) (10) (11)
| 标记 | 含义 | 生产意义 |
|---|---|---|
| (1) | 规则行号(加 --line-numbers 才有) | 删特定规则用 |
| (2) | pkts:命中规则的包数 | 排错关键:0 = 没命中、持续涨 = 工作中 |
| (3) | bytes:命中规则的字节数 | 同上 |
| (4) | target:动作(ACCEPT / DROP / 跳子链等) | 命中后做啥 |
| (5) | prot:协议(tcp / udp / icmp / all) | 限定协议 |
| (6) | opt:选项(! = 取反 / -- 通常) | 几乎不用看 |
| (7) | in:入网卡(* = 任意) | 限定从哪个网卡进 |
| (8) | out:出网卡 | 限定到哪个网卡出 |
| (9) | source:源 IP / 网段 | 0.0.0.0/0 = 任意 |
| (10) | destination:目的 IP / 网段 | 同上 |
| (11) | 额外 match:state / port / mark / 等 | dpt = destination port / sport = source port |
pkts 列 —— 排查 "规则没生效" 的真相
$ iptables -L INPUT -n -v --line-numbers | grep 30080
3 0 0 ACCEPT tcp ... tcp dpt:30080
^^^^^^^
pkts = 0 → 规则没被命中
pkts = 0 的可能原因:
- 流量根本没到这条 chain(路由问题)
- 上面有更宽的规则先匹配走了(永远从上往下读 chain)
- 规则条件写错(比如 port 写错、source 写错)
排查:
# 重置 counter
iptables -Z
# 触发一次流量
curl http://node:30080
# 再看
iptables -L INPUT -n -v --line-numbers | grep 30080
# pkts 应该 ≥ 1
pkts 没变 = 流量没到这条规则。回去看路由 / 上面的链。
K8s 的几个关键 chain
$ iptables -t nat -L -n | grep "^Chain"
Chain PREROUTING # 包刚进、未路由前
Chain INPUT
Chain OUTPUT
Chain POSTROUTING # 包出、已路由后
Chain DOCKER
Chain DOCKER-USER
Chain KUBE-SERVICES # 所有 Service 入口
Chain KUBE-NODEPORTS # NodePort 服务
Chain KUBE-POSTROUTING # 出口 SNAT
Chain KUBE-MARK-MASQ # 给包打 mark 等 POSTROUTING 时 SNAT
Chain KUBE-SVC-XXXXX # 每个 Service 一条
Chain KUBE-SEP-XXXXX # 每个 Endpoint 一条
跟踪 Service 流量:
$ iptables -t nat -L KUBE-SERVICES -n -v | grep 10.96.1.5
45 2700 KUBE-SVC-MYAPP123 tcp -- ... 10.96.1.5 /* default/my-svc */ tcp dpt:80
↑ ↑^^^^^^^^^^^^^^^^ ↑
(1) (2) (3)
| 标记 | 含义 |
|---|---|
| (1) | 命中 45 个包 → 有人访问过 |
| (2) | 跳到 KUBE-SVC-MYAPP123 链("实际选哪个 endpoint"在那里) |
| (3) | dpt:80 = 目的端口 80(ClusterIP 端口) |
继续看 KUBE-SVC-MYAPP123:
$ iptables -t nat -L KUBE-SVC-MYAPP123 -n -v
pkts bytes target prot opt ...
23 1380 KUBE-SEP-AAA all -- ... /* probability 0.33 */ statistic mode random probability 0.33
15 900 KUBE-SEP-BBB all -- ... /* probability 0.50 */ statistic mode random probability 0.50
7 420 KUBE-SEP-CCC all -- ... /* */
3 个 endpoint,按概率分流(实际 3 个 endpoint 用 1/3 / 1/2 / 剩余 = 33% / 33% / 34%)。
继续看 KUBE-SEP-AAA:
$ iptables -t nat -L KUBE-SEP-AAA -n -v
pkts bytes target prot opt ...
23 1380 DNAT tcp ... to:10.244.0.5:8080
最终 DNAT 把流量改写到 pod IP。
反面教材
误删 K8s 链导致 Service 全挂
$ iptables -F # 想清自己加的规则
$ iptables -t nat -F # 把 NAT 表也清了
# K8s Service 全断!kube-proxy 几秒后重建、但期间集群网络异常
K8s 节点上不要随便 -F flush nat 表——kube-proxy 的链全在里面。
-F 之后流量恢复要等 kube-proxy 下一次 sync(默认 30 秒)。
5. tcpdump —— 抓包看真相
最常用:
$ tcpdump -i any -nn -c 5 'port 80'
14:30:00.123456 IP 192.168.1.5.43210 > 93.184.215.14.80: Flags [S], seq 1234567890, win 64240, options [mss 1460,sackOK,TS val 12345 ecr 0,nop,wscale 7], length 0
14:30:00.234567 IP 93.184.215.14.80 > 192.168.1.5.43210: Flags [S.], seq 9876543210, ack 1234567891, win 65535, options [mss 1460,sackOK,TS val 67890 ecr 12345,nop,wscale 7], length 0
14:30:00.234780 IP 192.168.1.5.43210 > 93.184.215.14.80: Flags [.], ack 1, win 502, options [nop,nop,TS val 12346 ecr 67890], length 0
14:30:00.234890 IP 192.168.1.5.43210 > 93.184.215.14.80: Flags [P.], seq 1:79, ack 1, win 502, options [nop,nop,TS val 12346 ecr 67890], length 78: HTTP: GET / HTTP/1.1
14:30:00.500000 IP 93.184.215.14.80 > 192.168.1.5.43210: Flags [P.], seq 1:1200, ack 79, win 65535, options [nop,nop,TS val 67891 ecr 12346], length 1199: HTTP: HTTP/1.1 200 OK
每行:
14:30:00.123456 IP 192.168.1.5.43210 > 93.184.215.14.80: Flags [S], seq 1234567890, win 64240, length 0
↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑
(1) (2) (3) (4) (5) (6) (7) (8) (9) (10) (11)
| 标记 | 含义 | 解读 |
|---|---|---|
| (1) | 时间戳(微秒级) | 00:00:00.001234 |
| (2) | 协议(IP / IP6 / ARP) | |
| (3) | 源 IP | |
| (4) | 源端口 | 注意 tcpdump 用 . 分隔 IP 和端口(不是 :) |
| (5) | 目的 IP | |
| (6) | 目的端口 | |
| (7) | 关键字 Flags | |
| (8) | TCP flags 缩写(见下表) | 三次握手 / 数据 / 关闭都在这 |
| (9) | seq = TCP 序号 | 第一次握手时是初始随机值 |
| (10) | win = 接收窗口 | 流控用 |
| (11) | length = 载荷字节数 | 0 = 只有 TCP 头无数据 |
TCP Flags 缩写表(必背)
S SYN 建连请求
S. SYN-ACK 建连应答(S + .)
. ACK 单纯 ACK
P PSH 推送(应用层有数据要立即送)
P. PSH-ACK 最常见的 HTTP 请求 / 响应数据包
F FIN 正常关闭
F. FIN-ACK
R RST 异常关闭 / 拒绝
RP RST + PSH 极少
. (单纯 ACK)
三次握手 + 数据交换 + 关闭 完整序列
[S] client → server SYN
[S.] server → client SYN-ACK
[.] client → server ACK ← 握手完成
[P.] HTTP:... client → server PSH-ACK 含数据
[.] server → client ACK
[P.] HTTP:... server → client PSH-ACK 含响应
[.] client → server ACK
[F.] client → server FIN-ACK
[F.] server → client FIN-ACK
[.] client → server ACK ← 关闭完成
异常信号
[R] RST 对端拒连 / 应用 crash
[R.] RST-ACK 同上
重复 [S] SYN 重传 包丢失 / server 没回
重复 seq TCP 重传 中间丢包
排查"应用偶发性 ECONNREFUSED":
$ tcpdump -i any -nn 'host <server> and port <port>'
# 看到 [R] = server 主动拒(端口没监听 / 防火墙)
排查"应用偶发性慢":
# 看到大量重传(同 seq 出现多次)
$ tcpdump -i any -nn 'host <server>' | head
14:30:00.001 IP A > B: Flags [P.], seq 100:200, length 100
14:30:00.500 IP A > B: Flags [P.], seq 100:200, length 100 ← 重传!
14:30:01.500 IP A > B: Flags [P.], seq 100:200, length 100 ← 又重传
→ 网络丢包。结合 mtr 找哪一跳。
排查"K8s 跨节点 pod 不通"——同时两端抓
# 终端 1:源节点 m1
ssh m1 'tcpdump -i any -nn host 10.244.1.5'
# 终端 2:目的节点 m2
ssh m2 'tcpdump -i any -nn host 10.244.0.5'
# 终端 3:触发流量
kubectl exec pod-a -- curl http://10.244.1.5:8080
判断:
| 现象 | 解读 |
|---|---|
| m1 抓到 SYN 出、m2 没收到 | 中间路径丢包 / CNI 配置错 |
| m2 收到 SYN、没 SYN-ACK 出 | pod 没监听 / 防火墙挡 |
| m2 发 SYN-ACK、m1 没收到 | 回路 NAT / 路由问题 |
| 两边都通、应用还是错 | L7 应用问题 |
看 HTTP / DNS 完整内容
$ tcpdump -i any -nn -A -s 0 'port 80'
^^ ^^^^
| s 0 = 不截断(默认只抓前 96 字节)
-A = 显示 ASCII
# 输出含完整 HTTP 内容
14:30:00.123456 IP 1.2.3.4.43210 > 5.6.7.8.80: Flags [P.] ... length 78: HTTP: GET / HTTP/1.1
GET / HTTP/1.1
Host: example.com
User-Agent: curl/8.4.0
Accept: */*
# 看 DNS 内容
$ tcpdump -i any -nn 'port 53' -A
14:30:00.123456 IP 1.2.3.4.54321 > 10.96.0.10.53: 12345+ A? kubernetes.default.svc.cluster.local. (54)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
query: 12345 是 ID、A 是记录类型、问 kubernetes.default...
6. nc -zv —— 端口连通性测试(最快)
$ nc -zv 10.96.0.1 443
Connection to 10.96.0.1 443 port [tcp/*] succeeded!
$ nc -zv 10.96.0.99 443
nc: connect to 10.96.0.99 port 443 (tcp) failed: Connection refused
$ nc -zv 10.0.0.99 443
nc: connect to 10.0.0.99 port 443 (tcp) failed: Operation timed out
| 返回 | 含义 |
|---|---|
succeeded | TCP 三次握手通了(不验证 L7) |
Connection refused | 对端回 RST了 = 端口可达但没监听 |
Operation timed out | 包发出去没回应:网络不通 / 防火墙挡 |
No route to host | 本地路由表里没到对端的路 |
Network is unreachable | 整个网络层不通 |
Connection refused vs timeout 的区别很关键:
- refused = 包到了对端、对端主动拒(网络通)
- timeout = 包没到对端 / 对端没回(网络不通 / 防火墙挡)
排错时分清这两种 → 节省一半时间。
7. dig —— DNS 查询
$ dig kubernetes.default.svc.cluster.local @10.96.0.10
; <<>> DiG 9.18.18 <<>> kubernetes.default.svc.cluster.local @10.96.0.10
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 12345
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;;
;; QUESTION SECTION:
;kubernetes.default.svc.cluster.local. IN A
;;
;; ANSWER SECTION:
kubernetes.default.svc.cluster.local. 30 IN A 10.96.0.1
↑ ↑ ↑
(1) (2)(3)
;;
;; Query time: 5 msec
;; SERVER: 10.96.0.10#53(10.96.0.10) (UDP)
;; WHEN: Mon May 27 14:30:00 UTC 2026
;; MSG SIZE rcvd: 99
ANSWER SECTION 是关键。字段:
| 标记 | 含义 |
|---|---|
| (1) | TTL(秒),下次同样查询前的缓存有效期 |
| (2) | 记录类(IN = Internet,几乎永远 IN) |
| (3) | 记录类型(A = IPv4 / AAAA = IPv6 / CNAME / MX / SRV / TXT) |
status 状态码
| status | 含义 | 排错 |
|---|---|---|
NOERROR | 成功 | - |
NXDOMAIN | 域名不存在 | 域名拼错 / 没注册 |
SERVFAIL | DNS server 出错 | server 故障 / 上游配置错 |
REFUSED | DNS server 拒绝(你没权问) | 通常 DNS 配置问题 |
flags 标志
flags: qr aa rd ra
↑ ↑ ↑ ↑
(1) (2) (3) (4)
| 标记 | 含义 |
|---|---|
| (1) | qr = Response (有响应) |
| (2) | aa = Authoritative Answer (权威应答,CoreDNS 给 cluster.local 是这个) |
| (3) | rd = Recursion Desired (客户端要求递归) |
| (4) | ra = Recursion Available (server 支持递归) |
| - | tc = Truncated (响应被截断,要换 TCP 重查) |
排查 "DNS 解析慢"
$ dig +stats kubernetes.default
;; Query time: 5234 msec ← 5 秒!太慢
Query time > 100ms 在内网就要查:
- CoreDNS pod 是否健康
- 上游 DNS 是否慢(CoreDNS forward 给上游)
- pod 网络到 CoreDNS pod 是否慢
详见 04-dns-deep.md。
8. mtr —— 路径 + 丢包
$ mtr -rn -c 30 8.8.8.8
HOST: m1 Loss% Snt Last Avg Best Wrst StDev
1.|-- 192.168.1.1 0.0% 30 0.5 0.6 0.4 1.0 0.2
2.|-- 10.0.0.1 0.0% 30 2.1 2.5 1.8 5.2 0.7
3.|-- ??? 100.0% 30 0.0 0.0 0.0 0.0 0.0
4.|-- 203.0.113.1 0.0% 30 12.0 12.5 11.8 20.1 1.5
5.|-- 8.8.8.8 0.0% 30 25.0 26.0 24.5 35.0 2.0
逐列:
| 列 | 含义 | 解读 |
|---|---|---|
HOST | 每跳的 IP / 主机名 | ??? = 那一跳不响应 ICMP(不一定是问题) |
Loss% | 那一跳的丢包率(用 ICMP / TCP / UDP 探测) | 看末跳丢包决定真问题 |
Snt | 探测包发送数 | -c 设的 |
Last | 最近一次 RTT(毫秒) | - |
Avg | 平均 RTT | 关键 |
Best / Wrst | 最好 / 最差 | 看抖动 |
StDev | 标准差 = 抖动大小 | 大 = 网络不稳定 |
关键判读
1.|-- 路由器 0.0% ← 第一跳正常
2.|-- ISP 接入 0.0%
3.|-- ??? 100.0% ← 中间跳不响应(多数情况是设备策略,不一定问题)
4.|-- 主干 0.0% ← 后面的跳又正常 = 第 3 跳"看不见"但流量过去了
5.|-- 目的 0.0% ← 终点 OK
只看 100% 丢包不要慌——很多路由器策略不响应 ICMP TTL exceeded。看末跳 + 整体:
- 末跳 0% 丢包 = 包到了
- 末跳 30% 丢包 = 中间确实丢
- 中间某一跳 30% 丢包但后面 0% = 那跳的 ICMP 限速、不是真问题
TCP 探测(防火墙挡 ICMP 时)
$ mtr -rn -T -P 443 -c 30 example.com # 用 TCP 443 探测
K8s 集群里跨节点排错常用 -T -P <port> —— 模拟真实业务流量、绕开 ICMP 限制。
9. journalctl -u kubelet -f 关键日志关键字(网络相关)
K8s 节点 NotReady / pod 起不来时:
$ journalctl -u kubelet --since "10 min ago" | grep -iE 'network|cni|cidr|route'
常见关键字 + 含义:
| 关键字 | 含义 | 排查方向 |
|---|---|---|
cni plugin not initialized | CNI 没装 / 没起 | 看 /etc/cni/net.d 有无配置 / CNI pod 是否 Ready |
network plugin returns error | CNI 调用失败 | CNI pod 日志 / 节点连通 |
failed to setup network for pod | 给 pod 配网络失败 | 看具体错(IP 分配 / route / namespace) |
Failed to update CIDR map | pod CIDR 分配 / 同步问题 | 重启 controller-manager / 看 nodeIPAM 状态 |
IP allocation failed | IPAM 池满 / 配置错 | 看 pod CIDR 大小 |
error syncing pod | 各种 pod 同步问题 | 看上下文 |
Couldn't find network status for X | pod 创建中、CNI 在配 | 短暂正常、持续 = 问题 |
10. 综合实战:排查 "pod 起来但访问不通"
$ kubectl get pod my-pod -o wide
NAME STATUS IP NODE
my-pod Running 10.244.1.5 m2
# 别的 pod 访问 my-pod 失败
$ kubectl exec test-pod -- curl http://10.244.1.5:8080
# timeout
# 1. L4 端口连通?
$ kubectl exec test-pod -- nc -zv 10.244.1.5 8080
# refused / timeout?
# 2. 在 m2 上看 pod 是不是真在监听
$ ssh m2
$ PID=$(crictl inspect $(crictl ps -q --name my-pod) | jq '.info.pid')
$ nsenter -t $PID -n ss -lntp
# 看到 0.0.0.0:8080 LISTEN 了?
# 3. 看节点路由
$ ip route get 10.244.1.5
# via cni0 dev cni0 ← 直连 OK
# 4. 看 m2 上抓包
$ tcpdump -i any -nn host 10.244.0.5 and port 8080
# 看 SYN 进来了? SYN-ACK 出去了?
# 5. 看 iptables NetworkPolicy 是否挡
$ iptables -L FORWARD -n -v | grep DROP
# 6. cilium / calico 等用户态网络也可能拦
$ hubble observe --verdict DROPPED --pod default/my-pod
每步对应这篇里讲的输出读法。练熟之后排错速度 10x。
下一步
| 文档 | 用途 |
|---|---|
| 00-mental-model.md | 原理 + 心智模型(先看完) |
| 本篇 | 字典级输出解读 |
| 02-namespaces.md | 容器网络底层:怎么自己手搓 K8s pod 网络 |
| 03-k8s-network-deep.md | Service / Ingress 流量端到端 |
| 04-dns-deep.md | DNS + CoreDNS + ndots:5 |
| 05-troubleshooting.md | 故障排查方法论 |
命令速查
每个命令独立文档在 ../commands/,按"我现在用 X 命令、参数 / 坑速查"找去。