2026-02-20 08:00:00
在去年的 11 月,我入手了一台新的国行版的小米 15 作为我的主力机。其实之前手上的 Redmi K70 Ultra 与这台小米 15 是同一年发布的产品,性能上也不相上下,但我在 Gap Year 期间在全国多地旅行,意识到没能将自己所见的夜景拍下是一件挺可惜的事,于是就想换一台拍照更好的手机。新发布的小米 17 在价格上正处于高位,且相对于 15 来说并没有太大的提升,所以我就直接入了小米 15。
入手以后,我按照自己的使用习惯打开了设置中的「谷歌基础服务」,安装好「Google Play」后便正常使用。但就在这几个月的使用过程中,我发现我的 fcm 推送——无论是 outlook 邮件通知,还是某外观形似纸飞机的即时通讯软件,他们的 fcm 推送好像自始至终都没有正常 work 过。它们的通知偶尔会在我解锁手机后突然冒出来,但更多的时候它们就是不见了。一直要到我手动打开对应的 app,才会收到之前积压的通知。
最近收到了海外院校的 offer,准备在 9 月份去读水硕了,所以就想在出国之前把这个问题解决掉。毕竟在国内生活的时候,大部分软件能通过 mipush 给我推送消息,这些依赖 fcm 推送的 app 并不是我使用频率最高的 app,我每天睡醒手动打开一下就能收到通知了,即使回复消息的及时性不太好也无伤大雅,真有急事的话我的家人朋友们也知道哪些方式能更快的 reach 我。但到了国外,fcm 推送就变得非常重要了,所以要尽快处理掉这个问题,要不然我就得考虑换手机了。
我需要先解决 FCM 在亮屏状态下也无法接收消息的问题。在「电话」界面的拨号盘输入 *#*#426#*#* 可以进入 FCM Diagnostics 界面,里面有一些关于 FCM 连接状态的日志输出。


在这个界面中,我发现 FCM 的连接状态其实是正常的,日志里也没有什么明显的错误信息。于是我就开始怀疑是不是 HyperOS 的某些省电机制在后台干扰了 FCM 的正常工作。通过在小红书的一番搜索,我发现在设置界面打开需要 FCM 推送的 App 的「自启动」权限,并且把电池优化设置为「不优化」,似乎能解决这个问题。

通过 IM 软件(用一个号给另一个号发消息)测试,我发现在亮屏状态下,FCM 可以在 App 后台被关闭的情况下正常接收消息了。
然而,当我把手机熄屏后静置一分钟后,FCM 就完全不工作了。无论是邮件通知还是 IM 消息,都无法通过 FCM 推送到手机上。只有在我点亮屏幕的一瞬间,之前积压的通知才会突然冒出来。再通过 FCM Diagnostics 界面查看,FCM 的连接状态要么是 disconnected,要么是刚刚 connected 几秒。这说明在锁屏状态下,FCM 的连接会被系统断开,导致无法接收消息。
PS: 我发现在充电状态下,即使手机锁屏,FCM 也能保持连接并正常接收消息。这就更加印证了是 HyperOS 的省电机制在干扰 FCM 的正常工作了。
在小红书和小绿书(酷安)一番搜索后,我找到了前人的一些尝试和解决方案:
这是我最不喜欢的方案,因为 MIUI 优化确实是我喜欢 HyperOS (MIUI) 的一个重要原因。关闭 MIUI 优化后,我发现电池的剩余电量信息没法展示在状态栏电池图标内,一定会出现在电池图标右侧,这对于 HyperOS 出现超级岛(灵动岛)后的状态栏空间是一个极大的浪费。当然,还有一些别的功能也会受到影响,但这是我最在意的一个。
另外,在 HyperOS 3 的开发者模式中已经找不到「关闭 MIUI 优化」这个选项了,虽然有用户反馈说可以通过重置设置状态之类的手段来让这个选项重新出现,但我觉得这并不是一个很好的解决方案。
在 HyperOS 2 上,有用户反馈对「电量和性能」这个系统应用动刀可以解决 FCM 熄屏断连的问题。我尝试使用 adb shell 来冻结,经过我的测试这并不是一个有用的方案,并且它可能会导致一些系统调度异常,并且不要进入超级省电模式,因为这个模式的 UI 就是「电量和性能」这个应用提供的!!!
adb 命令如下
adb shell pm uninstall --user 0 com.miui.powerkeeper
如果你想恢复的话,可以通过下面的命令重新安装这个应用:
adb shell cmd package install-existing com.miui.powerkeeper
还有人反馈说可以通过替换为国际版的「电量和性能」应用来解决这个问题,但我没找到 HyperOS 3 国际版的「电量和性能」应用的安装包,下载小米 15 海外版的完整 Rom 解包后,这个应用的 apk 也没法直接覆盖更新或者通过 adb 安装。

