目录

Linux-15 网络操作与排查:ip/ss 的正确用法、curl -w 定位耗时、DNS 链路与 ndots 陷阱

这一篇是上手部分里最实用的一篇:线上故障有很大比例是网络问题,而排查网络问题靠的是「分段定位」的能力

先看五个问题:

  1. ifconfig 为什么被废弃了?ip 命令怎么对应过去?
  2. ss -tlnp 里每个字母是什么意思?输出里的 Recv-Q/Send-Q 到底表示什么?
  3. 一个请求很慢,怎么判断慢在 DNS、TCP 握手、TLS,还是服务端处理?
  4. 容器里 ping 域名能通,但程序解析失败,为什么?
  5. tcpdump 抓到包了,为什么看不到 HTTP 内容?

1. 工具的换代:net-tools → iproute2

1.1 为什么 ifconfig 被废弃

ifconfigroutenetstatarp 属于 net-tools 包,诞生于 1990 年代,从 2001 年起就基本停止维护。它们被 iproute2ipss)取代的原因不是「新的更时髦」,而是老工具表达不了现代内核的能力

内核能力 net-tools iproute2
一个网卡多个 IP ⚠️ 只能靠 eth0:1 这种虚拟别名,且只显示第一个 ✅ 原生支持,全部显示
策略路由(多路由表) ❌ 完全不支持 ip rule / ip route table X
网络命名空间 ❌ 不支持(容器基础设施 ip netns
IPv6 ⚠️ 支持不完整 ✅ 完整
隧道/VLAN/bridge/veth ❌ 需要各种独立工具 ip link add type ... 统一管理
获取数据的方式 /proc文本解析,慢且易变 netlink 套接字(二进制,高效)
netstat 在万连接场景 ❌ 遍历 /proc/net/tcp 逐行解析,极慢 ss 走 netlink,快几十倍
# 最直观的性能差异(一台有几万连接的机器上)
time netstat -tan | wc -l          # real 0m8.2s
time ss -tan | wc -l               # real 0m0.1s     <- 快近百倍

现在多数发行版的最小安装已经不带 net-tools 了ifconfig: command not found),所以必须掌握新工具。

1.2 对应关系表

旧命令(net-tools) 新命令(iproute2)
ifconfig ip addr / ip a
ifconfig eth0 up/down ip link set eth0 up/down
ifconfig eth0 192.168.1.10/24 ip addr add 192.168.1.10/24 dev eth0
ifconfig eth0 mtu 9000 ip link set eth0 mtu 9000
route -n ip route / ip r
route add default gw 1.1.1.1 ip route add default via 1.1.1.1
arp -a ip neigh / ip n
netstat -tlnp ss -tlnp
netstat -s ss -s / nstat
netstat -i ip -s link
netstat -g ip maddr
iptunnel ip tunnel
ipmaddr ip maddr
vconfig ip link add link eth0 name eth0.10 type vlan id 10
brctl ip link add br0 type bridge

2. ip 命令

2.1 统一语法

ip [选项] 对象 命令 [参数]
#          ^^^^ addr / link / route / neigh / netns / rule / tunnel / maddr

全局选项(很实用但常被忽略):

ip -c a                    # ✅ -c 彩色输出(可读性大幅提升)
ip -br a                   # ✅ -br brief,一行一个,最适合快速查看
ip -j a                    # -j JSON 输出(脚本用)
ip -j -p a                 # -p pretty,格式化的 JSON
ip -4 a                    # 只看 IPv4
ip -6 a                    # 只看 IPv6
ip -s link                 # -s statistics,带收发包统计
ip -d link                 # -d details,详细信息(bridge/vlan 的参数)
ip -o a                    # -o oneline,把多行压成一行(便于 grep/awk)

# 推荐加进 ~/.bashrc
alias ipa='ip -c -br addr'
alias ipr='ip -c route'

2.2 ip addr:地址管理

ip a                                    # 查看所有网卡与地址
ip -br a                                # 简洁版
# lo      UNKNOWN  127.0.0.1/8 ::1/128
# eth0    UP       192.168.1.10/24 fe80::5054:ff:fe12:3456/64
# docker0 DOWN     172.17.0.1/16

ip a show eth0                          # 只看一个网卡
ip a show up                            # 只看 UP 的
ip -4 -br a                              # 只看 IPv4

# 完整输出的解读
ip a show eth0
# 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
# ^  ^^^^  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^ ^^^^^^^^^^^^^ ^^^^^^^^        ^^^^^^^^^
# 索引 网卡名  标志位                          MTU     队列规则       链路状态          队列长度
#     link/ether 52:54:00:12:34:56 brd ff:ff:ff:ff:ff:ff
#                ^^^^^^^^^^^^^^^^^ MAC 地址
#     inet 192.168.1.10/24 brd 192.168.1.255 scope global dynamic eth0
#          ^^^^^^^^^^^^^^^                    ^^^^^^^^^^^^ ^^^^^^^ 由 DHCP 分配
#        valid_lft 85543sec preferred_lft 85543sec       <- DHCP 租期
#
# 关键标志位:
#   UP        管理上启用了(你 ip link set up 了)
#   LOWER_UP  【物理层链路真的通了】(网线插了 / 虚拟网卡对端存在)
#   NO-CARRIER 网线没插 / 对端不存在
#   PROMISC   混杂模式(抓包时会出现)
# ── 增删地址(临时,重启失效)──
sudo ip addr add 192.168.1.20/24 dev eth0             # 加一个 IP(可以加多个!)
sudo ip addr add 192.168.1.21/24 dev eth0 label eth0:1   # 带别名(兼容老工具显示)
sudo ip addr del 192.168.1.20/24 dev eth0
sudo ip addr flush dev eth0                            # 清空该网卡所有地址
sudo ip addr add 10.0.0.1/32 dev lo                    # 给 lo 加 IP(做 VIP/anycast 常用)

2.3 ip link:网卡本身

ip link                                 # 所有网卡的链路层信息
ip -br link
ip -s link show eth0                    # ✅ 带收发统计(丢包排查第一步)
# 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 ...
#     RX:  bytes packets errors dropped  missed   mcast
#     123456789  987654      0       0       0    1234
#                            ^^^^^^ ^^^^^^^ ^^^^^^ 这三个非 0 就要深挖
#     TX:  bytes packets errors dropped carrier collsns
#     987654321  876543      0       0       0       0

sudo ip link set eth0 up / down          # 启用/禁用
sudo ip link set eth0 mtu 1400           # 改 MTU
sudo ip link set eth0 promisc on         # 混杂模式(抓包)
sudo ip link set eth0 address 52:54:00:aa:bb:cc   # 改 MAC
sudo ip link set eth0 txqueuelen 10000   # 改发送队列长度
sudo ip link set dev eth0 name eth1      # 改名(需先 down)

# 创建虚拟设备(容器网络的基础,第 35 篇细讲)
sudo ip link add br0 type bridge                              # 网桥
sudo ip link add veth0 type veth peer name veth1              # veth 对
sudo ip link add link eth0 name eth0.100 type vlan id 100     # VLAN
sudo ip link add dummy0 type dummy                            # 假网卡(测试用)
sudo ip link del veth0                                        # 删除
ip -d link show br0                     # -d 看 bridge 的详细参数

2.4 ip route:路由

ip route                                # 查看主路由表
ip r                                    # 简写
# default via 192.168.1.1 dev eth0 proto dhcp src 192.168.1.10 metric 100
# ^^^^^^^ ^^^ ^^^^^^^^^^^ ^^^^^^^^                ^^^^^^^^^^^^ ^^^^^^^^^^
# 默认路由 经过  网关地址    从哪个网卡出          源地址        优先级(小的优先)
# 192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.10
# 172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1

# ✅ 最有用的一条:查「到某个目标会走哪条路」
ip route get 8.8.8.8
# 8.8.8.8 via 192.168.1.1 dev eth0 src 192.168.1.10 uid 1000
#             ^^^^^^^^^^^ 会走这个网关     ^^^^^^^^^^^^ 用这个源 IP
ip route get 10.0.5.7                   # 排查「为什么访问不到某个内网段」
ip route get 8.8.8.8 from 10.0.0.5      # 指定源地址测试策略路由

