目录

Linux-13 网络配置与排查

前置阅读:Linux-12 IO 模型与 epoll

这一篇讲的是Linux 侧的网络配置和工具——网卡、路由、防火墙、以及排查连通性的命令行工具箱。

明确分工,避免重复:TCP/IP 协议本身、抓包分析、TCP 内核参数调优在 network 系列里已经写透了,本篇不重复,只在相关处给出链接:

1. ip 命令族:忘掉 ifconfig

ifconfigroutenetstatarp 属于 net-tools 包,已经停止维护十多年,很多现代发行版默认不再安装。取代它们的是 iproute2 提供的 ipss

旧命令 新命令
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 看不到

UPLOWER_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:查看连接状态

ssnetstat 快得多——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 里的 retransrtt 是判断网络质量的直接依据,比 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 镜像里是救命的——那些镜像里连 nccurlping 都没有,但只要有 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.localapi.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、安全组、路由黑洞

refusedtimeout 的区别是整个排查中信息量最大的一点

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 环境里都是常态。对应关系:ifconfigip addr/ip linkrouteip routenetstatssarpip neigh。另外 ssnetstat 快得多,因为 netstat 是逐行解析 /proc/net/tcp,连接多时极慢,而 ss 通过 netlink 直接从内核批量取数据。

Q:路由表的匹配规则是什么?怎么确认一个目标 IP 会走哪条路由?

规则是最长前缀优先:先找出所有能匹配目标 IP 的路由,选掩码最长(最具体)的那条;前缀长度相同时选 metric 最小的。所以 10.0.0.0/24 会优先于 10.0.0.0/8,而 default0.0.0.0/0)前缀最短,只在没有更具体的路由时才生效。确认方式不要人工推演,直接问内核ip route get 8.8.8.8 会明确告诉你走哪个网关、从哪个网卡、用什么源 IP,无路由时直接返回 Network is unreachable。这是排查「访问不了某个 IP」的第一条命令。

Q:nc -zv 返回 Connection refusedConnection 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=1450K8s 里这个问题特别常见: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,对外用 DROPDROP 是静默丢包,客户端要一直等到 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_WAITTIME_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.localapi.example.com.svc.cluster.localapi.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