这个方案就。。。算了吧,虽然我在小米社区有 5 级账号,但我没有这个精力去参加小米高考(听说还停办了),况且解锁 Bootloader 以后可能还会面临支付软件无法使用等一系列问题,如果要掩盖相关痕迹又要折腾一番,感觉得不偿失了。
该软件已在 Google Play 下架并且长期没有更新,经测试在 HyperOS 3 没法阻止 FCM 在锁屏状态下被系统断开连接。
虽然不知道是什么原理,但有用户反馈说安装 Gboard 键盘可以让 FCM 在锁屏状态下保持连接并正常接收消息了。我也试了一下,确实在安装了 Gboard 并将它设置为默认输入法后,FCM 在锁屏状态下确实能保持连接了。
不过这个方案也不是很完美,毕竟我并不喜欢 Gboard 的输入体验,所以我不太能接受这个方案。
尽管我对安卓开发没多少经验,只有在大二做课设的时候接触过,但 AI 时代赋予了我 vibe coding 的能力

所以我也让 AI 基于 HeartbeatFixerForGCM 的开源代码修改了一些保活 FCM 的逻辑,大概是下面这些思路:
总之,这些尝试都没有成功,FCM 在锁屏状态下依然会被系统断开连接。
就在我山重水复疑无路,准备物色下一台手机的时候,我看到一篇小红书笔记中提到,可以先卸载更新「Google Play 服务」,再重新更新到最新版的方案。帖主的解释是先把国内优化版的 Play 服务卸载掉,再从 Play Store 安装一个没有被国内优化过的版本,能解决这个问题了。
虽然没法在 Play Store 上直接找到「Google Play 服务」这个应用,但可以通过手机浏览器搜索 「Google Play Services」,点开 google.com 上的那个链接,就可以自动跳转到 Play Store 上的「Google Play 服务」应用界面。你也可以直接点这里。
我也试了一下,确实在卸载更新「Google Play 服务」以后,FCM 在锁屏状态下就能保持连接了。虽然这个方案听起来有点玄学且我也无法窥见真正起作用的原理,但既然有效果了,我也就不纠结了。
但就当我以为问题解决了,在写这篇文章的时候,我尝试重启手机,结果发现问题又回来了。并且在重启后,重复上面的卸载更新「Google Play 服务」的步骤,问题依然没有解决,这让我很苦恼,于是开始回忆之前的操作步骤,但始终没让我再复现之前的状态了。。。
就在我和一位资深玩机网友讨论这个问题的时候,他提出国内 OS 确实会在熄屏后为了省电而断开 fcm 的长连接,但仍然会保留定时检查的机制,这与我在测试过程中的部分孤例似乎是吻合的。这个定时检查的间隔时长比较长,据他推测在 10~20 分钟左右。我自己也进行了一轮测试,流程是这样的
因为时间间隔比较长,所以我只测试了一轮半,第一轮的时间差是 28 分钟,第二轮的时间差达到了 38 分钟。
得出结论:在熄屏状态下,虽然 FCM 的连接会被系统断开,但系统会每隔 30 分钟左右(不准确数据)自动唤醒一次 FCM 来检查是否有新的消息,如果有的话就会收到通知了。
目前来说,要在国内版的小米手机上接收 FCM 推送,只能在使用 Gboard + 实时推送 / 定时检查机制的保底方案中二选一了。
前者通过 Gboard 输入法的常驻特性来保持 FCM 连接,能够在熄屏状态下实时接收消息,但需要牺牲输入体验;后者则是通过系统定时唤醒 FCM 来检查是否有新的消息,虽然不需要牺牲输入体验了,但可能会有超过半小时的延迟。
2026-01-31 08:00:00
如果大家对目前中国大陆境内的网络环境足够了解,应该就会知道 dl.google.com 在很多情况下是可以直连访问的。比如,你可以通过 google.cn/chrome/?standalone=1 这个 URL 直接在境内的网络环境下下载 Chrome 的离线安装包,最终的下载域名就是 dl.google.com。
我平常的使用习惯是 24 小时开启代理工具的 TUN,让所有流量先经过一张虚拟网卡,再根据分流规则自动判断要不要走代理。这个习惯大部分时候都挺省心的——直到最近我用 yay 滚 Arch 的时候,突然遇到了 dl.google.com 的 SSL 连接建立失败。

而且不止是 yay,我的浏览器也返回了相同的结果:

当时我的第一反应是:是不是我那套分流规则又抽风了?(毕竟不是我自己写的,出事先甩锅很合理。)
我特意去查了分流规则,针对 SNI 为 dl.google.com 的流量是直连访问的。

这就很奇怪了。按理说:
dl.google.com 本身在国内网络环境里经常是能直连的说实话,这个问题我之前也遇到过,但那时手上有优先级更高的事,就直接关掉代理工具绕过了它完成更新。好在我现在刚处理完手头事情,正处于无事可做的状态,于是决定认真把这个坑填了。
我先把代理工具的 fake-ip 关掉,换成真实 IP 解析(避免再引入额外变量),然后用 curl -vv 去访问 dl.google.com 的下载链接,看看它到底要连到哪里去。

