主播高三了,接下来一年没有时间手动维护这个博客。不过好在静态站点本身几乎不需要维护,理论上只要cf不挂我的站就不会挂。但由于某些原因,cf在国内解析质量一直不很稳定,故而本站使用了pages优选,这就带来了隐患——优选域名一旦出问题我的也会跟着爆炸。前段时间就遇到了此问题:

所以自动化维护域名解析就成了必选项。
于是我让GLM帮我写了个 Cloudflare Worker(dns-guard),每 5 分钟做一次检测,故障时自动改写 DNS 记录,恢复后自动切回。利用多个优选域名列表来降低优选域名故障风险。如下是记录:
一、先化简问题:内容与解析是解耦的
动手之前先搞清楚”挂了”到底意味着什么。排查时发现一个关键事实:这个站的内容其实不依赖 Pages 自定义域的证书与路由——zone 里存在一条 Workers 路由 blog.yuer6327.top/*,任何解析到 Cloudflare 边缘的请求都会被这个 Worker 接管并出内容。Pages 自定义域甚至处于 deactivated 状态,站点照样正常访问。
这意味着高可用问题可以降维:只要访客的 DNS 能把域名解析到任何 Cloudflare 边缘 IP,站点就能访问。于是要守护的只有一条 DNS 记录,故障切换的本质是”始终让这条记录指向一个可达的 Cloudflare 入口”。按可靠性递增、速度递减,入口分三级:
- 优选域名(CNAME,灰云):快,但依赖第三方;
- 其他优选域名:同为第三方,互为备份;
- 兜底记录(
AAAA 100::,橙云):一个合法的占位 IPv6 地址,橙云代理后请求直接落 Cloudflare 边缘,由上述路由 Worker 出内容——完全不依赖任何第三方域名,代价是走默认线路,绕路慢。
第三级是整个系统的安全网,也是”优选挂了至少能访问”的保证。
二、探测信号:如何知道”国内解析正常”
这是整个设计里最关键也最容易做错的决策。Worker 只能跑在 Cloudflare 海外边缘节点上,“从上海拨一条线路实测”这种路由层探测在 Worker 里做不到。但我的目标不是”路由质量”而是”解析正确”——而解析是可以在远端验证的:向国内的公共 DNS 发起 DoH 查询,解析工作由境内解析器完成,返回的结果就是国内用户实际拿到的答案。
因此健康主信号设计为:并行向阿里 DNS(dns.alidns.com)、腾讯 DNSPod(doh.pub)、360(doh.360.cn)三家查询本站 A 记录,跟随 CNAME 链取最终 A 记录,全部落在 Cloudflare 官方 IPv4 段内才算这一家投”健康”。三家做多数投票:≥2/3 健康才判定健康,容忍单家解析器抽风;应答不足两家视为”不可判定”,此时退化为海外 HTTP 探测(非 5xx 即视为线路可用)作为降级信号。
这里踩过一个值得记录的坑:Cloudflare IPv4 段匹配用位运算实现,(ip & mask) === base 在 JS 里返回的是带符号 Int32,所有 128.0.0.0 以上的地址(172.x、162.x 这些 CF 最常见的段)与运算结果是负数,永远不等于无符号的 base。第一版代码因此把所有候选全部误判为失效——如果直接上线,系统会在首次检测时把所有子域切到兜底。这类 bug 本地冒烟测试(wrangler dev + 真实 DoH)能立刻暴露,比上线后发现便宜得多。
候选优选域名的有效性校验复用同一套逻辑:国内多数公共 DNS 解析到 CF 段即有效。一个经验是不要求候选域名自己能打开网页——cloudflare.182682.xyz 这类域名解析完全正常但根路径没有服务,作为 CNAME 目标毫无问题。
也如实写下这套探测的边界:它能发现 DNS 层的失效(域名换 CDN、NXDOMAIN、脱离 CF 段),发现不了路由层的地域性问题(比如某 IP 段被部分运营商丢包)。后者需要国内拨测源(17ce/BOCE 一类),是另一个量级的架构,不在本系统的目标内。
三、状态机:切换与恢复的节奏控制
自动改 DNS 最大的风险是振荡——探测有噪声,一次抖动就切线路,比故障本身更伤。所以状态机大部分代码其实花在”不切换”上:
- 故障切换:连续 2 个 tick(约 10 分钟)不健康才动手,切到候选列表中优先级最高的有效候选;全部失效才动兜底;
- 恢复切回:处于低优先级线路时持续跟踪高优先级候选,连续 3 个 tick 健康才升级回去——恢复比故障更保守,因为”恢复”的误判往往来自探测噪声;
- 宽限期:切换后跳过 2 个 tick 不做评估,等 DNS TTL(300s)传播,避免用旧缓存评估新记录;
- 防抖:两次切换最小间隔 10 分钟;
- 极端分支:兜底模式下仍解析失败,说明 Cloudflare 本身故障,改记录没有意义,只推送告警。
所有状态存 Workers KV,事件(切换/失败/告警)经 Bark 推送到手机,每次附带切换后的记录内容,事后可审计。
后来把 4 个子域(blog/english/opan/wordle)纳入守护时做了一次重构:起初每个子域一个独立状态机,很快意识到这是伪需求——四条记录永远同步切换、解析路径完全一致,独立状态机只放大了 KV 写入量(4 倍)和误判面。现在只有一个共享状态机:健康检查查代表域名,决策一次做出,同一动作批量应用到全部记录。当多个对象的行为被设计成完全一致时,状态就该只存一份,这个教训值得单独记下来。
四、实现选型
为什么不是 GitHub Actions 定时任务:公开仓库的 schedule workflow 在 60 天无活动后会被 GitHub 自动停用,对”一年无人值守”是致命的;Worker Cron 没有这个问题,且与同账户的 DNS API 天然集成。免费额度完全够:每 5 分钟一次是每天 288 次调用,远低于 10 万次/天的上限;KV 免费额度 1000 写/天,通过”状态有变化才写 + 稳定时每小时心跳刷一次时间戳 + 测速快照每轮一次”的写入策略控制在 350 次/天以内。这里也修过一个典型 bug:心跳判断拿”刚更新完的时间戳”去算是否过了 1 小时,永远得 0,导致稳定状态下面板的”最近检测”永远停在最后一次事件——正确做法是用更新前的时间戳判断。
切换动作:调 Cloudflare API PATCH 记录。zone ID 和记录 ID 首次查询后缓存在 KV。有个细节:优选(CNAME)与兜底(AAAA)记录类型不同,个别情况下 PATCH 不接受跨类型修改,代码里做了”删除后重建”的回退路径。兜底依赖每个子域在 zone 里都有自己的 Workers 路由——这是新增守护对象的前置条件,没有路由的子域(比如早已废弃的 oss)纳入守护反而有害。
测速为什么不参与决策:面板上展示各候选的 TTFB(Worker 边缘视角,3 次取中位数),但刻意不允许它触发切换。原因第一轮数据就说明了:所有候选都是 Cloudflare 同一张 anycast 网,从海外 Worker 看延迟全在 4~34ms,这个数字测的是”海外 → CF 边缘”,而国内访客的体验由”运营商 → CF 边缘”那一大段决定,Worker 根本看不见。按海外测速自动切换,大概率选出”海外快、国内慢”的候选。
KV 最终一致性:KV 是跨数据中心最终一致的(约 60 秒窗口),手动切换后紧邻的 tick 可能读到旧状态并做一次多余评估。没有为此引入强一致方案——误切换本来就被”连续 2 次失败 + 10 分钟防抖”双重门槛挡住,竞态的实际影响只是一个 tick 内自愈的多余动作。
五、验证
上线前用生产环境做了完整演练:通过应急接口手动切到兜底 → 用国内 DoH 确认解析变为 CF 段、站点正常出内容 → 切回优选 → 确认记录与解析链还原。系统自动切换的路径与手动切换走的是同一段代码(applyRecord),演练覆盖的即真实故障路径。最终的自愈延迟约为:检测确认 10 分钟 + TTL 传播 5 分钟,最坏 15 分钟——对一个静态博客足够了。
六、小结
这个系统没有引入任何新组件:探测靠公共 DoH、执行靠同账户 DNS API、调度靠 Worker Cron、状态靠 KV。设计上真正花心思的只有三处:把”站点高可用”化简为”解析正确”(利用 Workers 路由的内容接管);用国内公共解析器获得国内视角的探测信号;以及用多重阈值把”自动切换”的振荡风险压到可接受。剩下的都是工程细节。
已知取舍:路由层地域性故障超出探测能力;候选优选域名池需要人工维护(失效会被自动跳过,不需要手动清理);Worker 免费版单次请求有子请求数上限,候选数量不宜无限增长。如果未来需要真正的”国内体验感知”,正确方向是接入国内拨测平台的回调,把拨测结果作为第四级信号引入状态机——架构上已经预留了这个位置。