从 Express 到 Cloudflare Workers:一次 POC 验证之旅

从 Express 到 Cloudflare Workers:一次 POC 验证之旅

记录从零开始构建 Cloudflare Workers + Hyperdrive + MySQL POC 的全过程。虽然整体项目迁移尚未开始,但这次 POC 验证了核心链路的可行性,并重点记录了数据库配置中遇到的"天坑"与解决方案。

📖 目录

🎯 项目背景

为什么要探索 Cloudflare Workers?

我们现有的后端服务基于 Express + MySQL,随着用户增长,我们开始调研更现代化的架构方案。在正式迁移之前,我们需要通过一个 POC (概念验证) 项目来确认 Cloudflare Workers 是否能满足我们的需求。

主要期望解决的问题:

  1. 延迟问题 - 用户分布全球,单一服务器延迟高
  2. 性能瓶颈 - Node.js 单线程模型限制
  3. 运维成本 - 需要管理服务器、负载均衡等

Cloudflare Workers 提供了一个诱人的解决方案:

  • ✅ 全球 300+ 边缘节点,毫秒级延迟
  • ✅ 自动扩展,无需运维
  • ✅ 按请求付费,成本可控

POC 验证目标

本次 POC 仅针对核心技术链路进行验证,而非全量迁移。目标如下:

  1. Cloudflare Workers 能否稳定连接我们自建的 MySQL 数据库?
  2. 是否有类似 Express 的框架可用,以降低未来迁移的认知负担?
  3. 摸清潜在的技术坑和配置复杂度。

💡 技术选型

核心技术栈

组件 传统方案 Workers POC 方案 原因
Web 框架 Express Hono API 95% 相似
数据库 MySQL MySQL + Hyperdrive 连接池和加速
ORM Prisma (直接 SQL) Workers 兼容性
驱动 N/A mysql2 Cloudflare 官方推荐

为什么选择 Hono?

复制代码
// Express 代码
app.get('/users/:id', async (req, res) => {
  const user = await getUser(req.params.id);
  res.json({ user });
});

// Hono 代码 - 几乎一样!
app.get('/users/:id', async (c) => {
  const user = await getUser(c.req.param('id'));
  return c.json({ user });
});

优势

  • API 相似度 95%+
  • 专为边缘运行时优化
  • 包大小极小
  • TypeScript 原生支持

🚧 实施过程

第一阶段:初始尝试

想法:直接复用 Prisma + PlanetScale SDK 的组合

复制代码
// src/db.ts - 初始版本
import { PrismaClient } from '@prisma/client/edge';
import { PrismaPlanetScale } from '@prisma/adapter-planetscale';

export function createPrismaClient(env: Env) {
  const client = new Client({ url: env.HYPERDRIVE.connectionString });
  const adapter = new PrismaPlanetScale(client);
  return new PrismaClient({ adapter });
}

结果:❌ 失败

第二阶段:遇到的问题

问题 1:Prisma /edge 导入冲突
复制代码
Error: Prisma Client was configured to use the `adapter` option 
but it was imported via its /edge endpoint.

解决 :移除 /edge

复制代码
- import { PrismaClient } from '@prisma/client/edge';
+ import { PrismaClient } from '@prisma/client';
问题 2:--no-engine 标志冲突
复制代码
Error: Prisma Client was configured to use the `adapter` option 
but `prisma generate` was run with `--no-engine`.

解决 :移除 --no-engine

复制代码
// package.json
"scripts": {
-  "db:generate": "prisma generate --no-engine",
+  "db:generate": "prisma generate",
}
问题 3:Prisma Engine 无法在 Workers 中运行

即使修复了上述问题,仍然失败:

复制代码
{
  "error": "Database connection failed: "
}

根本原因

  • Cloudflare Workers 不支持运行二进制文件
  • Prisma Query Engine 是一个 Rust 二进制程序
  • Workers 环境缺少必要的系统调用

影响

  • 包大小暴涨:79 KiB → 2351 KiB
  • 启动时间增加:1ms → 12ms
  • 依然无法连接数据库

第三阶段:转向 PlanetScale SDK

尝试:绕过 Prisma,直接使用 SDK

复制代码
import { Client } from '@planetscale/database';

export async function query(env: Env, sql: string) {
  const client = new Client({ url: env.HYPERDRIVE.connectionString });
  const result = await client.execute(sql);
  return result.rows;
}

