2288 字
11 分钟
Tailscale MagicDNS 无上游 Resolver:VPS DNS 静默失效 23 小时

这是 Tailscale DNS 死锁 的续集,但根因完全不同。

上次的死锁是:Tailscale 掉线 → DNS 断 → Tailscale 无法重连 → 无法自愈。这次是另一种失效模式:Tailscale 活得好好的,DNS 照样全断。而且没有任何报警,服务一个接一个悄悄崩溃,持续了整整 23 小时才被发现。

症状#

检查 VPS 上的 systemd 服务状态:

Terminal window
systemctl status hotwatch-worker --no-pager -l
● hotwatch-worker.service - HotWatch ingest/scheduler daemon
Active: activating (auto-restart) (Result: exit-code)
Main PID: 176679 (code=exited, status=1/FAILURE)
Jun 12 00:27:01 racknerd-b078f4c systemd[1]: hotwatch-worker.service: Failed with result 'exit-code'.
Jun 12 00:27:11 racknerd-b078f4c systemd[1]: hotwatch-worker.service: Scheduled restart job, restart counter is at 2232.

restart counter 已经到了 2232。这个 worker 每隔十秒自动重启一次,崩溃循环已经持续了大约 6 小时以上。

再看 API 服务:

Terminal window
systemctl status hotwatch-api --no-pager | tail -5
Jun 12 00:18:09 racknerd-b078f4c hotwatch[159008]: heartbeat down for 84813s; service still running but invisible to Registry

API 服务本身活着,但 heartbeat 已经断了 84813 秒,换算过来是 23.5 小时。服务自己在跑,但对外完全不可见——Registry 认为它不存在。

排查 Worker 崩溃#

看崩溃日志的关键行:

Terminal window
journalctl -u hotwatch-worker --no-pager | grep -A 3 "failure in name"
Jun 12 00:26:59 racknerd-b078f4c hotwatch[176679]: '[Errno -3] Temporary failure in name resolution' thrown while requesting HEAD https://huggingface.co/sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2/resolve/main/adapter_config.json
Jun 12 00:26:59 racknerd-b078f4c hotwatch[176679]: Retrying in 1s [Retry 1/5].

Errno -3 是 glibc 的 EAGAIN/DNS SERVFAIL——域名解析完全失败。

Worker 每次启动都要初始化 sentence-transformer 模型(paraphrase-multilingual-MiniLM-L12-v2),而 SentenceTransformer 在加载时会尝试从 HuggingFace Hub 获取元数据,即使本地已经有完整的模型缓存(~/.cache/huggingface/,458 MB)。DNS 一断,Hub 连不上,模型加载失败,进程退出,systemd 10 秒后重启,循环往复。

完整 traceback 显示错误在 SentenceTransformer.__init__transformers.utils.hub.cached_fileshuggingface_hub.file_download.hf_hub_download 链路上。

定位 DNS 故障#

检查 DNS 配置:

Terminal window
cat /etc/resolv.conf
/etc/resolv.conf
# resolv.conf(5) file generated by tailscale
# For more info, see https://tailscale.com/s/resolvconf-overwrite
# DO NOT EDIT THIS FILE BY HAND -- CHANGES WILL BE OVERWRITTEN
nameserver 100.100.100.100
nameserver fd7a:115c:a1e0::53
search tailc2559b.ts.net nyu.edu aliyuncs.com

熟悉的面孔。Tailscale 接管了 DNS,所有查询走 100.100.100.100——Tailscale 的 MagicDNS 本地 stub。

先验证 stub 本身是否可达:

Terminal window
ping -c 1 100.100.100.100
64 bytes from 100.100.100.100: icmp_seq=1 ttl=64 time=0.342 ms

ping 通。tailscaled 进程在跑,stub 在监听。但 DNS 查询却全部失败。

测试几个域名:

Terminal window
dig google.com +short
dig huggingface.co +short
dig registry.example.com +short

三个全部无输出,没有任何 DNS 响应。

发现根因#

检查 Tailscale DNS 的内部配置:

Terminal window
tailscale dns status

输出里有一段关键信息:

=== MagicDNS configuration ===
Resolvers (in preference order):
(no resolvers configured, system default will be used: see 'System DNS configuration' below)
=== System DNS configuration ===
(failed to read system DNS configuration: 500 Internal Server Error: exit status 1)

问题找到了。

Tailscale MagicDNS 的工作方式是:接收所有 DNS 查询 → 处理自身 tailnet 的 .ts.net 域名 → 将外部查询转发给上游 resolver。但这里上游 resolver 为空——管理员面板里没有配置任何 Global nameservers。

MagicDNS 转发外部查询的规则:

  1. 如果 Tailscale 管理面板配置了 Global nameservers → 用那些
  2. 如果没配置 → 尝试读取”系统 DNS 配置”作为默认
  3. 读系统 DNS 配置失败(这里返回 500)→ 无上游 resolver → 所有外部查询 SERVFAIL

