个人博客全站 CDN 化实践:踩坑与收获 | 一方天地
前言
我的博客一直是个"麻雀虽小五脏俱全"的架构:Vue 前端 + Spring Boot 后端部署在同一台云服务器上,Nginx 做分流,图片和安装包放在阿里云 OSS。这套架构跑了大半年一直很稳,但最近我把它彻底改造了一番——前端整体迁到 OSS 静态托管,lantech.top 全站接入 CDN,服务器退化成一台纯 API 服务器。
这篇文章记录这次迁移:CDN 到底帮我解决了什么问题,以及过程中踩过的一个个坑。如果你也打算给自己的小站上 CDN,希望这些真实的踩坑记录能让你少走点弯路。
为什么要上 CDN
先说说迁移前的架构长什么样:
lantech.top → 服务器 Nginx
├─ /api/* → 反代到 Spring Boot
└─ 其它 → 返回前端 index.html / js / css
oss.lantech.top → OSS(图片 / CLI / APK)
这套架构能用,但有三个让我一直不太舒服的点:
1. OSS 不支持 HTTP/2。 我的图片、安装包都放在 OSS 上,但 OSS 的自定义域名只支持 HTTP/1.1。浏览器对单域名的并发连接数有限(6~8 个),文章里图一多,加载就得排队,首屏明显变慢。
2. 源站 IP 完全暴露。 lantech.top 直接 A 记录解析到服务器 IP,任何人扫一下就能拿到我的真实 IP 直接打。虽然我做了些防护,但源站裸奔总归不踏实。
3. 怕被刷流量。 图片和 APK 都是公开可下载的,如果被人恶意刷流量,OSS 的下行流量费是要真金白银掏的。虽然小站被盯上的概率不高,但没有护栏就睡不安稳。
CDN 恰好能一并解决这三个问题:边缘节点提供 HTTP/2 和 Brotli 压缩、把源站藏在节点后面、自带 IP 限频和流量封顶。于是我决定动手。
目标架构
改造后的架构是这样:
┌──────────────────────┐
用户请求 │ 阿里云 CDN │
───────────────► │ lantech.top │
│ (HTTP/2 · Brotli) │
└──────┬─────────┬──────┘
│ │
/api/* 条件源站 │ │ 其它路径 基础源站
│ │
┌───────────▼───┐ ┌─▼───────────────┐
│ 云服务器 │ │ OSS 静态托管 │
│ Nginx │ │ lantech-web │
│ → Spring │ │ (前端 dist) │
│ Boot API │ └─────────────────┘
└───────────────┘
独立链路(不动):
oss.lantech.top → CDN → OSS(图片 / CLI / APK)
用一张图概括整个请求流向:

几个关键决策:
- 新建一个 bucket
lantech-web专门放前端,和图片 bucket 分开。一来 index.html 必须在 bucket 根目录,混在一起不整洁;二来前端和图片可以用完全不同的缓存策略。 /api走条件源站回源到服务器,其它路径走基础源站回源到 OSS。这样一个域名下同时承载动态接口和静态页面。- 图片链路
oss.lantech.top完全不动,本次只迁移lantech.top这条。
CDN 到底解决了什么
迁移跑通之后,回头看 CDN 给我带来的实际收益:
1. HTTP/2 + Brotli,白送的性能优化。 这是最直接的好处。原来 OSS 上 200 多个 js/css 小文件在 HTTP/1.1 下要排队下载,现在走 CDN 的 HTTP/2 多路复用,一条连接并行拉取,首屏快了不少。Brotli 压缩也是 CDN 自动加的,文本资源体积又小了一截。
2. 源站 IP 隐藏。 现在 lantech.top CNAME 到 CDN,外部只能打到 CDN 边缘节点,拿不到我服务器的真实 IP。配合后面要做的安全组"锁源"(只放行 CDN 回源 IP 段),服务器就彻底隐身了。
3. 防刷护栏。 CDN 控制台可以直接配 IP 访问限频(比如 /api 每 IP 每秒 20 次)和流量封顶(超量自动停止服务)。再叠上阿里云的费用告警,"被刷流量导致账单爆炸"这个焦虑算是有了兜底。
4. 异地加速(锦上添花)。 我的访问主要来自河北,异地很少,所以这条对我用处不大。但 CDN 的就近节点分发,对有全国访客的站点是核心价值。
过程中踩的坑(重点)
这部分是本文真正的价值所在。看似简单的"加个 CDN",我足足折腾了大半天,踩了六个坑。每一个都记录如下,希望能帮你避开。
坑 1:Nginx 的 Referer 校验误拦 API
迁移后 API 一直返回 403。排查发现,我之前在 Nginx 的 location /api/ 里配了 Referer 防盗链:
location /api/ {
valid_referers blocked lantech.top *.lantech.top;
if ($invalid_referer) {
return 403;
}
proxy_pass http://172.17.0.1:8081/;
}
问题是 valid_referers 里没有 none——它允许"Referer 被剥离"和"指定域名"的请求,但不允许"完全没有 Referer 头"的请求。而 CDN 回源、App 调用、curl 直连时,经常压根不带 Referer,全被误判成非法请求 403 了。
教训:API 不适合用 Referer 防盗链。 Referer 头本来就可以随便伪造,防护价值约等于零,只会误伤正常调用。API 的防刷应该靠 CDN 的 IP 限频,不是 Referer。我最后把这段校验整个删了。
坑 2:CDN 回源走 HTTP,造成 301 死循环
修完 403 又遇到更诡异的:访问 API 返回 301,而且 Location 指向和请求一模一样的地址,无限循环。
原因是我 Nginx 配了 HTTP 强制跳转 HTTPS,而 CDN 默认用 HTTP(80) 回源。于是:
CDN → HTTP 回源服务器 80 → Nginx 301 跳到 https://lantech.top → 又指回 CDN → 循环
解法:把 CDN 回源协议改成 HTTPS(443),同时配好回源 HOST 和 SNI 为 lantech.top(这样服务器能返回正确的证书、命中正确的 server 块)。改完 CDN 直接 HTTPS 回源命中 443,不再经过 80 的跳转,循环消失。
坑 3:"高级回源"只支持精确匹配,/api/* 不生效
这是最坑的一个。我需要让 /api/* 走服务器、其它走 OSS,阿里云 CDN 的"高级回源"里匹配方式只有"等于"和"不等于"。我填了"Path 等于 /api/*",以为 * 是通配符——结果一条真实 API 都匹配不到(/api/article/list 并不等于字面量 /api/*)。
而更糟的是,这期间 /static/... 的请求居然被回源到了服务器(返回 nginx 的 404),说明默认源站也配错了。
解法:放弃"高级回源",改用"条件源站"(或规则引擎),那里支持 URI 通配符 /api/*,能正确做前缀匹配。同时确认基础源站只有 OSS 一个,服务器只出现在条件源站里。正确的分工是:
- 基础源站 =
lantech-web(OSS),兜底所有路径 - 条件源站 = 服务器 IP,只在 URI 匹配
/api/*时命中,优先级高于基础源站
坑 4:默认源站配错,静态资源回源到服务器
承接坑 3,有段时间 /static/js/xxx.js 返回的 404 页面,body 里赫然写着 nginx/1.21.5——这个 404 是服务器返回的,不是 OSS。说明静态资源被错误地回源到了服务器,而服务器上根本没有这些文件。
判断技巧:看 404/403 页面的 Server 头和返回格式。 OSS 返回的是 XML(<Error><Code>...),带 x-oss-request-id;Nginx 返回的是 HTML 错误页(<center>nginx/1.21.5</center>)。这一眼就能看出请求被路由到了哪个源站,排查时特别有用。
坑 5:文件明明传了,CDN 却返回缓存的旧 404
修好源站后,有一个 js 文件始终 404——但直接访问 bucket 里这个文件是 200!原来是 CDN 把这个文件之前的 404 缓存住了(我给 js 配了 1 年缓存,文件没传好时 CDN 请求过一次,把 404 也缓存了)。
解法:去 CDN 控制台精确刷新这个 URL 的缓存。 目录刷新有时对深层文件不彻底,精确刷新单个 URL 最可靠。
教训:缓存是把双刃剑。 长缓存能加速,但一旦缓存了错误响应,就得手动清。这也是为什么"自动刷新"功能对固定文件名的资源(图片、APK)很有用——文件一变更就自动失效 CDN 缓存。
坑 6:前端打包的 base 指向了旧域名
最早首页能开,但引用的 js/css 全指向 oss.lantech.top(图片域名),根本没走新的 lantech.top CDN。排查发现是 Vite 打包时 base(publicPath)设成了 /,导致 HTML 里把资源写死成了那个域名。
解法:改用正确的环境变量重新打包,让 base 指向 https://lantech.top/,把新 dist 重新上传到 lantech-web bucket。改完 HTML 里引用的就是 lantech.top/static/...,静态资源才走上新 CDN,1 年缓存规则也才生效。
缓存策略心得
这次迁移让我对"缓存"有了更深的理解,总结成两条:
带 hash 的文件名 = 免刷新。 我的 js/css 都是 index-P9btHglL.js 这种带内容 hash 的文件名,内容一变文件名就变,是全新的 URL,CDN 上根本没有旧缓存。所以它们可以放心配 1 年长缓存,更新时天然不需要刷新。
固定文件名 = 要么短缓存,要么配自动刷新。 index.html、API、lantech-client.apk 这种文件名固定的,更新后 CDN 上还有旧缓存。处理方式:index.html 和 API 我直接配 0 秒不缓存;图片和 APK 这种要缓存的,就靠 OSS 的"缓存自动刷新"功能,文件一变更就自动让 CDN 失效。
我最终的缓存配置:
| 资源 | 缓存时间 | 原因 |
|---|---|---|
*.html | 0 秒(不缓存) | 入口文件,发版要立刻生效 |
/api/ | 0 秒(不缓存) | 动态数据,缓存了数据就乱 |
*.js *.css | 1 年 | 带 hash,内容变则文件名变 |
总结
整个迁移下来,我得到了一套更现代、更安全的架构:前端静态化托管在 OSS,全站走 CDN,服务器退化成纯 API 服务器,源站 IP 彻底隐藏。成本上几乎没增加(CDN 流量费比 OSS 直连还便宜一半,小站一个月几毛钱),但性能和安全性都上了一个台阶。
如果要给同样想迁移的人几个建议,我会说:
- 先想好架构再动手:哪个域名做主站、API 和静态怎么分流、bucket 怎么划分,想清楚再配。
- 排查看响应头:
Server、x-oss-request-id、返回内容格式(XML 还是 HTML),能快速定位问题出在哪一层。 - 缓存策略要想清楚:hash 文件长缓存、固定名文件短缓存或不缓存,别一刀切。
- 留好回滚预案:DNS 改 CNAME 之前,确保旧链路还能用,出问题能秒级切回。
最后,这次迁移我是和 AI 助手一起完成的——从分析现状、设计方案,到排查那六个坑,再到写这篇文章。有个能记住上下文、随时讨论细节的搭档,确实让这种"看似简单的运维活"顺畅了不少。
本文的封面图和架构图由 AI 生成,迁移过程与文章写作均有 AI 助手参与。