结果:❌ 仍然失败

复制代码
{
  "error": "DatabaseError at Connection.execute"
}

分析:PlanetScale SDK 是为 PlanetScale 的 HTTP API 设计的,与标准 MySQL/Hyperdrive 协议不完全兼容。

第四阶段:Cloudflare 官方方案 ✅

在 Cloudflare Dashboard 创建 Hyperdrive 后,官方文档给出了示例代码

复制代码
import { createConnection } from "mysql2/promise";

export default {
  async fetch(request, env, ctx): Promise<Response> {
    const connection = await createConnection({
      host: env.HYPERDRIVE.host,
      user: env.HYPERDRIVE.user,
      password: env.HYPERDRIVE.password,
      database: env.HYPERDRIVE.database,
      port: env.HYPERDRIVE.port,
      disableEval: true,  // Required for Workers!
    });
    
    const [results] = await connection.query("SELECT * FROM users");
    ctx.waitUntil(connection.end());
    return Response.json({ results });
  },
}

关键发现

  1. 使用 mysql2/promise,不是 PlanetScale SDK
  2. Hyperdrive 提供结构化连接信息(host/user/password)
  3. 需要 disableEval: true 配置
  4. 需要 nodejs_compat 兼容性标志

💥 踩过的坑(特别收录:Hyperdrive 数据库配置篇)

在配置 Hyperdrive 连接自建 MySQL 8.0 (Docker) 的过程中,我们遇到了几个极其隐蔽且致命的问题,在此特别记录。这些是本次 POC 最宝贵的收获之一。

坑 5:MySQL 8.0 身份验证协议不兼容 (AuthSwitchRequest) 🛑

现象 : Cloudflare Dashboard 测试连接或 Worker 运行时报错: Hyperdrive does not currently support MySQL AuthSwitchRequest messages

🔍 原因 : MySQL 8.0 默认使用 caching_sha2_password 认证插件,而 Hyperdrive 目前仅支持旧版的 mysql_native_password。即使你创建的用户使用了旧版加密,MySQL 服务端默认的握手协议如果还是 SHA2,连接会在握手阶段就断开。

解决方案(终极版) : 必须强制 MySQL 服务端默认使用 mysql_native_password 启动。仅修改用户密码是不够的!

修改 docker-compose.yml(推荐):

复制代码
services:
  mysql:
    image: mysql:8.0
    # 👇 必须添加这行启动命令
    command: --default-authentication-plugin=mysql_native_password
    volumes:
       # 或者挂载配置文件(Plan B)
       - ./my.cnf:/etc/mysql/conf.d/native_password.cnf

坑 6:Docker 远程 Root 权限缺失 (Error 1044) 🛑

现象 : 尝试给新用户授权时报错: Error 1044: Access denied for user 'root'@'%' to database 'nano_poc'

🔍 原因 : 在 Docker 环境中,远程连接的 root 用户 (root@%) 往往默认没有 WITH GRANT OPTION 权限(即"转授权限")。你虽然是 root,但你不能给别人发证。拥有完整权限的是容器内部的 root@localhost

解决方案: 不要用 Navicat/Workbench 授权,直接进入容器内部操作:

复制代码
# 1. 进入容器
docker exec -it mysql bash

# 2. 登录本地 root (拥有最高权限)
mysql -u root -p

# 3. 赋予远程 root 转授权限 (修复根源)
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION;
FLUSH PRIVILEGES;

# 4. 创建兼容 Hyperdrive 的用户
CREATE USER 'hyper_user'@'%' IDENTIFIED WITH mysql_native_password BY 'Password123!';
GRANT ALL PRIVILEGES ON nano_poc.* TO 'hyper_user'@'%';

坑 7:连接字符串中的特殊字符地雷 🛑

现象: 配置看起来都对,但连接时报错,或者解析出的用户名/主机名乱码。

🔍 原因 : 密码中包含 @:/ 等 URL 特殊字符。例如密码是 P@ssword,连接字符串 mysql://user:P@ssword@host 会让解析器搞不清哪个 @ 是分隔符。

解决方案

  1. 对密码进行 URL Encode (例如 @ 变成 %40)。
  2. 或者:专门为 Hyperdrive 创建一个不含特殊字符的密码(纯字母数字),规避解析风险。

坑 1-4回顾 (Workers 开发篇)