现在回头看我能很笃定地说:这里解析出来的这个 IP 来自 Google 的海外 CDN,而不是国内机房/国内可达的那一类。

如果大家不清楚的话:dl.google.com 针对国内访客的 DNS 解析结果,很多时候会返回国内可达的 IP(否则你也没法在境内直连下载)。而这里返回的这个海外 IP 在我这条网络上是不可达的;再加上我在喵喵工具里给 dl.google.com 配的是直连,于是就变成了:
DNS 给了一个「海外 IP」
- 规则要求 DIRECT = 直连到一个我连不上的地方 = TLS 握手失败
所以这并不是「直连规则没生效」,而更像是:规则生效得非常彻底,但 DNS 把我带沟里了。
Mihomo 内核目前的 DNS 配置项主要是下面四个:
nameserver: 默认解析服务器(大部分域名都走这里)direct-nameserver: 直连域名的解析服务器(较新版本才有)proxy-server-nameserver: 节点域名解析(跟这次没啥关系)default-nameserver: 用来解析 DNS 配置里「域名形式」的 nameserver(也先不展开)dl.google.com 被规则指定为直连域名,所以 Mihomo 理论上应该优先参考 direct-nameserver;如果没设置,就回落到 nameserver。
而我当时的 nameserver 配置是:
https://dns.alidns.com/dns-query我当时的直觉很简单:既然解析结果像是从海外 CDN 池里出来的,那就先验证一下是不是这条阿里 DNS(DoH)返回的就是海外 IP。
阿里 DoH 提供了一个 JSON 查询接口,所以我直接用 curl 去请求:
curl -s 'https://dns.alidns.com/resolve?name=dl.google.com&type=A'

返回的 IP 就是我之前遇到的那个海外 IP。到这一步我基本可以确认:至少在我当前这条网络出口下,阿里 DNS 对 dl.google.com 的解析结果就是“那一类”我访问不到的 IP。
写到这里必须强调一下:这事并不是「阿里 DNS 永远解析错」这么简单,我后来做了一圈对照,发现它其实很“苛刻”:
**只有在「移动宽带」+「阿里 DNS(包括 223.5.5.5 或 alidns 的 DoH)」这两个条件同时成立时,问题才可能稳定复现。**两个条件缺一不可。
更具体一点就是:
dl.google.com 的解析结果通常就正常我也用 itdog 做了下全国解析测试,移动网络下的复现比例确实更高。

为什么会这样?老实说我没有能力给一个“全网唯一真相”的解释,我只能说现象非常一致,而且足够让我下结论:问题不是 TUN 本身,而是 TUN 下我的 DNS 选择把 dl.google.com 导向了一个在移动网络里不可达的地址池。
既然问题出在「移动宽带 + 阿里 DNS」这个组合上,那解决方式也就很朴素了:别让 dl.google.com 继续走阿里 DNS 解析。
可以配置 direct-nameserver 或者 nameserver-policy,可以配置 119.29.29.29 等其他公共 DNS,或者干脆把 DNS 解析交给家里的路由器。
direct-nameserver:
- 192.168.8.1
nameserver-policy:
"dl.google.com": [119.29.29.29]
这么搞完之后,yay 更新恢复正常,浏览器也能直连访问 dl.google.com。
2025-12-23 08:00:00
Vercel 默认的缓存配置其实并不合理,但鲜有人注意。


