返回文章
NO.
011
DATE
更新于 2026-07-12
READ
~10 min
STATUS
原文

TAGS: Cloudflare 架构 案例研究

把博客搬到 eigentime.org:一次跨 Cloudflare 账号迁移的诚实复盘

从 yesyes.qzz.io 到 eigentime.org,横跨两个隔离的 Cloudflare 账号。这篇不只讲成功路径,也如实记录我踩到的每一个坑:主分支的部署地雷、www 死活不跳转、悄悄盖过仓库级的环境 Secret,以及一次把人吓一跳的假报错。

这个博客原来挂在 yesyes.qzz.io —— 一个 qzz.io 提供的免费二级域名。免费域名的问题不在域名本身,而在它所属的共享域段:qzz.io 上鱼龙混杂,Google Search Console 一绑上就给我标了「Harmful downloads」,复审也没过。内容干干净净,标记却甩不掉,因为那是域段信誉的连带。

与其反复申诉,不如换一个自己的、干净的域名。我买下了 eigentime.org,托管在 Cloudflare。听起来只是改个域名,实际做下来发现:真正难的从来不是搬内容,而是搬那些藏在配置里、平时根本不会去想的东西。 这篇把整个过程如实记下来,包括每一个我踩到的坑。

先摊开家底:这篇文章的适用边界