坑 1:Prisma 在 Workers 中不可用

错误认知:Prisma 支持 Edge Runtime,应该可以在 Workers 中使用

正确理解

  • Prisma Edge 需要 Data Proxy 或 Accelerate
  • 直接使用 Prisma Client + Adapters 不可行
  • Workers 环境无法运行 Prisma Engine

教训:不要假设"支持边缘"就意味着支持所有边缘环境。

坑 2:本地开发的陷阱

错误做法 :期望 wrangler dev 完美模拟生产环境

正确做法

  • 了解 Hyperdrive 本地模拟只支持 Postgres
  • 直接部署到 Workers 测试
  • 或配置本地直连 MySQL(绕过 Hyperdrive)

教训:边缘计算的本地开发体验与传统开发不同,需要调整工作流程。

坑 3:nodejs_compat 兼容性标志

遗漏配置:直接使用 mysql2 导致部署失败

复制代码
Error: Could not resolve "mysql2/promise"

正确配置

复制代码
# wrangler.toml
compatibility_flags = ["nodejs_compat"]

教训:仔细阅读官方文档,Workers 需要显式启用 Node.js 兼容性。

坑 4:Hyperdrive 连接字符串的迷惑

错误用法

复制代码
// PlanetScale SDK 方式
const client = new Client({
  url: env.HYPERDRIVE.connectionString  // ❌ 这个不对
});

正确用法

复制代码
// mysql2 方式
const connection = await createConnection({
  host: env.HYPERDRIVE.host,           // ✅ 使用结构化信息
  user: env.HYPERDRIVE.user,
  password: env.HYPERDRIVE.password,
  database: env.HYPERDRIVE.database,
  port: env.HYPERDRIVE.port,
  disableEval: true,
});

教训:Hyperdrive 提供结构化连接信息,不是单一连接字符串。

✅ 最终方案 (POC 版)

架构设计

复制代码
┌─────────────┐
│   Client    │
└──────┬──────┘
       │
       v
┌─────────────────────────────────┐
│   Cloudflare Workers (Hono)     │
│   ┌──────────────────────────┐  │
│   │ Routes & Middleware      │  │
│   └─────────┬────────────────┘  │
│             v                    │
│   ┌──────────────────────────┐  │
│   │ Database Layer (mysql2)  │  │
│   └─────────┬────────────────┘  │
└─────────────┼────────────────────┘
              v
     ┌────────────────┐
     │   Hyperdrive   │
     └────────┬───────┘
              v
     ┌────────────────┐
     │     MySQL      │
     └────────────────┘

数据库连接层

复制代码
// src/db.ts
import { createConnection, type Connection } from 'mysql2/promise';

export async function createDbConnection(env: Env): Promise<Connection> {
  return await createConnection({
    host: env.HYPERDRIVE.host,
    user: env.HYPERDRIVE.user,
    password: env.HYPERDRIVE.password,
    database: env.HYPERDRIVE.database,
    port: env.HYPERDRIVE.port,
    disableEval: true,
  });
}

export async function query<T>(
  env: Env,
  sql: string,
  params: any[] = [],
  ctx?: ExecutionContext
): Promise<T[]> {
  const connection = await createDbConnection(env);
  
  try {
    const [results] = await connection.query(sql, params);
    return results as T[];
  } finally {
    if (ctx) {
      ctx.waitUntil(connection.end());
    } else {
      await connection.end();
    }
  }
}

路由实现

复制代码
// src/routes/users.ts
import { Hono } from 'hono';
import { query, queryOne, type Env } from '../db';

const users = new Hono<{ Bindings: Env }>();

users.get('/', async (c) => {
  const userList = await query<User>(
    c.env,
    'SELECT id, email, username, created_at FROM users',
    [],
    c.executionCtx
  );
  
  return c.json({ success: true, users: userList });
});

users.post('/', async (c) => {
  const body = await c.req.json();
  
  await query(
    c.env,
    'INSERT INTO users (email, username, password) VALUES (?, ?, ?)',
    [body.email, body.username, body.password],
    c.executionCtx
  );
  
  return c.json({ success: true }, 201);
});

export default users;

📊 性能对比

包大小

方案 大小 gzip 说明
Prisma 2351 KiB 879 KiB ❌ 太大
PlanetScale SDK 79 KiB 20 KiB ❌ 不兼容
mysql2 1450 KiB 421 KiB ✅ 最优

