← Back to Blog
EN中文

个人网站全栈架构升级:从免费版到 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-small API 生成 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-0023-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 的下一章。