tailscale status 同时显示了一个健康检查警告:

# Health check:
# - Tailscale failed to fetch the DNS configuration of your device: exit status 1

这个错误就是导致”系统 DNS 配置读取失败”的原因。Tailscale daemon 无法从 OS 正确读取原生 DNS 配置,于是整个外部查询转发链路就断了。

结果:100.100.100.100 能解析 xxx.tailc2559b.ts.net 这样的 tailnet 内部地址,但对一切外部域名都返回 SERVFAIL。

这和上次的”DNS 死锁”有什么不同?#

Tailscale DNS 死锁本次:MagicDNS 无上游
Tailscale 状态掉线,与控制平面断连正常运行,节点连接完好
100.100.100.100进程存活但上游断连进程存活但从未有上游
触发原因网络中断管理面板未配置 resolver
自愈能力无(死锁)无(配置缺失)
影响范围所有外部 DNS所有外部 DNS
表象差异伴随 Tailscale 离线Tailscale 看起来完全正常

这次的情况更隐蔽。Tailscale 面板显示节点在线,tailscale status 显示 active,进程健康,没有任何直接的”Tailscale 有问题”的信号。只有从服务日志里看到 DNS 解析失败,再一路追溯到 tailscale dns status 才能发现根因。

尝试 —accept-dns=false(无效)#

第一个想法是沿用上次的修复方案——告诉 Tailscale 不要接管 DNS:

Terminal window
tailscale set --accept-dns=false
echo "exit: $?"
exit: 0

命令成功执行。但重启 tailscaled 后:

Terminal window
systemctl restart tailscaled
cat /etc/resolv.conf
# resolv.conf(5) file generated by tailscale
# For more info, see https://tailscale.com/s/resolvconf-overwrite
# DO NOT EDIT THIS FILE BY HAND -- CHANGES WILL BE OVERWRITTEN
nameserver 100.100.100.100
nameserver fd7a:115c:a1e0::53

resolv.conf 没有变化,100.100.100.100 依然在。

这和上次 Raspberry Pi 上的行为不同。在 Pi 上 --accept-dns=false 会让 Tailscale 把 resolv.conf 还给 NetworkManager。但在这台 Ubuntu VPS 上,tailscaled 在重启后继续写入自己的 DNS——可能是因为没有 NetworkManager,也没有其他 DNS manager 可以”接手”,Tailscale 就继续维持自己的 stub。

NOTE

--accept-dns=false 的行为在不同 OS 和 DNS 管理器环境下不一致。在有 NetworkManager 的系统(如 Pi)上,它会正确释放控制权。在纯 systemd 的 headless VPS 上,效果可能不同。

实际生效的修复#

思路转变:既然 Tailscale 坚持写 100.100.100.100,那就让 100.100.100.100 的上游可以正常工作——或者绕过它。

检查 systemd-resolved 的状态:

Terminal window
systemctl status systemd-resolved --no-pager | head -5
● systemd-resolved.service - Network Name Resolution
Active: active (running) since Wed 2026-06-10 11:47:55 UTC; 1 day 12h ago

systemd-resolved 在跑,而且它的 stub 监听在 127.0.0.53

Terminal window
ss -lnp | grep ":53 "
udp UNCONN 0 0 127.0.0.53%lo:53 0.0.0.0:* users:(("systemd-resolve",...))
tcp LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:* users:(("systemd-resolve",...))

现在给 eth0 接口配置真实的 DNS:

Terminal window
resolvectl dns eth0 8.8.8.8 8.8.4.4
resolvectl domain eth0 "~."

~. 的意思是”这个接口负责所有域名的解析”——相当于把它设为 default route 接口。

验证 systemd-resolved 的状态:

Terminal window
resolvectl status eth0
Link 2 (eth0)
Current Scopes: DNS
Protocols: +DefaultRoute -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
DNS Servers: 8.8.8.8 8.8.4.4
DNS Domain: ~.

eth0 现在有了真实的 upstream。但还需要让系统的 glibc resolver(curl、Python 等都走这里)能访问到 systemd-resolved 的 stub,而不是绕过它直接走 resolv.conf 里的 100.100.100.100

把 resolv.conf 切换为指向 systemd-resolved stub 的 symlink:

Terminal window
ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf
cat /etc/resolv.conf
# This is /run/systemd/resolve/stub-resolv.conf managed by man:systemd-resolved(8).
# ...
nameserver 127.0.0.53
options edns0 trust-ad
search tailc2559b.ts.net nyu.edu aliyuncs.com

DNS 查询路径变成了:glibc → 127.0.0.53(systemd-resolved stub)→ eth0 配置的 8.8.8.8

测试:

