Linux-15 网络操作与排查:ip/ss 的正确用法、curl -w 定位耗时、DNS 链路与 ndots 陷阱
这一篇是上手部分里最实用的一篇:线上故障有很大比例是网络问题,而排查网络问题靠的是「分段定位」的能力。
先看五个问题:
ifconfig为什么被废弃了?ip命令怎么对应过去?ss -tlnp里每个字母是什么意思?输出里的Recv-Q/Send-Q到底表示什么?- 一个请求很慢,怎么判断慢在 DNS、TCP 握手、TLS,还是服务端处理?
- 容器里
ping域名能通,但程序解析失败,为什么? tcpdump抓到包了,为什么看不到 HTTP 内容?
1. 工具的换代:net-tools → iproute2
1.1 为什么 ifconfig 被废弃
ifconfig、route、netstat、arp 属于 net-tools 包,诞生于 1990 年代,从 2001 年起就基本停止维护。它们被 iproute2(ip、ss)取代的原因不是「新的更时髦」,而是老工具表达不了现代内核的能力:
| 内核能力 | 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-Q 和 Send-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 +stats、tcpdump port 53 数查询次数 |
| TCP 握手 | 网络 RTT 高、丢包导致 SYN 重传、服务端 accept 队列满 | mtr、ss -tln 看 Recv-Q、nstat 看 ListenOverflows |
| TLS 握手 | 证书链长、OCSP 装订缺失、密码套件协商慢、不支持会话复用 | openssl s_client -connect host:443 |
| 服务端处理 | 应用慢(DB 查询、下游调用、GC) | 应用侧 tracing/pprof |
| 内容传输 | 响应体大、带宽不足、TCP 窗口小 | size_download、speed_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(对端应用卡住/半关闭) |
LISTEN 的 Recv-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-Q 和 Send-Q 是什么意思?
关键在于它们在不同状态下含义完全不同:
LISTEN状态:Recv-Q= 当前 accept 队列(全连接队列)里积压的连接数,Send-Q= 队列的最大长度(backlog)ESTAB状态:Recv-Q= 收到但应用还没读走的字节数,Send-Q= 已发送但对端还没 ACK 的字节数
这给出两个高价值的诊断:
- LISTEN 行的
Recv-Q持续大于 0(尤其等于Send-Q)= accept 队列满了,新连接会被丢弃或 RST,客户端表现为「连接超时」。说明应用 accept 不过来(卡住了、或 backlog 太小)。交叉验证用nstat -az TcpExtListenOverflows - 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,是为了让 myservice、myservice.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 内容,可能是什么原因?
三个,按概率:
- 流量是 HTTPS(最常见)—— 应用数据加密,只能看到 TLS 握手的明文部分(SNI 里有域名)。解法是在应用侧看日志、用 mitmproxy、或导出 TLS 密钥给 Wireshark
- 抓包长度被截断 —— 老版本 tcpdump 的默认 snaplen 只有 68/96 字节,加
-s 0完整抓包 - 数据分散在多个 TCP 包里 —— 单个包看不出完整内容,要
-w存成 pcap 后用 Wireshark 的 Follow TCP Stream,或命令行的tcpflow -r、tshark -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 已死:
ifconfig→ip a、netstat→ss、route→ip route、arp→ip neigh。核心原因是老工具表达不了策略路由和网络命名空间,且走/proc文本解析太慢 ip route get <IP>是排查「为什么访问不到」的第一条命令ss的Recv-Q/Send-Q在 LISTEN 和 ESTAB 下含义不同 —— LISTEN 行的 Recv-Q 积压 = accept 队列满curl -w把请求拆成六段,是定位「接口慢」最有效的单个技巧;--resolve用来绕过 DNS 测后端- DNS 有两条路径:libc 的
getaddrinfo(走 NSS)vs Go 的纯 Go 解析器(只读 hosts + resolv.conf)→ 用getent hosts和dig对比定位 - K8s 的
ndots:5让外部域名解析多发 3~7 次无效查询,域名末尾加点或调dnsConfig解决 tcpdump生产上必须加过滤 +-c+-w,抓完检查dropped by kernel- 中间跳丢包通常无意义(ICMP 限速),只看最后一跳
- 大量
CLOSE-WAIT= 应用没 close(),是代码 bug 不是网络问题
下一篇讲 SSH 与远程工作流:密钥认证的完整原理、~/.ssh/config 能省掉多少事、ProxyJump 跳板机、三种端口转发分别解决什么问题、以及 sshd 的安全加固清单。
xingliuhua