18 KiB
平台化演进路线图
基于当前架构的扩展性评估,分析从「博客 API」演进为「可扩展平台」的差距与路径。
1. 当前架构评估
1.1 已具备的扩展基础
| 维度 | 现状 | 评级 |
|---|---|---|
| 分层架构 | Handler → Service → Model,职责清晰 | 优 |
| 插件系统 | WASM 沙箱、11 个 Hook、热加载、安全模型 | 优 |
| 多数据库 | SQLite/PostgreSQL/MySQL + 编译时 SQL 检查 | 优 |
| 错误处理 | 统一 AppError + i18n + IntoResponse | 优 |
| 认证 | JWT + Refresh Token + 角色权限 | 良 |
| 测试覆盖 | 125 个测试(65 单元 + 60 集成) | 良 |
| 代码质量 | #![deny(unsafe_code)]、零 clippy 警告 |
优 |
1.2 核心判断
当前系统是一个功能完整的博客 API,但还不是一个平台框架。
差距在于缺少一层「平台基础设施」:
当前: HTTP → Handler → Service → Model → DB
↓
Plugin (WASM)
目标: HTTP → Handler → Service → Model → DB
↓ ↓ ↓
Cache EventBus FullText
(Redis) (通知/任务) (Meilisearch)
↓ ↓
Plugin Background
System Workers
2. 必须补齐的短板
P0 — 不补就做不了复杂平台
2.1 缓存层(Redis)
现状: 无任何缓存。所有查询直接打到数据库。
问题: 热点数据(文章列表、分类树、标签云)在高并发下会成为瓶颈。复杂平台的配置、会话、计数器都需要缓存。
方案:
// src/cache/mod.rs
#[async_trait]
pub trait CacheStore: Send + Sync {
async fn get(&self, key: &str) -> Option<String>;
async fn set(&self, key: &str, value: &str, ttl: Option<Duration>) -> AppResult<()>;
async fn delete(&self, key: &str) -> AppResult<()>;
async fn incr(&self, key: &str, delta: i64) -> AppResult<i64>;
}
// src/cache/memory.rs — 开发阶段用
pub struct MemoryCache { /* HashMap + TTL */ }
// src/cache/redis.rs — 生产环境用
pub struct RedisCache { /* redis::aio::Connection */ }
集成点:
services/post.rs— 文章详情缓存(slug → post),列表缓存(分页参数 → 结果集)services/auth.rs— Token 黑名单、用户权限缓存services/comment.rs— 评论计数缓存middleware/rate_limit.rs— 已有RateLimitStoretrait,补充RedisStore实现
2.2 后台任务队列
现状: 无异步任务机制。所有操作在请求线程同步完成。
问题: 定时发布、邮件发送、图片处理、统计分析、Webhook 回调都需要异步执行。
方案:
// src/worker/mod.rs
#[async_trait]
pub trait JobQueue: Send + Sync {
async fn enqueue(&self, job: Job) -> AppResult<()>;
async fn dequeue(&self) -> Option<Job>;
}
pub enum Job {
SendEmail { to: String, subject: String, body: String },
ProcessImage { media_id: String, operations: Vec<ImageOp> },
ScheduledPublish { post_id: String, publish_at: String },
WebhookNotify { url: String, payload: serde_json::Value },
RebuildSearchIndex { post_ids: Vec<String> },
GenerateSitemap,
}
实现选择:
| 方案 | 优点 | 缺点 |
|---|---|---|
tokio::spawn + 内存队列 |
零依赖,简单 | 进程重启丢失 |
| SQLite 持久化队列 | 无额外依赖,可靠 | 性能上限低 |
| Redis Stream | 成熟、支持消费者组 | 引入 Redis 依赖 |
| 独立 sidekiq-rs / tower-worker | 专业方案 | 重量级 |
建议: 先实现 SQLite 持久化队列(与现有架构一致),后期按需切换 Redis Stream。
2.3 全站搜索
现状: services/post.rs 使用 LIKE '%keyword%' 全表扫描,无索引利用。
问题: 文章量上千后搜索变慢,无法支持高亮、分词、相关性排序。
方案 A — SQLite FTS5(推荐起步):
-- migrations/003_fts5.sql
CREATE VIRTUAL TABLE IF NOT EXISTS posts_fts USING fts5(
title,
content,
content=posts,
content_rowid=rowid
);
-- 触发器自动同步
CREATE TRIGGER posts_ai AFTER INSERT ON posts BEGIN
INSERT INTO posts_fts(rowid, title, content) VALUES (new.rowid, new.title, new.content);
END;
// models/search.rs
pub async fn search_posts(pool: &Pool, query: &str, page: i64, page_size: i64) -> AppResult<(Vec<PostSearchResult>, i64)> {
// SELECT * FROM posts_fts WHERE posts_fts MATCH ? ORDER BY rank LIMIT ? OFFSET ?
}
方案 B — Meilisearch(后期升级):
当文章量超过 10 万或需要中文分词时切换。通过插件 Hook OnPostCreated/OnPostUpdated/OnPostDeleted 同步索引。
2.4 数据库事务
现状: sync_tags 等多步操作缺少事务包裹,中间失败会导致数据不一致。
修复:
// services/post.rs
pub async fn create_post(pool: &Pool, /* ... */) -> AppResult<PostResponse> {
let mut tx = pool.begin().await?;
let post = sqlx::query_scalar!("INSERT INTO posts ...")
.fetch_one(&mut *tx)
.await?;
for tag_id in &tag_ids {
sqlx::query!("INSERT INTO posts_tags ...", post.id, tag_id)
.execute(&mut *tx)
.await?;
}
tx.commit().await?;
Ok(post)
}
P1 — 不补就做不好
2.5 实时推送(WebSocket / SSE)
现状: 纯请求-响应模式,无服务端推送能力。
场景: 新评论通知作者、审核状态变更通知、在线访客计数、协作编辑。
方案:
// src/server/mod.rs — 新增 WebSocket 路由
.route("/ws", get(ws_handler))
.route("/api/v1/events", get(sse_handler))
// src/services/event.rs
pub struct EventBus {
tx: broadcast::Sender<Event>,
}
pub enum Event {
NewComment { post_slug: String, comment: CommentResponse },
PostPublished { post: PostResponse },
SystemNotification { message: String },
}
推荐 SSE(Server-Sent Events)起步:
- 比 WebSocket 简单,单向推送足够
- 浏览器原生支持,无需额外库
- axum 集成简单(
axum::response::sse::Sse)
2.6 通知系统
现状: 无任何通知机制。
方案:
// src/services/notification.rs
pub struct NotificationService {
email: Option<EmailSender>,
webhook: Option<WebhookSender>,
in_app: InAppNotifier,
}
pub trait Notifier: Send + Sync {
async fn send(&self, notification: &Notification) -> AppResult<()>;
}
pub struct Notification {
pub recipient: String,
pub event: NotificationEvent,
pub channels: Vec<Channel>, // Email, InApp, Webhook
}
通知触发点(复用现有 Hook):
| 事件 | 通知对象 | 渠道 |
|---|---|---|
| 新评论 | 文章作者 | InApp + Email |
| 评论审核通过 | 评论者 | |
| 文章发布 | 订阅者 | Email + Webhook |
| 用户注册 | 管理员 | InApp |
| 登录异常 | 用户 |
2.7 媒体处理管线
现状: 文件原样存储,无压缩、缩略图、格式转换。
方案:
// src/services/media.rs — 扩展
pub enum ImageOperation {
Resize { width: u32, height: u32 },
ConvertToWebP,
GenerateThumbnail { size: u32 },
Compress { quality: u8 },
}
pub async fn process_upload(
pool: &Pool,
user_id: &str,
file: Bytes,
filename: &str,
) -> AppResult<MediaResponse> {
// 1. 校验文件类型和大小
// 2. 图片:生成缩略图 + WebP 转换 + 压缩
// 3. 存储原始文件和处理后文件
// 4. 记录数据库
// 5. 可选:上传到 S3
}
2.8 API 版本化策略
现状: /api/v1 硬编码前缀,无版本管理策略。
方案:
// server/mod.rs
let api_v1 = axum::Router::new()
.route("/posts", get(post::list).post(post::create))
// ...
let api_v2 = axum::Router::new()
.route("/posts", get(post_v2::list).post(post_v2::create))
// ... breaking changes
let app = axum::Router::new()
.nest("/api/v1", api_v1)
.nest("/api/v2", api_v2)
版本兼容原则:
- 新增字段:向前兼容,不算 breaking change
- 删除/重命名字段:新版本
- 新增端点:在当前版本添加
- 废弃端点:响应头
Deprecation: true,至少保留一个大版本
2.9 审计日志
现状: 无操作记录。
方案:
-- migrations/004_audit_log.sql
CREATE TABLE IF NOT EXISTS audit_logs (
id TEXT PRIMARY KEY,
user_id TEXT,
action TEXT NOT NULL, -- 'post.create', 'user.update_role', 'comment.delete'
resource_type TEXT NOT NULL, -- 'post', 'user', 'comment', 'media'
resource_id TEXT,
detail TEXT, -- JSON 格式的变更详情
ip_address TEXT,
user_agent TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_audit_user ON audit_logs(user_id);
CREATE INDEX IF NOT EXISTS idx_audit_action ON audit_logs(action);
CREATE INDEX IF NOT EXISTS idx_audit_created ON audit_logs(created_at);
// src/middleware/audit.rs
pub async fn audit_middleware(
// 记录所有写操作(POST/PUT/DELETE)
// 自动提取 user_id、action、resource 信息
)
P2 — 做大时才需要
2.10 水平扩展
现状: 单实例部署,内存存储(限流器、会话)。
需要改造:
| 组件 | 当前 | 目标 |
|---|---|---|
| 限流器 | MemoryStore |
RedisStore(trait 已预留) |
| 会话/缓存 | 无 | Redis |
| 文件存储 | 本地磁盘 | S3 兼容对象存储 |
| 搜索 | SQLite FTS5 | Meilisearch(独立服务) |
| 任务队列 | 内存 | Redis Stream / 独立 worker |
改造后架构:
┌─────────┐
│ Nginx │ (负载均衡)
└────┬────┘
┌────────────┼────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ App :3001│ │ App :3002│ │ App :3003│ (无状态实例)
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
└─────────────┼─────────────┘
│
┌─────────┼─────────┐
▼ ▼ ▼
┌──────┐ ┌────────┐ ┌──────┐
│ Redis│ │ PostgreSQL│ │ S3 │
└──────┘ └────────┘ └──────┘
2.11 可观测性
现状: tracing 日志已有,但缺 metrics 和分布式追踪。
方案:
# Cargo.toml 新增
[dependencies]
prometheus = "0.13"
metrics = "0.24"
metrics-exporter-prometheus = "0.16"
关键指标:
| 类别 | 指标 |
|---|---|
| HTTP | 请求量、延迟分布、错误率(按路由/方法/状态码) |
| 数据库 | 连接池使用率、查询延迟、慢查询 |
| 插件 | 加载时间、Hook 执行时间、内存占用 |
| 业务 | 文章发布量、评论量、注册量、活跃用户 |
| 系统 | CPU、内存、磁盘、网络 |
2.12 内容版本与草稿自动保存
现状: 只有 draft/published 两种状态,无版本历史。
方案:
-- migrations/005_post_revisions.sql
CREATE TABLE IF NOT EXISTS post_revisions (
id TEXT PRIMARY KEY,
post_id TEXT NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
revision_number INTEGER NOT NULL,
title TEXT NOT NULL,
content TEXT NOT NULL,
excerpt TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE(post_id, revision_number)
);
- 每次更新文章时自动创建一个 revision
- 支持对比任意两个版本
- 支持回滚到指定版本
- 草稿自动保存:前端定时 POST,后端 upsert
2.13 CI/CD + Docker
现状: guide.md 有 Dockerfile 模板但未实际创建。
需要创建:
Dockerfile— 多阶段构建(builder → runtime)docker-compose.yml— 开发环境(app + redis + postgres).github/workflows/ci.yml— fmt + clippy + test + builddocker-compose.prod.yml— 生产部署模板
3. 架构改造要点
3.1 事件总线(EventBus)
当前 Plugin Hook 是同步链式调用,无法解耦。平台级系统需要事件驱动架构。
// src/eventbus/mod.rs
use tokio::sync::broadcast;
pub struct EventBus {
tx: broadcast::Sender<Arc<Event>>,
}
#[derive(Debug, Clone)]
pub enum Event {
PostCreating { data: PostCreatingData },
PostCreated { post_id: String },
PostUpdated { post_id: String, old: PostSnapshot, new: PostSnapshot },
PostDeleted { post_id: String },
CommentCreated { comment_id: String, post_slug: String },
UserRegistered { user_id: String },
LoginAttempt { email: String, success: bool, ip: String },
}
impl EventBus {
pub fn new(capacity: usize) -> Self {
let (tx, _) = broadcast::channel(capacity);
Self { tx }
}
pub async fn emit(&self, event: Event) {
let _ = self.tx.send(Arc::new(event));
}
pub fn subscribe(&self) -> broadcast::Receiver<Arc<Event>> {
self.tx.subscribe()
}
}
订阅者:
EventBus.emit(PostCreated)
├── PluginManager.dispatch_action(PostCreated) // 插件 Hook
├── NotificationService.notify(author) // 邮件/站内通知
├── SearchIndexer.index(post) // 搜索索引同步
├── AuditLogger.log("post.create", ...) // 审计日志
└── CacheInvalidator.invalidate("posts:*") // 缓存失效
3.2 Repository 抽象
当前 Model 层直接写 SQL,耦合数据库实现。复杂平台需要抽象层以便 mock、缓存、换引擎。
// src/repositories/mod.rs
#[async_trait]
pub trait PostRepository: Send + Sync {
async fn find_by_slug(&self, slug: &str) -> AppResult<Option<Post>>;
async fn list(&self, filter: PostFilter, page: i64, page_size: i64) -> AppResult<Paginated<Post>>;
async fn create(&self, input: CreatePostInput) -> AppResult<Post>;
async fn update(&self, id: &str, input: UpdatePostInput) -> AppResult<Post>;
async fn delete(&self, id: &str) -> AppResult<()>;
}
// src/repositories/sqlx_post.rs
pub struct SqlxPostRepository { pool: Pool }
// src/repositories/cached_post.rs
pub struct CachedPostRepository<P: PostRepository> {
inner: P,
cache: Arc<dyn CacheStore>,
}
好处:
CachedPostRepository装饰器模式,为任意 Repository 添加缓存- 测试时 mock
PostRepository,隔离数据库 - 未来切换 ORM 或搜索引擎时只改 Repository 实现
3.3 配置热更新
当前 AppConfig 启动时一次性加载。平台级系统需要运行时配置变更。
// src/config/runtime.rs
pub struct RuntimeConfig {
inner: Arc<RwLock<AppConfig>>,
}
impl RuntimeConfig {
pub async fn reload(&self, new_config: AppConfig) {
*self.inner.write().await = new_config;
}
pub async fn get(&self) -> tokio::sync::RwLockReadGuard<'_, AppConfig> {
self.inner.read().await
}
}
可热更新的配置项:
- 每页文章数
- 限流阈值
- 插件启用/禁用
- CORS 白名单
- 功能开关(Feature Flags)
4. 演进路线
阶段 1 — 基础设施(2-3 周)
Week 1:
├── SQLite 事务包裹关键操作(sync_tags、create_post、delete_post)
├── FTS5 全文搜索(零依赖,SQLite 内建)
└── EventBus trait + broadcast 实现
Week 2:
├── CacheStore trait + MemoryCache 实现
├── 文章列表/详情缓存集成
└── 后台任务队列(SQLite 持久化方案)
Week 3:
├── 媒体处理管线(image crate:缩略图 + WebP)
├── 审计日志表 + 中间件
└── 集成测试覆盖新功能
阶段 2 — 核心功能(2-3 周)
Week 4:
├── SSE 实时推送(新评论、通知)
├── 通知系统框架(Email + InApp)
└── 邮件发送(lettre crate)
Week 5:
├── 内容版本/修订历史
├── 草稿自动保存 API
└── API 版本化策略落地
Week 6:
├── Admin Dashboard API(统计、管理)
├── RedisStore 实现(限流器 + 缓存)
└── 集成测试 + 性能测试
阶段 3 — 平台化(按需)
├── Repository 抽象层
├── 配置热更新
├── Prometheus metrics 集成
├── Docker + docker-compose + CI/CD
├── Meilisearch 替换 FTS5
├── S3 对象存储支持
└── 水平扩展验证(多实例部署测试)
5. 依赖新增预估
阶段 1
| 依赖 | 用途 | 体积影响 |
|---|---|---|
tokio (broadcast) |
EventBus | 已有,零增长 |
image |
图片处理 | +~3MB 编译 |
| 无新增 | FTS5 是 SQLite 内建 | 零 |
阶段 2
| 依赖 | 用途 | 体积影响 |
|---|---|---|
lettre |
邮件发送 | +~2MB |
redis |
缓存/限流/队列 | +~1MB |
阶段 3
| 依赖 | 用途 | 体积影响 |
|---|---|---|
prometheus |
指标采集 | +~1MB |
rust-s3 |
对象存储 | +~2MB |
meilisearch-sdk |
搜索引擎客户端 | +~0.5MB |
6. 风险与注意事项
6.1 性能
- WASM 插件每次 Hook 调用有 ~10-100μs 开销,高频场景需评估
- Redis 引入网络延迟,缓存策略需权衡 TTL 和一致性
- EventBus broadcast 需控制容量,避免内存溢出
6.2 兼容性
- FTS5 需要 SQLite 编译时启用(大部分发行版已默认启用)
imagecrate 的 WebP 编码需要webpfeature flag- Meilisearch 需要独立部署,增加运维复杂度
6.3 迁移策略
- 所有新功能通过 feature flag 控制,不影响现有功能
- 缓存默认关闭(
MemoryCache),生产环境启用 Redis - 搜索默认 FTS5,通过配置切换 Meilisearch
- 每个阶段完成后确保
cargo test+cargo clippy全部通过