# ── 增删路由 ──
sudo ip route add 10.10.0.0/16 via 192.168.1.254            # 加静态路由
sudo ip route add 10.10.0.0/16 dev eth1                     # 直连路由(无网关)
sudo ip route add default via 192.168.1.1                   # 加默认网关
sudo ip route del 10.10.0.0/16
sudo ip route replace default via 192.168.1.2               # 替换(不用先删)
sudo ip route flush cache                                    # 清路由缓存

# ── 策略路由(多网卡/多线路场景,ifconfig 时代做不到的事)──
ip rule                                 # 查看路由规则
# 0:      from all lookup local
# 32766:  from all lookup main
# 32767:  from all lookup default
ip route show table all                 # 所有路由表
ip route show table 100                 # 指定表
sudo ip route add default via 10.0.0.1 table 100
sudo ip rule add from 10.0.0.5 table 100 priority 1000
#                 ^^^^^^^^^^^^ 从这个源 IP 出去的流量走表 100
cat /etc/iproute2/rt_tables             # 表名与编号的映射

2.5 ip neigh:ARP 表

ip neigh                                # ARP/邻居表
# 192.168.1.1 dev eth0 lladdr 52:54:00:00:00:01 REACHABLE
#                                                ^^^^^^^^^ 状态
# 状态:REACHABLE 可达 / STALE 过期待验证 / DELAY 等待确认 /
#      PROBE 正在探测 / FAILED 解析失败 / PERMANENT 静态

sudo ip neigh flush all                 # 清空 ARP 表(排查 MAC 冲突/切换 VIP 后用)
sudo ip neigh flush dev eth0
sudo ip neigh add 192.168.1.5 lladdr 52:54:00:aa:bb:cc dev eth0 nud permanent  # 静态 ARP
sudo ip neigh del 192.168.1.5 dev eth0

# 排查 IP 冲突(同一个 IP 对应两个 MAC)
sudo arping -I eth0 -c 3 192.168.1.10
# 或者
sudo ip neigh | awk '{print $5}' | sort | uniq -d      # 找出重复的 MAC

2.6 ip netns:网络命名空间

这是容器网络的基础(第 35 篇细讲),这里给出手动操作的方式:

sudo ip netns add test                          # 创建命名空间
ip netns list                                    # 列出
sudo ip netns exec test ip a                     # ✅ 在命名空间里执行命令
sudo ip netns exec test bash                     # 进入一个 shell
sudo ip netns del test

# 把 veth 一端放进命名空间(容器网络的核心操作)
sudo ip link add veth0 type veth peer name veth1
sudo ip link set veth1 netns test
sudo ip netns exec test ip addr add 10.1.1.2/24 dev veth1
sudo ip netns exec test ip link set veth1 up
sudo ip netns exec test ip link set lo up
sudo ip addr add 10.1.1.1/24 dev veth0 && sudo ip link set veth0 up
sudo ip netns exec test ping -c1 10.1.1.1        # 通了

# 查看某个容器/进程的网络命名空间
sudo ls -l /proc/<PID>/ns/net
sudo nsenter -t <PID> -n ip a                    # ✅ 进入某进程的网络命名空间
sudo nsenter -t $(docker inspect -f '{{.State.Pid}}' mycontainer) -n ss -tlnp
#                                                                  ^^^^^^^^^^
#  ✅ 这招非常实用:容器里没有 ss/ip 命令时,用宿主机的工具查容器的网络

2.7 配置持久化

ip 命令的修改重启就丢,持久化要看发行版用的是哪套网络管理:

# 先确认在用哪个
systemctl is-active NetworkManager systemd-networkd networking
ls /etc/netplan/ /etc/NetworkManager/system-connections/ /etc/systemd/network/ 2>/dev/null

# ── Ubuntu 18.04+:netplan ──
sudo vim /etc/netplan/01-netcfg.yaml
# network:
#   version: 2
#   ethernets:
#     eth0:
#       addresses: [192.168.1.10/24]
#       routes:
#         - to: default
#           via: 192.168.1.1
#       nameservers:
#         addresses: [8.8.8.8, 1.1.1.1]
sudo netplan try                        # ✅ 试用(120 秒内不确认就自动回滚,防锁死)
sudo netplan apply

# ── RHEL/Rocky 8+:NetworkManager(nmcli)──
nmcli con show                          # 连接列表
nmcli dev status                         # 设备状态
sudo nmcli con mod eth0 ipv4.addresses 192.168.1.10/24 \
     ipv4.gateway 192.168.1.1 ipv4.dns "8.8.8.8 1.1.1.1" ipv4.method manual
sudo nmcli con up eth0
# 老式的 /etc/sysconfig/network-scripts/ifcfg-eth0 在 RHEL 9 已弃用

# ── systemd-networkd(容器宿主/精简系统常用)──
sudo tee /etc/systemd/network/10-eth0.network > /dev/null <<'EOF'
[Match]
Name=eth0
[Network]
Address=192.168.1.10/24
Gateway=192.168.1.1
DNS=8.8.8.8
EOF
sudo systemctl restart systemd-networkd

# ⚠️ 改网络配置前的保命措施(改错了 ssh 就断了)
sudo sh -c 'sleep 300 && reboot' &      # 5 分钟后自动重启(改成功了就 kill 掉这个)
# 或者用 screen/tmux 里执行,或者确保有 VNC/串口控制台能救

3. ss:看连接

3.1 参数

ss = socket statistics。

参数 全称 作用
-t --tcp TCP
-u --udp UDP
-x --unix Unix socket
-4 / -6 只看 IPv4 / IPv6
-l --listening 只看监听状态
-a --all 所有(含监听与非监听)
-n --numeric 不解析端口名与主机名(快得多,且不会卡在 DNS)
-r --resolve 解析主机名
-p --processes 显示占用的进程(需 root 才能看别人的)
-e --extended 扩展信息(uid、inode、cgroup)
-m --memory socket 内存占用(排查内存被 socket 吃掉)
-i --info TCP 内部信息(RTT、拥塞窗口、重传)
-o --options 计时器信息(keepalive、重传倒计时)
-s --summary 汇总统计(各状态的连接数)
-K --kill 强制关闭匹配的连接(需内核支持)
-Z / -z SELinux 上下文
-H 不显示表头(脚本用)

3.2 开篇第二问:ss -tlnp 与输出解读

sudo ss -tlnp
#         ||||
#         |||└─ p = processes  显示进程
#         ||└── n = numeric    不解析名字
#         |└─── l = listening  只看监听
#         └──── t = tcp        只看 TCP

# 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=5000,fd=3))
# LISTEN 0      511        127.0.0.1:6379       0.0.0.0:*     users:(("redis-server",pid=800,fd=6))
# LISTEN 0      4096            [::]:22            [::]:*     users:(("sshd",pid=900,fd=4))

Recv-QSend-Q 的含义在不同状态下完全不同,这是最容易误读的地方:

LISTEN 状态 ESTAB 状态
Recv-Q 当前全连接队列(accept 队列)里积压的连接数 收到但应用还没读走的字节数
Send-Q 全连接队列的最大长度backlog 已发送但对端还没 ACK 的字节数

这给了两个非常实用的诊断:

# ① LISTEN 行:Recv-Q 持续大于 0 = 应用 accept 不过来(accept 队列积压)
sudo ss -tln
# State  Recv-Q Send-Q Local Address:Port
# LISTEN 128    128          0.0.0.0:8080
#        ^^^ 队列满了!新连接会被丢弃或 RST -> 客户端表现为「连接超时/被拒绝」
# 排查:应用是不是卡住了(13 篇的 D 状态/死锁)、backlog 是不是设太小
cat /proc/sys/net/core/somaxconn         # 系统上限(默认 4096,老内核 128)
# Go 里 net.Listen 的 backlog 取 somaxconn,不能单独设
# 也看半连接队列(SYN 队列)溢出
nstat -az TcpExtListenOverflows TcpExtListenDrops
# TcpExtListenOverflows  1234    <- 非 0 且增长 = accept 队列溢出过
# TcpExtListenDrops      1234

# ② ESTAB 行:Recv-Q 大 = 应用读得慢;Send-Q 大 = 网络发不出去或对端不 ACK
sudo ss -tn state established
# Recv-Q Send-Q Local Address:Port  Peer Address:Port
# 524288 0      10.0.0.5:8080       10.0.0.9:52341
# ^^^^^^ 接收缓冲区堆积 512KB -> 应用处理不过来(业务逻辑慢、goroutine 阻塞)
# 0      1048576 10.0.0.5:8080      10.0.0.9:52342
#        ^^^^^^^ 发送缓冲区堆积 1MB -> 对端接收慢、网络拥塞、或对端假死

