mirror of
https://github.com/RaisFast/raisfast.git
synced 2026-09-23 16:02:25 +00:00
8.9 KiB
8.9 KiB
性能优化方案
基于 20 并发压力测试的瓶颈分析,覆盖中间件、数据库、CMS 引擎三个层面。 当前测试环境:macOS 单机,dev 编译,SQLite 本地文件。
当前基准性能
20 并发 × 5 秒,httpx Python 客户端
原生 GET /posts (列表) 1,046 RPS P50=18.5ms
CMS GET /cms/articles 531 RPS P50=38.9ms
原生 GET /posts/{slug} 1,047 RPS P50=18.0ms
CMS GET /cms/articles/{id} 1,043 RPS P50=18.3ms
原生 POST /posts 287 RPS P50=66.0ms
CMS POST /cms/articles 313 RPS P50=54.4ms
请求热路径
HTTP 请求到达
│
├─ CORS ~0μs
├─ TraceLayer (日志 span) ~1μs
├─ Request ID (UUID v7) ~1μs
├─ Metrics (计时器) ~1μs
├─ Locale (Header 解析) ~5μs
│
├─ ★ Rate Limit (全局 Mutex + O(N)) ~10-100μs ← 瓶颈 #1
│
├─ JWT 验证 (每次新建 DecodingKey) ~10-50μs ← 瓶颈 #2
│
├─ CMS 路由额外:
│ ├─ Registry.get() (std::sync::RwLock) ~1-5μs ← 瓶颈 #3
│ ├─ SQL 1: COUNT(*) ~500-2000μs
│ ├─ SQL 2: SELECT ... LIMIT ~1000-5000μs
│ └─ Relation 解析 (额外 N 次查询) ← 瓶颈 #4
│
└─ 原生路由额外:
├─ Cache RwLock 读 ~1μs (命中时)
├─ SQL 1: LEFT JOIN 主查询 ~1000-5000μs
├─ SQL 2: COUNT(*) ~500-2000μs
└─ SQL 3: 批量查 tags ~1000-3000μs
瓶颈 #1:Rate Limit 全局 Mutex + O(N) retain
位置:src/middleware/rate_limit.rs:77-95
现状:
struct MemoryStore {
entries: tokio::sync::Mutex<HashMap<String, Entry>>,
}
async fn check(&self, key: &str, config: &RateLimitConfig) -> bool {
let mut entries = self.entries.lock().await; // 所有请求竞争一把锁
entries.retain(|_, entry| { ... }); // O(N) 全量遍历清理
let entry = entries.entry(key.to_string()) // 每次分配 String
.or_insert(Entry { count: 0, window_start: now });
// ...
}
问题:
| 问题 | 影响 |
|---|---|
单一 tokio::sync::Mutex |
所有 API 请求串行化,高并发下排队 |
retain() 每次全量遍历 |
O(N),N = 活跃 IP 数,随流量线性增长 |
key.to_string() 每次分配 |
即使 key 已存在也分配新 String |
| 锁持有期间做清理 | 阻塞所有其他请求 |
方案:
- 替换为
DashMap<String, Entry>(分片锁,16 个分片) - 去掉
retain(),改为惰性过期(检查时判断单条是否过期) - 用
entry()API 避免 key 重复分配 - 后台定时任务清理过期条目(已有 300 秒清理,保留)
预估提升:RPS +100~200%
瓶颈 #2:JWT DecodingKey 每次创建
位置:src/services/auth.rs:134-145
现状:
pub fn verify_token(token: &str, secret: &str) -> AppResult<Claims> {
jsonwebtoken::decode::<Claims>(
token,
&DecodingKey::from_secret(secret.as_bytes()), // 每次分配+拷贝
&Validation::default(),
)
// ...
}
方案:
启动时创建 DecodingKey 存入 AppState:
pub struct AppState {
pub jwt_decoding_key: DecodingKey, // 新增
// ...
}
预估提升:-10~30μs/请求
瓶颈 #3:ContentTypeRegistry 用 std::sync::RwLock
位置:src/content_type.rs:48
现状:
pub struct ContentTypeRegistry {
inner: std::sync::RwLock<HashMap<String, Arc<ContentTypeSchema>>>,
}
问题:
std::sync::RwLock在 tokio 异步运行时中会阻塞线程- 如果有写操作(注册新 Content Type),读端也会被阻塞
- 每次 CMS 请求都需要获取读锁
方案:
替换为 arc-swap::ArcSwap<HashMap<...>>:
pub struct ContentTypeRegistry {
inner: ArcSwap<HashMap<String, Arc<ContentTypeSchema>>>,
}
// 读:无锁,直接 load()
fn get(&self, name: &str) -> Option<Arc<ContentTypeSchema>> {
self.inner.load().get(name).cloned()
}
// 写:替换整个 map
fn register(&self, schema: ContentTypeSchema) {
let mut new_map = self.inner.load().as_ref().clone();
new_map.insert(schema.name.clone(), Arc::new(schema));
self.inner.store(Arc::new(new_map));
}
预估提升:消除尾部延迟,CMS 请求 -1~5μs
瓶颈 #4:OneToMany/ManyToMany 的 N+1 查询
位置:src/content_type/resolver.rs:53-63
现状:
// OneToMany — 每个 item 单独查询
for item in items.iter_mut() {
resolve_one_to_many(pool, ct, field.name, rel, item).await?;
}
// ManyToMany — 同上
for item in items.iter_mut() {
resolve_many_to_many(pool, ct, field.name, rel, item).await?;
}
20 条记录 × 1 个 OneToMany 字段 = 20 次额外 SQL 查询。
方案:
改为和 ManyToOne 一样的批量模式:
OneToMany: 收集所有 item 的 id → WHERE fk_col IN (id1, id2, ...) → 按 fk_col 分组 → 分发回各 item
ManyToMany: 收集所有 item 的 id → WHERE junction.source_id IN (...) JOIN target → 按 source_id 分组 → 分发
预估提升:N 次查询 → 1 次查询,CMS 列表 -50%
瓶颈 #5:SQLite PRAGMA 未优化
位置:src/db/connection.rs:33-43
现状:
.after_connect(|conn, _meta| {
sqlx::query("PRAGMA journal_mode = WAL").execute(&mut *conn).await?;
sqlx::query("PRAGMA foreign_keys = ON").execute(&mut *conn).await?;
Ok(())
})
缺少的 PRAGMA:
| PRAGMA | 当前值 | 建议值 | 说明 |
|---|---|---|---|
busy_timeout |
未设置(默认 0) | 5000 |
写冲突时等待 5 秒而非立即报错 |
synchronous |
FULL(默认) |
NORMAL |
WAL 模式下 NORMAL 足够安全,性能提升显著 |
cache_size |
-2000(~2MB,默认) |
-64000(64MB) |
增大页缓存,减少磁盘 IO |
temp_store |
DEFAULT |
MEMORY |
临时表在内存中,排序/聚合更快 |
mmap_size |
0(默认) |
268435456(256MB) |
内存映射 IO,减少系统调用 |
预估提升:SQL 延迟 -30~50%
瓶颈 #6:CMS 列表无缓存
现状:
- 原生
/posts走CachedPostRepository(内存缓存,命中时 <0.5ms) - CMS
/cms/articles走ContentRepository(每次都查数据库)
方案:
为 CMS 动态路由加缓存层:
缓存 key: "cms:{plural}:{tenant}:{page}:{page_size}:{status}:{filters_hash}"
缓存 TTL: 可配置,默认 30 秒
失效策略: create/update/delete 时清除该 content type 的所有缓存
预估提升:热数据从 38ms → <1ms,等效 RPS +10x
优化路线图
第一阶段:低风险高收益(1-2 天)
| # | 优化项 | 改动量 | 预估收益 |
|---|---|---|---|
| 1 | SQLite PRAGMA 调优 | 5 行 | SQL -30~50% |
| 2 | JWT DecodingKey 缓存 | 3 行 | -10~30μs/请求 |
| 3 | CMS 列表内存缓存 | ~100 行 | 热数据 +10x |
第二阶段:中等改动(2-3 天)
| # | 优化项 | 改动量 | 预估收益 |
|---|---|---|---|
| 4 | Rate Limit → DashMap | ~80 行 | RPS +100~200% |
| 5 | ContentTypeRegistry → ArcSwap | ~50 行 | 消除尾部延迟 |
| 6 | OneToMany/ManyToMany 批量化 | ~100 行 | CMS 列表 -50% |
第三阶段:进阶优化(可选)
| # | 优化项 | 说明 |
|---|---|---|
| 7 | CMS 列表预 JOIN | 将 ManyToOne 改为 LEFT JOIN 合并到主查询,减少 1 次 SQL |
| 8 | 响应压缩 (gzip/brotli) | 中间件层压缩 JSON,减少传输时间 |
| 9 | 连接池调优 | 动态调整 max_connections 基于 CPU 核数 |
| 10 | release 编译优化 | LTO + codegen-units=1,预估 +20~30% |
优化后预估性能
当前 (20 并发, dev):
原生列表: 1,046 RPS P50=18.5ms
CMS 列表: 531 RPS P50=38.9ms
详情: 1,043 RPS P50=18.0ms
第一阶段后 (dev):
原生列表: 1,500-2,000 RPS P50=10-13ms
CMS 列表: 3,000-5,000 RPS P50=4-7ms (缓存命中)
详情: 1,500-2,000 RPS P50=10-13ms
全部优化后 (dev):
原生列表: 2,000-3,000 RPS P50=7-10ms
CMS 列表: 1,500-2,500 RPS P50=8-13ms (缓存未命中)
详情: 3,000-5,000 RPS P50=4-7ms
全部优化后 (release):
原生列表: 5,000-8,000 RPS P50=2-4ms
CMS 列表: 3,000-5,000 RPS P50=4-7ms
详情: 8,000-15,000 RPS P50=1-3ms
目标:release + 全部优化后,达到 PocketBase 同一梯队(3,000-8,000 RPS)。
横向对比参考
| 系统 | 语言 | 列表 RPS | 详情 RPS |
|---|---|---|---|
| PocketBase | Go | 2,000-5,000 | 3,000-6,000 |
| raisfast (当前 dev) | Rust | 531-1,046 | 1,043 |
| raisfast (预估 release 全优化) | Rust | 3,000-8,000 | 8,000-15,000 |
| Strapi | Node.js | 100-300 | 200-400 |
| Ghost | Node.js | 200-500 | 300-600 |
| Directus | Node.js | 80-200 | 150-300 |