216 条网络命令(三):DNS、TLS、队列、内核参数
第三篇。上一篇是 netfilter 与抓包;这一篇上到应用层:DNS、TLS、队列与内核参数。
九、DNS(13 条)
dig 家族
dig example.com A # 基本查询
dig +short NS example.com # 权威服务器
dig @ns1.example.com example.com A # 直接问权威
dig @127.0.0.53 example.com A # 问本地 stub
坑:只看一次结果,不比对解析器与 TTL。
三个公共解析器结果不一致 = 缓存未过期或劫持。
dig +tcp vs dig +notcp —— 区分两种失败
dig +tcp example.com TXT # 走 TCP
dig +notcp example.com DNSKEY # 强制 UDP
dig +bufsize=512 +ignore example.com TXT
判据:+tcp 能返回、+notcp 超时 → 大 UDP 包在路径上被丢了。
A 记录正常不代表 DNS 全好,大记录类型(TXT/DNSKEY)走的是另一条路。
getent ahosts example.com
走 glibc 的解析路径,和 dig 不是一条路。
为什么重要:dig 直接发 DNS 查询,getent 走 NSS + nscd + glibc。
「dig 能解析但应用解析不了」时,差别就在这里。
5 秒 DNS 延迟的物质基础
strace -f -e trace=network -s 60 python3 -c "socket.getaddrinfo('x.example.com',80)"
输出里会有这么一行:
sendmmsg(3, [{qtype=A}, {qtype=AAAA}], 2, MSG_NOSIGNAL) = 2
一次系统调用,同一个 socket、同一个源端口,两个包背靠背发出。
报文里 \0\1 是 A(type 1),\0\34 是 AAAA(type 28)。两个包五元组完全相同 → 对 conntrack 来说是一条会话,两条报文几乎同时到达,争抢同一个表项创建槽位。
改配置后的对照:
| 配置 | 系统调用序列 | 两包间隔 |
|---|---|---|
| 默认 | sendmmsg(fd3, [A][AAAA], 2) |
微秒级 |
single-request-reopen |
sendto(A) → recvfrom → connect → sendto(AAAA) |
70 ms |
后者把并发改成了串行,竞态消失。
两条修复路径:
- 改客户端(
single-request-reopen):从 glibc 侧消除并发 - 改路径(NodeLocal DNSCache):让查询不出节点,不经过 conntrack
两条都不需要动 DNS 服务器——问题从来不在那一侧。
resolvectl flush-caches
关键:缓存有六层(浏览器/系统/本地 stub/运营商/公共递归/权威)。
你只能清第 3 层。清了还不生效,说明等的是 TTL。
十、TLS 与 HTTP(22 条)
openssl s_client
openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null \
2>&1 | grep "Verify return code"
关键:-servername 传 SNI,不传会拿到默认证书。
判据:Verify return code: 0,且证书链至少 2 张(叶子 + 中间)。
背景:浏览器有补链缓存,curl/Java 没有。
所以「浏览器正常、命令行报错」最常见的根因就是链不全。
openssl verify
openssl verify -CAfile ca.pem -untrusted intermediate.crt server.crt
坑:只验叶子不验链。
openssl x509
openssl x509 -in client.crt -noout -text | grep -A2 "Extended Key Usage"
判据:mTLS 的客户端证书 EKU 必须是 clientAuth。
坑:「证书尚未生效」常见于容器时钟漂移,只有部分机器报错。
curl -w —— 一条命令拆开三层耗时
curl -sS -o /dev/null -w \
"dns=%{time_namelookup} tcp=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer} total=%{time_total}\n" \
https://example.com
哪个数大,问题就在那一层。
坑:只看 total,不知道卡在 DNS 还是 TLS。
curl -4 / -6 / --http2 -v
curl -6 -s -o /dev/null -w "%{time_total}\n" https://example.com
curl -sv --http2 https://example.com 2>&1 | grep ALPN
判据:v4 与 v6 耗时差应 < 1 秒。
坑:ALPN 协商成功 ≠ 请求真的走了 h2。要看响应头是不是 HTTP/2。
curl -I 看缓存与 Range
curl -sI https://example.com | grep -iE "cache-control|age|etag"
curl -sI -H "Range: bytes=0-99" https://example.com | head -3
关键:no-cache 是「可存但用前验证」,no-store 才是「不存」。
判据:Range 请求应返回 206。返回 200 说明 Range 被代理剥掉了。
十一、队列与网卡(14 条)
ip -s link show <iface>
ip -s link show eth0
输出里的 dropped 分两个方向,含义完全不同:
rx_dropped 涨 → 本机来不及处理(软中断/缓冲区)
tx_dropped 涨 → 本机发不出去(无路由/无 peer/队列满)
rx_errors + rx_crc_errors 涨 → 线缆/光模块/双工
tx_carrier_errors 涨 → 链路闪断
完整统计在 /sys/class/net/<dev>/statistics/,那里有 21 个具体错误计数器,比 ip 合并的 errors/dropped 精确得多:
for f in /sys/class/net/eth0/statistics/*; do
printf '%-28s %s\n' "$(basename $f)" "$(cat $f)"
done
21 个全为 0,说明从驱动到链路层没有一处出错。
ethtool
ethtool eth0 | grep -E "Speed|Duplex|Link detected"
ethtool -S eth0 | grep -iE "err|drop|crc|fcs"
ethtool -a eth0
判据:Speed 符合预期、Duplex: Full。
坑:一端自协商、一端写死,是最容易出双工不匹配的配法。
链路起来了、小流量正常,很多人就到这儿停了。
坑:坏帧在进内核之前就被网卡丢掉了,tcpdump 根本看不到。
容器里没有 ethtool 怎么办
ip -d link show eth0
输出里有:
gso_max_size 65536 gso_max_segs 65535
tso_max_size 524280 tso_max_segs 65535
这比 ethtool -k 的 on/off 更有用——它给出聚合的上限数值。
判据:平均 TX 包大小超过 MTU,说明聚合在生效(不是错误)。
1686 B 的均值意味着「聚合过,但很不充分」。
上限是 512 KB,说明流量都是并发小请求,攒不起大包。
tc -s qdisc show dev <iface>
tc -s qdisc show dev eth0
关键:多队列网卡会列出多个实例(mq / fq_codel)。
判据:dropped 不增长。
坑:丢包发生在主机队列时,链路层计数器看不出来。
tc qdisc add ... netem —— 造故障
tc qdisc add dev eth0 root netem delay 100ms
tc qdisc change dev eth0 root netem delay 100ms 20ms # 100ms ± 20ms 抖动
tc qdisc change dev eth0 root netem loss 1%
tc qdisc change dev eth0 root netem delay 10ms reorder 25%
tc qdisc change dev eth0 root netem delay 50ms rate 10mbit
tc qdisc del dev eth0 root # 清理!
关键:只在出口方向生效。加在不同接口,测的是不同链路。
铁律:演练完必须清理。忘了 tc qdisc del,就是留下一个持续故障。
下一篇:《216 条网络命令(四):故障注入、二层配置、反例》