3.3 state 过滤

ss -tan state established           # 已建立
ss -tan state time-wait
ss -tan state syn-sent
ss -tan state syn-recv              # ✅ 大量 SYN-RECV = 可能被 SYN flood
ss -tan state close-wait            # ✅ 大量 CLOSE-WAIT = 应用没调 close()(代码 bug)
ss -tan state fin-wait-1
ss -tan state connected             # 所有已连接(除 listen/closed)
ss -tan state bucket                # time-wait + syn-recv(内核用小结构存的)
ss -tan state big                   # 其他状态

# 组合过滤(表达式很强大)
ss -tn 'sport = :8080'                      # 本地端口
ss -tn 'dport = :443'                       # 目标端口
ss -tn 'dst 10.0.0.0/8'                     # 目标网段
ss -tn 'src 10.0.0.5 and dport = :3306'
ss -tn '( dport = :80 or dport = :443 )'
ss -tn 'dst :443 and state established'
ss -tm 'sport = :8080'                      # 带内存占用

3.4 常用配方

# ── 端口占用(最高频)──
sudo ss -tlnp | grep :8080                  # 谁占了 8080
sudo ss -tulnp                              # 所有监听的 TCP+UDP 端口
sudo ss -tlnp sport = :8080
sudo lsof -i :8080                          # 另一种方式(更慢)
sudo fuser -n tcp 8080                      # 只输出 PID

# ── 连接状态统计(排查连接泄漏、TIME_WAIT 堆积)──
ss -s                                       # 汇总
# Total: 1234
# TCP:   567 (estab 234, closed 300, orphaned 0, timewait 298)
ss -tan | awk 'NR>1 {print $1}' | sort | uniq -c | sort -rn
#   298 TIME-WAIT
#   234 ESTAB
#    12 CLOSE-WAIT        <- ⚠️ 这个多了是代码 bug
#     5 LISTEN

# ── 按对端 IP 统计(找出谁的连接最多)──
ss -tn state established | awk 'NR>1 {print $5}' | cut -d: -f1 | sort | uniq -c | sort -rn | head
# ✅ 排查「谁在打我」或「哪个下游依赖连接数异常」

# ── 某个进程的所有连接 ──
sudo ss -tnp | grep 'pid=5000'
sudo ss -tanp | grep myapp | wc -l          # 连接数
ls /proc/5000/fd | wc -l                    # 或者数 fd(13 篇)

# ── TCP 连接质量(RTT、重传、拥塞窗口)──
sudo ss -tin dst 10.0.0.9
# ESTAB 0 0 10.0.0.5:8080 10.0.0.9:52341
#     cubic wscale:7,7 rto:204 rtt:2.5/0.5 mss:1448 cwnd:10 bytes_sent:12345
#     bytes_acked:12345 segs_out:100 segs_in:98 retrans:0/3 rcv_rtt:4 rcv_space:29200
#     ^^^^^ 拥塞算法  ^^^^^^^^ 超时重传时间(ms)  ^^^^^^^^^ RTT/抖动  ^^^^^^^ 拥塞窗口
#                                                                  ^^^^^^^^^ 重传次数
# rtt 大 -> 链路延迟高;retrans 多 -> 丢包;cwnd 小 -> 吞吐受限

# ── 强制关闭连接(谨慎)──
sudo ss -K dst 10.0.0.9                     # 关闭到该 IP 的所有连接
sudo ss -K 'dport = :3306'
# 用途:某个下游挂了导致连接卡死,且不想重启服务时

# ── netstat 的对应写法(老机器上) ──
netstat -tlnp        ->  ss -tlnp
netstat -an          ->  ss -an
netstat -s           ->  nstat  或  ss -s
netstat -r           ->  ip route
netstat -i           ->  ip -s link
netstat -tanp | grep :80  ->  ss -tanp 'sport = :80'

4. 连通性与路径排查

4.1 ping

ping -c 4 8.8.8.8                  # -c 发 4 个包就停(不加会一直发)
ping -i 0.2 -c 20 host             # -i 间隔 0.2 秒(<0.2 需要 root)
ping -W 1 -c 1 host                # -W 单个包的超时(秒)
ping -w 5 host                     # -w 整体超时
ping -s 1400 host                  # -s 指定包大小(测 MTU)
ping -q -c 100 host                # -q 只显示汇总统计
ping -D -c 3 host                  # -D 每行加时间戳
ping -f host                       # ⚠️ flood ping(需 root,压测用,慎用)
ping -I eth0 host                  # 指定出口网卡
ping -4 / -6 host                  # 强制协议版本

# ✅ 测 MTU(排查「小包能通大包不通」的经典问题)
ping -M do -s 1472 -c 2 8.8.8.8    # -M do = 禁止分片;1472 + 28 = 1500
# ping: local error: message too long, mtu=1500     <- 说明路径 MTU 小于 1500
# 二分法找出实际 MTU
for s in 1472 1452 1422 1400 1372; do
  ping -M do -s $s -c 1 -W 1 8.8.8.8 >/dev/null 2>&1 && echo "MTU >= $((s+28))" || echo "MTU < $((s+28))"
done
# 常见场景:VPN/隧道(GRE/IPsec/VXLAN)会减小有效 MTU,
#          导致「ssh 能连上但 scp 大文件卡住」「HTTP 小请求正常大响应超时」

ping 不通不代表网络不通

很多云厂商、防火墙默认屏蔽 ICMP。所以 ping 失败要交叉验证:
   ping    -> ICMP 协议
   nc -zv  -> 真正的 TCP 连接(更可信)
   curl    -> 应用层

4.2 traceroute 与 mtr

traceroute 8.8.8.8                 # 默认用 UDP 探测
traceroute -I 8.8.8.8              # 用 ICMP(更接近 ping 的路径)
sudo traceroute -T -p 443 host      # ✅ 用 TCP 探测指定端口(防火墙环境下最可靠)
traceroute -n host                  # 不解析域名(快)
traceroute -m 15 host               # 最大跳数
traceroute -q 1 host                # 每跳只发 1 个包

# ✅ mtr:traceroute + ping 的结合,持续统计每跳的丢包与延迟
mtr -n 8.8.8.8                      # 交互式
mtr -n -c 100 -r 8.8.8.8            # ✅ 报告模式,发 100 个包后输出报告
mtr -n -T -P 443 -c 50 -r host      # TCP 模式测 443
# HOST                Loss%   Snt   Last   Avg  Best  Wrst StDev
# 1. 192.168.1.1       0.0%   100    0.5   0.6   0.4   1.2   0.1
# 2. 10.0.0.1         20.0%   100    5.2   5.5   4.8  12.3   1.1      <- 中间跳丢包
# 3. 8.8.8.8           0.0%   100   12.1  12.3  11.8  15.2   0.5      <- 但终点不丢!

关键认知:中间跳的丢包/高延迟往往不代表问题。

因为路由器对 ICMP TTL 超时响应的优先级很低(甚至限速),忙的时候会丢弃或延迟回复,但转发正常流量毫无问题。判断规则:

只看【最后一跳】(真正的目标)的 Loss% 和 Avg。
中间跳的丢包只有在【它之后的所有跳都持续丢包】时才有意义。

4.3 端口连通性

# ── nc(netcat)——推荐 ──
nc -zv host 8080                    # ✅ -z 只扫描不发数据,-v 显示结果
# Connection to host 8080 port [tcp/*] succeeded!
nc -zv -w 3 host 8080               # -w 超时 3 秒
nc -zv host 20-25                   # 扫端口范围
nc -zvu host 53                     # -u 测 UDP(结果不可靠,UDP 无连接)

# 其他 nc 用法
nc -l 9000                          # 监听 9000(临时服务端,测试连通性)
nc -l 9000 > received.bin           # 接收文件
nc host 9000 < send.bin             # 发送文件
echo "GET / HTTP/1.0" | nc host 80  # 手动发 HTTP 请求
nc -U /var/run/docker.sock          # 连 Unix socket
# ⚠️ nc 有多个实现(OpenBSD/GNU/Nmap),参数不完全兼容,脚本里注意