这两张图都是我博客在 PageSpeed Insights 上测得的,测试步骤如下:
取第二次结果的目的是为了让 PageSpeed Insights 所命中的 Vercel CDN 节点完成回源,并将内容缓存在 CDN 节点上,这样第二次访问的时候就会直接从 CDN 的缓存中得到结果,不需要回源。
那我们看图一的测试结果,正常吗?针对首页的单个 html 加载时长达到了 450ms,看着不算慢,但其实细究下来是有问题的。
Vercel 采用的是 Amazon 提供的全球 CDN 网络,在我们的首次访问之后,CDN 节点应当该已经缓存了首页的内容,第二次访问的时候应该是直接从 CDN 节点的缓存中获取内容。
共计 4.5 个 RTT。
PageSpeed Insights 测试时使用的节点大概率是在美国,Amazon CDN 在美国的节点覆盖非常广泛,单个 RTT 时长控制在 5ms 以内绰绰有余,所以理论加载时长应该在 22.5ms 左右。加上 DNS 解析时长(这个也不多,因为两分钟前有过一次访问,这次不是冷启动)和一些不可控的网络抖动,50ms 以内应该是完全没有问题的。
但实际测得的时长却高达 450ms,差了近 9 倍,这就很不合理了。
我们再来看图二的结果,单个 HTML 加载时长降到了 41ms,完全符合预期。
为什么会有这么大的差异呢?原因就在于 Vercel 对缓存控制的设置上。
在 Vercel 上部署的网站,默认情况下,Vercel 会对 HTML 文件设置如下的缓存控制头:
cache-control: public, max-age=0, must-revalidate
这个设置的含义是:
public:响应可以被任何缓存区缓存,包括浏览器和 CDN。max-age=0: 响应的最大缓存时间为 0 秒,意味着响应一旦被缓存后立即过期。must-revalidate:一旦响应过期,缓存必须向源服务器验证其有效性。结合这三个指令,Vercel 实际上是告诉 CDN 节点:你可以缓存这个 HTML 文件,但每次在使用缓存之前都必须回源验证其有效性。由于 max-age=0,缓存一旦存储就立即过期,因此每次请求都会触发回源验证。
尽管 HTTP/1.1 和 HTTP/2 中的缓存验证通常使用条件请求(如文件的 ETag 或 Last-Modified 头)来节省传输流量,但这仍然需要与源服务器进行往返通信,从而增加了额外的延迟开销。
所以,在 Vercel 默认配置下,任何请求的响应都不会被 CDN 节点直接缓存,大致的流程如下:
sequenceDiagram
participant Client
participant CDN
participant Vercel
Client->>CDN: 请求 HTML 文件
CDN->>Vercel: 条件请求验证缓存
Vercel->>CDN: 返回最新的 HTML 文件或 304 Not Modified
CDN->>Client: 返回 HTML 文件
要解决这个问题,我们需要调整 Vercel 上的缓存控制设置,使得 HTML 文件能够被 CDN 节点缓存一段时间,而不需要每次都回源验证。
Vercel 允许我们在项目的根目录下创建一个 vercel.json 文件以对 Vercel 的部署行为进行一系列的配置,其中就包括 HTTP 的响应头的配置。
我的博客采用 Nuxt.js 框架构建,生成的构建产物大概分为两类:
在部署流程上,我的博客在每次推送后会先 Github Actions 中构建成静态页面,再部署到 Vercel 上,所以我在我项目的 public 目录下创建了 vercel.json 文件(这样 vercel.json 文件就会在构建产物的根目录),内容如下:
{
"headers": [
{
"source": "/(.*)",
"headers": [
{
"key": "Cache-Control",
"value": "public, max-age=0, s-maxage=600, must-revalidate"
}
]
},
{
"source": "/(.*)\\.(css|js)",
"headers": [
{
"key": "Cache-Control",
"value": "public, max-age=31536000, immutable"
}
]
}
]
}
这里我对所有的 CSS 和 JS 文件设置了 max-age=31536000, immutable,这样这些静态资源文件就可以被浏览器和 CDN 长时间缓存。而对于所有其他文件(主要是 HTML 文件),我设置了 max-age=0, s-maxage=600, must-revalidate,这样 HTML 文件就可以被 CDN 缓存 10 分钟,在这 10 分钟内的请求都可以直接从 CDN 节点的缓存中获取内容,而不需要回源验证。
这样一来,经过修改后的缓存控制设置,HTML 文件的请求流程变成了:
sequenceDiagram
participant Client
participant CDN
Client->>CDN: 请求 HTML 文件
CDN->>Client: 直接返回缓存的 HTML 文件
从而大大减少了请求的延迟,提高了页面加载速度。
Vercel 所采用的架构并不是传统的 「源站 - CDN」 架构,而是更接近 「全球多区域存储 + CDN边缘缓存」 的架构,所以即使是回源请求,Vercel 也会尽量从离用户最近的存储节点获取内容,从而减少延迟。但这并不意味着回源请求的延迟可以忽略不计,尤其是在追求极致的加载速度时,合理的缓存控制仍然是非常重要的。
2025-12-10 08:00:00
在我的一台优化线路 vps 上,我的 443 端口要承担两个职责
因此,我需要一个能够在同一台服务器的同一端口上同时处理这两种职责的方案。
其实我很早就知道 Nginx 的 stream 关键词可以实现 Layer 4 (即 TCP 字节流原样转发)下基于 SNI 识别实现的分流功能,但我其实一直是 Caddy 的忠实用户,写了不少 Caddy 相关的博文。因此,尽管 Nginx 在不久前已经支持了 ACME v2,但 Caddyfile 的简洁和易用性依然让我更倾向于使用 Caddy 来实现这个功能。
经过一番查阅,Caddy 最新版本(v2.10)并不支持 Layer 4 的流量代理功能,但有一个名为 caddy-l4 的社区模块可以实现这个功能,在 Github 上有 1.5k stars,且最近也有更新维护,于是我决定尝试使用这个模块来实现我的需求。
尽管 Caddy 官方提供的 APT 源中的 Caddy 版本并不包含 caddy-l4 模块,但我仍然建议先通过 APT 安装 Caddy 的基础版本,然后再通过 Caddy 官方提供的在线构建页面选择需要的模块来生成自定义的 Caddy 二进制文件,下载后替换掉系统中的 Caddy 可执行文件。这样做的好处是可以方便地完成 systemd 服务的配置。但注意关闭 Caddy 的 APT 源,以免后续自动更新覆盖掉自定义编译的版本。
后续更新可以通过
caddy upgrade
命令来完成,caddy 会自动列出当前二进制文件所包含的模块,并自动触发官网的在线构建来生成新的二进制文件并进行替换,只需手动重启 systemd 服务即可完成更新。
如果 Caddy 官方提供的在线构建失败(最近挺不稳定的),可以参考文档使用 xcaddy 在本地编译 Caddy:
xcaddy build --with github.com/mholt/caddy-l4
这是我先前博客站点的 Caddyfile 配置:
zhul.in {
root * /var/www/zhul.in
encode zstd gzip
file_server
handle_errors {
rewrite * /404.html
file_server
}
}
www.zhul.in {
redir https://zhul.in{uri}
}
zhul.in 和 www.zhul.in 都占用了 80 和 443 端口,因此需要把这两个站点的 443 端口的监听改到其他端口,把 443 端口交给 caddy-l4 来处理。
修改后的 Caddyfile 如下:
http://zhul.in:80, https://zhul.in:8443 {
root * /var/www/zhul.in
encode zstd gzip
file_server
handle_errors {
rewrite * /404.html
file_server
}
}
http://www.zhul.in:80, https://www.zhul.in:8443 {
redir https://zhul.in{uri}
}
随后,添加 caddy-l4 的配置:
{
layer4 {
:443 {
@zhulin tls sni zhul.in www.zhul.in
route @zhulin {
proxy 127.0.0.1:8443
}
@proxy tls sni osxapps.itunes.apple.com
route @proxy {
proxy 127.0.0.1:20443
}
}
}
}
这里的写法还挺简单的,首先在 layer4 块中监听 443 端口,然后通过 @name tls sni domain 的方式定义基于 SNI 的匹配规则,随后通过 route @name 定义匹配到该规则时的处理方式,这里使用 proxy ip:port 来实现流量的转发。
由于我的妙妙流量伪装成了 Apple 的 itunes 流量,因此在上面的配置中的 SNI 特征是 osxapps.itunes.apple.com,这些流量会被转发到本地的 20443 端口,由另一个奇妙服务来处理。
caddy-l4 还提供了一些其他的匹配方式和处理方式,具体可以参考他们在 Github 中给到的 examples。
完成配置后,重启 Caddy 服务:
sudo systemctl restart caddy
2025-11-25 08:00:00
在上一篇博客中,我提到过一个核心观点——对于流量少、访客的地理位置不集中的小型站点,DNS 冷启动不是偶发的“意外”,而是一种被动的“常态”。
对于大多数站长而言,自己的站点流量不是一时半刻就能提上去的,因此我们的访客大概率都要走完一遍完整的 DNS 解析过程。上一篇博客中我提到过更改为距离访客物理位置更近的权威 DNS 服务器来提升速度,但 TLD(域名后缀)的 Nameservers 是我们无法改变的,也就是下图中红色背景的那一段解析过程。
sequenceDiagram
autonumber
participant User as 用户/浏览器
participant Local as 本地DNS<br>递归解析器
participant Root as 根域名服务器
participant TLD as 顶级域服务器<br>(TLD Server)
participant Auth as 权威DNS服务器
Note over User,Auth: DNS 递归查询完整流程
User->>Local: 查询域名 www.example.com
Note over Local: 检查缓存 (MISS)
Local->>Root: 查询 .com 的 TLD 服务器
Root-->>Local: 返回 .com TLD 服务器地址
%% --- 重点高亮区域开始 ---
rect rgb(255, 235, 235)
Note right of Local: ⚠️ 本文核心讨论区域 <br> (TLD 解析时延)
Local->>TLD: 查询 example.com 的权威服务器
Note left of TLD: 这里的物理距离与 Anycast 能力<br>决定了是否存在数百毫秒的延迟
TLD-->>Local: 返回 example.com 的权威服务器地址
end
%% --- 重点高亮区域结束 ---
Local->>Auth: 查询 www.example.com 的 A 记录
Auth-->>Local: 返回 IP 地址 (e.g., 1.1.1.1)
Note over Local: 缓存结果 (TTL)
Local-->>User: 返回最终 IP 地址
User->>Auth: 建立 TCP 连接 / HTTP 请求
所以,如果你还没有购买域名,但想要像个 geeker 一样追求极致的首屏加载(哪怕你并没有多少访客),你该选择哪个 TLD 呢?
一个简单的方法是,直接去 ping TLD 的 nameserver,看看访客所请求的公共 DNS 服务器在这一段解析中所花费的时常。
以我的域名 zhul.in 为例,在 Linux 下,可以通过 dig 命令拿到 in 这个 TLD 的 Nameserver
dig NS in.

