Linux-13 网络配置与排查
这一篇讲的是Linux 侧的网络配置和工具——网卡、路由、防火墙、以及排查连通性的命令行工具箱。
明确分工,避免重复:TCP/IP 协议本身、抓包分析、TCP 内核参数调优在 network 系列里已经写透了,本篇不重复,只在相关处给出链接:
- 抓包与 tcpdump 语法 → network-21 抓包实战
- 按症状索引的网络故障排查 → network-22 故障排查手册
- TIME_WAIT、keepalive、BBR 等内核参数 → network-10 TCP 实战调优
1. ip 命令族:忘掉 ifconfig
ifconfig、route、netstat、arp 属于 net-tools 包,已经停止维护十多年,很多现代发行版默认不再安装。取代它们的是 iproute2 提供的 ip 和 ss。
| 旧命令 | 新命令 |
|---|---|
ifconfig |
ip addr / ip link |
ifconfig eth0 up |
ip link set eth0 up |
route -n |
ip route |
arp -a |
ip neigh |
netstat -tlnp |
ss -tlnp |
netstat -i |
ip -s link |
不只是「换个名字」——ip 能看到 ifconfig 根本看不到的东西:一个网卡的多个 IP(ifconfig 只显示第一个)、IP 的作用域和生命周期、多路由表、策略路由。容器和 K8s 环境里这些都是常态。
1.1 查看网卡与地址
# 看所有网卡的 IP(最常用)
ip -br addr
# lo UNKNOWN 127.0.0.1/8 ::1/128
# eth0 UP 10.0.0.5/24 fe80::5054:ff:fe12:3456/64
# docker0 DOWN 172.17.0.1/16
# ^^^^ -br = brief,输出紧凑,适合快速查看
# 详细信息
ip addr show eth0
# 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq state UP
# ^ 索引 ^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^ ^^^^^^^ 队列规则
# link/ether 52:54:00:12:34:56 brd ff:ff:ff:ff:ff:ff
# inet 10.0.0.5/24 brd 10.0.0.255 scope global eth0
# inet 10.0.0.6/24 scope global secondary eth0 <- 第二个 IP,ifconfig 看不到
UP 和 LOWER_UP 要分开看:
UP 管理状态:管理员启用了这个网卡(ip link set up)
LOWER_UP 物理状态:网线插着、载波信号正常
^ 只有 UP 没有 LOWER_UP = 网线没插好 / 对端交换机端口关了
NO-CARRIER 明确表示物理链路断开
# 快速定位物理链路问题
ip link show eth0 | grep -o "NO-CARRIER\|LOWER_UP"
# NO-CARRIER <- 网线/光模块问题,别再查上层了
1.2 配置地址与网卡
# 加 IP(立即生效,重启丢失)
ip addr add 10.0.0.100/24 dev eth0
ip addr del 10.0.0.100/24 dev eth0
# 启停网卡
ip link set eth0 up
ip link set eth0 down
# 改 MTU
ip link set eth0 mtu 1450
# 改 MAC(虚拟化和容器里偶尔需要)
ip link set eth0 address 52:54:00:aa:bb:cc
注意:
ip命令的改动全部是临时的,重启即失效。持久化要写配置文件,而配置方式因发行版而异——Ubuntu 用 netplan,RHEL 系用 NetworkManager,这是最容易搞混的地方:
# Ubuntu 18.04+:/etc/netplan/01-netcfg.yaml
network:
version: 2
ethernets:
eth0:
addresses: [10.0.0.5/24]
routes:
- to: default
via: 10.0.0.1
nameservers:
addresses: [223.5.5.5, 119.29.29.29]
netplan try # ✅ 试用,120 秒内不确认就自动回滚
netplan apply # 确认无误后应用
netplan try 是个救命功能——远程改网络配置最怕改完连不上,try 会在超时后自动回滚。
# RHEL/Rocky 9+:用 nmcli
nmcli con mod eth0 ipv4.addresses 10.0.0.5/24 \
ipv4.gateway 10.0.0.1 \
ipv4.dns "223.5.5.5" \
ipv4.method manual
nmcli con up eth0
1.3 MTU 问题:一类隐蔽的故障
MTU 不匹配的典型症状:小请求正常,大请求卡死或超时。
原因:
客户端发一个 1500 字节的包
v
中间某跳的 MTU 只有 1400(VPN、隧道、云网络)
v
路由器需要分片,但包设置了 DF(Don't Fragment)位
v
路由器丢弃并回 ICMP "Fragmentation Needed"
v
如果防火墙把 ICMP 全 ban 了 -> 客户端收不到通知 -> 【永远重传,直到超时】
这就是「PMTU 黑洞」
# 探测到目标的真实 MTU:用不分片的 ping 逐步试
ping -M do -s 1472 8.8.8.8 # 1472 + 28 字节头 = 1500
# ping: local error: message too long, mtu=1450
# ^^^^^^^^ 直接告诉你了
ping -M do -s 1422 8.8.8.8 # 1422 + 28 = 1450
# 64 bytes from 8.8.8.8: icmp_seq=1 ttl=115 time=32.1 ms <- 成功
# 调整 MTU
ip link set eth0 mtu 1450
MTU 相关的常见场景:
| 场景 | 典型 MTU |
|---|---|
| 标准以太网 | 1500 |
| PPPoE 拨号 | 1492 |
| WireGuard VPN | 1420 |
| K8s 的 VXLAN overlay(Flannel/Calico) | 1450 |
| IPsec 隧道 | 1400 左右 |
| 巨帧(内网存储/数据库专线) | 9000 |
K8s 里 MTU 问题特别常见——Pod 之间用 VXLAN 封装,每个包多 50 字节头部。如果 CNI 的 MTU 配成 1500 而底层网络也是 1500,封装后就变成 1550,超了。表现是「Pod 之间小包能通,传大文件或大响应就卡死」。
MSS 与 MTU 的关系见 network-04 MTU 与 MSS。
2. 路由表
ip route
# default via 10.0.0.1 dev eth0 proto dhcp metric 100
# 10.0.0.0/24 dev eth0 proto kernel scope link src 10.0.0.5
# 172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1
2.1 匹配规则:最长前缀优先
这是路由的核心规则,也是排查路由问题的基础:
① 找出所有能匹配目标 IP 的路由
② 选【前缀最长】(掩码最具体)的那条
③ 前缀长度相同时,选 metric 最小的
目标 10.0.0.99:
10.0.0.0/24 匹配,前缀 24 <- 胜出,最具体
10.0.0.0/8 匹配,前缀 8
default (0/0) 匹配,前缀 0 <- 兜底
目标 8.8.8.8:
只有 default 匹配 -> 走默认网关
ip route get 直接让内核告诉你会走哪条路,这比人工推演可靠得多:
ip route get 8.8.8.8
# 8.8.8.8 via 10.0.0.1 dev eth0 src 10.0.0.5 uid 1000
# ^^^^^^^^^^^^ 走这个网关 ^^^^^^^^^^^^^^^^ 源 IP 会是这个
ip route get 10.0.0.99
# 10.0.0.99 dev eth0 src 10.0.0.5 <- 同网段,直连不走网关
**排查「为什么访问不了某个 IP」的第一条命令就是 ip route get。**它会明确告诉你是「无路由」还是「走了错的网关」:
ip route get 192.168.99.1
# RTNETLINK answers: Network is unreachable <- 根本没有路由
2.2 常用操作
# 加路由
ip route add 192.168.2.0/24 via 10.0.0.254 dev eth0
ip route add default via 10.0.0.1
# 删除
ip route del 192.168.2.0/24
# 加一条不通的路由(黑洞,用于屏蔽)
ip route add blackhole 1.2.3.4/32
# 查看所有路由表(不只是 main)
ip route show table all | head
ip rule show
# 0: from all lookup local
# 32766: from all lookup main
# 32767: from all lookup default
Linux 有多张路由表,ip route 默认只看 main。容器网络、多网卡、VPN 场景会用到策略路由:
# 策略路由:来自特定网段的流量走特定网关
echo "200 vpn" >> /etc/iproute2/rt_tables
ip route add default via 10.8.0.1 table vpn
ip rule add from 192.168.100.0/24 lookup vpn priority 100
2.3 转发与反向路径过滤
# IP 转发:容器网络、NAT 网关必须开
cat /proc/sys/net/ipv4/ip_forward
# 1
sysctl -w net.ipv4.ip_forward=1
「Docker 容器突然无法访问外网」的一个经典原因就是 ip_forward 被某个安全脚本关掉了。
# 反向路径过滤(rp_filter):收到包时检查源 IP 是否可从这个网卡路由回去
cat /proc/sys/net/ipv4/conf/all/rp_filter
# 0 = 关闭 1 = 严格模式 2 = 松散模式
坑:多网卡机器上
rp_filter=1(严格模式)会造成非对称路由的包被静默丢弃。比如从 eth1 收到的包,但路由表说回去要走 eth0,严格模式就直接丢包,且不产生任何日志。表现为「ping 不通但抓包能看到包进来了」。多网卡场景应该设为 2(松散模式)或 0。
# 确认是不是 rp_filter 在丢包
nstat -az | grep -i rpfilter
# IpExtInRPFilter 8923 0.0 <- 有丢包计数
3. ss:查看连接状态
ss 比 netstat 快得多——netstat 逐行解析 /proc/net/tcp(连接多时极慢),ss 直接通过 netlink 从内核批量拿数据。
ss -tlnp
# -t TCP -u UDP -l 只看 LISTEN -n 不解析域名(关键,否则很慢)
# -p 显示进程 -a 全部 -s 汇总统计
# State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
# LISTEN 0 4096 0.0.0.0:8080 0.0.0.0:* users:(("myapp",pid=1234,fd=3))
必须加 -n。不加会对每个 IP 做反向 DNS 解析,几千条连接时会卡几十秒。
3.1 高频用法
# 查端口被谁占用(最常用)
ss -tlnp | grep :8080
# 或者
ss -tlnp "sport = :8080"
# 统计各状态的连接数 —— 排查连接问题的第一步
ss -tan | awk 'NR>1 {print $1}' | sort | uniq -c | sort -rn
# 12034 ESTAB
# 5821 TIME-WAIT
# 892 CLOSE-WAIT <- 有问题,见 §3.2
# 12 LISTEN
# 全局汇总
ss -s
# Total: 18923
# TCP: 18234 (estab 12034, closed 5821, orphaned 12, timewait 5821)
# 按状态过滤
ss -tan state established
ss -tan state time-wait
ss -tan state close-wait # 排查 fd 泄漏
# 看某个进程的所有连接
ss -tanp | grep "pid=1234"
# 按对端地址过滤
ss -tan dst 10.0.3.100
ss -tan dst 10.0.3.0/24
# 看连接的详细内核信息(RTT、拥塞窗口、重传)
ss -tin
# ESTAB 0 0 10.0.0.5:45678 10.0.3.9:3306
# cubic wscale:7,7 rto:204 rtt:2.5/1.2 mss:1448 cwnd:10 bytes_sent:892340
# ^^^^^^^^^^ RTT 2.5ms ^^^^^^^ 拥塞窗口
# retrans:0/12 <- 重传次数
ss -tin 里的 retrans 和 rtt 是判断网络质量的直接依据,比 ping 准确——因为它是这条实际业务连接的统计。
3.2 各状态的含义与排查方向
| 状态 | 含义 | 堆积说明什么 |
|---|---|---|
LISTEN |
监听中 | — |
SYN-SENT |
已发 SYN 等回应 | 对端不可达或防火墙丢包 |
SYN-RECV |
收到 SYN 等 ACK | 可能是 SYN flood |
ESTAB |
连接建立 | 正常 |
FIN-WAIT-1/2 |
主动关闭,等对端 | 对端不响应关闭 |
TIME-WAIT |
主动关闭方等 2MSL | 正常现象,量大时调内核参数 |
CLOSE-WAIT |
被动关闭,等本地应用 close() | 应用 bug,漏了 close |
LAST-ACK |
被动关闭方等最后 ACK | 短暂出现正常 |
最重要的区分:
TIME-WAIT 堆积 -> 主动关闭方(通常是客户端/网关)
-> 协议要求的正常状态,等 2MSL(60s)
-> 调内核参数缓解:tcp_tw_reuse、加大端口范围、用长连接
-> 详见 network-10
CLOSE-WAIT 堆积 -> 被动关闭方(通常是服务端)
-> 【应用没调用 close()】,纯粹是代码 bug
-> 调内核参数完全无效,没有超时机制
-> 查代码:错误路径是否漏了 close/defer
规律:看到 CLOSE-WAIT 堆积就去查代码,看到 TIME-WAIT 堆积才考虑内核参数。
4. 防火墙:iptables 与 nftables
4.1 iptables 的四表五链
五链(按数据包经过的位置):
PREROUTING 刚进入,还没路由决策 <- DNAT 在这里
INPUT 目标是本机
FORWARD 需要转发出去(容器/网关)
OUTPUT 本机发出
POSTROUTING 即将离开 <- SNAT/MASQUERADE 在这里
四表(按功能,优先级 raw > mangle > nat > filter):
raw 跳过连接跟踪
mangle 修改包头(TTL、TOS)
nat 地址转换
filter 过滤(ACCEPT/DROP) <- 最常用
数据包的完整路径:
进入 --> PREROUTING --> 路由决策 --+--> INPUT --> 本机应用
(DNAT) |
+--> FORWARD --+
|
本机应用 --> OUTPUT ------------------------------+--> POSTROUTING --> 出去
(SNAT)
记住这张图能解决大部分「规则写了不生效」的问题——比如把 DNAT 写在 POSTROUTING 里(太晚了,路由决策已经做完),或者把针对容器流量的规则写在 INPUT 里(容器流量走的是 FORWARD)。
# 查看规则(-n 不解析,-v 显示计数,--line-numbers 显示行号)
iptables -L -n -v --line-numbers
iptables -t nat -L -n -v
# 计数器是排查利器:看规则到底有没有被命中
iptables -L INPUT -n -v
# pkts bytes target prot opt in out source destination
# 8923 534K ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:22
# 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:8080
# ^^^^ 计数 0 = 这条规则从未命中,说明流量被前面的规则处理了或根本没来
「计数器为 0」是判断规则是否生效的最直接方法。
# 常用操作
iptables -A INPUT -p tcp --dport 8080 -j ACCEPT # 追加到末尾
iptables -I INPUT 1 -p tcp --dport 8080 -j ACCEPT # 插入到第 1 条 <- 注意顺序
iptables -D INPUT 3 # 删除第 3 条
iptables -F INPUT # 清空整条链(危险)
# 只允许特定来源
iptables -A INPUT -p tcp --dport 3306 -s 10.0.0.0/24 -j ACCEPT
iptables -A INPUT -p tcp --dport 3306 -j DROP
# 保留已建立连接(必须有,否则出去的包回不来)
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
坑:iptables 规则是按顺序匹配、第一条命中即生效。所以
-A(追加)和-I(插入)的区别很关键——如果链的前面已经有-j DROP,后面追加的ACCEPT永远不会被执行。改规则前先-L --line-numbers看清顺序。
iptables 规则不持久化,重启就丢:
# Debian/Ubuntu
apt install iptables-persistent
netfilter-persistent save
# RHEL 系
service iptables save
4.2 DROP vs REJECT
iptables -A INPUT -p tcp --dport 8080 -j DROP # 静默丢弃,客户端【超时】
iptables -A INPUT -p tcp --dport 8080 -j REJECT # 回 ICMP,客户端【立即】收到拒绝
| DROP | REJECT | |
|---|---|---|
| 客户端体验 | 一直等到超时(几十秒) | 立即 Connection refused |
| 隐蔽性 | 高(看起来像机器不存在) | 低 |
| 对外网 | 推荐,增加扫描成本 | |
| 对内网 | 推荐,快速失败便于排查 |
内网用 DROP 是排查噩梦——「连不上」但没有任何错误信息,要等 TCP 超时,而且分不清是防火墙、路由、还是服务没起。内网一律用 REJECT。
4.3 nftables:新一代方案
nftables 是 iptables 的官方继任者(内核 3.13+,RHEL 8 / Debian 10 起默认):
# 现代发行版上 iptables 命令其实是 nftables 的兼容层
iptables -V
# iptables v1.8.7 (nf_tables)
# ^^^^^^^^^ 说明底层已经是 nftables
# 查看真实的 nftables 规则
nft list ruleset
nftables 的优势:单一命令替代 iptables/ip6tables/arptables/ebtables、原生支持集合与映射(大量 IP 的匹配从 O(n) 变 O(1))、规则更新是原子的、语法更一致。
# nftables 的集合:屏蔽一批 IP,性能远好于 iptables 的多条规则
nft add table inet filter
nft add set inet filter blacklist '{ type ipv4_addr; }'
nft add element inet filter blacklist '{ 1.2.3.4, 5.6.7.8 }'
nft add rule inet filter input ip saddr @blacklist drop
现状:新项目用 nft,但排查线上问题时 iptables -L -n -v 仍是必备技能——K8s 的 kube-proxy(iptables 模式)会生成成千上万条 iptables 规则,你必须能读懂它们。
# 看 kube-proxy 生成的规则(了解 Service 是怎么实现的)
iptables -t nat -L KUBE-SERVICES -n | head
4.4 firewalld
RHEL 系的上层封装,用「区域(zone)」的概念:
firewall-cmd --state
firewall-cmd --list-all
# 开放端口(--permanent 才持久化)
firewall-cmd --permanent --add-port=8080/tcp
firewall-cmd --permanent --add-service=https
firewall-cmd --reload # 必须 reload 才生效
# 临时开放(重启失效,用于测试)
firewall-cmd --add-port=8080/tcp
--permanent 加了之后必须 --reload,这是最常见的困惑:「我加了规则怎么没生效」——因为 --permanent 只写配置文件不改运行时。
5. 工具箱
5.1 curl:不只是发请求
curl 是后端最常用的工具,但大部分人只用了 -X、-H、-d。
基础用法:
curl -i URL # 显示响应头 + body
curl -I URL # 只要响应头(HEAD 请求)
curl -v URL # 显示完整交互过程(含请求头、TLS 握手)
curl -s URL # 静默,不显示进度条 <- 脚本里必加
curl -o file URL # 保存到文件
curl -O URL # 用远端文件名保存
curl -L URL # 跟随 302 跳转 <- 很常需要
curl -k URL # 跳过 TLS 证书校验(自签证书调试用)
curl --limit-rate 200k URL # 限速,模拟慢网络
发送数据:
# POST 表单(自动加 Content-Type: application/x-www-form-urlencoded,自动变 POST)
curl -d 'user=alice&pass=123' URL
curl -d 'user=alice' -d 'pass=123' URL # 等价
# POST JSON
curl -H 'Content-Type: application/json' -d '{"id":1}' URL
# 从文件读 body(@ 前缀)
curl -H 'Content-Type: application/json' -d @payload.json URL
# 自动 URL 编码
curl --data-urlencode 'q=hello world&special=a+b' URL
# 上传文件(multipart/form-data)
curl -F 'file=@photo.png' -F 'desc=avatar' URL
curl -F 'file=@photo.png;type=image/png' URL # 指定 MIME
# 其他方法
curl -X PUT -d '...' URL
curl -X DELETE URL
cookie 与认证:
curl --cookie "name=value" URL
curl -c cookies.txt URL # 保存服务器返回的 cookie
curl -b cookies.txt URL # 带上之前保存的 cookie(模拟会话)
curl -u user:pass URL # Basic Auth
curl -H "Authorization: Bearer $TOKEN" URL
-w 计时是最被低估的功能——它能把请求的各阶段耗时打出来,是判断「慢在哪一层」的利器:
cat > /tmp/curl-format.txt <<'EOF'
DNS 解析: %{time_namelookup}s
TCP 连接: %{time_connect}s
TLS 握手: %{time_appconnect}s
开始传输: %{time_starttransfer}s
-------------------------
总计: %{time_total}s
状态码: %{http_code}
下载量: %{size_download} bytes
EOF
curl -w "@/tmp/curl-format.txt" -o /dev/null -s https://api.example.com/health
# DNS 解析: 0.012s
# TCP 连接: 0.045s <- 减去 DNS = 33ms 建连
# TLS 握手: 0.213s <- 减去 TCP = 168ms!TLS 握手很慢
# 开始传输: 0.421s <- 减去 TLS = 208ms 服务端处理
# -------------------------
# 总计: 0.423s
这一条命令就能把 423ms 分解到四层:DNS 12ms、建连 33ms、TLS 168ms、服务端 208ms。如果 TLS 占了 168ms,说明没开会话复用或证书链太长——不需要抓包就能定位到层。
# 一行版本,不用配置文件
curl -o /dev/null -s -w 'dns=%{time_namelookup} conn=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer} total=%{time_total}\n' URL
其他实用技巧:
# 强制走指定 IP(绕过 DNS,测试单个后端实例)
curl --resolve api.example.com:443:10.0.3.17 https://api.example.com/health
# ^^^^^^^^^ 比改 /etc/hosts 方便,且只影响这一次请求
# 通过 Unix socket(第 8 篇讲过)
curl --unix-socket /var/run/docker.sock http://localhost/containers/json
# 只看响应码(健康检查脚本用)
curl -s -o /dev/null -w '%{http_code}' URL
# 设置超时(脚本里必须加,否则可能永久挂起)
curl --connect-timeout 3 --max-time 10 URL
# 重试
curl --retry 3 --retry-delay 2 --retry-connrefused URL
**生产建议:脚本里的 curl 一律加 -s --connect-timeout N --max-time N。**不加超时的 curl 在网络异常时会挂住整个脚本或定时任务。
5.2 端口连通性:nc / telnet
# nc 测试 TCP 端口(推荐)
nc -zv 10.0.3.9 3306
# Connection to 10.0.3.9 3306 port [tcp/mysql] succeeded!
# -z 只扫描不发数据 -v 输出详情 -w 3 超时 3 秒
nc -zv -w 3 10.0.3.9 3306 2>&1 | grep -q succeeded && echo "通" || echo "不通"
# 批量扫端口
nc -zv 10.0.3.9 20-25 2>&1 | grep succeeded
# UDP
nc -zvu 10.0.3.9 53
# 起一个临时服务端(测试防火墙是否放通)
nc -l 9999 # 在服务端监听
nc 10.0.3.9 9999 # 在客户端连接,然后互相打字测试
# 传文件(应急用)
nc -l 9999 > received.tar # 接收端
nc 10.0.3.9 9999 < send.tar # 发送端
没有 nc 时用 bash 内置的 /dev/tcp(极简容器里很有用):
timeout 3 bash -c 'cat < /dev/null > /dev/tcp/10.0.3.9/3306' && echo "通" || echo "不通"
这个技巧在 distroless 镜像里是救命的——那些镜像里连 nc、curl、ping 都没有,但只要有 bash 就能测端口。
5.3 DNS:dig
# 基本查询
dig example.com
dig example.com +short # 只要结果 <- 脚本用
dig example.com A +short
dig example.com AAAA +short
dig example.com MX +short
# 指定 DNS 服务器(判断是否本地 DNS 有问题)
dig @8.8.8.8 example.com +short
dig @223.5.5.5 example.com +short
# 反向解析
dig -x 8.8.8.8 +short
# 看完整解析链路(排查 DNS 配置问题)
dig example.com +trace
# 看 TTL 和权威应答
dig example.com | grep -A2 "ANSWER SECTION"
# ;; ANSWER SECTION:
# example.com. 300 IN A 93.184.216.34
# ^^^ TTL 300 秒
排查 DNS 的标准顺序:
# ① 本地 DNS 配置
cat /etc/resolv.conf
# nameserver 10.0.0.2
# search default.svc.cluster.local svc.cluster.local
# options ndots:5 <- K8s 里这个值很关键,见下
# ② 用本地 DNS 查
dig example.com +short
# ③ 用公共 DNS 查(对比,判断是本地 DNS 的问题还是域名本身的问题)
dig @8.8.8.8 example.com +short
# ④ 如果结果不同 -> 本地 DNS 缓存/配置问题
# ⑤ 如果都查不到 -> 域名本身或权威 DNS 问题,用 +trace 看链路
坑:K8s 里
options ndots:5会导致大量无效的 DNS 查询。它的含义是「域名里的点少于 5 个就先尝试拼接 search 域」。所以查api.example.com(2 个点)会依次尝试api.example.com.default.svc.cluster.local、api.example.com.svc.cluster.local… 全部失败后才查真正的api.example.com——一次解析变成 5 次查询。解决办法是外部域名末尾加点(api.example.com.,表示绝对域名)或在 Pod 里配dnsConfig.options调小 ndots。
getent 反映的是应用真正看到的解析结果:
# dig 只查 DNS,getent 走完整的 NSS 流程(含 /etc/hosts、nscd 缓存)
getent hosts example.com
# 93.184.216.34 example.com
# 「dig 能解析但程序说找不到主机」时用这个
# 常见原因是 /etc/hosts 里有冲突条目,或 /etc/nsswitch.conf 配置异常
5.4 路径追踪
# traceroute 用 UDP,很多防火墙会挡
traceroute 8.8.8.8
# 改用 ICMP 或 TCP,成功率高得多
traceroute -I 8.8.8.8
traceroute -T -p 443 example.com # 用 TCP 443,最贴近真实业务路径
# mtr:traceroute + ping 的结合,持续统计每一跳的丢包率 <- 推荐
mtr -rwc 100 8.8.8.8
# -r 报告模式 -w 宽输出 -c 100 发 100 个包
# HOST Loss% Snt Last Avg Best Wrst StDev
# 1. 10.0.0.1 0.0% 100 0.5 0.6 0.4 1.2 0.1
# 2. 172.16.0.1 0.0% 100 1.2 1.4 1.1 3.2 0.3
# 3. 10.255.0.1 12.0% 100 25.3 28.1 22.1 89.2 9.8
# ^^^^^ 第 3 跳开始丢包
# 4. 8.8.8.8 0.0% 100 32.1 32.5 31.8 38.2 1.1
# ^^^^ 但终点不丢包!
关键判读:中间跳丢包但终点不丢包,是正常的——很多路由器对 ICMP 做了限速(deprioritize),不代表转发数据有问题。只有「终点丢包」或「从某一跳开始到终点全都丢包」才是真问题。
6. 实战:分层排查连通性
这是一套通用流程。核心原则:从下往上逐层排除,不要跳层猜。
场景:应用报 dial tcp 10.0.3.9:3306: i/o timeout。
# ① 物理/链路层:网卡状态
ip -br link show eth0
# eth0 UP <- 如果是 NO-CARRIER,到此为止,查网线/交换机
# ② 网络层:本机 IP 和路由
ip -br addr show eth0
ip route get 10.0.3.9
# 10.0.3.9 via 10.0.0.1 dev eth0 src 10.0.0.5
# ^^^^^^^^^^^^ 有路由,且网关正确
# 如果是 "Network is unreachable" -> 路由配置问题
# ③ 网络层连通性:ping
ping -c 3 -W 2 10.0.3.9
# 3 packets transmitted, 3 received, 0% packet loss <- IP 层通
# 注意:ping 不通【不一定】是网络问题,很多环境禁 ICMP,要结合 ④
# ④ 传输层:端口是否可达 —— 这一步最关键
nc -zv -w 3 10.0.3.9 3306
# nc: connect to 10.0.3.9 port 3306 (tcp) failed: Connection timed out
# 三种结果对应三种完全不同的原因:
# succeeded -> 端口通,问题在应用层(认证、协议、慢查询)
# Connection refused -> 【能到达但没人监听】:服务没起,或 accept 队列满(第 12 篇)
# timed out -> 【包被丢了】:防火墙 DROP、安全组、路由黑洞
refused 和 timeout 的区别是整个排查中信息量最大的一点:
Connection refused -> 包到了对端,对端内核回了 RST
-> 说明网络通,是服务端没监听那个端口
-> 去服务端查:ss -tlnp | grep 3306
Connection timed out -> 包发出去了但没有任何回应
-> 说明包在路上被静默丢弃了
-> 查防火墙(DROP 规则)、云安全组、rp_filter
# ⑤ 如果是 timeout,检查本机出站防火墙
iptables -L OUTPUT -n -v | head
sudo nft list ruleset 2>/dev/null | grep -A5 output
# ⑥ 到服务端确认(如果能登上去)
ss -tlnp | grep 3306
# LISTEN 0 4096 127.0.0.1:3306 <- 找到问题了!只监听了 127.0.0.1
# ^^^^^^^^^ 只在本地环回上监听,外部根本连不上
# 应该是 0.0.0.0:3306 或具体的内网 IP
# ⑦ 服务端入站防火墙
iptables -L INPUT -n -v | grep 3306
# 计数器为 0 -> 规则没命中,包可能在更前面就被丢了
# ⑧ 云环境别忘了安全组 / 网络 ACL
# 这是最容易忽略的一层 —— 主机上的 iptables 全通,但云平台的安全组挡着
「只监听 127.0.0.1」是极高频的原因,尤其在容器化之后——服务配置里写了 bind 127.0.0.1,同机访问正常,一旦跨容器/跨主机就不通。
# 快速确认所有服务的监听地址
ss -tlnp | awk 'NR>1 {print $4, $NF}'
# 127.0.0.1:3306 users:(("mysqld",pid=8821,fd=32)) <- 只本地
# 0.0.0.0:8080 users:(("myapp",pid=1234,fd=3)) <- 全网卡 ✅
# [::]:22 users:(("sshd",pid=823,fd=3))
如果排查到第 ④ 步是 succeeded(端口通)但应用还是超时,说明问题在应用层——这时候才需要抓包,见 network-21 抓包实战。按症状索引的完整排查手册见 network-22 故障排查手册。
规律:网络排查一律「网卡 → 路由 → ping → 端口 → 防火墙 → 监听地址 → 安全组」逐层往上,nc -zv 返回的 refused/timeout 决定了后面往哪个方向查。
7. 面试题
Q:为什么推荐用 ip 而不是 ifconfig?
ifconfig 属于 net-tools 包,已经停止维护十多年,很多现代发行版默认不安装。功能上也有实质差距:ifconfig 只显示网卡的第一个 IP(一个网卡配多个 IP 时会漏),看不到 IP 的 scope 和生命周期,不支持多路由表和策略路由——而这些在容器和 K8s 环境里都是常态。对应关系:ifconfig → ip addr/ip link,route → ip route,netstat → ss,arp → ip neigh。另外 ss 比 netstat 快得多,因为 netstat 是逐行解析 /proc/net/tcp,连接多时极慢,而 ss 通过 netlink 直接从内核批量取数据。
Q:路由表的匹配规则是什么?怎么确认一个目标 IP 会走哪条路由?
规则是最长前缀优先:先找出所有能匹配目标 IP 的路由,选掩码最长(最具体)的那条;前缀长度相同时选 metric 最小的。所以 10.0.0.0/24 会优先于 10.0.0.0/8,而 default(0.0.0.0/0)前缀最短,只在没有更具体的路由时才生效。确认方式不要人工推演,直接问内核:ip route get 8.8.8.8 会明确告诉你走哪个网关、从哪个网卡、用什么源 IP,无路由时直接返回 Network is unreachable。这是排查「访问不了某个 IP」的第一条命令。
Q:nc -zv 返回 Connection refused 和 Connection timed out 有什么区别?分别该查什么?
这个区别是整个网络排查中信息量最大的一点。refused 说明包成功到达了对端,对端内核发现没有进程监听该端口,回了 RST——所以网络链路是通的,问题在服务端:服务没启动、监听在错误的地址(最常见是只监听了 127.0.0.1)、或 accept 队列溢出。timed out 说明包发出去后没有任何回应,被静默丢弃了——查防火墙的 DROP 规则、云平台安全组、路由黑洞、以及多网卡下的 rp_filter 严格模式。规律:refused 往服务端查,timeout 往中间链路查。
Q:小请求正常、大请求超时,可能是什么原因?
典型的 MTU 不匹配导致的 PMTU 黑洞。客户端发大包(如 1500 字节),中间某跳的 MTU 更小(VPN 1420、K8s VXLAN 1450),需要分片但包设了 DF 位,路由器丢弃并回 ICMP Fragmentation Needed——如果防火墙把 ICMP 全 ban 了,客户端收不到这个通知,就会永远重传直到超时。而小包不超过最小 MTU,所以完全正常。排查用 ping -M do -s 1472 目标(不分片 + 指定大小),失败时内核会直接告诉你 mtu=1450。K8s 里这个问题特别常见:Pod 间走 VXLAN 封装每包多 50 字节,CNI 的 MTU 配成 1500 而底层也是 1500 就会溢出。
Q:iptables 的四表五链是什么?为什么规则写了不生效?
五链是数据包经过的位置:PREROUTING(刚进入,DNAT 在这里)、INPUT(目标是本机)、FORWARD(转发,容器流量走这里)、OUTPUT(本机发出)、POSTROUTING(即将离开,SNAT 在这里)。四表按功能分,优先级 raw > mangle > nat > filter。规则不生效的常见原因:① 链选错了——比如针对容器流量的规则写在 INPUT 里,但容器流量走的是 FORWARD;② 表选错了——DNAT 写在 POSTROUTING(路由决策已经做完,太晚了);③ 顺序问题——iptables 按顺序匹配、第一条命中即生效,如果前面已有 DROP,后面 -A 追加的 ACCEPT 永远执行不到,要用 -I 插到前面。排查最有效的手段是 iptables -L -n -v 看计数器——为 0 说明这条规则从未被命中。
Q:内网防火墙该用 DROP 还是 REJECT?
内网用 REJECT,对外用 DROP。DROP 是静默丢包,客户端要一直等到 TCP 超时(几十秒),而且完全分不清是防火墙拦了、路由不通、还是服务没起——这在内网是排查噩梦。REJECT 会回 ICMP 端口不可达,客户端立即得到 Connection refused,快速失败便于定位。对外网服务用 DROP 是为了隐蔽性,让端口扫描看起来像机器不存在,增加扫描成本和时间。
Q:怎么快速判断一个 HTTP 请求慢在哪一层?
用 curl -w 的计时变量,一条命令就能把总耗时分解到四层:
curl -o /dev/null -s -w 'dns=%{time_namelookup} conn=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer} total=%{time_total}\n' URL
各阶段是累计值,相邻两个相减才是该阶段的耗时:time_namelookup 是 DNS,time_connect - time_namelookup 是 TCP 建连,time_appconnect - time_connect 是 TLS 握手,time_starttransfer - time_appconnect 是服务端处理(TTFB)。比如 TLS 那段占了 168ms,说明没开会话复用或证书链太长;TTFB 占大头就是服务端慢。这一步不需要抓包就能定位到层,是排查慢请求的第一手段。
Q:CLOSE_WAIT 和 TIME_WAIT 堆积分别该怎么处理?
完全不同的两回事。TIME_WAIT 出现在主动关闭方,是 TCP 协议要求的正常状态(等 2MSL 约 60 秒,确保最后的 ACK 到达并让旧连接的残包消散)。堆积可以通过内核参数缓解:tcp_tw_reuse=1、加大 ip_local_port_range、更根本的是改用长连接和连接池。CLOSE_WAIT 出现在被动关闭方——对端已发 FIN,内核在等本地应用调用 close()。堆积纯粹是应用 bug,最常见是错误路径提前 return 漏了关闭资源。调任何内核参数都无效,因为 CLOSE_WAIT 没有超时机制,只能由应用 close() 结束。规律:看到 CLOSE_WAIT 去查代码,看到 TIME_WAIT 才考虑内核参数。
Q:K8s 里 DNS 解析很慢,可能是什么原因?
大概率是 /etc/resolv.conf 里的 options ndots:5。它的含义是「域名中的点少于 5 个时,先尝试拼接 search 域再查」。所以解析外部域名 api.example.com(只有 2 个点)会依次尝试 api.example.com.default.svc.cluster.local、api.example.com.svc.cluster.local、api.example.com.cluster.local,全部 NXDOMAIN 后才查真正的 api.example.com——一次解析放大成 4~5 次查询,高 QPS 下会把 CoreDNS 打满。解决办法:外部域名末尾加点写成绝对域名(api.example.com.),或在 Pod spec 里用 dnsConfig.options 把 ndots 调成 2,或者对固定的外部依赖直接用 hostAliases。另外排查时要注意 dig 只查 DNS,而 getent hosts 走完整的 NSS 流程(含 /etc/hosts 和缓存),「dig 能解析但程序报找不到主机」时要用 getent 对比。
上一篇:Linux-12 IO 模型与 epoll | 下一篇:Linux-14 启动流程与 systemd
xingliuhua