启动时间

方案 冷启动
Prisma ~12ms
PlanetScale SDK ~1ms
mysql2 ~40ms

实际请求延迟

测试环境:中国上海 → Cloudflare 香港节点

端点 延迟 说明
/health ~45ms 纯计算
/db-test ~120ms 简单查询
/users (列表) ~150ms 复杂查询
/users (创建) ~180ms 写操作

对比传统服务器

  • 单服务器:300-500ms
  • Workers + Hyperdrive:120-180ms
  • 性能提升 60%+

🎓 经验总结

1. 选择正确的工具

不要:强行使用不兼容的工具

:根据运行环境选择专门设计的工具

  • Workers 环境 ≠ Node.js 环境
  • Prisma 需要 Data Proxy/Accelerate
  • 使用官方推荐的 mysql2

2. 理解边缘计算的限制

  • 无文件系统
  • 无长连接
  • 有执行时间限制(CPU 时间 50ms,总时间 30s)
  • Node.js API 支持有限

3. 拥抱约束,寻找替代

传统方式 Workers 替代
Express Hono
Prisma 原生 SQL
Session JWT/Cookies
文件上传 R2 Storage

4. 本地开发策略

由于 Hyperdrive 本地限制:

  1. 快速迭代:直接部署测试(部署只需 5秒)
  2. Mock 数据:本地开发时 mock 数据库响应
  3. 单元测试:重点测试业务逻辑,而非集成

5. 渐进式迁移 (Future Plan)

POC 验证成功后,我们计划按以下步骤推进迁移:

  1. ✅ 先迁移无状态 API
  2. ✅ 再迁移读操作
  3. ⏳ 迁移写操作
  4. ⏳ 保留原服务器作为 fallback

🚀 未来优化方向

1. 连接池优化

目前每个请求都创建新连接。可以探索:

  • Hyperdrive 的连接池配置
  • Workers 的全局变量缓存

2. 查询缓存

复制代码
// 使用 Workers KV 缓存查询结果
const cached = await env.CACHE.get('users:list');
if (cached) return JSON.parse(cached);

const users = await query(env, 'SELECT * FROM users');
await env.CACHE.put('users:list', JSON.stringify(users), {
  expirationTtl: 60, // 60 秒
});

3. 考虑 Prisma Accelerate

如果需要 Prisma 的类型安全:

复制代码
// prisma/schema.prisma
datasource db {
  provider  = "mysql"
  url       = env("DATABASE_URL")
  directUrl = env("DIRECT_DATABASE_URL")  // Hyperdrive URL
}

🏆 总结

本次 POC 成功验证了 Cloudflare Workers 在数据库连接、框架适配和性能表现上的可行性。

成功要点

  1. Hono 完美替代 Express - 迁移成本极低
  2. mysql2 + Hyperdrive 是官方推荐方案 - 稳定可靠
  3. 性能提升显著 - 延迟降低 60%+
  4. 运维成本降低 - 无需管理服务器

最终建议

如果你的应用满足:

  • 主要是 REST API
  • 数据库操作相对简单
  • 需要全球低延迟
  • 希望降低运维成本

那么 Cloudflare Workers 是一个极其值得尝试的方案!

作者: JerryLau

日期: 2025-12-24

项目: https://backend-cloudflare-poc.apecc.workers.dev

如果这篇文章对您有帮助,欢迎分享和讨论! 🎉

相关推荐
千里马-horse2 小时前
CallbackInfo
c++·node.js·napi·callbackinfo
亮子AI4 小时前
【npm】如何创建自己的npm私有仓库?
前端·npm·node.js
莫渊博-天下无病5 小时前
node高版本安装
运维·node.js
tmj016 小时前
前端JavaScript(浏览器)与后端JavaScript(Node.js)
javascript·node.js
爱干饭的boy6 小时前
MacBook安装node.js/maven/mysql
mysql·node.js·maven
千里马-horse7 小时前
Checker Tool
c++·node.js·napi
友莘居士7 小时前
Windows下Node.js 执行Web3.js 的智能合约环境搭建
windows·node.js·web3
o__A_A7 小时前
登录鉴权与 Token 管理
node.js
天远数科7 小时前
Node.js Crypto 模块详解:如何处理金融借贷信用风险探查加密数据交互
大数据·金融·node.js·交互