路与名 — 路由表和 DNS 的层次
一句话总结
路由通过最长前缀匹配决定“这个目标应从哪个 interface 发出、经过谁”;Linux 只有启用 ip_forward,才会转发别人的数据包。名称解析则由多个层次组成,包括 /etc/hosts、stub 和上游服务器,而 dig 只查看最后一层。
为什么需要这些知识
发往子网之外的数据包会交给网关。网关再次查看自己的路由表,选择下一个网关。这条链路就是互联网。每一 hop 只知道“这个网段在那一侧”,没有任何设备掌握完整路径。因此,路由事故通常源于某一 hop 不认识某个网段。尤其常见的是去程存在、回程缺失;症状却只是 timeout,很难区分。
名称解析同样存在多个层次。过去,/etc/resolv.conf 中记录服务器地址,过程到此为止。如今 Ubuntu 使用 systemd-resolved 在 127.0.0.53 监听 stub,并按不同 link 转发给不同上游服务器,还能通过 ~domain 路由规则,仅把特定域名发送给特定服务器。VPN 只把企业域名交给企业 DNS,正是利用了这一机制。它很方便,但如果不知道是哪一层作出的响应,就无法解释“dig 正常,curl 却失败”。
它是如何运作的
如果路由表同时包含 default via 10.20.1.1 和 10.99.0.0/24 via 10.20.2.10,那么 10.99.0.5 会走后一条。更长的 prefix 获胜,与记录顺序无关。ip route get 10.99.0.5 会直接向内核询问这一判断,比人手阅读路由表后推测更准确。
Linux 默认不是路由器,会丢弃目标不是自身地址的数据包。net.ipv4.ip_forward=1 会改变这一行为,而且每个 namespace 都有独立设置。Docker 宿主机、Kubernetes 节点和 VPN 服务器都会把这个值设为 1。
名称解析从应用程序调用 libc 的 getaddrinfo 开始。按照 /etc/nsswitch.conf 中 hosts: files dns 的顺序,先查找 /etc/hosts;找不到时询问 resolv.conf 中的 stub;stub 再根据 link 设置转发到上游。dig 会跳过这些层,直接询问服务器。因此,服务器返回什么用 dig 查看,应用程序实际看到什么则用 getent hosts 查看。
在实际工作中会是什么样
回程路由。 接入新网段并在路由器上添加路径后,ping 仍然失败。原因是对端路由器不知道如何返回新网段。请求已经到达,响应却迷失路径。在双方分别运行 ip route get,就会看到其中一侧错误地通过 default 指向别处。
端口 53 冲突。 安装 dnsmasq 后无法启动。通过 ss -ulpn 'sport = :53' 查看,会发现 systemd-resolve 占用了 127.0.0.53。解决方法是在 dnsmasq 中使用 listen-address 和 bind-interfaces,只绑定自己的地址。
名称解析缓慢或异常时
即使路由正确,缓慢问题也经常来自名称解析。由于层次很多,很难看出时间消耗在哪里。
附加 search domain。 如果 resolv.conf 的 search 清单列出多个域名,查询短名称时,解析器会逐个附加这些域名并依次尝试。名称越不存在,尝试次数越多;服务器响应越慢,延迟还会成倍增加。这是 Kubernetes Pod 内查询外部域名缓慢的经典原因,因此解决办法是在末尾加点,以绝对名称查询。
同时查询 IPv6。 getaddrinfo 会同时查询 A 和 AAAA。如果其中一个查询丢失,就会等待 timeout 后重试,因此出现“偶尔停顿大约 5 秒”的症状。5 秒通常正是 resolver 的默认 timeout,因此,时长恰好固定的延迟,应优先怀疑 timeout。
cache 过期不及时。 地址修改后仍持续返回旧值时,必须分辨是哪一层 cache。应用程序自身可能保存结果,Java runtime 就经常缓存很久;stub resolver 也可能缓存;上游服务器还会按 TTL 保存。正确做法是先降低 TTL,再修改地址。等到修改当天才降低 TTL,已经太迟。
诊断顺序仍遵循前面的原则:先用 getent hosts 查看应用程序实际看到的响应;如果异常,再检查 /etc/hosts 和 stub 设置;只有怀疑服务器本身时,才用 dig 直接查询。如果一开始就运行 dig,就会跳过中间层,无法重现应用程序的问题,只得到“服务器正常”的结论。
下一次实操要做什么
你将使用三个 namespace 搭建主机—路由器—主机拓扑,配置默认路由、ip_forward 和静态路由,再用 traceroute 统计 hop。随后,使用 dnsmasq 建立自己的 DNS 服务器,并接入 resolved 的路由域名;最后确认 /etc/hosts 的优先级高于 DNS,并列使用 getent 和 dig 观察结果。