Terminal window
dig google.com +short
142.250.177.238
Terminal window
dig huggingface.co +short
3.170.185.14
3.170.185.35
# ...
Terminal window
curl -s https://registry.example.com/health
{"status":"ok","services_count":13}

DNS 完全恢复。

持久化#

resolvectl dns 的设置在重启后会丢失。在 netplan 配置里固化:

/etc/netplan/50-cloud-init.yaml(片段)
network:
version: 2
ethernets:
eth0:
addresses:
- xxx.xxx.xxx.xxx/24
routes:
- to: default
via: xxx.xxx.xxx.1
nameservers:
addresses:
- 8.8.8.8
- 8.8.4.4
- 1.1.1.1
Terminal window
netplan apply

同时在 /etc/systemd/resolved.conf.d/fallback.conf 里加一层 fallback 保底:

/etc/systemd/resolved.conf.d/fallback.conf
[Resolve]
FallbackDNS=8.8.8.8 8.8.4.4 1.1.1.1

这样即使 netplan 的配置某天出问题,systemd-resolved 还有全局 fallback。

重启服务#

DNS 通了,重启两个服务:

Terminal window
systemctl restart hotwatch-worker hotwatch-api

等几十秒后查看 worker 日志:

00:31:51 INFO hotwatch.embedding: loaded sentence-transformer paraphrase-multilingual-MiniLM-L12-v2 (dim=384)
00:32:13 INFO hotwatch.scheduler: scheduled arxiv_cs_cl every 3600s
00:32:13 INFO hotwatch.scheduler: scheduled reddit_singularity_rss every 1800s
# ... 11 个数据源全部调度完成
00:32:13 INFO hotwatch.scheduler: worker starting with 11 sources

模型正确从本地缓存加载,11 个数据源全部调度完成,服务稳定运行。

API 的 heartbeat 也在重启后重新连上了 Registry。

整个调用链的故障路径#

这次事故的传播路径:

Tailscale 管理面板未配置 Global nameservers
MagicDNS 100.100.100.100 无外部 resolver
系统 DNS 配置读取失败(tailscaled 内部错误)
所有外部域名 SERVFAIL(不影响 .ts.net)
┌─────────────────────┬──────────────────────────────────────┐
│ hotwatch-worker │ hotwatch-api │
│ 每次启动尝试连接 │ 正常运行,但 heartbeat 到外部 │
│ huggingface.co 失败 │ registry 失败,服务"隐身" │
│ 崩溃 → 重启循环 │ │
│ 累计 2241 次 │ 23.5 小时不可见 │
└─────────────────────┴──────────────────────────────────────┘

经验#

1. Tailscale MagicDNS 需要显式配置上游 resolver

在 Tailscale 管理面板 → DNS 里,至少要添加一个 Global nameserver(如 8.8.8.8)。否则 MagicDNS 只能解析 tailnet 内部域名,对所有外部域名无能为力。这个配置缺失不会有任何错误提示——服务会安静地全部 SERVFAIL。

2. VPS 上 --accept-dns=false 行为与家用设备不同

在有 NetworkManager 的设备上,--accept-dns=false 会正确释放 resolv.conf 控制权。在 headless Ubuntu VPS 上,效果不一定相同——Tailscale 可能会继续维持自己的 DNS stub。遇到这种情况,绕过 Tailscale stub、直接走 systemd-resolved 是更可靠的方案。

3. “Tailscale 在线”不等于”DNS 正常”

Tailscale 节点显示 active、连接正常,但 DNS 可以完全失效。两者的状态是解耦的。如果服务出现大量 Errno -3SERVFAIL,要把 Tailscale DNS 配置列为排查项。

4. systemd-resolved stub 是更健壮的基准

/etc/resolv.conf 指向 systemd-resolved stub(/run/systemd/resolve/stub-resolv.conf127.0.0.53),配合 resolvectl 配置每个接口的上游,是比直接写死 nameserver 更灵活、更可控的方案。Tailscale 的 MagicDNS 可以和 systemd-resolved 协同工作,不需要二选一。

5. 异常重启次数是很好的事故信号

restart counter is at 2232 是一个非常明显的信号,表明这个问题已经持续了很长时间。systemd 服务的重启计数器默认不重置,检查它可以快速判断问题的严重程度和持续时间。


如果你的 VPS 上也在跑 Tailscale,可以用一行命令快速验证 DNS 是否有上游:

Terminal window
tailscale dns status | grep -A3 "Resolvers"

如果输出是 (no resolvers configured, system default will be used) 并且系统 DNS 读取失败,那这篇文章说的情况就会发生在你身上。

Tailscale MagicDNS 无上游 Resolver:VPS DNS 静默失效 23 小时
https://blog.lishuyu.app/posts/tailscale-magicdns-no-upstream-vps/
作者
猫猫魔女
发布于
2026-06-12
许可协议
CC BY-NC-SA 4.0