Files
raisfast/docs/serverless.md
T

652 lines
20 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.
# raisfast Serverless 部署适配方案
> 版本:1.0 · 日期:2026-05-08
>
> 目标:一套代码同时支持传统服务器和 Serverless 部署,
> 通过 `serverless` feature flag 零改动切换运行模式。
---
## 1. 产品定位
### 1.1 市场空白
| 竞品 | 语言 | Serverless | 冷启动 | 单实例 RPS | 插件系统 |
|------|------|------------|--------|-----------|---------|
| Strapi | Node.js | Vercel 有限 | ~500ms | ~2K | JS only |
| Payload | Node.js | Vercel only | ~500ms | ~2K | JS only |
| Directus | Node.js | 不支持 | ~800ms | ~1.5K | JS only |
| Ghost | Node.js | 不支持 | ~600ms | ~1K | 闭源 |
| WordPress | PHP | 不支持 | ~1s | ~500 | PHP only |
| **raisfast** | **Rust** | **全平台** | **<5ms** | **50K+** | **WASM/JS/Lua** |
**核心差异化**Rust 性能 + Serverless 部署 + 多语言插件 + 多数据库 = 市场唯一。
### 1.2 支持的部署目标
| 平台 | 运行时 | 数据库 | 存储 | 冷启动 |
|------|--------|--------|------|--------|
| AWS Lambda | `provided` (AL2) | RDS / Aurora | S3 | <5ms |
| Vercel Edge | `wasm32` | Vercel Postgres | R2 / S3 | <5ms |
| Cloudflare Workers | `wasm32` | D1 / Hyperdrive | R2 | <5ms |
| Netlify Functions | `provided` | Neon / PlanetScale | S3 | <10ms |
| Fly.io / Railway | Docker | 内置 PostgreSQL | Volume | <5ms |
| 传统 VPS / Docker | 原生 | SQLite / PG / MySQL | 本地 / S3 | N/A |
---
## 2. 架构总览
### 2.1 双模式架构
```
┌─────────────────────────────────────────────────────────┐
│ 业务逻辑层(共享) │
│ models/ · services/ · handlers/ · content_type/ │
│ repositories/ · middleware/ · errors/ · oauth/ │
├─────────────────────────────────────────────────────────┤
│ IO 抽象层(trait
│ Storage · CacheStore · SearchEngine · RateLimitStore │
│ EventBus · JobQueue · EmailSender · SmsSender │
├──────────────────┬──────────────────────────────────────┤
│ 传统模式(默认) │ Serverless 模式 │
├──────────────────┼──────────────────────────────────────┤
│ TcpListener │ Lambda handler / Vercel / Workers │
│ LocalStorage │ S3Storage / R2Storage │
│ MemoryCache │ RedisCache / D1Cache │
│ MemoryRateLimit │ RedisRateLimit │
│ Tantivy (disk) │ Tantivy (RAM) / Meilisearch │
│ tokio::spawn ×10 │ 请求内同步执行 │
│ broadcast EventBus│ 同步 fan-out / 外部队列 │
│ 文件日志 │ stdout JSON only │
│ plugin 磁盘加载 │ 编译时嵌入 / DB 加载 │
│ ServeDir │ CDN / S3 presigned URL │
└──────────────────┴──────────────────────────────────────┘
```
### 2.2 编译切换
```toml
# 传统服务器(默认)
cargo build
# Serverless 模式
cargo build --features serverless --no-default-features
# Serverless + PostgreSQL + S3
cargo build --features "serverless db-postgres storage-s3" --no-default-features
```
---
## 3. 改造清单
### 3.1 Feature Flag 设计
```toml
[features]
default = ["db-sqlite", "search-tantivy", "plugin-js", "plugin-lua", "openapi"]
serverless = [] # 启用 serverless 运行模式
# IO 后端(可独立选择)
storage-s3 = ["aws-sdk-s3", "aws-config"]
cache-redis = ["redis"] # 新增
rate-limit-redis = ["redis"] # 新增
search-external = [] # 新增:外部搜索服务(Meilisearch 等)
```
`serverless` feature 本身不引入新依赖,只改变**运行时行为**:
- 禁用所有 `tokio::spawn` 后台任务
- 事件处理从异步订阅改为同步 inline
- 日志只输出 stdout
- Worker/Cron 改为请求内单次触发
---
### 3.2 改造项详细清单
按依赖关系排序,前面的项是后续项的前置条件。
---
#### 3.2.1 运行模式开关
**文件**: `src/config/app.rs`
新增字段:
```rust
pub serverless: bool, // 读 SERVERLESS 环境变量,默认 false
```
**效果**:所有需要条件行为的代码通过 `config.serverless` 判断,
而非到处写 `#[cfg(feature = "serverless")]`。编译时 feature 控制依赖,
运行时 config 控制行为。
---
#### 3.2.2 HTTP 入口适配器
**现状**: `src/server.rs` 使用 `TcpListener::bind` + `axum::serve`
**改造**: 将 Router 构建与服务器启动分离。
```
src/server.rs (当前):
start() → TcpListener → axum::serve(router)
改造后:
build_app() → Router ← 共享,不变
start_standalone() ← 传统模式
start_serverless() ← 新增:返回 Router 或 handler
```
**新增文件**: `src/serverless.rs`
| 平台 | 入口 | 说明 |
|------|------|------|
| AWS Lambda | `lambda_http::run(router.into_service())` | 使用 `aws-lambda-rust-runtime` |
| Vercel | `#[vercel_runtime]` handler | 使用 `vercel_runtime` crate |
| Cloudflare Workers | `workers-rs` entry | 使用 `workers` crate |
| 通用 | 导出 `make_router()` 函数 | 任何平台调用即可 |
**关键**axum Router 是纯数据结构,与运行时无关。
只需将 Router 转为各平台的 handler 类型。
---
#### 3.2.3 后台任务同步化
**现状**: 8-10 个 `tokio::spawn` + `loop {}` 死循环。
| 任务 | 文件 | Serverless 方案 |
|------|------|----------------|
| 事件→插件 Hook | `server.rs:1365` | `emit_with_aspects()` 已同步执行 Hook,改 EventBus::emit() 也同步调用插件 |
| 事件→审计日志 | `server.rs:1418` | 同步写入 audit_log 表(INSERT 很快) |
| 事件→Webhook 投递 | `server.rs:1574` | 同步 HTTP POST + 超时 5s,失败记入 jobs 表 |
| 事件→Job 入队 | `worker/enqueuer.rs:24` | 同步入队(INSERT 很快) |
| Cron 调度器 | `worker/scheduler.rs:353` | 每次请求检查 + 外部 cron trigger |
| Worker 执行器 | `worker/runner.rs:46` | 请求内单次 dequeue + execute |
| Rate-limit 清理 | `server.rs:1155` | 在 check() 内惰性清理,不启动后台任务 |
| 日志清理 | `main.rs:37` | 不需要(serverless 无文件日志) |
| 插件文件监视 | `plugins.rs:350` | 不启动(serverless 禁用 hot-reload |
**核心改造**: `EventBus` 新增 `emit_sync()` 方法,在 `serverless` 模式下
直接调用所有订阅者(审计、webhook、入队),不经过 broadcast channel。
```
传统模式: emit() → broadcast channel → 各 spawn 的 subscriber
Serverless: emit_sync() → 直接调用 audit_write() + webhook_deliver() + job_enqueue()
```
---
#### 3.2.4 文件系统消除
**原则**: 所有文件 IO 走已有的 `Storage` trait,不直接调用 `std::fs`
| 当前位置 | 用途 | 改造方案 |
|----------|------|----------|
| `storage/local.rs` | 上传文件 | 已有 S3Storageserverless 默认用 S3 |
| `worker/handlers/thumbnail.rs:70` | 缩略图 | 改调 `Storage::put()` |
| `worker/handlers/sitemap.rs:80` | sitemap.xml | 改调 `Storage::put()` |
| `content_type/schema.rs:850-855` | schema TOML | 改为 DB 存储或编译时嵌入 |
| `content_type/schema.rs:1343` | 迁移 SQL 文件 | 改为 DB 存储或编译时嵌入 |
| `search/tantivy.rs:35` | 搜索索引目录 | 改为 `Index::create_in_ram` |
| `logging.rs:62,102` | 日志文件 | serverless 模式只输出 stdout |
| `plugins.rs:442+` | 插件磁盘加载 | 编译时嵌入或从 DB/Storage 加载 |
| `server.rs:1081` | ServeDir 静态文件 | 改为 CDN / S3 presigned URL 重定向 |
| `cli/server_cmd.rs:18` | PID 文件 | serverless 不写 PID |
---
#### 3.2.5 日志系统
**现状**: `src/logging.rs` — DailyRollingWriter 写 JSON 日志到 `./logs/`
**改造**:
```rust
// logging.rs
if config.serverless {
// 只初始化 stdout subscriber,不创建文件 appender
init_stdout_only();
} else {
// 当前逻辑:stdout + 文件
init_with_file_logging();
}
```
Serverless 平台自动捕获 stdout
- AWS Lambda → CloudWatch Logs
- Vercel → Vercel Logs
- Cloudflare Workers → Workers Logs
---
#### 3.2.6 搜索引擎
**现状**: `search-tantivy` feature 使用本地文件系统索引。
**改造方案**:
| 方案 | 适用场景 | 工作量 |
|------|----------|--------|
| `Index::create_in_ram` | 小型站点(<10万文档) | 低 |
| Meilisearch / OpenSearch | 大型站点 | 中 |
| Cloudflare Workers AI | Workers 部署 | 中 |
新增 `search-external` feature,通过 HTTP API 调用外部搜索服务。
`SearchEngine` trait 已有,只需新增实现。
---
#### 3.2.7 缓存系统
**现状**: 只有 `MemoryCache`moka)。
**改造**: 新增 `cache-redis` feature。
```rust
// cache.rs 新增
#[cfg(feature = "cache-redis")]
pub struct RedisCache { /* redis::aio::Connection */ }
#[cfg(feature = "cache-redis")]
impl CacheStore for RedisCache {
async fn get(&self, key: &str) -> Option<String> { /* GET */ }
async fn set(&self, key: &str, value: &str, ttl: Option<Duration>) { /* SET EX */ }
// ...
}
```
`CacheStore` trait 不需要改,只新增实现。
---
#### 3.2.8 限流系统
**现状**: 只有 `MemoryStore`DashMap),跨进程不共享。
**改造**: 新增 `rate-limit-redis` feature。
```rust
// rate_limit.rs 新增
#[cfg(feature = "rate-limit-redis")]
pub struct RedisRateLimitStore { /* ... */ }
// 滑动窗口算法:SORTED SET + ZADD + ZCOUNT + ZREMRANGEBYSCORE
```
---
#### 3.2.9 插件系统
**现状**: 从 `{plugin_dir}/` 磁盘加载 + `notify` 文件监视热更新。
**改造**:
| 改造项 | 方案 |
|--------|------|
| 插件源 | 编译时嵌入(`include_bytes!`)或从 DB/Storage 加载 |
| 热更新 | serverless 模式禁用 |
| WASM 编译缓存 | `OnceLock` 懒初始化,冷启动时编译一次 |
| JS/Lua VM | `OnceLock` 懒初始化 |
| 插件 metrics | 写入 DB 或丢弃 |
**关键**:冷启动时加载插件的开销。
WASM 编译约 50-100ms/moduleJS/Lua VM 约 10-20ms。
插件数量 < 20 时,总开销 < 500ms,可接受。
---
#### 3.2.10 静态资源服务
**现状**: `ServeDir` 直接从磁盘提供 `/uploads/*``/static/*`
**改造**:
```
/uploads/{key} → 302 重定向到 Storage::url() (S3/R2 presigned URL)
/static/* → 编译时嵌入或 CDN
/admin/* → rust-embed(已实现)
```
---
#### 3.2.11 Content Type Schema
**现状**: 从 `{content_type_dir}/*.toml` 文件读取 schema 定义。
**改造**: 新增 `content_type_source` 配置项:
```env
# 传统模式
CONTENT_TYPE_SOURCE=dir
CONTENT_TYPE_DIR=./content_types
# Serverless 模式
CONTENT_TYPE_SOURCE=db # 从 content_types 表读取
```
需要新增 DB 表存储 schema 定义(TOML 文本)。
首次启动时自动导入已有的 TOML 文件到 DB。
---
### 3.3 改造项依赖关系
```
3.2.1 运行模式开关 (config.serverless)
├── 3.2.2 HTTP 入口适配器
├── 3.2.3 后台任务同步化
│ └── 依赖:EventBus 改造
├── 3.2.4 文件系统消除
│ ├── thumbnail/sitemap → Storage::put()
│ ├── 搜索索引 → in-ram
│ ├── content-type schema → DB
│ └── plugin → 编译时嵌入/DB
├── 3.2.5 日志系统 (stdout only)
├── 3.2.6 搜索引擎 (in-ram / external)
├── 3.2.7 缓存系统 (Redis)
├── 3.2.8 限流系统 (Redis)
├── 3.2.9 插件系统 (懒加载 / 禁用热更新)
├── 3.2.10 静态资源 (CDN/S3)
└── 3.2.11 Content Type Schema (DB)
```
---
## 4. 实施路线
### Phase 1:核心跑通(3 天)
让 raisfast 在 AWS Lambda 上跑通一个完整请求。
| 步骤 | 内容 | 预计耗时 |
|------|------|----------|
| 1 | `serverless` feature flag + `config.serverless` | 2h |
| 2 | 日志 stdout-only 模式 | 1h |
| 3 | 禁用所有 `tokio::spawn` 后台任务 | 2h |
| 4 | EventBus 同步模式 (`emit_sync`) | 4h |
| 5 | Worker/Cron 请求内单次触发 | 4h |
| 6 | thumbnail/sitemap 改走 Storage::put() | 2h |
| 7 | Tantivy in-ram 模式 | 2h |
| 8 | `ServeDir` → S3 redirect | 2h |
| 9 | AWS Lambda 适配器 + 端到端测试 | 4h |
**里程碑**`curl https://xxx.lambda-url.aws.com/api/v1/posts` 返回数据。
### Phase 2:状态外部化(3 天)
消除所有进程内状态依赖。
| 步骤 | 内容 | 预计耗时 |
|------|------|----------|
| 1 | Redis CacheStore 实现 | 1d |
| 2 | Redis RateLimitStore 实现 | 1d |
| 3 | Content Type Schema DB 存储 | 0.5d |
| 4 | Plugin 编译时嵌入 | 0.5d |
**里程碑**:多实例并发请求,缓存/限流/配置一致。
### Phase 3:多平台适配(3 天)
| 步骤 | 内容 | 预计耗时 |
|------|------|----------|
| 1 | Vercel 适配器 | 1d |
| 2 | Cloudflare Workers 适配器 | 1d |
| 3 | Meilisearch SearchEngine 实现 | 0.5d |
| 4 | 文档 + 部署模板 (SAM / Serverless Framework) | 0.5d |
**里程碑**:一键部署到 AWS / Vercel / Cloudflare。
### Phase 4:生产优化(持续)
- 冷启动优化:lazy init → eager preload
- 外部搜索:OpenSearch / Elasticsearch / Typesense
- 外部队列:SQS / EventBridge / Cloudflare Queues
- CDN 集成:CloudFront / Cloudflare CDN
- 监控:OpenTelemetry / Prometheus remote write
---
## 5. 技术风险与应对
| 风险 | 影响 | 应对 |
|------|------|------|
| 冷启动慢(插件编译) | >2s 时用户体验差 | 预编译 WASM 到 DB;保持实例温热(最低并发 1) |
| SQLite 不支持并发写入 | Lambda 多实例冲突 | Serverless 模式强制 PostgreSQL/D1 |
| 请求内同步 webhook 增加延迟 | 慢 webhook 拖慢请求 | 超时 5s + 异步入队失败重试 |
| Redis 额外依赖 | 部署复杂度增加 | 提供 MemoryCache 降级模式(单实例可用) |
| Tantivy in-ram 内存占用 | 大索引 OOM | 限制索引大小 / 使用外部搜索 |
| Content Type Schema 从 DB 加载变慢 | 冷启动 +1s | OnceLock 缓存 + 懒加载 |
---
## 6. 配置参考
### 6.1 传统服务器模式
```env
DATABASE_URL=sqlite:./data/raisfast.db
STORAGE_DRIVER=local
UPLOAD_DIR=./uploads
LOG_DIR=./logs
WORKER_ENABLED=true
PLUGIN_DIR=./plugins
CONTENT_TYPE_DIR=./content_types
BUILTIN_TENANTABLE=false
```
### 6.2 AWS Lambda + PostgreSQL + S3
```env
SERVERLESS=true
DATABASE_URL=postgres://user:pass@aurora-cluster.region.rds.amazonaws.com/raisfast
STORAGE_DRIVER=s3
S3_BUCKET=raisfast-media
AWS_REGION=us-east-1
REDIS_URL=redis://cache.xxxxxx.ng.0001.use1.cache.amazonaws.com:6379
SEARCH_DRIVER=tantivy # in-ram
WORKER_ENABLED=false # 请求内触发
PLUGIN_HOT_RELOAD=false
CONTENT_TYPE_SOURCE=db
LOG_DRIVER=stdout
```
### 6.3 Cloudflare Workers + D1 + R2
```env
SERVERLESS=true
DATABASE_URL=d1://raisfast-db
STORAGE_DRIVER=r2
R2_BUCKET=raisfast-media
SEARCH_DRIVER=noop
WORKER_ENABLED=false
PLUGIN_HOT_RELOAD=false
CONTENT_TYPE_SOURCE=db
LOG_DRIVER=stdout
```
---
## 7. 新增 trait / 实现
### 7.1 EventBus 同步模式
```rust
impl EventBus {
/// Serverless 模式:同步调用所有订阅者
pub fn emit_sync(&self, event: Event, handlers: &[SyncEventHandler]) {
for handler in handlers {
let _ = handler(event.clone()); // 忽略错误,继续下一个
}
}
}
```
### 7.2 RedisCache(新增)
```rust
#[cfg(feature = "cache-redis")]
pub struct RedisCache {
client: redis::Client,
}
#[cfg(feature = "cache-redis")]
impl CacheStore for RedisCache {
async fn get(&self, key: &str) -> Option<String>;
async fn set(&self, key: &str, value: &str, ttl: Option<Duration>);
async fn delete(&self, key: &str);
async fn delete_prefix(&self, prefix: &str) -> u64;
}
```
### 7.3 RedisRateLimitStore(新增)
```rust
#[cfg(feature = "rate-limit-redis")]
pub struct RedisRateLimitStore {
client: redis::Client,
}
#[cfg(feature = "rate-limit-redis")]
impl RateLimitStore for RedisRateLimitStore {
async fn check(&self, key: &str, config: &RateLimitConfig) -> bool;
async fn cleanup_expired(&self, window_secs: u64);
}
```
### 7.4 ExternalSearchEngine(新增)
```rust
#[cfg(feature = "search-external")]
pub struct ExternalSearchEngine {
base_url: String,
api_key: String,
}
#[cfg(feature = "search-external")]
impl SearchEngine for ExternalSearchEngine {
// HTTP 调用 Meilisearch / OpenSearch / Elasticsearch
}
```
---
## 8. 预期性能
### 8.1 冷启动时间估算
| 阶段 | 耗时 | 说明 |
|------|------|------|
| 二进制加载 | <1ms | Rust 编译为原生码 |
| DB 连接池 | 5-20ms | PostgreSQL TCP 握手 |
| Schema 检查 | <1ms | `_migrations` 表已存在 → 跳过 |
| Options 加载 | 2-5ms | 首次 SELECT → 缓存 |
| Content Type 加载 | 5-10ms | 从 DB 读 TOML → 解析 |
| Plugin 加载 | 50-200ms | WASM 编译 + JS/Lua VM init |
| Tantivy 索引 | 50-500ms | 从 DB 重建 RAM 索引(视数据量) |
| **总计** | **100-750ms** | 首次请求;后续请求 <1ms |
对比:
- Node.js CMS 冷启动:500ms-2s
- Go CMS 冷启动:50-200ms
- raisfast 冷启动:100-750ms(插件/索引加载是瓶颈)
### 8.2 热请求性能
| 指标 | 传统模式 | Serverless |
|------|----------|-----------|
| API 响应 P50 | <1ms | <2ms(多一次 Redis/DB |
| API 响应 P99 | <5ms | <10ms |
| 单实例 RPS | 50K+ | 受平台限制 |
---
## 9. 文件改动总览
### 新增文件
| 文件 | 说明 |
|------|------|
| `src/serverless.rs` | Serverless 入口适配器 |
| `src/cache/redis.rs` | Redis CacheStore 实现 |
| `src/middleware/rate_limit_redis.rs` | Redis RateLimitStore 实现 |
| `src/search/external.rs` | 外部搜索服务实现 |
| `src/storage/r2.rs` | Cloudflare R2 Storage 实现 |
| `src/content_type/db_loader.rs` | Content Type 从 DB 加载 |
| `src/plugins/embedded.rs` | 编译时插件嵌入 |
### 修改文件
| 文件 | 改动 |
|------|------|
| `Cargo.toml` | 新增 `serverless``cache-redis``rate-limit-redis``search-external` features |
| `src/config/app.rs` | 新增 `serverless``log_driver``content_type_source` 字段 |
| `src/logging.rs` | 新增 stdout-only 模式 |
| `src/eventbus.rs` | 新增 `emit_sync()` 方法 |
| `src/server.rs` | 条件禁用 `tokio::spawn`Router 与启动分离 |
| `src/lib.rs` | 条件选择缓存/限流/存储后端 |
| `src/main.rs` | 条件禁用日志清理 loop |
| `src/worker/handlers/thumbnail.rs` | 改走 Storage::put() |
| `src/worker/handlers/sitemap.rs` | 改走 Storage::put() |
| `src/search/tantivy.rs` | 新增 in-ram 模式 |
| `src/plugins.rs` | 条件禁用 hot-reload,新增嵌入式加载 |
| `src/content_type/schema.rs` | 新增 DB 加载路径 |
| `src/storage.rs` | 新增 R2 factory |
| `src/cache.rs` | 新增 Redis 实现 |
| `src/middleware/rate_limit.rs` | 新增 Redis 实现 |
### 不改动的文件
| 目录/文件 | 说明 |
|-----------|------|
| `src/models/` | 数据结构,纯逻辑 |
| `src/services/` | 业务逻辑,已通过 trait 隔离 IO |
| `src/handlers/` | 路由处理,只调 service + 返回 JSON |
| `src/repositories/` | 数据访问,已用 sqlx + dialect 抽象 |
| `src/errors/` | 错误定义,纯逻辑 |
| `src/oauth/` | OAuth 流程,纯 HTTP |
| `src/db/dialect.rs` | SQL 方言,已适配多 DB |
| `src/macros.rs` | 宏定义,纯逻辑 |
| `migrations/` | Schema 文件,不变 |
| `frontend/admin/` | Admin SPA,不变 |
---
## 10. 结论
### 可行性:✅ 完全可行
项目已具备良好抽象基础:
- `Storage` / `CacheStore` / `SearchEngine` / `RateLimitStore` / `JobQueue` trait 全部就位
- Repository 层已通过 `Pool` 类型别名 + `dialect::translate()` 实现多 DB 适配
- 业务逻辑与 IO 层完全分离
### 风险:可控
核心风险只有冷启动延迟,通过以下手段可控:
- 保持温热实例(所有平台都支持)
- 预编译插件(消除 WASM 编译开销)
- 懒加载搜索索引(首次搜索时才构建)
### 投入产出
- **开发投入**~9 个工作日
- **市场价值**:成为唯一支持 Serverless 的 Rust Headless CMS
- **用户价值**:零运维 + 按需付费 + 自动扩缩容 + 全球边缘部署