# ── 其他方式 ──
timeout 3 bash -c '</dev/tcp/host/8080' && echo open || echo closed
#                  ^^^^^^^^^^^^^^^^^^^ bash 内置的 /dev/tcp(不需要装任何工具!)
curl -sS --connect-timeout 3 telnet://host:8080 && echo open
telnet host 8080                    # 老式,Ctrl-] 然后 quit 退出
nmap -p 8080 host                   # 需要安装,功能最全
nmap -sT -p 1-1000 host             # TCP 连接扫描

5. DNS 解析链路

这一章解释开篇第四问,也是容器时代最容易出问题的一环。

5.1 完整链路

   程序调用 getaddrinfo("api.example.com")
              |
              v
   ① /etc/nsswitch.conf     决定按什么顺序查
      hosts: files dns myhostname
             ^^^^^ 先查文件   ^^^ 再查 DNS
              |
              v
   ② /etc/hosts             静态映射(命中就直接返回,不查 DNS)
              |
              v
   ③ /etc/resolv.conf       DNS 服务器地址、search 域、ndots
              |
              v
   ④ 本地 DNS 缓存/存根      systemd-resolved(127.0.0.53) / dnsmasq / nscd
              |
              v
   ⑤ 上游 DNS 服务器         递归查询 -> 根 -> TLD -> 权威

关键点:这条链路只对「使用 libc 的 getaddrinfo」的程序有效。 而 Go 在 CGO_ENABLED=0 时用纯 Go 实现的解析器,它只读 /etc/hosts/etc/resolv.conf,完全跳过 nsswitch.conf 和 NSS 模块(01 篇 2.3 提过)——这就是开篇第四问的答案。

# 验证 Go 用的是哪个解析器
GODEBUG=netdns=2 ./myapp 2>&1 | head
# go package net: hostLookupFilesDNS               <- 纯 Go
# go package net: cgo resolver                     <- 走 libc

GODEBUG=netdns=go  ./myapp          # 强制纯 Go
GODEBUG=netdns=cgo ./myapp          # 强制走 libc(需要编译时启用 CGO)

# 差异的实际后果
# ① nsswitch.conf 里配了 ldap/mdns/nis 等 NSS 模块 -> 纯 Go 解析器不认
# ② 某些 /etc/hosts 的边界行为不同
# ③ 纯 Go 解析器对 search 域和 ndots 的处理曾有细微差异(现已基本对齐)
# ④ musl(Alpine)的 DNS 实现也有自己的差异(不支持 search 的某些用法、
#    早期版本对 >512 字节响应的 TCP 回退处理不同)

5.2 resolv.conf 与 ndots 陷阱

cat /etc/resolv.conf
# nameserver 127.0.0.53          <- systemd-resolved 的本地存根
# options edns0 trust-ad
# search example.com

# 字段含义
# nameserver   DNS 服务器(最多 3 个,按顺序尝试)
# search       ✅ 搜索域:查询不完整域名时自动追加这些后缀
# domain       单个搜索域(已被 search 取代)
# options ndots:N       ✅ 域名中点的数量【小于 N】时,先拼 search 域再查
# options timeout:N     单次查询超时(默认 5 秒)
# options attempts:N    重试次数(默认 2)
# options rotate        轮询多个 nameserver(默认总是先用第一个)
# options single-request 顺序发 A 和 AAAA 查询(并发有问题时用)

K8s 里的 ndots:5 陷阱(这是很多「服务间调用偶发延迟」的根因):

# Pod 里的 resolv.conf
cat /etc/resolv.conf
# search myns.svc.cluster.local svc.cluster.local cluster.local
# nameserver 10.96.0.10
# options ndots:5
#                ^^ 关键:域名里的点少于 5 个,就【先尝试拼接 search 域】

# 于是访问 api.example.com(2 个点,小于 5)会依次查询:
#   ① api.example.com.myns.svc.cluster.local    -> NXDOMAIN
#   ② api.example.com.svc.cluster.local          -> NXDOMAIN
#   ③ api.example.com.cluster.local              -> NXDOMAIN
#   ④ api.example.com                            -> ✅ 终于成功
# 4 次查询,前 3 次全是无效的!而且每次还可能同时发 A 和 AAAA -> 实际 8 次 DNS 请求
# 后果:外部域名解析延迟高、CoreDNS 负载被无效查询打满

# ✅ 解法一:域名末尾加点(表示「这是完全限定域名,别拼 search」)
curl https://api.example.com./path
# Go 代码里:http.Get("https://api.example.com./path")

# ✅ 解法二:给 Pod 单独降低 ndots
# spec:
#   dnsConfig:
#     options:
#       - name: ndots
#         value: "2"

# ✅ 解法三:部署 NodeLocal DNSCache(节点级 DNS 缓存)

# 验证解析次数
sudo tcpdump -i any -n port 53 &
curl -s https://api.example.com > /dev/null
# 能数出实际发了几个 DNS 查询

5.3 dig:DNS 排查的主力

dig example.com                     # 完整输出
dig +short example.com              # ✅ 只要结果
dig +short example.com A            # 指定记录类型
dig example.com AAAA / MX / NS / TXT / SOA / CNAME / SRV / PTR / CAA
dig -x 8.8.8.8                      # ✅ 反向解析(PTR)
dig @8.8.8.8 example.com            # ✅ 指定 DNS 服务器(绕过本地配置测试)
dig @10.96.0.10 svc.myns.svc.cluster.local    # 直接问 K8s 的 CoreDNS
dig +trace example.com              # ✅ 完整追踪:根 -> TLD -> 权威(排查 NS 配置)
dig +norecurse @8.8.8.8 example.com # 不递归(看缓存里有没有)
dig +noall +answer example.com      # 只显示 answer 段
dig +stats example.com              # 显示查询耗时
dig +tcp example.com                # 用 TCP(测试 >512 字节响应)
dig +dnssec example.com             # 查 DNSSEC 签名
dig -f domains.txt +short           # 批量查询

# 输出解读
dig example.com
# ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 12345
#                                 ^^^^^^^^^^^^^ NOERROR=成功 NXDOMAIN=域名不存在
#                                               SERVFAIL=服务器故障 REFUSED=被拒绝
# ;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
#           ^^ ^^ ^^ qr=响应 rd=期望递归 ra=服务器支持递归 aa=权威答案
# ;; ANSWER SECTION:
# example.com.        300     IN      A       93.184.216.34
#                     ^^^ TTL(缓存秒数)        ^^^^^^^^^^^^^ 结果
# ;; Query time: 23 msec              <- ✅ 解析耗时
# ;; SERVER: 127.0.0.53#53(127.0.0.53)    <- 实际问的谁
# ── 其他工具 ──
host example.com                    # 简洁
host -t MX example.com
nslookup example.com                # 老式,交互式(输出格式不适合脚本)
nslookup example.com 8.8.8.8

# ✅ getent hosts:走完整的 NSS 链路(与程序的行为一致!)
getent hosts example.com
getent ahosts example.com           # 显示所有地址与协议族
# 【重要区别】
#   dig          -> 只查 DNS,不看 /etc/hosts,不走 nsswitch
#   getent hosts -> 走完整 NSS 链路(hosts 文件 + DNS + 其他模块)
# 所以「dig 能解析但程序不行」或反过来,用 getent 对比就能定位是哪一层的问题

# systemd-resolved 专用
resolvectl status                    # ✅ 看每个网卡实际用的 DNS 服务器
resolvectl query example.com         # 通过 resolved 查询
resolvectl statistics                # 缓存命中率
sudo resolvectl flush-caches         # ✅ 清 DNS 缓存
resolvectl dns                       # 只看 DNS 服务器列表

5.4 DNS 排查流程

# ① 先确认是不是 DNS 问题(用 IP 直连能否成功)
curl -sS -o /dev/null -w '%{http_code}\n' http://93.184.216.34/ -H 'Host: example.com'
curl --resolve example.com:443:93.184.216.34 https://example.com/   # ✅ 更方便

# ② 分层验证
getent hosts example.com            # 程序视角(走 NSS)
dig +short example.com              # 纯 DNS 视角
dig +short @8.8.8.8 example.com     # 绕过本地 DNS
grep example.com /etc/hosts          # 有没有被 hosts 覆盖

# ③ 看配置
cat /etc/resolv.conf
cat /etc/nsswitch.conf | grep hosts
resolvectl status

