Cloudflare 将 cdnjs 完整迁移到了自己的 Developer Platform。这个开源 CDN 每天处理约 90 亿次请求,因此这不只是一次内部“吃自己的狗粮”,更是对 Workers、Workflows 及整个平台容量边界的真实压力测试。迁移过程中暴露出的限制,也推动了平台配额和能力的提升,并让所有开发者受益。
90 亿次请求改变了迁移问题的尺度
把 90 亿除以一天的 86400 秒,平均吞吐量约为每秒 10.4 万次请求。实际流量不会均匀分布,峰值、热门库发布和区域性访问都会制造更高压力。
在这个尺度上,迁移不能只验证“请求能否返回 200”。工程团队需要同时关注:
- 缓存命中率:少量命中率下降也会把大量请求压向源站。
- 尾延迟:平均延迟正常,并不代表 P95、P99 或跨区域访问正常。
- 缓存键稳定性:查询参数、大小写或路径归一化错误都可能产生重复缓存。
- 回源保护:缓存冷启动或热门文件失效时,必须避免请求集中冲击存储层。
- 自动化任务可靠性:库索引、元数据更新和发布流程需要可重试、可追踪,并保持幂等。
这也是 cdnjs 对 Developer Platform 的价值所在:它用生产流量检验平台构件是否真的能够承载互联网级公共服务,而不是只在小型示例中工作。
Workers 处理请求,Workflows 承接长流程
从平台职责来看,可以把这类 CDN 拆成两条路径。
请求路径对延迟极度敏感,适合由 Workers 在边缘完成 URL 校验、缓存查询、响应头设置和必要的回源。任何多余网络调用都会被放大,因此热路径应保持短小、确定且可观测。
控制路径则处理库发布、索引构建、元数据同步和失败重试。这类工作可能跨越多个步骤,不应绑在单次 HTTP 请求的执行时间内。Workflows 的意义在于持久化步骤状态,并为长时间任务提供重试和恢复边界。
来源摘要没有披露 cdnjs 的具体组件图、存储选择或缓存规则,因此不能把某一种示例架构当作其实际实现。更稳妥的理解是:迁移把一个高流量 CDN 放到了 Cloudflare 自己提供的开发构件上,并以实际负载推动了 Workers 和 Workflows 限制的提升。
可以这样实践:构造一个最小边缘 CDN Worker
下面是一个可改造的最小示例。它限制只代理静态资源扩展名,先读取 Cloudflare 缓存,未命中时再访问源站。运行前需要安装 Node.js,并把 ASSET_ORIGIN 改成自己的 HTTPS 静态资源源站。
创建 package.json:
{
"name": "edge-cdn-worker",
"private": true,
"scripts": {
"dev": "wrangler dev",
"deploy": "wrangler deploy"
},
"devDependencies": {
"wrangler": "^4.0.0"
}
}
创建 wrangler.toml:
name = "edge-cdn-worker"
main = "src/index.js"
compatibility_date = "2025-01-01"
[vars]
ASSET_ORIGIN = "https://static.example.com"
创建 src/index.js:
const ALLOWED_EXTENSIONS = /\.(css|js|json|map|svg|png|jpg|jpeg|webp|woff2?)$/i;
export default {
async fetch(request, env, ctx) {
if (request.method !== "GET" && request.method !== "HEAD") {
return new Response("Method Not Allowed", { status: 405 });
}
const incoming = new URL(request.url);
let pathname;
try {
pathname = decodeURIComponent(incoming.pathname);
} catch {
return new Response("Bad Request", { status: 400 });
}
if (pathname.includes("..") || !ALLOWED_EXTENSIONS.test(pathname)) {
return new Response("Not Found", { status: 404 });
}
const cacheKey = new Request(incoming.origin + incoming.pathname, {
method: "GET"
});
const cache = caches.default;
const cached = await cache.match(cacheKey);
if (cached) {
const response = new Response(cached.body, cached);
response.headers.set("X-Edge-Cache", "HIT");
return response;
}
const origin = new URL(env.ASSET_ORIGIN);
origin.pathname = incoming.pathname;
origin.search = "";
const upstream = await fetch(origin, {
headers: { Accept: request.headers.get("Accept") || "*/*" }
});
if (!upstream.ok) {
return new Response("Asset unavailable", { status: upstream.status });
}
const response = new Response(upstream.body, upstream);
response.headers.set("Cache-Control", "public, max-age=300, s-maxage=86400");
response.headers.set("X-Content-Type-Options", "nosniff");
response.headers.set("X-Edge-Cache", "MISS");
if (request.method === "GET") {
ctx.waitUntil(cache.put(cacheKey, response.clone()));
}
return response;
}
};
安装并本地运行:
npm install
npm run dev
curl -i http://localhost:8787/library/example.min.js
登录 Cloudflare 后可以部署:
npx wrangler login
npm run deploy
这个示例用于展示请求路径,不等同于 cdnjs 的实现。生产环境还要补充源站身份验证、内容完整性校验、请求合并、分层缓存、速率限制、结构化日志和告警。对于带版本号且内容不可变的文件,还可以使用更长的缓存周期;可变路径则应采用较短 TTL 或显式失效机制。
大规模迁移应按指标推进
迁移公共 CDN 时,最危险的做法是一次性切换全部流量。可以先建立旧系统与新系统的响应对比,再按域名、地区或流量比例逐步放量。每个阶段至少应检查状态码分布、缓存命中率、回源请求量、P95/P99 延迟和字节一致性。
上线前可以使用这份检查表:
- 缓存键是否明确处理查询参数、压缩格式和请求头。
- Worker 故障时是否有回退路径,源站是否具备限流保护。
- 发布与索引任务是否幂等,重试是否会产生重复数据。
- 日志采样是否足以定位问题,又不会产生失控的成本。
- 平台 CPU、请求体、子请求和工作流配额是否覆盖峰值,而不只是日均值。
- 是否准备了按区域和按比例回滚的开关。
cdnjs 的迁移说明了一个关键事实:平台能力只有在真实、持续且高并发的负载下才会显露边界。对普通团队而言,最值得借鉴的并非直接复制某个架构,而是把边缘请求路径、后台长流程和渐进式迁移分别设计,并用生产指标决定下一步。