复盘之前先交代本站架构——你得先对照自己的技术栈,才知道这篇文章哪些部分对你适用:

  • 公开站点:Astro 静态构建,托管在 Cloudflare Workers Static Assets;
  • 动态层:一个 Worker(/api/* 的浏览与点赞统计,外加预留的 CMS 和付费文章路由);
  • 数据:一个 Workers KV namespace——所有动态数据只有这一处(计数、访客指纹、点赞);
  • 周边:DNS 和邮箱(Email Routing)在 Cloudflare,GitHub Actions 负责构建部署,评论是 Giscus(数据存在 GitHub Discussions 里,不占我的存储)。

所以这次迁移真正要搬的「有状态资产」只有:一个 Worker + 一个 KV + 域名绑定。全文经验都以此为边界。

如果你的站比我的重——比如用 R2 存图片、D1 存数据、Queues 跑任务、Durable Objects 维护状态——这些资源同样锚定在账号里,跨账号迁移时每一个都需要自己的导出、导入与校验路径。我没有踩过那些坑,也不打算在这里无中生有地教你做法:请把它们逐个列进你自己的迁移清单,去查 Cloudflare 对应产品的官方文档(R2 有 S3 兼容 API 和 Super Slurper 迁移工具;D1 有 export/import 命令),然后像本文对 KV 做的那样——为每一类数据先写好「导出 → 导入 → 逐项校验」的脚本,再动手切流。

一个反直觉的决定:不搬 Zone

第一个岔路口就有点反直觉。新域名 eigentime.org另一个 Cloudflare 账号下(注册、DNS、邮箱路由都在那边),而博客的 Worker 和统计数据 KV 在账号下。Cloudflare 的 Worker 自定义域必须绑定同账号内的 Zone,所以旧 Worker 没法直接挂上新域。

两条路:把新域的 Zone 搬回旧账号,或者把 Worker 和数据搬去新账号。我选了后者。理由很实在——新域的邮箱路由、DNS、注册都已经在新账号里跑得好好的,搬一个 Worker 加约 40KB 的 KV 数据,远比搬注册域名 + Zone + 邮箱风险低。最终正式域、正式 Worker、正式数据落在同一个账号里,长期结构才清晰。

于是目标结构变成:新账号重建 Worker + 全量 KV,旧账号最终只做逐路径 301,作为回滚保障至少留 12 个月。

迁移目标结构:旧账号只保留逐路径 301 规则与冻结的回滚快照(Worker 和只读 KV),新账号承载 eigentime.org、生产 Worker 与迁移后的全量 KV

数据搬家:把整个 KV 命名空间搬过去

统计不只是首页那个访客数字。KV 里有两百多个键:每个页面的浏览量、点赞、独立访客指纹、站点总量……只复制首页数字就等于把历史清零。

我写了个小脚本 kv-migrate.mjs,三条命令:导出、导入、校验。两个账号的凭据只从命令行环境读,绝不写进源码,导出文件默认落到系统临时目录并被 .gitignore 拦住——统计数据不进 Git。校验会逐键比对,并对聚合计数做值比对。

这一步很顺,226 个键、值一致。真正的坑在后面。

坑一:主分支上的「部署地雷」

迁移的配置改动(把 KV 命名空间 ID 换成新账号的)留在迁移分支上,没合进 main。这是有意的——因为一旦合并,推送 main 就会触发生产部署。

问题是:绑定自定义域要求新账号里 Worker 已经存在,得先部署一次把它建出来。但如果从 main 部署,main 上的配置还指着账号的 KV ID,用新账号的令牌去部署会直接失败(新账号里根本没有那个命名空间)。

正确做法是从迁移分支引导那次部署——分支上的配置才是新 KV。一个很容易忽略的先后关系:「先有 Worker 才能绑域名」和「先有正确配置才能部署 Worker」是两个必须拆开处理的前提。

坑二:www 死活不跳转

新域绑好、apex 正常返回 200 之后,我跑验证脚本,www.eigentime.org/ 却返回 200 而不是 301——它没跳到根域,而是直接把首页服务出来了。

Worker 代码里明明写了 www→apex 的 301。为什么不生效?

因为这个站的 wrangler.toml 里,run_worker_first 只列了 /api/*/cms-api/* 这几条动态路径,再加上 not_found_handling: "404-page"结果是:静态页面和 404 全部由 Assets 层直接服务,压根不进 Worker。 Worker 顶部那些 host 级重定向(www→apex、.xml 去尾斜杠、乃至后面要用的旧域 301),对普通页面统统不生效——只对那几条 API 路径生效。

这是整次迁移最有价值的一课:在「Worker + 静态资源」这种架构里,host 级重定向不该指望 Worker,而应该放在 Zone 级的 Redirect Rule 里。 改用一条 Redirect Rule 之后,www→apex 立刻正常。(附带一个小插曲:Cloudflare 会提示「这条规则可能不生效,因为 www 没有被代理」——那是因为要重定向的主机名需要一条被代理的 DNS 记录;补一条代理占位记录即可。同样的提示在后面切流时又出现了一次,解法一致。)

坑三:悄悄盖过仓库级的环境 Secret

切换部署账号时,我一度以为这个私有仓库没有「环境级」Secret,工作流是从仓库级读凭据的。后来才发现仓库里存在一个 production 环境,新账号的凭据就设在那一层。

关键在于:GitHub Actions 里,环境级 Secret/变量优先于仓库级。 部署工作流跑在 environment: production,所以它读到的是环境级的新账号凭据;仓库级还留着旧账号的值,被悄悄盖住,正好当作回滚凭据。

这本身没出错,但它暴露了一个真实的隐患:任何没有声明 environment 的工作流,会落回仓库级——也就是旧账号。 我给一个手动的 staging 工作流补上了 environment,否则它被触发时会静默部署到错误的账号。凭据的「层级」是排障时最容易忽略、又最能坑人的地方。

一次把人吓一跳的假报错

切流前做最后一次 KV 同步,校验脚本红着脸报了 FAILED:目标命名空间比源多了 28 个键。

心一沉,仔细一看——没有任何键缺失,也没有任何值不一致。 那 28 个「多出来的」键,是新域上线这段并行期里它自己积累的真实访客指纹。我的校验脚本要求两边键集合完全相等,可对一次「二次同步」来说这假设本来就错了:新域是活的,会长出自己的键。

我把校验语义改成「源 ⊆ 目标且值一致」——缺键或值不符才算失败,目标多出的键只提示、不报错。一个会喊「狼来了」的工具,比没有工具更危险:它会让你在真出事时也懒得看。

切流:让旧域逐路径 301

最后一步,让 yesyes.qzz.io 把每一条路径都 301 到 eigentime.org 的同一条路径。

同样地——不用 Worker 的环境变量做这件事(理由和坑二一样,旧 Worker 也是同样的配置,页面根本不进代码)。而是在旧域的 Zone 上加一条 Redirect Rule:命中 *yesyes.qzz.io,重定向到 concat("https://eigentime.org", path),保留查询参数,301。

验证要盯死几件事:必须是 301 不是 302;必须逐路径映射而不是所有旧 URL 都跳首页;查询参数要保留;整条链路只有一跳。逐条 curl 确认无误,才算切干净。

收尾时的最后一个小坑

以为大功告成,却发现项目展示页里还残留一处旧域链接。第一反应是去改博客仓库的 homepage 字段——改完重新生成数据,旧域还在。

原来那处链接来自另一个仓库(公开的模板仓库)的 Website 字段,而博客仓库本身是私有的、根本不在公开展示列表里。「站内的域名引用」和「从上游仓库同步来的元数据」是两回事:前者随构建变量切换,后者只有改了上游仓库的设置、再重新同步才会变。改对仓库,问题消失。

我仍然不确定、留待观察的事

诚实一点,有几件事我没有百分百把握:

  • SEO 权重能不能干净地迁过去。 301 是标准做法,但这次的 301 源头(旧域)背着一个「有害下载」的安全标记。Google 会不会对一个被标记的域名发出的 301 打折扣,我不知道,只能盯着新域的收录曲线看几周。切流当天倒是有两个好信号:新域的 GSC 没有任何安全问题提示,Change of Address 也一次提交成功——我原本最担心它会因为旧域的标记被直接拒掉。
  • 计数的精确性。 最终同步是「旧盖新」,在导出和开启 301 之间有一个极小的窗口,那段时间旧域的少量写入没被捕获。计数本就是近似值,但它不是分毫不差的。

切流当天的后记

写完上面那节的几个小时里,「不确定」清单核销了一条,另外踩出了两个当天才暴露的坑:

  • 邮箱通了。 双向各发一封实测:收、发都正常。「保留了记录」终于变成了「验证过能用」——原来清单里最让我不安的一条,划掉了。
  • 回滚不再是假设。 给旧账号的 Worker 临时开了 workers.dev 子域做冒烟:旧站还能正常服务,统计接口返回的是切流那一刻的旧数据快照,和新账号当天的数值一减,差值正好是切流后的增量。数字对得上,「停掉 301 规则、旧站顶上」这条退路才算真的存在,而不是文档里的一行字。
  • 一个没预料到的副作用:RSS 订阅者被「轰炸」了一次。 feed 的 guid 默认跟着完整 URL 走,域名一换,所有历史文章的 guid 全变了,阅读器把它们当新文章重推了一遍。修复是把 guid 显式钉死成与域名无关的恒定值:此后 link 永远跟着当前域名走,guid 永不再变。教训:换域名前先检查 feed 的 guid 策略——它比 301 更容易被忘掉,而且发生了就收不回来。
  • 一封吓人的额度预警。 切流当天 Cloudflare 提示新账号的 KV 已用掉每日免费写入额度的 50%。排查后是虚惊:两轮全量导入本身就写了 400 多次,是一次性的。但它逼我正视了统计实现的操作放大——一次页面浏览要发约 38 次 KV 读。顺手做了三层加固:接口同源校验、会话内去重、聚合计数走边缘缓存,一次浏览的读操作降到约 4 次。

迁移「做完」和「收敛完成」是两个时间点——上面每一条,都是在切流之后才冒出来的。

第二天的追记

发布这篇复盘不到半天,清单又长出三条——都补在这里,凑成一份更完整的换域名检查清单:

  • 写额度当晚真的打穿了。 后记里那封「50% 预警」有一半不是虚惊:迁移导入加首日流量,深夜把每日 1,000 次 KV 写入额度耗尽,统计接口开始抛 500——阅读数消失、点赞点了会自动弹回。修复分两层:后端把「写不进」降级成「读还能用」(阅读数照常显示,点赞失败时诚实返回不可用),前端给点赞失败加了一句人话提示。额度次日上午自动重置。教训:在免费额度上做统计,「额度耗尽」不是异常而是必然,第一天就该给它设计降级路径。
  • 搜索结果里的站点是 http:// 新域当天就被 Google 收录(这本身是 301 和地址更改生效的好消息——连没手动提交的旧文都被映射过来了),但结果里显示的是 http 版本。排查发现新 Zone 忘了开 Always Use HTTPS,明文请求一直被 200 服务。旧 Zone 当年开过,迁移清单里却没有这一项:Zone 级的安全开关(强制 HTTPS、HSTS)不会跟着域名「搬家」,每个新 Zone 都要重新设置一遍。
  • 搜索结果里没有站点图标。 主因是收录太新(Google 的图标抓取管线比收录滞后几天),但排查时暴露了一个真实短板:站点只声明了 SVG 图标,/favicon.ico 是 404——而 Google 的图标抓取器和一票爬虫、阅读器会盲请求这个路径。补上了多尺寸 ICO 和 apple-touch-icon 兜底。

这三条有个共同点:都不是迁移「失败」,而是迁移检查清单的盲区——只有在真实流量、真实收录、真实额度周期里跑上一两天才会暴露。复盘文写完不等于复盘结束,把它们记回来,给下一个换域名的人省两天。

几条能带走的经验

  1. 搬域名,难的不是内容,是那些不显眼的依赖:邮件记录、评论的映射键、第三方回调白名单、上游仓库的元数据、CI 凭据的层级。
  2. 在静态优先的架构里,host 级重定向属于 Zone,不属于 Worker——先搞清楚每条请求到底进不进你的代码。
  3. 凭据要分层看:环境级会静默盖过仓库级,排障「部署到了错地方」先查这个。
  4. 让工具只在真出事时报警:校验的语义要贴合真实场景,否则假警报会训练你忽略它。
  5. 每一步都留可回滚的退路:旧账号的 Worker、KV、域名和那条 301 规则,我会至少留到明年——迁移「做完」和「可以拆旧的」是两个时间点。

搬完之后回头看,这个博客一直在讲「先给 AI 一份可验证的边界,再让它动手」。这次迁移某种意义上是同一件事的人肉版:把每一步都变成一条可以 curl 验证的断言,红了就停,绿了才走下一步。 域名换了,这套纪律没变。

评论 →

CC BY-NC-SA 4.0

评论

评论由 GitHub Discussions 提供。登录 GitHub 后即可评论。 前往对应 Discussion