# ④ 看解析耗时
dig example.com | grep 'Query time'
curl -w 'dns=%{time_namelookup}\n' -o /dev/null -sS https://example.com

# ⑤ 抓包看实际发了什么
sudo tcpdump -i any -n -s0 port 53
# 能看到实际查询的域名(含 search 域拼接的结果)、用了哪个服务器、有没有超时重试

# ⑥ 容器里的额外检查
kubectl exec pod -- cat /etc/resolv.conf
kubectl exec pod -- getent hosts myservice
kubectl -n kube-system logs -l k8s-app=kube-dns --tail=50     # CoreDNS 日志
kubectl -n kube-system top pod -l k8s-app=kube-dns            # CoreDNS 是否过载

6. curl:把一次请求拆成六段

6.1 开篇第三问:-w 定位耗时

这是排查「接口慢」最有效的单个技巧:

curl -o /dev/null -sS -w '
DNS 解析:      %{time_namelookup}s
TCP 握手完成:  %{time_connect}s
TLS 握手完成:  %{time_appconnect}s
请求发出前:    %{time_pretransfer}s
首字节到达:    %{time_starttransfer}s
总耗时:        %{time_total}s
--
重定向耗时:    %{time_redirect}s
HTTP 状态:     %{http_code}
下载大小:      %{size_download} bytes
下载速度:      %{speed_download} B/s
本地端口:      %{local_port}
远端 IP:       %{remote_ip}:%{remote_port}
' https://api.example.com/health

这些时间是「从开始到该阶段完成」的累计值,相减才是每段的耗时:

0 ────────────────────────────────────────────────────────────> 时间

|--- DNS ---|--- TCP ---|--- TLS ---|-- 服务端处理 --|-- 传输 --|
0        namelookup   connect   appconnect     starttransfer   total
                                 pretransfer

每段耗时 =
   DNS 解析     = time_namelookup
   TCP 握手     = time_connect      - time_namelookup
   TLS 握手     = time_appconnect   - time_connect
   服务端处理   = time_starttransfer - time_pretransfer     ← 【最关键】
   内容传输     = time_total        - time_starttransfer

每一段慢分别意味着什么

慢在哪 典型原因 下一步排查
DNS(>100ms) DNS 服务器慢/超时重试、ndots 导致多次无效查询、没有缓存 dig +statstcpdump port 53 数查询次数
TCP 握手 网络 RTT 高、丢包导致 SYN 重传、服务端 accept 队列满 mtrss -tln 看 Recv-Q、nstat 看 ListenOverflows
TLS 握手 证书链长、OCSP 装订缺失、密码套件协商慢、不支持会话复用 openssl s_client -connect host:443
服务端处理 应用慢(DB 查询、下游调用、GC) 应用侧 tracing/pprof
内容传输 响应体大、带宽不足、TCP 窗口小 size_downloadspeed_download
# 把格式存成文件复用
cat > ~/.curl-format <<'EOF'
    time_namelookup:  %{time_namelookup}s\n
       time_connect:  %{time_connect}s\n
    time_appconnect:  %{time_appconnect}s\n
   time_pretransfer:  %{time_pretransfer}s\n
      time_redirect:  %{time_redirect}s\n
 time_starttransfer:  %{time_starttransfer}s\n
                    ----------\n
         time_total:  %{time_total}s\n
EOF
curl -w '@/home/lhx/.curl-format' -o /dev/null -sS https://api.example.com

# 连续测 10 次看波动(判断是偶发还是稳定慢)
for i in $(seq 10); do
  curl -o /dev/null -sS -w '%{time_namelookup} %{time_connect} %{time_starttransfer} %{time_total}\n' \
       https://api.example.com
done | column -t

6.2 curl 常用参数

