Files
tamabox/outputs/serverless-migration-feasibility-2026-09-27.md
T
2026-10-15 11:41:19 +08:00

160 lines
9.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# TamaBox-v4 迁移 Cloudflare Workers + D1 可行性评估
- 日期:2026-09-27
- 评估对象:将 TamaBox-v4(Go + Flamego + GORM + PG/Redis)改造为 **TypeScript + Cloudflare Workers + D1** 的 serverless 形态
- 参照项目:[OpenListTeam/OpenList-Worker](https://github.com/OpenListTeam/OpenList-Worker)
- 范围:**只读评估,未改动任何源码**
---
## 0. 结论速览
| 项 | 结论 |
|---|---|
| 技术可行性 | 可行 —— 没有理论上的不可能 |
| 工程性价比 | 🔴 **低** —— 是「整体重写」,不是「改造」 |
| 主要阻碍 | ① 图像管线(cgo WebP / cwebp 子进程 / freetype 中文渲染)② 进程内有状态子系统 ③ PG 专有 SQL |
| 建议 | **方案 A(不迁移)**;若必须上边缘,走**方案 B(静态+只读下沉)** |
一句话:**OpenList 是「无状态代理」,TamaBox 是「有状态 Web 应用」,两者的边缘适配成本差一个数量级。**
---
## 1. 先看清参照物:OpenList-Worker 为什么能做成
| 维度 | OpenList(Go 原版) | OpenList-Worker(TS) |
|---|---|---|
| 仓库体积 | 9.8 MB | 21.7 MB(**比原版还大**) |
| 语言 | Go | TypeScript |
| 代码量 | — | 416 个 `.ts`,约 2.6 MB 源码 |
| Star | 24,800 | 372 |
| 立项 | — | 2025-08-23(约一年) |
| 维护方 | 官方团队 | 官方团队 |
即使是**官方团队 + 24.8k star** 的项目,重写一年后 Worker 版代码量反而超过原版。而更关键的是**应用形态**:
**OpenList 的本质是无状态的文件列表 + 反代:**
- 没有多用户会话状态(只有一个管理员)
- 没有服务端可变状态(限流 / 封禁 / 去重全都不需要)
- 存储是「可插拔后端」,官方甚至支持降级到 `map` / KV,并明说 *serverless 下禁止回退到内存*
- 没有图像处理、没有服务端模板渲染 —— 前端是现成 SPA,Worker 只负责注入 CDN 地址
所以它干的其实是「**把无状态代理逻辑搬到边缘**」—— 这正是边缘计算最擅长的形态。
**TamaBox 恰恰相反,它是一个有状态的多用户 Web 应用。**
---
## 2. TamaBox 的形态画像
代码规模:**34,664 行 Go**
| 模块 | LOC | 说明 |
|---|---|---|
| `route/` | 7,645 | 业务逻辑(vote 1297 / share 970 / profile 606 …) |
| `internal/db` | 6,950 | GORM 模型 + 仓储,15 张表 |
| `internal/i18n` | 3,689 | 四语言字典(以数据为主) |
| `internal/security` | 2,675 | 审核、自动封禁、IP 情报 |
| `internal/route` | 1,653 | 中间件与路由装配 |
| `internal/template` | 825 | 模板函数 / Markdown / minify |
| `internal/shared` | 758 | IP、去重、并发工具 |
| `internal/conf` | 587 | 配置(ini + 单环境变量) |
| `internal/cmd` | 482 | CLI 子命令(web/censor/uid/ban/restore) |
| `internal/storage` | 440 | R2 / OSS 上传 + WebP 转换 |
| 模板 | 42 个 html | 服务端 `html/template` 渲染 |
| 中间件 | — | Session / CSRF / Redis cache / reCAPTCHA |
---
## 3. 分层可行性矩阵
### P0 —— 硬阻断(必须重构,无法直搬)
| # | 阻断点 | 位置 | 为什么在 Workers 上不行 |
|---|---|---|---|
| 1 | **WebP 编码是 cgo** | `internal/storage/s3.go:16,23,192` | `chai2010/webp` 依赖 libwebp 的 C 代码(`//go:build cgo`)。Workers 是 V8 isolate,跑不了 cgo → 只能换 `@jsquash/webp`(WASM) |
| 2 | **GIF→WebP 走 `cwebp` 子进程** | `internal/storage/s3.go:200-233` | `exec.LookPath("cwebp")` + `os.CreateTemp`。Workers 无子进程、无可写磁盘 → **完全不可实现** |
| 3 | **分享图 freetype 渲染中文** | `route/question/share.go:121-131, 387-720` | `os.ReadFile(font_path)` 读磁盘 OTF + freetype 光栅化 + jpeg 编码。必须整体重写;且大图中文字形光栅化是 CPU 密集,易撞 Workers CPU 上限 |
| 4 | **进程内状态**(Session / 缓存 / 限流 / 封禁 / 去重 / 统计) | `internal/route/route.go:386-395`、`internal/cache`、`internal/security/autoban`、`internal/shared/duplicate.go`、`internal/route/ratelimit.go` | Workers 请求间无共享可变状态 → 全部要迁 Durable Objects / KV / D1,成本明显上升 |
| 5 | **cache 中间件硬编码 Redis** | `internal/route/route.go:386-395` | 无条件走 `cacheRedis.Initer()`,不判断配置是否为空。Workers 无法用原生 TCP Redis |
| 6 | **启动期副作用** | `internal/cmd/web.go:57-105`、`internal/db/db.go:58-127` | `ListenAndServe` 常驻监听 + `AutoMigrate` + `fixPostgresSequences` + backfill。serverless 无常驻启动流程 |
| 7 | **常驻 goroutine 定时任务** | `ratelimit.go:41`、`autoban.go:250`、`duplicate.go:34`、`share.go:207` | `for { sleep; cleanup }` 模式 → 需改 Cron Triggers + DO alarm |
| 8 | **PG 专有 SQL** | `db.go:211-234`(序列 `nextval/setval`)、`follow_ups.go:68`(`FOR UPDATE`)、各处 `datatypes.JSON`(JSONB) | D1 是 SQLite:**无序列、无 `FOR UPDATE`、无 JSONB** |
| 9 | **SMTP 发信** | `internal/mail/mail.go:129-147` | Workers 禁止裸 TCP 25/465/587 → 须换 Resend / SendGrid 等 HTTP 邮件 API |
| 10 | **服务端 Go 模板** | `templates/` 42 个 + `internal/template/template.go:29-229` | `html/template` 的**上下文敏感转义**是安全边界,迁到 JS 模板引擎需谨慎等价实现 |
| 11 | **OTel/Uptrace gRPC 上报** | `internal/tracing/tracing.go:15-27`、`internal/cmd/web.go:41-48` | Workers 无 gRPC、无后台 batch 上报协程 |
### P1 —— 需换库 / 改写(有明确对应物)
- **S3/R2 上传** → `@aws-sdk/client-s3`(R2 兼容 S3 协议,可行)
- **七牛 / 阿里云文本审核** → `fetch` 直调或 JS SDK(阿里云 `tea`/`darabonba` 运行时需重做)
- **Excel 导出 `excelize`** → `exceljs`
- **CSV 导出** `encoding/csv` → 手写(简单)
- **QR `skip2/go-qrcode`** → `qrcode` JS 库
- **Markdown `blackfriday` + 自研 XSS 白名单** → `marked` + `DOMPurify`(**白名单规则需逐条复刻**,属安全关键)
- **i18n 的 `fmt` 风格 `%s` 占位** → 需实现等价格式化
- **软删除 + 唯一索引语义** → SQLite 下槽位行为需重新设计
### P2 —— 基本可直接移植
- `internal/imagecheck`(纯标准库像素校验)
- `internal/captcha/math.go`(**JSON 算数题,非图片、无字体**)
- `internal/i18n` 字典(纯数据)
- CSRF(无状态 HMAC,本身可移植)
- `internal/security/ipinfo`(离线签名,零网络、零 DNS)
- `internal/shared/ip.go`(IP 解析)
- `bodylimit` / `ipblacklist`
- `embed.FS`(转 Assets / 模块)
> 补充:`internal/template/minify.go` 当前**未被路由装配实际使用**,迁移时可直接丢弃。
---
## 4. 成本量化
- 源码 **34.6k 行 Go** → 等价重写预计 **40k+ 行 TS**
- `templates/` **42 个模板 + 25 个模板函数**全部重写
- 全部测试重写(含 `route/vote/vote_stats_test.go` 这类带 DB 的门控测试)
- 参考标定:OpenList 官方团队 + 24.8k star,重写约 **一年**,产出 416 个 TS 文件 / 2.6 MB
- TamaBox 的形态比 OpenList **更「重」**(有状态 + 关系型 DB + 图像 + 模板),且为**单人维护**
---
## 5. 容易被忽略的坑
1. **中国大陆可用性**:Workers 是海外节点。文本审核走七牛/阿里云(国内 API),海外边缘回源国内延迟高;面向大陆用户还有备案/合规问题(cf. 项目已有 `edgeone-noicp-cf-tunnel-guide`、`edgeone-origin-whitelist-guide`)
2. **D1 限额**:SQLite 单库上限(付费 10 GB)、写并发受限;`questions` / `vote_submissions` 属写密集
3. **Workers CPU 限额**:免费 10 ms、付费 30 s/请求 且**按 CPU 计费**。分享图渲染、Excel 导出都吃 CPU
4. **Durable Objects 是付费项**,按请求 + 时长计费 → 把 Session / 限流 / 封禁搬过去后,成本模型从「一台 1C1G」变成「按量」
5. **当前生产是 1C1G 小服务器**:迁移的收益(免运维 / 弹性)对这个体量并不显著
---
## 6. 建议方案
### 方案 A(推荐):不迁移
形态不匹配 + 单人维护 + 当前 1C1G 成本已很低。收益(免运维、弹性)不足以覆盖重写成本与风险。
### 方案 B(若确要上边缘,渐进式)
只把**无状态**的部分下沉边缘:
- **下沉**:静态资源(`static/`、模板产物)+ 只读接口(公开提问页、投票公开页、`/explore`)→ EdgeOne Pages / Workers
- **保留源站**:写路径、图像处理、审核、邮件、导出 → 继续跑 Go
项目已有 EdgeOne 回源白名单脚本,这套「边缘 + 源站」形态其实已在用。**风险最低、可随时回退。**
### 方案 C(完全重写)
仅当能接受**功能裁剪**(砍分享图 / Excel 导出 / 聊天 / 投票表单)且投入 2–3 个月专职。且建议优先 **EdgeOne(腾讯云,有大陆节点)** 而非纯 Cloudflare Workers。
---
## 附:评估依据
- 全部源码只读探查:`internal/**`(110 个 Go 文件)、`route/**`(22 个)、`templates/**`(42 个模板)
- 依赖清单:`go.mod`(含 `chai2010/webp`、`disintegration/imaging`、`golang/freetype`、`xuri/excelize`、`uptrace/uptrace-go` 等)
- 对照数据来自 GitHub API 与 OpenList-Worker `README` / 提交历史