从 Express 到 Cloudflare Workers:一次 POC 验证之旅
记录从零开始构建 Cloudflare Workers + Hyperdrive + MySQL POC 的全过程。虽然整体项目迁移尚未开始,但这次 POC 验证了核心链路的可行性,并重点记录了数据库配置中遇到的"天坑"与解决方案。
📖 目录
🎯 项目背景
为什么要探索 Cloudflare Workers?
我们现有的后端服务基于 Express + MySQL,随着用户增长,我们开始调研更现代化的架构方案。在正式迁移之前,我们需要通过一个 POC (概念验证) 项目来确认 Cloudflare Workers 是否能满足我们的需求。
主要期望解决的问题:
- 延迟问题 - 用户分布全球,单一服务器延迟高
- 性能瓶颈 - Node.js 单线程模型限制
- 运维成本 - 需要管理服务器、负载均衡等
Cloudflare Workers 提供了一个诱人的解决方案:
- ✅ 全球 300+ 边缘节点,毫秒级延迟
- ✅ 自动扩展,无需运维
- ✅ 按请求付费,成本可控
POC 验证目标
本次 POC 仅针对核心技术链路进行验证,而非全量迁移。目标如下:
- Cloudflare Workers 能否稳定连接我们自建的 MySQL 数据库?
- 是否有类似 Express 的框架可用,以降低未来迁移的认知负担?
- 摸清潜在的技术坑和配置复杂度。
💡 技术选型
核心技术栈
| 组件 | 传统方案 | 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 });
},
}
关键发现:
- 使用 mysql2/promise,不是 PlanetScale SDK
- Hyperdrive 提供结构化连接信息(host/user/password)
- 需要
disableEval: true配置 - 需要 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 会让解析器搞不清哪个 @ 是分隔符。
✅ 解决方案:
- 对密码进行 URL Encode (例如
@变成%40)。 - 或者:专门为 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 本地限制:
- 快速迭代:直接部署测试(部署只需 5秒)
- Mock 数据:本地开发时 mock 数据库响应
- 单元测试:重点测试业务逻辑,而非集成
5. 渐进式迁移 (Future Plan)
POC 验证成功后,我们计划按以下步骤推进迁移:
- ✅ 先迁移无状态 API
- ✅ 再迁移读操作
- ⏳ 迁移写操作
- ⏳ 保留原服务器作为 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 在数据库连接、框架适配和性能表现上的可行性。
成功要点
- ✅ Hono 完美替代 Express - 迁移成本极低
- ✅ mysql2 + Hyperdrive 是官方推荐方案 - 稳定可靠
- ✅ 性能提升显著 - 延迟降低 60%+
- ✅ 运维成本降低 - 无需管理服务器
最终建议
如果你的应用满足:
- 主要是 REST API
- 数据库操作相对简单
- 需要全球低延迟
- 希望降低运维成本
那么 Cloudflare Workers 是一个极其值得尝试的方案!
作者: JerryLau
日期: 2025-12-24
项目: https://backend-cloudflare-poc.apecc.workers.dev
如果这篇文章对您有帮助,欢迎分享和讨论! 🎉