参数 作用
-o FILE / -O 保存到文件 / 用远程文件名
-s 静默(不显示进度条)
-sS 静默但仍显示错误(脚本里推荐这个组合)
-v 显示完整的请求/响应头与 TLS 握手过程
--trace-ascii - -v 更详细(含数据内容)
-I 只发 HEAD 请求(只要响应头)
-L 跟随重定向
--max-redirs N 限制重定向次数
-H 'K: V' 加请求头(可多次)
-d 'data' POST 数据(自动变成 POST)
-d @file.json 从文件读 body
--data-urlencode URL 编码
-F 'file=@x.png' multipart 上传
-X METHOD 指定方法
-u user:pass Basic 认证
--resolve host:port:IP 强制把域名解析到指定 IP(绕过 DNS 测试后端)
--connect-to 类似但更灵活
-x http://proxy:8080 用代理
--noproxy '*' 忽略环境变量里的代理
--connect-timeout N 连接超时
-m N / --max-time 整体超时(脚本必加,否则可能永久挂起)
--retry N 失败重试
--retry-delay / --retry-max-time 重试策略
-k / --insecure 跳过证书校验(调试用,生产禁止
--cacert / --cert / --key 自定义 CA 与客户端证书(mTLS)
--compressed 声明支持压缩并自动解压
--http1.1 / --http2 / --http3 强制协议版本
-4 / -6 强制 IP 版本
--interface eth1 指定出口网卡
-w FORMAT 输出格式化的统计信息
-c/-b cookies.txt 保存/读取 cookie
--unix-socket /path 通过 Unix socket 请求(调试 docker.sock 常用
# ── 排查配方 ──

# ① 绕过 DNS 直接测某台后端(灰度/故障隔离时极有用)
curl --resolve api.example.com:443:10.0.0.5 https://api.example.com/health -v
# 好处:Host 头、SNI、证书校验都是正常的,只是把解析结果换掉了

# ② 看完整的握手与头部
curl -v https://api.example.com 2>&1 | head -40
# * Connected to api.example.com (93.184.216.34) port 443
# * TLSv1.3 (OUT), TLS handshake, Client hello
# * SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
# * Server certificate: subject: CN=example.com
# *  expire date: Dec 25 00:00:00 2026 GMT          <- 证书过期时间
# > GET / HTTP/2
# < HTTP/2 200

# ③ 只看响应头与状态码
curl -sSI https://api.example.com | head -1
curl -o /dev/null -sS -w '%{http_code}\n' https://api.example.com

# ④ 测试 Unix socket(Docker API、某些服务的管理端口)
curl --unix-socket /var/run/docker.sock http://localhost/containers/json | jq .
curl --unix-socket /run/myapp/myapp.sock http://localhost/metrics

# ⑤ 脚本里的健壮写法
curl -fsS --connect-timeout 5 -m 30 --retry 3 --retry-delay 2 \
     -H 'Content-Type: application/json' -d @payload.json \
     https://api.example.com/v1/x
#     ^^ -f 让 HTTP 4xx/5xx 返回非零退出码(默认 curl 只要拿到响应就算成功!)

# ⑥ 检查证书
curl -vI https://example.com 2>&1 | grep -E 'expire|subject|issuer'
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
     | openssl x509 -noout -dates -subject -issuer

7. tcpdump

7.1 基本语法

sudo tcpdump [选项] [过滤表达式]
参数 作用
-i IFACE 指定网卡(-i any 抓所有
-n 不解析主机名(必加,否则会卡在 DNS 反查)
-nn 连端口名也不解析
-c N 抓 N 个包后停止(生产上必加,防止刷屏/写爆磁盘)
-w FILE 写入 pcap 文件(给 Wireshark 分析)
-r FILE 读取 pcap 文件
-s N 每个包抓多少字节(-s 0 表示完整抓,新版默认已是完整)
-A ASCII 显示包内容(看 HTTP 明文)
-X / -XX 十六进制 + ASCII 显示
-v / -vv / -vvv 详细程度
-e 显示链路层头(MAC 地址)
-t / -tt / -ttt 时间戳格式(无/Unix 时间/相对上一个包)
-q 简洁输出
-l 行缓冲(配合管道给 grep 时必加,见 05 篇缓冲一章)
-C N 文件达到 N MB 就切分
-W N 最多保留 N 个文件(配合 -C 做滚动抓包)
-G N 每 N 秒切一个文件
-Z user 抓包后降权到指定用户
-D 列出可用网卡

7.2 过滤表达式(BPF)

# ── 按主机 ──
tcpdump -ni any host 10.0.0.5
tcpdump -ni any src host 10.0.0.5
tcpdump -ni any dst host 10.0.0.5
tcpdump -ni any net 10.0.0.0/8

# ── 按端口 ──
tcpdump -ni any port 443
tcpdump -ni any src port 8080
tcpdump -ni any portrange 8000-8100
tcpdump -ni any 'tcp port 80 or tcp port 443'

# ── 按协议 ──
tcpdump -ni any tcp / udp / icmp / arp
tcpdump -ni any 'ip6'

# ── 组合(and / or / not)──
tcpdump -ni any 'host 10.0.0.5 and port 3306'
tcpdump -ni any 'src 10.0.0.5 and not port 22'      # ✅ 排除 ssh(否则抓到自己的流量)
tcpdump -ni any '(port 80 or port 443) and host 10.0.0.5'

# ── 按 TCP 标志位(排查连接建立/重置问题)──
tcpdump -ni any 'tcp[tcpflags] & tcp-syn != 0'                    # 所有 SYN
tcpdump -ni any 'tcp[tcpflags] & (tcp-syn|tcp-ack) == tcp-syn'    # 只有 SYN(新连接请求)
tcpdump -ni any 'tcp[tcpflags] & tcp-rst != 0'                    # ✅ RST(连接被重置)
tcpdump -ni any 'tcp[tcpflags] & tcp-fin != 0'                    # FIN
# 排查「connection reset by peer」:抓 RST 看是谁发的

# ── 按包大小 ──
tcpdump -ni any 'greater 1000'
tcpdump -ni any 'less 100'

# ── 抓 HTTP 请求行(匹配包内容的字节)──
tcpdump -ni any -A 'tcp port 80 and tcp[((tcp[12:1] & 0xf0) >> 2):4] = 0x47455420'
#                                                                      ^^^^^^^^^^ "GET "

7.3 常用配方

# ✅ 生产上的安全写法(限制包数 + 不解析 + 写文件)
sudo tcpdump -ni eth0 -c 1000 -w /tmp/cap.pcap 'host 10.0.0.9 and port 3306'
sudo tcpdump -ni eth0 -s0 -w /tmp/cap-%Y%m%d-%H%M%S.pcap -G 60 -W 10 'port 443'
#                                                        ^^^^^ 每 60 秒一个文件,最多 10 个

# 分析已抓的包
tcpdump -nr /tmp/cap.pcap | head -50
tcpdump -nr /tmp/cap.pcap 'tcp[tcpflags] & tcp-rst != 0'      # 只看 RST
tcpdump -nnr /tmp/cap.pcap -A | grep -A5 'HTTP/'

# 实时看某个 IP 的交互(配合 -l 和 grep)
sudo tcpdump -ni any -l 'host 10.0.0.9' | grep -i 'flags \[R'

# 排查连接被重置
sudo tcpdump -ni any -c 100 'tcp[tcpflags] & tcp-rst != 0'
# 15:30:01.123 IP 10.0.0.9.3306 > 10.0.0.5.52341: Flags [R.], seq 1, ack 1
#                 ^^^^^^^^^^^^ 是【对端】发的 RST -> 服务端主动拒绝/关闭

# 排查 DNS
sudo tcpdump -ni any -s0 port 53
# 能看到实际查了哪些域名(含 search 域拼接)、超时重试

# 排查 MTU / 分片
sudo tcpdump -ni any 'ip[6:2] & 0x3fff != 0'          # 有分片的包

# 只看握手过程
sudo tcpdump -ni any -c 20 'tcp[tcpflags] & (tcp-syn|tcp-fin|tcp-rst) != 0'

# 容器里抓包(容器内通常没有 tcpdump)
sudo nsenter -t $(docker inspect -f '{{.State.Pid}}' mycontainer) -n \
     tcpdump -ni any -c 100 -w /tmp/container.pcap
# ✅ 用宿主机的 tcpdump,进入容器的网络命名空间

7.4 开篇第五问:为什么看不到 HTTP 内容

三个原因,按概率排序:

# ① 【最常见】流量是 HTTPS,内容加密了
sudo tcpdump -ni any -A port 443
# 只能看到 TLS 握手的明文部分(SNI 里有域名),应用数据全是密文
# ✅ 解法:
#   - 在应用侧抓(如 nginx 的 access_log、应用的 debug 日志)
#   - 用 mitmproxy 做中间人(测试环境)
#   - 导出 TLS 密钥给 Wireshark 解密(SSLKEYLOGFILE 环境变量,仅浏览器/curl 支持)
curl --insecure -v https://x 2>&1     # 或者直接用 curl -v 看明文

# ② 抓包长度被截断(老版本 tcpdump 默认 snaplen 只有 68/96 字节)
sudo tcpdump -ni any -s 0 -A port 80          # ✅ -s 0 = 完整抓包
#                        ^^^^ 新版默认已是 262144,但脚本里显式写更保险

# ③ 数据分散在多个 TCP 包里,单个包看不出完整内容
# ✅ 解法:抓成文件用 Wireshark 的 "Follow TCP Stream"
sudo tcpdump -ni any -s0 -w /tmp/http.pcap port 80
wireshark /tmp/http.pcap        # 右键 -> Follow -> TCP Stream
# 命令行替代
tcpflow -r /tmp/http.pcap -C    # 重组 TCP 流
tshark -r /tmp/http.pcap -q -z follow,tcp,ascii,0

tcpdump 的性能影响:抓包会把包复制一份到用户态,高流量下会明显消耗 CPU 并可能丢包。生产上务必:加过滤表达式缩小范围-c 限制数量-w 写文件而不是终端输出(格式化输出比写文件贵得多)。抓完检查是否丢包:

sudo tcpdump -ni eth0 -c 10000 -w /tmp/x.pcap port 443
# 10000 packets captured
# 12345 packets received by filter
# 2345 packets dropped by kernel        <- ⚠️ 丢了,说明抓不过来
# 对策:加 -s 96 只抓头部、缩小过滤范围、增大 -B 缓冲区

8. 网络统计与性能

8.1 丢包排查的分层

# ① 网卡层(硬件/驱动)
ip -s link show eth0
#     RX: bytes packets errors dropped missed mcast
#              ^^^^^^ ^^^^^^^ ^^^^^^ 非 0 且增长就有问题
ethtool -S eth0 | grep -iE 'drop|err|miss|discard'      # ✅ 网卡的详细统计
ethtool -g eth0                     # 环形缓冲区大小
sudo ethtool -G eth0 rx 4096 tx 4096    # 调大(丢包时的常见处理)
ethtool -k eth0                     # 卸载功能(GRO/TSO/LRO)
ethtool eth0                        # 速率/双工/link 状态

# ② 内核协议栈层
nstat -az | grep -iE 'drop|retrans|overflow|prune|listen'
# TcpExtListenOverflows    1234     <- accept 队列溢出
# TcpExtListenDrops        1234
# TcpExtTCPBacklogDrop     567      <- socket 接收队列满导致丢包
# TcpRetransSegs           8901     <- 重传(网络质量)
# TcpExtPruneCalled        12       <- 内存不足导致丢包
netstat -s | grep -iE 'listen|overflow|retrans'         # 老式写法
cat /proc/net/softnet_stat          # 每 CPU 的软中断处理统计(第 2 列非 0 = 丢包)

# ③ 应用层
ss -tln                             # Recv-Q(accept 队列积压)
ss -tn state established            # Recv-Q(应用读得慢)

# ④ 常见调优参数(改前要理解含义,别抄)
sysctl net.core.somaxconn                       # accept 队列上限(默认 4096)
sysctl net.ipv4.tcp_max_syn_backlog             # SYN 队列
sysctl net.core.netdev_max_backlog              # 网卡到协议栈的队列
sysctl net.ipv4.tcp_tw_reuse                    # TIME_WAIT 复用(客户端有用)
sysctl net.ipv4.ip_local_port_range             # 本地端口范围(端口耗尽时调)
sysctl net.core.rmem_max net.core.wmem_max      # socket 缓冲区上限

8.2 带宽与延迟测试

# iperf3(需要两端都装)
iperf3 -s                           # 服务端
iperf3 -c server_ip                 # 客户端,测上行
iperf3 -c server_ip -R              # 反向(测下行)
iperf3 -c server_ip -P 8            # 8 个并发流(单流可能受窗口限制)
iperf3 -c server_ip -u -b 100M      # UDP 测试,限速 100Mbps
iperf3 -c server_ip -t 30 -i 1      # 跑 30 秒,每秒报告

# 简易测速(没有 iperf 时)
# 服务端:nc -l 9000 > /dev/null
# 客户端:dd if=/dev/zero bs=1M count=1000 | nc server 9000
# 或者
curl -o /dev/null -w '%{speed_download}\n' http://server/bigfile

# 延迟与抖动
ping -c 100 -q host                 # 看 min/avg/max/mdev
mtr -n -c 100 -r host

# 实时流量(按网卡/按连接)
sar -n DEV 1                        # 每秒的收发包与字节数
iftop -i eth0                       # ✅ 按连接看实时带宽(需安装)
nethogs                             # ✅ 按【进程】看带宽(需安装)
nload / bmon / vnstat               # 其他可视化工具
cat /proc/net/dev                   # 原始数据

9. 排查流程速查

# ══ 「连不上」的分层排查 ══
# ① 本机网络配置
ip -br a                            # 有 IP 吗?
ip route                            # 有默认路由吗?
ip route get <目标IP>                # 会走哪条路?

# ② 到目标的连通性
ping -c3 <网关>                      # 网关通吗?
ping -c3 <目标IP>                    # 目标通吗?(注意 ICMP 可能被屏蔽)
mtr -n -c20 -r <目标IP>              # 路径上哪里有问题?

# ③ 端口层
nc -zv <目标IP> <端口>                # 端口开着吗?
sudo ss -tlnp | grep <端口>          # (在目标机上)服务真的在监听吗?
# 注意监听地址:127.0.0.1 只能本机访问,0.0.0.0 才对外
sudo iptables -L -n -v               # 防火墙拦了吗?
sudo nft list ruleset
sudo firewall-cmd --list-all         # RHEL
sudo ufw status                      # Ubuntu
# 云上还要检查【安全组】和【网络 ACL】

# ④ DNS
getent hosts <域名>
dig +short <域名>

# ⑤ 应用层
curl -v --connect-timeout 5 http://<目标>:<端口>/

# ══ 「很慢」的分段定位 ══
curl -o /dev/null -sS -w 'dns=%{time_namelookup} conn=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer} total=%{time_total}\n' <URL>
# 哪一段大就往哪个方向深挖(见 6.1 的对照表)

# ══ 「偶发失败/重置」══
sudo tcpdump -ni any -c 200 'tcp[tcpflags] & tcp-rst != 0'    # 谁发的 RST
nstat -az | grep -iE 'retrans|overflow|drop'                  # 内核层统计
ss -tln                                                        # accept 队列
dmesg -T | grep -iE 'nf_conntrack|table full|drop'            # conntrack 表满
sysctl net.netfilter.nf_conntrack_count net.netfilter.nf_conntrack_max

# ══ 「端口耗尽」══
ss -s                                          # TIME-WAIT 数量
sysctl net.ipv4.ip_local_port_range            # 可用端口范围
ss -tan state time-wait | wc -l
# 对策:连接复用(HTTP keep-alive)、tcp_tw_reuse、增大端口范围

10. 知识点扩展

10.1 命令速查

需求 命令
看 IP ip -c -br a
看路由 ip -c route / ip route get <IP>(查会走哪条路)
看 ARP ip neigh
看监听端口 sudo ss -tlnp
看所有连接 ss -tan
连接状态统计 ss -s / ss -tan | awk 'NR>1{print $1}' | sort | uniq -c
看 TCP 质量(RTT/重传) ss -tin dst <IP>
端口占用 sudo ss -tlnp | grep :8080 / sudo fuser -n tcp 8080
测端口通不通 nc -zv host 8080 / timeout 3 bash -c '</dev/tcp/host/8080'
测 MTU ping -M do -s 1472 -c2 host
路径质量 mtr -n -c 100 -r host
DNS(程序视角) getent hosts x
DNS(纯 DNS) dig +short x / dig @8.8.8.8 x / dig +trace x
清 DNS 缓存 sudo resolvectl flush-caches
分段计时 curl -o /dev/null -sS -w '...' URL
绕过 DNS 测后端 curl --resolve host:443:10.0.0.5 https://host/
抓包 sudo tcpdump -ni any -c 100 -w /tmp/x.pcap 'port 443'
抓 RST sudo tcpdump -ni any 'tcp[tcpflags] & tcp-rst != 0'
内核网络统计 nstat -az | grep -iE 'drop|retrans|overflow'
网卡统计 ip -s link / ethtool -S eth0
按进程看带宽 nethogs
按连接看带宽 iftop -i eth0
进容器网络排查 sudo nsenter -t <PID> -n ss -tlnp

10.2 常见 TCP 状态与含义

CLOSED -> SYN-SENT -> ESTABLISHED          (客户端发起)
CLOSED -> LISTEN -> SYN-RECV -> ESTABLISHED(服务端接受)

关闭时(主动方):
ESTABLISHED -> FIN-WAIT-1 -> FIN-WAIT-2 -> TIME-WAIT -> CLOSED
                                            ^^^^^^^^^ 等 2×MSL(通常 60s)

关闭时(被动方):
ESTABLISHED -> CLOSE-WAIT -> LAST-ACK -> CLOSED
               ^^^^^^^^^^ 【应用需要调用 close()】才会往下走
状态异常 含义与排查
大量 CLOSE-WAIT 应用没有调用 close() —— 几乎一定是代码 bug(如 Go 里 resp.Body 没 Close)
大量 TIME-WAIT 正常现象(主动关闭方必经);量大时考虑连接复用、tcp_tw_reuse
大量 SYN-RECV 可能被 SYN flood,或 accept 队列满
大量 FIN-WAIT-2 对端没有发 FIN(对端应用卡住/半关闭)
LISTENRecv-Q > 0 accept 队列积压,应用来不及 accept

10.3 常用内核网络参数

# 查看与临时修改
sysctl net.ipv4.tcp_tw_reuse
sudo sysctl -w net.core.somaxconn=8192
# 持久化
echo 'net.core.somaxconn = 8192' | sudo tee /etc/sysctl.d/99-net.conf
sudo sysctl --system                       # 重新加载所有配置
参数 作用 什么时候调
net.core.somaxconn accept 队列上限 高并发接入层(ss -tln 的 Recv-Q 满时)
net.ipv4.tcp_max_syn_backlog SYN 队列上限 大量新连接
net.core.netdev_max_backlog 网卡到协议栈的队列 高 PPS 场景丢包时
net.ipv4.ip_local_port_range 本地端口范围 客户端端口耗尽
net.ipv4.tcp_tw_reuse 复用 TIME_WAIT(仅对外连接有效 客户端 TIME_WAIT 过多
net.ipv4.tcp_fin_timeout FIN-WAIT-2 超时
net.ipv4.tcp_keepalive_time keepalive 首次探测间隔(默认 7200s!) 需要更快发现死连接
net.core.rmem_max / wmem_max socket 缓冲区上限 高带宽延迟积(长肥管道)
net.ipv4.tcp_congestion_control 拥塞算法(bbr / cubic 跨境/高丢包链路用 bbr
net.netfilter.nf_conntrack_max 连接跟踪表大小 nf_conntrack: table full
net.ipv4.tcp_syncookies SYN cookie 防护 默认开着即可

⚠️ net.ipv4.tcp_tw_recycle 已在内核 4.12 被移除(它在 NAT 环境下会导致连接被莫名丢弃)。网上很多老教程还在推荐它,不要用。

11. 面试题

Q:ifconfig 为什么被 ip 取代了?

不是「新的更时髦」,而是 net-tools 表达不了现代内核的能力:一个网卡多个 IP 只能靠 eth0:1 别名且只显示第一个、完全不支持策略路由(多路由表)、完全不支持网络命名空间(容器的基础设施)、IPv6 支持不完整、隧道/VLAN/bridge 各需独立工具。

另一个关键差异是获取数据的方式:net-tools 读 /proc 做文本解析,iproute2 走 netlink 二进制接口。所以在几万连接的机器上,netstat -tan 要几秒甚至十几秒,ss -tan 只要几十毫秒 —— 差近百倍。

net-tools 从 2001 年起基本停止维护,现在多数发行版的最小安装已经不带它了。

Q:ss -tlnp 输出里的 Recv-QSend-Q 是什么意思?

关键在于它们在不同状态下含义完全不同

  • LISTEN 状态Recv-Q = 当前 accept 队列(全连接队列)里积压的连接数Send-Q = 队列的最大长度(backlog)
  • ESTAB 状态Recv-Q = 收到但应用还没读走的字节数,Send-Q = 已发送但对端还没 ACK 的字节数

这给出两个高价值的诊断:

  1. LISTEN 行的 Recv-Q 持续大于 0(尤其等于 Send-Q)= accept 队列满了,新连接会被丢弃或 RST,客户端表现为「连接超时」。说明应用 accept 不过来(卡住了、或 backlog 太小)。交叉验证用 nstat -az TcpExtListenOverflows
  2. ESTAB 行的 Recv-Q = 应用读得慢(业务逻辑阻塞);Send-Q = 网络发不出去或对端不 ACK(对端假死、网络拥塞)

Q:一个 HTTP 请求很慢,怎么快速定位是哪一段慢?

curl -w 把请求拆成六个时间点,相减得到每段耗时:

curl -o /dev/null -sS -w 'dns=%{time_namelookup} conn=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer} total=%{time_total}\n' URL
  • DNS 慢 → DNS 服务器超时重试、ndots 导致多次无效查询、没缓存。用 tcpdump port 53 数实际发了几次查询
  • TCP 握手慢connect - namelookup)→ RTT 高、SYN 丢包重传、服务端 accept 队列满
  • TLS 握手慢appconnect - connect)→ 证书链长、OCSP 查询、无会话复用
  • 服务端处理慢starttransfer - pretransfer,即 TTFB)→ 应用自身问题,转向应用 tracing/pprof
  • 传输慢total - starttransfer)→ 响应体大、带宽不足

配套技巧是 curl --resolve host:443:10.0.0.5:强制解析到指定 IP,同时保持 Host 头、SNI、证书校验都正常 —— 用来单独测某台后端或绕过 DNS 验证问题。

Q:容器里 ping 域名能通,但 Go 程序解析失败,为什么?

因为它们走的是两条不同的解析路径

ping 是 C 程序,调用 libc 的 getaddrinfo(),会走完整的 NSS 链路/etc/nsswitch.conf/etc/hosts/etc/resolv.conf → DNS。

而 Go 在 CGO_ENABLED=0(静态编译)时使用纯 Go 实现的解析器,它只读 /etc/hosts/etc/resolv.conf,完全跳过 nsswitch.conf 和所有 NSS 模块。所以如果环境依赖 LDAP、mDNS、或其他 NSS 模块来解析,Go 程序就解析不到。

诊断:GODEBUG=netdns=2 ./myapp 会打印它实际用了哪个解析器;getent hosts x(走 NSS)与 dig +short x(纯 DNS)对比能定位是哪一层的问题。

Alpine 的 musl libc 也有自己的 DNS 实现差异(对 search 域的处理、早期版本对 >512 字节响应的 TCP 回退)。

Q:K8s 里的 ndots:5 是什么?为什么会造成性能问题?

ndots:N 的含义是:域名中的点数少于 N 时,先拼接 search 域再查询。K8s 默认给 Pod 设 ndots:5,是为了让 myservicemyservice.myns 这种短名字能被正确解析成集群内服务。

副作用是访问外部域名时:api.example.com 只有 2 个点(<5),于是会依次尝试:

api.example.com.myns.svc.cluster.local   -> NXDOMAIN
api.example.com.svc.cluster.local        -> NXDOMAIN
api.example.com.cluster.local            -> NXDOMAIN
api.example.com                          -> 成功

4 次查询里前 3 次全是无效的,而且每次可能同时发 A 和 AAAA 两个请求 —— 实际 8 次 DNS 请求。后果是外部调用延迟升高、CoreDNS 被无效查询打满。

三种解法:域名末尾加点api.example.com. 表示完全限定域名,不拼 search)、给 Pod 单独配 dnsConfig.options.ndots: "2"、部署 NodeLocal DNSCache。验证用 tcpdump -ni any port 53 数实际查询次数。

