这是 Tailscale DNS 死锁 的续集,但根因完全不同。
上次的死锁是:Tailscale 掉线 → DNS 断 → Tailscale 无法重连 → 无法自愈。这次是另一种失效模式:Tailscale 活得好好的,DNS 照样全断。而且没有任何报警,服务一个接一个悄悄崩溃,持续了整整 23 小时才被发现。
症状
检查 VPS 上的 systemd 服务状态:
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 服务:
systemctl status hotwatch-api --no-pager | tail -5Jun 12 00:18:09 racknerd-b078f4c hotwatch[159008]: heartbeat down for 84813s; service still running but invisible to RegistryAPI 服务本身活着,但 heartbeat 已经断了 84813 秒,换算过来是 23.5 小时。服务自己在跑,但对外完全不可见——Registry 认为它不存在。
排查 Worker 崩溃
看崩溃日志的关键行:
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.jsonJun 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_files → huggingface_hub.file_download.hf_hub_download 链路上。
定位 DNS 故障
检查 DNS 配置:
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.100nameserver fd7a:115c:a1e0::53search tailc2559b.ts.net nyu.edu aliyuncs.com熟悉的面孔。Tailscale 接管了 DNS,所有查询走 100.100.100.100——Tailscale 的 MagicDNS 本地 stub。
先验证 stub 本身是否可达:
ping -c 1 100.100.100.10064 bytes from 100.100.100.100: icmp_seq=1 ttl=64 time=0.342 msping 通。tailscaled 进程在跑,stub 在监听。但 DNS 查询却全部失败。
测试几个域名:
dig google.com +shortdig huggingface.co +shortdig registry.example.com +short三个全部无输出,没有任何 DNS 响应。
发现根因
检查 Tailscale DNS 的内部配置:
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 转发外部查询的规则:
- 如果 Tailscale 管理面板配置了 Global nameservers → 用那些
- 如果没配置 → 尝试读取”系统 DNS 配置”作为默认
- 读系统 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:
tailscale set --accept-dns=falseecho "exit: $?"exit: 0命令成功执行。但重启 tailscaled 后:
systemctl restart tailscaledcat /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.100nameserver fd7a:115c:a1e0::53resolv.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 的状态:
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 agosystemd-resolved 在跑,而且它的 stub 监听在 127.0.0.53:
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:
resolvectl dns eth0 8.8.8.8 8.8.4.4resolvectl domain eth0 "~."~. 的意思是”这个接口负责所有域名的解析”——相当于把它设为 default route 接口。
验证 systemd-resolved 的状态:
resolvectl status eth0Link 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:
ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.confcat /etc/resolv.conf# This is /run/systemd/resolve/stub-resolv.conf managed by man:systemd-resolved(8).# ...
nameserver 127.0.0.53options edns0 trust-adsearch tailc2559b.ts.net nyu.edu aliyuncs.comDNS 查询路径变成了:glibc → 127.0.0.53(systemd-resolved stub)→ eth0 配置的 8.8.8.8。
测试:
dig google.com +short142.250.177.238dig huggingface.co +short3.170.185.143.170.185.35# ...curl -s https://registry.example.com/health{"status":"ok","services_count":13}DNS 完全恢复。
持久化
resolvectl dns 的设置在重启后会丢失。在 netplan 配置里固化:
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.1netplan apply同时在 /etc/systemd/resolved.conf.d/fallback.conf 里加一层 fallback 保底:
[Resolve]FallbackDNS=8.8.8.8 8.8.4.4 1.1.1.1这样即使 netplan 的配置某天出问题,systemd-resolved 还有全局 fallback。
重启服务
DNS 通了,重启两个服务:
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 3600s00: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 -3 或 SERVFAIL,要把 Tailscale DNS 配置列为排查项。
4. systemd-resolved stub 是更健壮的基准
把 /etc/resolv.conf 指向 systemd-resolved stub(/run/systemd/resolve/stub-resolv.conf → 127.0.0.53),配合 resolvectl 配置每个接口的上游,是比直接写死 nameserver 更灵活、更可控的方案。Tailscale 的 MagicDNS 可以和 systemd-resolved 协同工作,不需要二选一。
5. 异常重启次数是很好的事故信号
restart counter is at 2232 是一个非常明显的信号,表明这个问题已经持续了很长时间。systemd 服务的重启计数器默认不重置,检查它可以快速判断问题的严重程度和持续时间。
如果你的 VPS 上也在跑 Tailscale,可以用一行命令快速验证 DNS 是否有上游:
tailscale dns status | grep -A3 "Resolvers"如果输出是 (no resolvers configured, system default will be used) 并且系统 DNS 读取失败,那这篇文章说的情况就会发生在你身上。