个人网站全栈架构升级:从免费版到 Cloudflare $5 生产级方案
我的个人网站 yuxu.ge 不仅仅是一个博客,它更像是我技术探索的试验田,承载了博客、相册、AI 助手、语义搜索、涂鸦评论等各种功能。最初,这一切都构建在 Cloudflare 慷慨的免费套餐之上。但随着功能越来越复杂,免费午餐的局限性也愈发明显。
最近,我下定决心将网站升级到 Cloudflare Workers 的 $5/月付费套餐。这不仅仅是每月多花五美元那么简单,它解锁了一整套强大的生产级工具,让我有机会对整个网站架构进行一次彻底的重构和升级。这篇博客将详细记录这次从"玩具"到"生产级"平台的完整思考和实践过程。
1. 升级前的架构:在免费镣铐中舞蹈
在免费套餐下,我用有限的资源"极限操作",搭建了一套看起来功能完备的系统。
架构概览 (免费版)
┌────────────────────────────────┐
│ yuxu.ge 用户 │
└────────────────────────────────┘
│
│ HTTPS
▼
┌─────────────────────────────────────────────────────────────────┐
│ Cloudflare Edge │
│ │
│ ┌────────────────┐ ┌─────────────────┐ ┌───────────────┐ │
│ │ 静态资源 │──▶│ Cloudflare Worker│──▶│ Cloudflare KV │ │
│ │ (GitHub Pages) │ └─────────────────┘ └───────────────┘ │
│ └────────────────┘ │ │ │
│ │ │ (Session) │
│ ▼ │ │
│ ┌────────────┐ │ │
│ │ OpenAI API │◀──────────────┘ │
│ └────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
│ (浏览器端降级)
▼
┌──────────────────────────┐
│ Browser │
│ ┌──────────────────────┐ │
│ │ ONNX Runtime (WASM) │ │
│ │ BGE-small-zh Model │ │
│ └──────────────────────┘ │
└──────────────────────────┘
核心系统剖析
AI 助手 & 语义搜索:这是网站的亮点。我实现了一套 RAG (Retrieval-Augmented Generation) 系统:
服务端模式:当用户提问时,Worker 调用 OpenAI 的
text-embedding-3-smallAPI 生成 512 维向量,然后在 KV 中进行暴力相似度搜索(没错,就是遍历!),同时结合本地加载的 BM25 倒排索引(一个 2.6MB 的search-inverted.json文件)进行关键词检索。结合两者结果,将上下文喂给gpt-4o-mini生成答案。浏览器模式 (降级):为了节省 API 调用和加速冷启动,我实现了一个纯浏览器端的降级方案。它通过 ONNX Runtime 加载
BGE-small-zh模型(384 维)在本地计算向量,实现纯前端的语义搜索。
涂鸦评论系统:允许用户在页面的任意位置发表评论。为了实现这一点,我将评论数据混合存储在静态构建的 JSON 文件和 KV 中。用户提交新评论时写入 KV,读取时则合并静态 JSON 和 KV 的数据。
认证系统:一个简单的 OTP (One-Time Password) 邮箱验证码系统。Session 信息、用户信息和 OTP 都存储在 KV 中,通过 HttpOnly Cookie 维持登录状态。
2. 瓶颈与阵痛:免费架构的"原罪"
这套架构虽然能跑,但随着访问量和功能复杂度的增加,问题也逐渐暴露,主要集中在四个方面。
安全问题:门户大开
- 无 CSRF 防护:所有 POST 请求都没有 CSRF token 校验,这是个严重的安全漏洞。
- 无速率限制:任何 API 端点,包括发送 OTP 验证码和提交评论,都可以被无限次调用。这不仅会耗尽 OpenAI 额度,还可能被用于进行攻击。
- OTP 暴力破解风险:6 位纯数字验证码,在没有速率限制的情况下,理论上可以被轻易破解。
数据一致性:评论的"薛定谔状态"
Cloudflare KV 是一个"最终一致性"的存储。这意味着一个数据中心的写入操作需要一些时间(通常是几十秒)才能同步到全球所有数据中心。这导致了几个诡异的问题:
- 评论"消失":用户提交评论后,写入请求可能落在了新加坡的节点。但当他刷新页面时,读取请求可能被路由到了东京的节点,此时数据还没同步过来,用户就会发现自己的评论"消失了",过一会又"神秘出现"。
- 涂鸦位置回退:我曾尝试让用户拖拽涂鸦评论的位置,但由于 KV 的延迟,拖拽操作经常在刷新后回退到旧位置,体验极差。
性能问题:每次发布都是一次"大动干戈"
- 全量索引构建:每次网站内容更新,都需要在 CI/CD 流程中完整地重新构建 2.6MB 的 BM25 倒排索引和所有文章的向量数据。随着文章增多,这个过程越来越慢。
- 巨大的 JSON 文件:2.6MB 的倒排索引需要在 Worker 启动时从静态资源中加载并解析,这增加了冷启动时间。
- 客户端性能:为了定位涂鸦评论,我用了一个
MutationObserver监听整个<body>的变化。这种"一刀切"的做法在复杂页面上会引发不必要的性能开销。
功能缺失:永远的"待办事项"
- 评论管理:没有删除或编辑评论的功能。
- 内容审核:全靠自觉,无法自动过滤垃圾评论或不当言论。
- 实时协作:涂鸦墙无法实时看到别人的操作,互动性大打折扣。
3. 解锁新世界:$5 付费套餐的能力矩阵
每月 5 美元的投资,像一把钥匙,打开了 Cloudflare 生态的另一扇大门:
| 能力 | 免费版限制 | 付费版解锁 ($5 Plan) | 解决的问题 |
|---|---|---|---|
| CPU 时间 | 10ms (Bundled) | 15分钟 (Unbound) | 复杂计算、增量索引、AI 推理 |
| D1 数据库 | 不可用 | 边缘 SQLite,强一致性 | 数据一致性、复杂查询、关系数据 |
| Durable Objects | 不可用 | 强一致性状态存储、WebSocket、Actor 模型 | 实时协作、状态管理 |
| Workers AI | 不可用 | 边缘 GPU 推理 (Embeddings, LLM等) | AI 成本、延迟、内容审核 |
| Vectorize | 不可用 | 原生向量数据库 | 向量搜索性能、可扩展性 |
| Logpush | 不可用 | 日志推送到对象存储 | 可观测性、问题排查 |
| KV 配额 | 较低 | 更高的读/写/列表操作配额 | 满足更高流量 |
这不再是简单的量变,而是质变。我拥有了在边缘构建一个真正健壮、可扩展应用所需的一切武器。
4. 架构升级蓝图:分四阶段走向生产级
我将整个升级过程分为四个阶段,循序渐进,确保每一步都稳固可靠。
新架构概览 (付费版)
┌────────────────────────────────┐
│ yuxu.ge 用户 │
└────────────────────────────────┘
│
│ HTTPS / WebSocket
▼
┌─────────────────────────────────────────────────────────────────┐
│ Cloudflare Edge │
│ │
│ ┌────────────────┐ ┌─────────────────┐ │
│ │ 静态资源 │──▶│ Cloudflare Worker│──────────┐ │
│ │ (GitHub Pages) │ └─────────────────┘ │ │
│ └────────────────┘ │ │ │
│ │ ▼ │
│ ┌────────────┐◀──────────────┤ ┌──────────────┐ │
│ │ D1 │ │ │ Durable │ │
│ │ (Database) │ │ │ Objects │ │
│ └────────────┘ │ │ (实时涂鸦墙) │ │
│ │ └──────────────┘ │
│ ┌────────────┐◀──────────────┤ │
│ │ Vectorize │ │ │
│ │ (向量搜索) │ │ │
│ └────────────┘ │ │
│ ▼ │
│ ┌────────────┐ ┌────────────┐ │
│ │ Workers AI │◀──────│ KV │ │
│ │ (GPU 推理) │ │ (缓存/OTP) │ │
│ └────────────┘ └────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Phase 1: 奠定基石 - 安全加固与 D1 数据迁移
第一步是解决最核心的安全和数据一致性问题。
1. 安全加固:
- CSRF 防护:采用经典的 Double Submit Cookie 模式。用户登录时,Worker 生成 CSRF token,一份存储在 HttpOnly 的 Session Cookie 中,另一份存储在普通 Cookie 中让前端读取。前端发起 POST 请求时携带
X-CSRF-Token请求头。 - 速率限制:利用 KV 实现滑动窗口计数器,以 IP 地址为 key,记录时间窗口内的请求次数。
2. 迁移到 D1:
D1 是 Cloudflare 的边缘 SQLite 数据库,提供强一致性,完美解决 KV 的痛点:
-- 用户表
CREATE TABLE users (
id TEXT PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
nickname TEXT,
created_at INTEGER DEFAULT (strftime('%s', 'now'))
);
-- 会话表
CREATE TABLE sessions (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
csrf_token TEXT NOT NULL,
expires_at INTEGER NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id)
);
-- 评论表
CREATE TABLE comments (
id TEXT PRIMARY KEY,
page_url TEXT NOT NULL,
content TEXT NOT NULL,
user_id TEXT NOT NULL,
anchor_id TEXT,
anchor_type TEXT,
anchor_offset_x REAL,
anchor_offset_y REAL,
rotation REAL,
status TEXT DEFAULT 'pending',
created_at INTEGER DEFAULT (strftime('%s', 'now')),
FOREIGN KEY (user_id) REFERENCES users(id)
);
CREATE INDEX idx_comments_page ON comments(page_url);
CREATE INDEX idx_comments_status ON comments(status);
Phase 2: 革新搜索 - 拥抱 Vectorize 和 Workers AI
接下来,重构整个搜索系统,用 Cloudflare 原生 AI 服务替代 OpenAI + KV 方案。
Vectorize 替代 KV 存储:
// 插入向量到 Vectorize
const vectors = [{
id: 'post_1_chunk_1',
values: embedding, // from Workers AI
metadata: { postId: 'post_1', text: '...' }
}];
await env.VECTOR_INDEX.insert(vectors);
// 查询相似向量
const results = await env.VECTOR_INDEX.query(queryVector, { topK: 5 });
Workers AI 替代 OpenAI Embeddings - 推荐 bge-m3:
如果你想寻找"最强"的替代品,目前 Cloudflare Workers AI 上托管的 BAAI (智源) bge-m3 是首选:
- 模型 ID:
@cf/baai/bge-m3 - 多语言霸主 (Multilingual): 相比 OpenAI 偏向英语,
bge-m3在中文(及其他100+种语言)的表现非常出色,非常适合混合语言环境 - 长上下文: 支持 8192 token 的输入,处理长论文摘要毫无压力
- 维度: 输出 1024 维向量
⚠️ 迁移警告: OpenAI 的 text-embedding-ada-002 和 3-small 默认是 1536 维。你不能直接混用。切换模型必须对向量数据库进行 Re-indexing (重新向量化)。
Cloudflare 提供了 OpenAI 兼容接口,甚至不需要改代码逻辑:
from openai import OpenAI
client = OpenAI(
api_key=os.environ.get("CLOUDFLARE_API_TOKEN"),
base_url=f"https://api.cloudflare.com/client/v4/accounts/{CF_ACCOUNT_ID}/ai/v1"
)
# 代码完全不用变,只需换 model name
response = client.embeddings.create(
model="@cf/baai/bge-m3", # 替换 text-embedding-3-small
input=["User intends to apply for a PhD in AI Agents.", "用户想申请人工智能博士"]
)
# 注意:返回的 embedding 长度是 1024,记得修改数据库 Schema
print(len(response.data[0].embedding)) # 1024
好处:
- 延迟从秒级降低到百毫秒级
- 成本大幅下降(Workers AI 几乎免费)
- 数据完全保留在 Cloudflare 生态内
- 中文语义理解能力优于 OpenAI
- 支持增量索引
图像智能打标 - Llama 3.2 Vision:
不要用老旧的 resnet-50(它只能输出像 "Egyptian cat" 这种死板的 ImageNet 分类)。在 Paid 计划下,可以使用 Vision-Language Model (VLM):
- 推荐模型:
@cf/meta/llama-3.2-11b-vision-instruct - 能力: 不仅能"认出"物体,还能理解场景、文字(OCR)和关系
// 智能打标器 Worker 示例
export default {
async fetch(request, env) {
const imageUrl = "https://example.com/your-image.jpg";
const imageRes = await fetch(imageUrl);
const imageBuffer = await imageRes.arrayBuffer();
const imageArray = [...new Uint8Array(imageBuffer)];
const response = await env.AI.run(
"@cf/meta/llama-3.2-11b-vision-instruct",
{
prompt: "Analyze this image and provide 5-10 relevant tags. Output ONLY a JSON array of strings.",
image: imageArray
}
);
return new Response(JSON.stringify(response));
}
};
对比:
- ResNet 输出:
{"label": "notebook", "score": 0.9}(太死板) - Llama 3.2 Vision 输出:
["personal knowledge management", "obsidian software", "digital garden", "graph view", "productivity"](能看懂屏幕截图里的软件界面!)
Phase 3: 赋予生命 - 用 Durable Objects 实现实时涂鸦墙
Durable Objects (DO) 是强一致性的、有状态的 Worker 实例,非常适合实现实时协作功能。
为每个可评论页面创建一个 DO 实例:
export class DoodleWall {
state: DurableObjectState;
sessions: WebSocket[] = [];
constructor(state: DurableObjectState) {
this.state = state;
}
async fetch(request: Request) {
// 升级为 WebSocket
const { webSocket, response } = new WebSocketPair();
this.sessions.push(webSocket);
webSocket.accept();
// 加载持久化的涂鸦数据发送给新客户端
const doodles = await this.state.storage.get('doodles') || {};
webSocket.send(JSON.stringify({ type: 'INIT', payload: doodles }));
webSocket.addEventListener('message', async (msg) => {
const data = JSON.parse(msg.data as string);
// 更新状态并持久化
await this.state.storage.put('doodles', updatedDoodles);
// 广播给所有客户端
this.broadcast(JSON.stringify({ type: 'UPDATE', payload: data }));
});
return response;
}
broadcast(message: string) {
this.sessions.forEach(session => {
try { session.send(message); } catch (e) {}
});
}
}
现在,当一个用户拖动涂鸦时,其他所有正在看这个页面的用户都能实时看到位置变化!
Phase 4: 迈向智能 - Workers AI 的深度应用
内容审核:在用户提交评论到 D1 之前,先调用 Workers AI 进行审核:
const { result } = await env.AI.run('@cf/meta/llama-2-7b-chat-fp16', {
prompt: `Is the following comment spam, hateful, or inappropriate?
Answer with only "safe" or "unsafe".
Comment: "${commentText}"`
});
if (result.includes('unsafe')) {
// 标记为待审核或直接拒绝
comment.status = 'rejected';
}
情感分析:可以对评论进行情感分析,用不同颜色或 emoji 标识评论的情绪。
5. 成本分析:$5 真的够吗?
这是大家最关心的问题。答案是:绰绰有余。
| 资源 | 免费额度 | 预估使用 | 额外成本 |
|---|---|---|---|
| 基础费 | - | - | $5/月 |
| D1 数据库 | 5亿读/500万写/1GB | 远低于限制 | $0 |
| Durable Objects | 按需计费 | 用户在线时计费 | $1-3/月 |
| Workers AI | 按神经元计算 | ~$0.0001/条评论 | <$1/月 |
| Vectorize | 免费公测 | - | $0 |
| 总计 | $6-9/月 |
这笔投资换来的是系统的稳定性、可扩展性和无限的可能性,性价比极高。
总结:从"能用"到"好用"的蜕变
这次架构升级,是我个人项目开发历程中的一个重要里程碑。它不仅仅是一次技术栈的更新,更是一次从"hacker"思维到"engineer"思维的转变。
- 告别妥协:不再需要为最终一致性设计复杂的补偿逻辑,不再需要担心安全漏洞,不再需要忍受缓慢的构建和部署。
- 拥抱原生:深度集成 Cloudflare 的原生服务,构建了一个高内聚、低延迟、易于维护的系统。
- 面向未来:新的架构为未来的功能迭代(如实时协作编辑、更智能的 AI 应用)打下了坚实的基础。
对于像我一样热爱折腾个人项目的开发者来说,Cloudflare Workers 生态提供了一条非常平滑的成长路径。你可以从免费开始,验证你的想法,当项目成长到一定阶段时,只需一顿午餐的钱,就能获得一整套世界级的边缘计算基础设施。
这,就是 yuxu.ge 的下一章。