随后可以挑选任何一个 Nameserver(公共 DNS 服务器其实有一套基于历史性能的选择策略),直接去 ping 这个域名

我这里的网络环境是杭州移动,如果我在我的局域网开一台 DNS 递归服务器,这个结果就是在上面那张时序图中红色部分所需要时长的最小值(DNS 服务器还需要额外的时长去处理请求)。
借助一些网站提供的多个地点 ping 延迟测试,我们可以推测这个 TLD 在全球哪些国家或地区部署了 Anycast(泛播)节点,下图为 iplark.com 提供的结果。

可以推测,in 的 TLD Nameserver 起码在日本、香港、美国、加拿大、欧洲、澳大利亚、巴西、印度、南非等多地部署了 Anycast 节点,而在中国大陆境内的延迟较高。
作为对比,我们可以通过同样的方法再看看 cn 域名的 TLD Nameserver 的 Anycast 节点。

经过 itdog.cn 的测试,推测 cn 域名的 TLD Nameserver 可能仅在北京有节点。
上面的测试方法只是一个简易的判断方法,在现实中会有很多的外部因素影响 DNS 冷启动的解析时长:
所以,我们需要有一个基于真实的 DNS 解析请求的测试方案
对于 DNS 冷启动相关的测试一直以来存在一个困境——公共 DNS 服务器不归我们管,我们无法登陆上去手动清除它的缓存,因此所有的测试都只有第一次结果才可能有效,后续的请求会直接打到缓存上。但这一次我们测试的是公共 DNS 服务器到 TLD Nameserver 这一段的延迟,在 Gemini 的提醒下,我意识到可以在不同地区测试公共 DNS 对随机的、不存在的域名的解析时长,这能够反应不同 TLD 之间的差异。
所以,测试代码在下面,你可以使用常见的 Linux 使用 bash 执行这段代码,需要确保装有 dig 和 shasum 命令,并且推荐使用 screen / tmux 等工具挂在后台,因为整个测试过程可能会持续十几分钟。如果你所采用的网络环境在中国大陆境内,我建议你把代码中的公共 DNS 服务器换成 223.5.5.5 / 119.29.29.29 ,应该会更符合境内访客的使用环境。
#!/bin/bash
# ================= 配置区域 =================
# CSV 文件名
OUTPUT_FILE="dns_benchmark_results.csv"
# DNS 服务器
DNS_SERVER="8.8.8.8"
# 待测试的 TLD 列表
# 包含:全球通用(com), 国别(cn, de), 热门技术(io, xyz), 以及可能较慢的后缀
TLDS_TO_TEST=("com" "net" "org" "cn" "in" "de" "cc" "site" "ai" "io" "xyz" "top")
# 每个 TLD 测试次数
SAMPLES=1000
# 每次查询间隔 (秒),防止被 DNS 服务器判定为攻击
# 1000次 * 0.1s = 100秒/TLD,总耗时约 15-20 分钟
SLEEP_INTERVAL=0.1
# ===========================================
# 初始化 CSV 文件头
echo "TLD,Domain,QueryTime_ms,Status,Timestamp" > "$OUTPUT_FILE"
echo "============================================="
echo " DNS TLD Latency Benchmark Tool"
echo " Target DNS: $DNS_SERVER"
echo " Samples per TLD: $SAMPLES"
echo " Output File: $OUTPUT_FILE"
echo "============================================="
echo ""
# 定义进度条函数
function show_progress {
# 参数: $1=当前进度, $2=总数, $3=当前TLD, $4=当前平均耗时
let _progress=(${1}*100/${2})
let _done=(${_progress}*4)/10
let _left=40-$_done
# 构建填充字符串
_fill=$(printf "%${_done}s")
_empty=$(printf "%${_left}s")
# \r 让光标回到行首,实现刷新效果
printf "\rProgress [${_fill// /#}${_empty// /-}] ${_progress}%% - Testing .${3} (Avg: ${4}ms) "
}
# 主循环
for tld in "${TLDS_TO_TEST[@]}"; do
# 统计变量初始化
total_time_accum=0
valid_count=0
for (( i=1; i<=${SAMPLES}; i++ )); do
# 1. 生成随机域名 (防止缓存命中)
# 使用 date +%N (纳秒) 确保足够随机,兼容 Linux/macOS
RAND_PART=$(date +%s%N | shasum | head -c 10)
DOMAIN="test-${RAND_PART}.${tld}"
TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S")
# 2. 执行查询
# +tries=1 +time=2: 尝试1次,超时2秒,避免脚本卡死
result=$(dig @${DNS_SERVER} ${DOMAIN} A +noall +stats +time=2 +tries=1)
# 提取时间 (Query time: 12 msec)
query_time=$(echo "$result" | grep "Query time" | awk '{print $4}')
# 提取状态 (status: NXDOMAIN, NOERROR, etc.)
status=$(echo "$result" | grep "status:" | awk '{print $6}' | tr -d ',')
# 3. 数据清洗与记录
if [[ -n "$query_time" && "$query_time" =~ ^[0-9]+$ ]]; then
# 写入 CSV
echo "${tld},${DOMAIN},${query_time},${status},${TIMESTAMP}" >> "$OUTPUT_FILE"
# 更新统计
total_time_accum=$((total_time_accum + query_time))
valid_count=$((valid_count + 1))
current_avg=$((total_time_accum / valid_count))
else
# 记录失败/超时
echo "${tld},${DOMAIN},-1,TIMEOUT,${TIMESTAMP}" >> "$OUTPUT_FILE"
current_avg="N/A"
fi
# 4. 显示进度条
show_progress $i $SAMPLES $tld $current_avg
sleep $SLEEP_INTERVAL
done
# 每个 TLD 完成后换行
echo ""
echo "✅ Completed .${tld} | Final Avg: ${current_avg} ms"
echo "---------------------------------------------"
done
echo "🎉 All Done! Results saved to $OUTPUT_FILE"
免责声明:以下测试结果仅供参考,不构成任何购买推荐,且仅代表测试当日(2025.11.24)的网络情况,后续不会进行跟进。DNS 冷启动对于大型站点几乎没有影响,仅小站需要关注。本次测试中,所有境内检测点使用 223.5.5.5 作为 DNS 服务器,境外检测点使用 8.8.8.8。
| 测试点/延迟(ms) | .com | .net | .org | .cn | .in | .de | .cc | .site | .ai | .io | .xyz | .top |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 🇨🇳 上海腾讯云 | 438 | 429 | 470 | 30 | 535 | 353 | 476 | 454 | 367 | 485 | 444 | 43 |
| 🇨🇳 北京腾讯云 | 425 | 443 | 469 | 17 | 350 | 420 | 466 | 647 | 582 | 461 | 559 | 9 |
| 🇭🇰 香港 Yxvm | 75 | 75 | 363 | 227 | 6 | 11 | 61 | 6 | 33 | 126 | 5 | 7 |
| 🇨🇳 彰化(台湾) Hinet | 90 | 87 | 128 | 213 | 59 | 38 | 76 | 37 | 73 | 94 | 36 | 47 |
| 🇯🇵 大阪 Vmiss | 20 | 19 | 244 | 309 | 15 | 24 | 17 | 35 | 19 | 65 | 37 | 90 |
| 🇸🇬 新加坡 Wap | 6 | 9 | 139 | 398 | 6 | 10 | 7 | 17 | 7 | 110 | 17 | 66 |
| 🇺🇸 洛杉矶 ColoCrossing | 7 | 7 | 307 | 137 | 4 | 64 | 5 | 62 | 5 | 49 | 47 | 231 |
| 🇩🇪 杜塞尔多夫 WIIT AG | 16 | 17 | 288 | 82 | 75 | 15 | 14 | 24 | 66 | 73 | 24 | 306 |
| 🇦🇺 悉尼 Oracle | 33 | 31 | 12 | 338 | 7 | 13 | 121 | 7 | 10 | 9 | 7 | 191 |
通过上面的数据,我们可以看到 .cn 和 .top 是所有测试的域名后缀中在中国大陆境内解析速度最快的,但选择 .cn 和 .top 意味着你需要牺牲其他地区访客的解析速度。而像 .com、.net、.org 这些通用的域名后缀在全球绝大部分地区表现良好,而在中国大陆境内的解析速度则相对较慢,因为他们没有在大陆境内部署 Anycast 节点。在 DNS 冷启动的场景下(如果你的站点访客少,那几乎每次访问都是冷启动),首屏加载时间会因此增加 500ms 甚至更多。
经 v2ex 的网友 Showfom 提醒,GoDaddy 作为注册局掌握的部分 TLD 的 Nameserver 同样在中国大路境内拥有 Anycast 节点,比如 .one、.tv、.moe 等。另, Amazon Registry Services 旗下的 .you 域名经我测试也有境内的 Anycast 节点。其他域名后缀可自行测试。
你可以点击这里下载完整的测试结果 CSV 文件进行进一步的分析。
2025-11-11 08:00:00
当我们谈论网站性能时,我们通常关注前端渲染、资源懒加载、服务器响应时间(TTFB)等。然而,在用户浏览器真正开始请求内容之前,有一个至关重要却鲜少在性能优化方面被提及的部分—— DNS 解析。对于默默无闻的小型站点而言,“DNS Cache Miss”(缓存未命中)或我称之为“DNS 冷启动”,会成为绕不过去的性能瓶颈,也就是本文标题所提到的“西西弗斯之石”。
要理解这块“石头”的重量,我们必须重温 DNS 解析的完整路径。这并非一次简单的查找,而是一场跨越全球的接力赛:
.com)的顶级域名服务器。sequenceDiagram
participant User as 用户/浏览器
participant Local as 本地DNS<br>递归解析器
participant Root as 根域名服务器
participant TLD as 顶级域服务器<br>(.com, .org等)
participant Auth as 权威DNS服务器
Note over User,Auth: DNS递归查询完整流程
User->>Local: 1. 查询域名<br>www.example.com
Note over Local: 检查缓存<br>未找到记录
Local->>Root: 2. 查询 .com 的TLD服务器
Root-->>Local: 3. 返回 .com TLD服务器地址
Local->>TLD: 4. 查询 example.com 的权威服务器
TLD-->>Local: 5. 返回 example.com 的权威服务器地址
Local->>Auth: 6. 查询 www.example.com 的A记录
Auth-->>Local: 7. 返回 IP地址 (e.g., 1.1.1.1)
Note over Local: 缓存结果<br>(根据TTL设置)
Local-->>User: 8. 返回最终IP地址
Note over User,Auth: 后续流程
User->>Auth: 9. 使用IP地址建立TCP连接<br>开始HTTP请求
对于首次或长时间未访问的请求,这个过程意味着至少 4 次网络往返(RTT),而在涉及到 CNAME 等情况时则会更多。对于那些拥有完美缓存的大型网站来说,这块石头可能已被别人推到了山顶;但对小型站点,它总是在山脚等待它的西西弗斯。
“既然 DNS 冷启动的代价如此之高,那我能否使用脚本定时访问自己的网站,提前让公共 DNS 缓存预热起来呢?”——这是我曾经设想的解题思路。
然而,这一思路在现代互联网的 Anycast(泛播)架构下,往往徒劳无功。
Anycast 的核心理念是:同一个 IP 地址在全球多个节点同时存在,用户请求会被路由到“距离最近”或“网络路径最优”的节点。
这意味着,Google DNS (8.8.8.8) 、Cloudflare DNS (1.1.1.1)、阿里 DNS (223.5.5.5)、腾讯 DNS (119.29.29.29) 等公共 DNS 服务器背后并不是一台中心化的服务器,而是一组分布在世界各地、动态路由的节点集群。
于是问题出现了:
从站长的视角来看,DNS 缓存不再是一个可预测的实体,而是分裂成一片片地理隔离、随时可变的“镜像迷宫”。
每个访客都在不同的山脚下推着自己的那块石头,仿佛世界上有成千上万个西西弗斯,孤独地在各自的路径上前行。
这也解释了为什么即便一个小型网站有规律地被脚本访问,仍可能在真实访客那里出现明显的 DNS 延迟。因为「预热」只是局部生效 —— 它温暖的是某一个任播节点的缓存,而不是整个网络的全貌。而当 TTL 到期或缓存被公共 DNS 服务器采用 LRU 等算法清理时,这份温度也会悄然散去。
从宏观上看,这让“小流量站点”陷入了某种宿命循环:
冷启动不再是偶发的“意外”,而是一种被动的“常态”。
西西弗斯的困境看似无解,但我们并非完全无能为力。虽然无法彻底消除 DNS 冷启动,但通过一系列策略,我们可以显著减轻这块石头的重量,缩短它每次滚落后被推上山顶的时间。
TTL(生存时间)是 DNS 记录中的一个关键值,它告知递归解析器(如公共 DNS、本地缓存)可以将一条解析记录缓存多久,尽管他们可能会被 LRU 算法淘汰。
拉长 TTL 可以有效提高缓存的命中率,减少 DNS 冷启动的情况,尽可能让西西弗斯之石保留在山顶上。
但拉长 TTL 是以牺牲灵活性作为代价的:如果你因为某些原因需要更换域名做对应的 IP 地址,过长的 TTL 可能会导致访客在很长一段时间内取得的都是已经失效的 IP 地址。
DNS 解析的最后一公里——从公共 DNS 服务器到你的权威 DNS 服务器——的耗时同样至关重要。如果你的域名所采用的 Nameserver 服务响应缓慢、全球节点稀少、又或者距离访客所请求的公共 DNS 服务器距离太远,那么即使用户的公共 DNS 节点就在身边,整个解析链条依然会被这最后一环拖慢。
如果我正在写的是一篇英文博客,那么我只需要说把 Nameserver 换成 Cloudflare、Google 等一线大厂就完事了。这些大厂提供免费的权威 DNS 托管业务,且在全球各地拥有大量节点,在这方面是非常专业且值得信赖的。
但我现在正在使用简体中文,根据我的博客统计数据,我的读者大多来自中国大陆,他们的站点访客大多也来自中国大陆,他们请求的公共 DNS 服务器大概率也都部署在中国大陆,而 Cloudflare/Google Cloud DNS 完全没有权威 DNS 服务器的中国大陆节点,这会拖慢速度。所以如果你的访客主要来自中国大陆境内,或许可以试试阿里云或者 Dnspod,他们主要的权威 DNS 服务器节点都在中国大陆境内,这在理论上可以减少公共 DNS 服务器与 权威 DNS 服务器之间的通信时长。
DNS 冷启动的问题,从未有完美的解决方案。它像是互联网架构中注定存在的一段“延迟的诗意”——每个访问者都从自己的网络拓扑出发,沿着看不见的路径,一步步推着那块属于自己的石头,直到抵达你的服务器山顶,换得屏幕上第一个像素的亮起。
对小型站点而言,这或许是命运的重量;但理解它、优化它、监测它,便是我们在这条漫长上坡路上,为石头磨出更光滑的棱角。