Q:tcpdump 抓到包了但看不到 HTTP 内容,可能是什么原因?

三个,按概率:

  1. 流量是 HTTPS(最常见)—— 应用数据加密,只能看到 TLS 握手的明文部分(SNI 里有域名)。解法是在应用侧看日志、用 mitmproxy、或导出 TLS 密钥给 Wireshark
  2. 抓包长度被截断 —— 老版本 tcpdump 的默认 snaplen 只有 68/96 字节,加 -s 0 完整抓包
  3. 数据分散在多个 TCP 包里 —— 单个包看不出完整内容,要 -w 存成 pcap 后用 Wireshark 的 Follow TCP Stream,或命令行的 tcpflow -rtshark -z follow,tcp,ascii,0

另外生产上抓包必须注意性能:抓包要把包复制到用户态,高流量下消耗 CPU 且可能丢包。务必加过滤表达式缩小范围、用 -c 限制数量、用 -w 写文件(格式化输出到终端比写文件贵得多)。抓完看输出里的 packets dropped by kernel 判断有没有丢。

Q:mtr 显示中间某一跳丢包 20%,是不是那里有问题?

通常不是。 路由器对 ICMP TTL 超时响应的处理优先级很低(很多设备还会限速),忙的时候会丢弃或延迟回复这类「给自己的」控制报文,但转发正常业务流量毫无问题

判断规则:只看最后一跳(真正的目标)的 Loss%Avg。中间跳的丢包只有在「它之后的所有跳都持续丢包」时才有意义 —— 那才说明丢包是真实发生在转发路径上的。

另外 traceroute/mtr 默认用 UDP 或 ICMP 探测,很多防火墙会屏蔽,导致中间几跳显示 ???。用 mtr -T -P 443(TCP 模式探测目标端口)更贴近真实业务流量的路径。

Q:服务出现大量 CLOSE-WAIT 状态的连接,说明什么?

几乎一定是应用代码的 bug:收到了对端的 FIN,但应用没有调用 close()

TCP 四次挥手中,被动关闭方收到 FIN 后进入 CLOSE_WAIT必须由应用主动调用 close() 才会发出自己的 FIN 进入 LAST_ACK。如果应用忘了关,连接就永久停在 CLOSE_WAIT,同时占着一个 fd,最终导致 too many open files

Go 里最常见的原因是 resp.Body 没有 Close()(而且还要读完,否则连接无法复用)、或者数据库/Redis 连接池的连接没有正确归还。

排查:ss -tan state close-wait 看数量和对端,ss -tanp state close-wait 定位进程,然后 ls -l /proc/<pid>/fd | grep socket | wc -l 确认 fd 增长。

对比:大量 TIME_WAIT 是正常的(主动关闭方必经,等 2×MSL),只在端口耗尽时才需要处理(连接复用、tcp_tw_reuse、扩大 ip_local_port_range)。注意 tcp_tw_recycle 已在内核 4.12 移除,网上老教程推荐它是错的(NAT 环境下会丢连接)。


小结

  • net-tools 已死ifconfigip anetstatssrouteip routearpip neigh。核心原因是老工具表达不了策略路由和网络命名空间,且走 /proc 文本解析太慢
  • ip route get <IP> 是排查「为什么访问不到」的第一条命令
  • ssRecv-Q/Send-Q 在 LISTEN 和 ESTAB 下含义不同 —— LISTEN 行的 Recv-Q 积压 = accept 队列满
  • curl -w 把请求拆成六段,是定位「接口慢」最有效的单个技巧;--resolve 用来绕过 DNS 测后端
  • DNS 有两条路径:libc 的 getaddrinfo(走 NSS)vs Go 的纯 Go 解析器(只读 hosts + resolv.conf)→ 用 getent hostsdig 对比定位
  • K8s 的 ndots:5 让外部域名解析多发 3~7 次无效查询,域名末尾加点或调 dnsConfig 解决
  • tcpdump 生产上必须加过滤 + -c + -w,抓完检查 dropped by kernel
  • 中间跳丢包通常无意义(ICMP 限速),只看最后一跳
  • 大量 CLOSE-WAIT = 应用没 close(),是代码 bug 不是网络问题

下一篇讲 SSH 与远程工作流:密钥认证的完整原理、~/.ssh/config 能省掉多少事、ProxyJump 跳板机、三种端口转发分别解决什么问题、以及 sshd 的安全加固清单。