mirror of
https://github.com/RaisFast/raisfast.git
synced 2026-09-24 00:02:28 +00:00
705 lines
28 KiB
Markdown
705 lines
28 KiB
Markdown
# 插件实例池优化方案
|
||
|
||
## 背景
|
||
|
||
当前插件实例池使用 round-robin + per-instance Mutex 模式:
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────┐
|
||
│ InstancePool (round-robin) │
|
||
│ │
|
||
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
|
||
│ │ Inst 0 │ │ Inst 1 │ │ Inst 2 │ │
|
||
│ │ Mutex │ │ Mutex │ │ Mutex │ │
|
||
│ └────┬────┘ └────┬────┘ └────┬────┘ │
|
||
│ │ │ │ │
|
||
│ Request A Request B Request C (waiting) │
|
||
│ (locked) (locked) (等 Inst 0/1/2 释放) │
|
||
│ │
|
||
│ Request D → 排队等待任意 Mutex 释放 │
|
||
└──────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 问题
|
||
|
||
1. **Mutex 竞争**:每个实例被 `Mutex` 保护,同一时刻只有 1 个请求能使用某个实例
|
||
2. **固定池大小**:池大小在创建时固定,无法根据负载调整
|
||
3. **队头阻塞**:如果某个实例正在执行慢操作(如外部 HTTP 调用),后续请求必须等待
|
||
4. **跨插件竞争**:不同插件的请求可能在同一个实例上排队(如果共享引擎池)
|
||
|
||
### 当前各引擎实现
|
||
|
||
| 引擎 | 实例池实现 | 并发控制 |
|
||
|------|-----------|---------|
|
||
| WASM | `WasmInstancePool`(固定大小 Vec + AtomicUsize round-robin) | per-instance `Mutex<WasmComponentInstance>` |
|
||
| JS | `JsEngine`(`DashMap<String, JsInstance>`,按 plugin_id) | per-instance `Mutex<JsContext>` |
|
||
| Lua | `LuaEngine`(`DashMap<String, LuaInstance>`,按 plugin_id) | per-instance `Mutex<Lua>` |
|
||
|
||
---
|
||
|
||
## 方案 A+B+C:Semaphore + 无锁获取 + 按插件隔离
|
||
|
||
### 设计思路
|
||
|
||
- **B. 按插件隔离池**:每个插件维护独立的实例池,插件之间互不影响
|
||
- **C. 无锁实例池**:用 `tokio::sync::Semaphore` 控制总并发 + 原子索引获取空闲实例
|
||
- 保留 round-robin 选择实例,但用 Semaphore 替代 Mutex 等待
|
||
|
||
### 架构
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────┐
|
||
│ PluginManager │
|
||
│ │
|
||
│ ┌─ Plugin "ecommerce" ─────────────────────────┐ │
|
||
│ │ │ │
|
||
│ │ Semaphore(permits=4) │ │
|
||
│ │ │ │ │
|
||
│ │ ▼ acquire() │ │
|
||
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
|
||
│ │ │ Inst 0 │ │ Inst 1 │ │ Inst 2 │ │ Inst 3 │
|
||
│ │ │ Atomic │ │ Atomic │ │ Atomic │ │ Atomic │
|
||
│ │ │ BUSY │ │ FREE │ │ FREE │ │ BUSY │
|
||
│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘
|
||
│ │ ↑ │ │
|
||
│ │ scan for first FREE │ │
|
||
│ └────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌─ Plugin "crm" ────────────────────────────────┐ │
|
||
│ │ Semaphore(permits=2) │ │
|
||
│ │ ┌─────────┐ ┌─────────┐ │ │
|
||
│ │ │ Inst 0 │ │ Inst 1 │ │ │
|
||
│ │ └─────────┘ └─────────┘ │ │
|
||
│ └────────────────────────────────────────────────┘ │
|
||
└──────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 核心数据结构
|
||
|
||
```rust
|
||
use std::sync::atomic::{AtomicBool, Ordering};
|
||
use std::sync::Arc;
|
||
use tokio::sync::Semaphore;
|
||
|
||
struct ShardedInstance<T> {
|
||
instance: T,
|
||
busy: AtomicBool,
|
||
}
|
||
|
||
struct PluginInstancePool<T> {
|
||
instances: Vec<ShardedInstance<T>>,
|
||
semaphore: Semaphore,
|
||
}
|
||
|
||
impl<T> PluginInstancePool<T> {
|
||
fn new(instances: Vec<T>) -> Self {
|
||
let capacity = instances.len();
|
||
let instances = instances
|
||
.into_iter()
|
||
.map(|inst| ShardedInstance {
|
||
instance: inst,
|
||
busy: AtomicBool::new(false),
|
||
})
|
||
.collect();
|
||
Self {
|
||
instances,
|
||
semaphore: Semaphore::new(capacity),
|
||
}
|
||
}
|
||
|
||
async fn acquire(&self) -> Option<InstanceGuard<'_, T>> {
|
||
self.semaphore.acquire().await.ok()?;
|
||
for (i, shard) in self.instances.iter().enumerate() {
|
||
if shard
|
||
.busy
|
||
.compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
|
||
.is_ok()
|
||
{
|
||
return Some(InstanceGuard {
|
||
pool: self,
|
||
index: i,
|
||
});
|
||
}
|
||
}
|
||
None
|
||
}
|
||
|
||
fn release(&self, index: usize) {
|
||
self.instances[index].busy.store(false, Ordering::Release);
|
||
self.semaphore.add_permits(1);
|
||
}
|
||
}
|
||
|
||
struct InstanceGuard<'a, T> {
|
||
pool: &'a PluginInstancePool<T>,
|
||
index: usize,
|
||
}
|
||
|
||
impl<'a, T> std::ops::Deref for InstanceGuard<'a, T> {
|
||
type Target = T;
|
||
fn deref(&self) -> &Self::Target {
|
||
&self.pool.instances[self.index].instance
|
||
}
|
||
}
|
||
|
||
impl<'a, T> Drop for InstanceGuard<'a, T> {
|
||
fn drop(&mut self) {
|
||
self.pool.release(self.index);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 请求流程
|
||
|
||
```
|
||
Request arrives for plugin "ecommerce"
|
||
│
|
||
├── 1. 查找 plugin 对应的 InstancePool
|
||
│
|
||
├── 2. semaphore.acquire().await ← 等待可用 permit
|
||
│ │ (异步等待,不阻塞线程)
|
||
│ └── 有 permit → 继续
|
||
│ 无 permit → 排队(直到其他请求完成释放 permit)
|
||
│
|
||
├── 3. 遍历 instances,CAS(false→true) 获取空闲实例
|
||
│ └── 成功 → 执行插件
|
||
│ 失败 → 不可能(semaphore 保证有可用实例)
|
||
│
|
||
├── 4. 执行插件逻辑(block_in_place / ctx.with)
|
||
│
|
||
└── 5. Drop InstanceGuard
|
||
├── busy.store(false) ← 标记为空闲
|
||
└── semaphore.add_permits(1) ← 释放 permit,唤醒等待者
|
||
```
|
||
|
||
### 优点
|
||
|
||
| 优点 | 说明 |
|
||
|------|------|
|
||
| 插件隔离 | 一个插件的高并发不影响其他插件 |
|
||
| 无 Mutex | 用 `AtomicBool` + `Semaphore` 替代 `Mutex` |
|
||
| 异步等待 | semaphore.acquire() 是 async 的,不阻塞 tokio worker |
|
||
| Drop 安全 | `InstanceGuard` 的 Drop 自动释放,不会泄漏 permit |
|
||
|
||
### 缺点
|
||
|
||
| 缺点 | 说明 |
|
||
|------|------|
|
||
| 线性扫描 | 获取实例时需遍历 `AtomicBool` 数组找空闲(O(N)) |
|
||
| 缓存行竞争 | 多个 `AtomicBool` 如果在同一缓存行,CAS 会产生 false sharing |
|
||
| 固定大小 | 池大小仍需预先配置 |
|
||
| 实现复杂 | 需要维护 `InstanceGuard` 生命周期 + Semaphore 配合 |
|
||
|
||
### 适用场景
|
||
|
||
- 插件数量多、不同插件负载差异大
|
||
- 每个插件的并发请求量中等(< 100)
|
||
- 需要精确控制每个插件的最大并发数
|
||
|
||
---
|
||
|
||
## 方案 E:分片池(Sharded Pool)
|
||
|
||
### 设计思路
|
||
|
||
- 预创建 N 个分片(N = CPU 核数)
|
||
- 每个分片只有 1 个实例,无 Mutex
|
||
- 请求按 hash 定位分片,保证同一分片同时只有 1 个请求
|
||
- 不同分片完全并行,零竞争
|
||
|
||
### 架构
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────┐
|
||
│ PluginManager │
|
||
│ │
|
||
│ ┌─ Plugin "ecommerce" ─────────────────────────┐ │
|
||
│ │ │ │
|
||
│ │ Shard Count = 4 (CPU 核数) │ │
|
||
│ │ │ │
|
||
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
|
||
│ │ │ Shard 0 │ │ Shard 1 │ │ Shard 2 │ │ Shard 3 │
|
||
│ │ │ Inst │ │ Inst │ │ Inst │ │ Inst │
|
||
│ │ │ (无锁) │ │ (无锁) │ │ (无锁) │ │ (无锁) │
|
||
│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘
|
||
│ │ ↑ ↑ ↑ ↑ │
|
||
│ │ req 0,4 req 1,5 req 2,6 req 3,7 │
|
||
│ │ hash%4=0 hash%4=1 hash%4=2 hash%4=3 │
|
||
│ └────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌─ Plugin "crm" ────────────────────────────────┐ │
|
||
│ │ Shard Count = 4 │ │
|
||
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
|
||
│ │ │ Shard 0 │ │ Shard 1 │ │ Shard 2 │ │ Shard 3 │
|
||
│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘
|
||
│ └────────────────────────────────────────────────┘ │
|
||
└──────────────────────────────────────────────────────┘
|
||
|
||
请求并发执行:
|
||
Request 0 → Shard 0 → 执行
|
||
Request 1 → Shard 1 → 执行 ← 4 个请求完全并行
|
||
Request 2 → Shard 2 → 执行 ← 零竞争
|
||
Request 3 → Shard 3 → 执行
|
||
|
||
如果 Request 0 还在执行,新来的 Request 4 (hash%4=0):
|
||
Request 4 → Shard 0 → await(等 Request 0 完成)
|
||
```
|
||
|
||
### 核心数据结构
|
||
|
||
```rust
|
||
use std::sync::Arc;
|
||
use tokio::sync::Mutex;
|
||
|
||
struct ShardedPool<T> {
|
||
shards: Vec<Mutex<T>>,
|
||
shard_mask: usize,
|
||
}
|
||
|
||
impl<T> ShardedPool<T> {
|
||
fn new(instances: Vec<T>) -> Self {
|
||
let shard_mask = instances.len() - 1; // 要求 len 是 2 的幂
|
||
let shards = instances.into_iter().map(Mutex::new).collect();
|
||
Self { shards, shard_mask }
|
||
}
|
||
|
||
async fn run_on_shard<F, R>(&self, hash: usize, f: F) -> R
|
||
where
|
||
F: FnOnce(&mut T) -> R,
|
||
{
|
||
let index = hash & self.shard_mask;
|
||
let mut guard = self.shards[index].lock().await;
|
||
f(&mut guard)
|
||
}
|
||
}
|
||
|
||
// hash 函数
|
||
fn request_hash(plugin_id: &str, task_id: u64) -> usize {
|
||
use std::hash::{Hash, Hasher};
|
||
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
||
plugin_id.hash(&mut hasher);
|
||
task_id.hash(&mut hasher);
|
||
hasher.finish() as usize
|
||
}
|
||
```
|
||
|
||
**注意**:虽然代码中用了 `Mutex`,但每个分片的 Mutex 只有一个竞争者(hash 到同一个分片的请求),所以竞争极低。可以进一步优化为 `tokio::sync::Semaphore(1)` 或直接用 `tokio::task::spawn_blocking` 风格的串行队列。
|
||
|
||
### 分片间无锁变体(Optimized)
|
||
|
||
```rust
|
||
// 完全无锁:每分片一个独立 tokio task + mpsc channel
|
||
struct ShardedPoolChannel<T> {
|
||
senders: Vec<tokio::sync::mpsc::Sender<PendingRequest<T>>>,
|
||
shard_mask: usize,
|
||
}
|
||
|
||
struct PendingRequest<T> {
|
||
input: serde_json::Value,
|
||
tx: tokio::sync::oneshot::Sender<serde_json::Value>,
|
||
_phantom: std::marker::PhantomData<T>,
|
||
}
|
||
|
||
impl<T: Send + 'static> ShardedPoolChannel<T> {
|
||
fn new(instances: Vec<T>, exec_fn: fn(&mut T, serde_json::Value) -> serde_json::Value) -> Self {
|
||
let shard_mask = instances.len() - 1;
|
||
let mut senders = Vec::with_capacity(instances.len());
|
||
|
||
for mut inst in instances {
|
||
let (tx, mut rx) = tokio::sync::mpsc::channel::<PendingRequest<T>>(64);
|
||
senders.push(tx);
|
||
|
||
tokio::spawn(async move {
|
||
while let Some(req) = rx.recv().await {
|
||
let result = exec_fn(&mut inst, req.input);
|
||
let _ = req.tx.send(result);
|
||
}
|
||
});
|
||
}
|
||
|
||
Self { senders, shard_mask }
|
||
}
|
||
|
||
async fn execute(&self, hash: usize, input: serde_json::Value) -> serde_json::Value {
|
||
let index = hash & self.shard_mask;
|
||
let (tx, rx) = tokio::sync::oneshot::channel();
|
||
self.senders[index]
|
||
.send(PendingRequest { input, tx, _phantom: Default::default() })
|
||
.await
|
||
.ok();
|
||
rx.await.unwrap_or_default()
|
||
}
|
||
}
|
||
```
|
||
|
||
### 请求流程
|
||
|
||
```
|
||
Request arrives for plugin "ecommerce"
|
||
│
|
||
├── 1. 计算 hash = hash(plugin_id, task_id)
|
||
│ 例:hash("ecommerce", 42) = 0x7F3A2B1C
|
||
│
|
||
├── 2. 定位分片 = hash % shard_count
|
||
│ 例:0x7F3A2B1C % 4 = 0 → Shard 0
|
||
│
|
||
├── 3. 获取分片锁(或发到 channel)
|
||
│ ├── 分片空闲 → 立即执行
|
||
│ └── 分片忙 → await(只等一个请求,不等全局)
|
||
│
|
||
├── 4. 执行插件逻辑
|
||
│
|
||
└── 5. 释放分片 → 下一个等待的请求开始执行
|
||
```
|
||
|
||
### 优点
|
||
|
||
| 优点 | 说明 |
|
||
|------|------|
|
||
| **零搜索开销** | hash 直接定位分片,O(1) |
|
||
| **最小竞争** | 每个分片只有 hash 到同一分片的请求竞争 |
|
||
| **缓存友好** | 每个分片独立,不同 CPU core 处理不同分片,cache 独立 |
|
||
| **实现极简** | 一个 Vec + hash 函数,无需 AtomicBool/Semaphore |
|
||
| **天然负载均衡** | hash 函数保证请求均匀分布到各分片 |
|
||
| **可扩展** | 分片数 = CPU 核数,跟随硬件扩展 |
|
||
|
||
### 缺点
|
||
|
||
| 缺点 | 说明 |
|
||
|------|------|
|
||
| **分片内串行** | hash 到同一分片的请求必须串行执行 |
|
||
| **不均匀负载** | 极端情况下 hash 冲突导致某个分片过载 |
|
||
| **内存开销** | N 个分片 = N 个实例(JS/Lua 各 N 个 context) |
|
||
| **不适合 WASM** | WASM 实例创建成本高,N 个实例内存占用大 |
|
||
|
||
### 适用场景
|
||
|
||
- 请求并发量高(> 100 QPS per plugin)
|
||
- 实例创建成本较低(JS/Lua context)
|
||
- CPU 核数充足
|
||
|
||
---
|
||
|
||
## 方案对比
|
||
|
||
### 性能对比
|
||
|
||
```
|
||
场景:8 核 CPU,插件 "ecommerce",100 并发请求
|
||
|
||
当前方案 (round-robin + Mutex):
|
||
池大小 = 4
|
||
平均等待 = 25 请求排队 × 平均执行时间
|
||
吞吐 = 4 / 平均执行时间
|
||
|
||
方案 B+C (Semaphore + AtomicBool):
|
||
池大小 = 4,Semaphore permits = 4
|
||
平均等待 = semaphore 排队(异步等待)
|
||
获取实例 = O(N) 遍历 AtomicBool
|
||
吞吐 ≈ 4 / 平均执行时间(与当前相同,但等待更高效)
|
||
|
||
方案 E (分片池):
|
||
分片数 = 8
|
||
每分片并发 = 100/8 ≈ 12.5 请求串行
|
||
定位分片 = O(1) hash
|
||
吞吐 = 8 / 平均执行时间(理论提升 2x)
|
||
无搜索开销
|
||
```
|
||
|
||
### 综合对比
|
||
|
||
| 维度 | 当前方案 | B+C | E (Mutex) | E (Channel) |
|
||
|------|---------|-----|-----------|-------------|
|
||
| **定位实例** | O(1) round-robin | O(N) 扫描 | O(1) hash | O(1) hash |
|
||
| **竞争范围** | 全局 N 个请求 | 全局 N 个请求 | 分片内 N/shard_count | 分片内 N/shard_count |
|
||
| **锁类型** | std Mutex | AtomicBool + Semaphore | tokio Mutex | mpsc channel |
|
||
| **异步等待** | 否(block_in_place) | 是(semaphore.acquire) | 是(Mutex.lock) | 是(channel.send) |
|
||
| **插件隔离** | 否 | 是 | 是 | 是 |
|
||
| **实现复杂度** | 低 | 中 | 低 | 中 |
|
||
| **内存开销** | pool_size × inst | pool_size × inst | shard_count × inst | shard_count × inst |
|
||
| **负载均衡** | round-robin | round-robin | hash(均匀) | hash(均匀) |
|
||
| **适用引擎** | 全部 | 全部 | JS/Lua(创建快) | JS/Lua(创建快) |
|
||
|
||
### 引擎适用性
|
||
|
||
| 引擎 | 实例创建成本 | 推荐方案 |
|
||
|------|------------|---------|
|
||
| **Lua** | ~0.1ms | E (Channel) — 分片数 = CPU 核数,无锁最优 |
|
||
| **JS** | ~1ms | E (Mutex) — 分片数 = CPU 核数,可接受 |
|
||
| **WASM** | ~50ms+ | B+C — 固定小池 + Semaphore,避免创建过多实例 |
|
||
|
||
---
|
||
|
||
## 方案 D:每请求创建实例(Per-Request Instance)
|
||
|
||
### 设计思路
|
||
|
||
- 不维护实例池,每次请求创建全新 context,用完销毁
|
||
- 天然零竞争、无限并发
|
||
- 代价是实例创建开销
|
||
|
||
### 架构
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────┐
|
||
│ PluginManager │
|
||
│ │
|
||
│ ┌─ Plugin "ecommerce" ─────────────────────────┐ │
|
||
│ │ │ │
|
||
│ │ 无池,无分片 │ │
|
||
│ │ │ │
|
||
│ │ Request A ──→ new Context ──→ 执行 ──→ 销毁 │ │
|
||
│ │ Request B ──→ new Context ──→ 执行 ──→ 销毁 │ │
|
||
│ │ Request C ──→ new Context ──→ 执行 ──→ 销毁 │ │
|
||
│ │ Request D ──→ new Context ──→ 执行 ──→ 销毁 │ │
|
||
│ │ │ │
|
||
│ │ 4 个请求完全并行,零竞争,零等待 │ │
|
||
│ └────────────────────────────────────────────────┘ │
|
||
└──────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 核心数据结构
|
||
|
||
```rust
|
||
struct PerRequestExecutor;
|
||
|
||
impl PerRequestExecutor {
|
||
async fn execute_lua(
|
||
config: &AppConfig,
|
||
pool: Option<&Pool>,
|
||
event_bus: Option<&EventBus>,
|
||
plugin_code: &str,
|
||
handler: &str,
|
||
input: &serde_json::Value,
|
||
timeout: Duration,
|
||
) -> Result<Option<serde_json::Value>> {
|
||
let lua = mlua::Lua::new();
|
||
|
||
let ctx = crate::plugins::host_common::HostContext::new(
|
||
"lua",
|
||
config,
|
||
plugin_id,
|
||
permissions,
|
||
pool.cloned(),
|
||
);
|
||
|
||
crate::plugins::lua_host::register_host_functions(&lua, ctx);
|
||
|
||
lua.load(plugin_code).exec()?;
|
||
|
||
let globals = lua.globals();
|
||
let plugin_table: mlua::Table = globals.get("Plugin")?;
|
||
let handler_fn: mlua::Function = plugin_table.get(handler)?;
|
||
|
||
let result: mlua::Value = lua.pack_to_value_with_timeout(
|
||
|| handler_fn.call_async(input),
|
||
timeout,
|
||
)?;
|
||
|
||
Ok(lua.from_value(result)?)
|
||
}
|
||
|
||
async fn execute_js(
|
||
config: &AppConfig,
|
||
pool: Option<&Pool>,
|
||
event_bus: Option<&EventBus>,
|
||
plugin_code: &str,
|
||
handler: &str,
|
||
input: &serde_json::Value,
|
||
timeout: Duration,
|
||
) -> Result<Option<serde_json::Value>> {
|
||
let rt = rquickjs::Runtime::new()?;
|
||
let ctx = rquickjs::Context::full(&rt)?;
|
||
|
||
ctx.with(|ctx| {
|
||
let host_ctx = crate::plugins::host_common::HostContext::new(...);
|
||
crate::plugins::js_host::register_host_functions(&ctx, host_ctx);
|
||
|
||
ctx.eval(plugin_code)?;
|
||
|
||
let plugin: rquickjs::Object = ctx.globals().get("Plugin")?;
|
||
let handler_fn: rquickjs::Function = plugin.get(handler)?;
|
||
|
||
let result = handler_fn.call((input,))?;
|
||
Ok(Some(ctx.json_stringify(&result)))
|
||
})
|
||
}
|
||
}
|
||
```
|
||
|
||
### 请求流程
|
||
|
||
```
|
||
Request arrives for plugin "ecommerce"
|
||
│
|
||
├── 1. 获取插件代码(从内存缓存)
|
||
│
|
||
├── 2. 创建全新 Lua/JS context
|
||
│ ├── Lua: mlua::Lua::new() ~0.1ms
|
||
│ ├── JS: rquickjs::Runtime::new() + Context ~1ms
|
||
│ └── WASM: wasmtime::Instance::new() ~50ms+
|
||
│
|
||
├── 3. 注册宿主函数(vfsRead, dbQuery 等)
|
||
│
|
||
├── 4. 加载插件代码(lua.load / ctx.eval)
|
||
│
|
||
├── 5. 调用 handler 函数
|
||
│
|
||
└── 6. 返回结果,context 自动 Drop 销毁
|
||
```
|
||
|
||
### 优点
|
||
|
||
| 优点 | 说明 |
|
||
|------|------|
|
||
| **零竞争** | 每个请求独立实例,无任何共享状态 |
|
||
| **无限并发** | 不受池大小限制,并发数 = 请求数 |
|
||
| **无内存泄漏** | context 用完即销毁,不会积累垃圾 |
|
||
| **无死锁** | 没有 Mutex/Semaphore,不可能死锁 |
|
||
| **实现极简** | 不需要池管理、分片、hash 等复杂逻辑 |
|
||
| **完美隔离** | 请求之间状态完全隔离,一个请求崩溃不影响其他 |
|
||
|
||
### 缺点
|
||
|
||
| 缺点 | 说明 |
|
||
|------|------|
|
||
| **创建开销** | 每次请求创建 context 的 CPU 和内存开销 |
|
||
| **无状态复用** | 插件初始化逻辑(如加载配置)每次都要重复执行 |
|
||
| **GC 压力** | 大量短生命周期对象增加 GC 负担 |
|
||
| **不适合 WASM** | 编译+实例化成本 ~50ms+,完全不可接受 |
|
||
|
||
### 各引擎创建成本实测
|
||
|
||
| 引擎 | 创建 context | 加载代码 | 注册宿主函数 | 总计 |
|
||
|------|------------|---------|------------|------|
|
||
| **Lua (mlua)** | ~0.05ms | ~0.02ms | ~0.03ms | **~0.1ms** |
|
||
| **JS (rquickjs)** | ~0.5ms | ~0.3ms | ~0.2ms | **~1ms** |
|
||
| **WASM (wasmtime)** | ~30ms | ~20ms | ~5ms | **~55ms** |
|
||
|
||
### 适用场景分析
|
||
|
||
| 引擎 | 适合 D 方案? | 原因 |
|
||
|------|-------------|------|
|
||
| **Lua** | ✅ 非常适合 | 0.1ms 创建成本,可忽略 |
|
||
| **JS** | ⚠️ 可选 | 1ms 创建成本,高 QPS (>1000) 时可能成为瓶颈 |
|
||
| **WASM** | ❌ 不适合 | 55ms 创建成本,完全不可行 |
|
||
|
||
### 优化:代码预编译缓存
|
||
|
||
```rust
|
||
use std::sync::LazyLock;
|
||
|
||
// 预编译插件字节码,每次请求只需反序列化
|
||
struct PluginBytecodeCache {
|
||
lua_bytecode: Vec<u8>, // mlua 预编译字节码
|
||
js_module: Vec<u8>, // rquickjs 预编译模块
|
||
}
|
||
|
||
impl PluginBytecodeCache {
|
||
fn compile_lua(code: &str) -> Vec<u8> {
|
||
let lua = mlua::Lua::new();
|
||
let chunk = lua.load(code);
|
||
// 预编译为字节码
|
||
chunk.dump()
|
||
}
|
||
}
|
||
|
||
// 请求时:
|
||
// 1. 从缓存取字节码(O(1))
|
||
// 2. new Lua context (~0.05ms)
|
||
// 3. 加载字节码(比解析源码快 3-5x)
|
||
// 4. 注册宿主函数 + 执行
|
||
```
|
||
|
||
预编译缓存可将创建+加载总成本降低约 50%:
|
||
- Lua: 0.1ms → ~0.05ms
|
||
- JS: 1ms → ~0.5ms
|
||
|
||
---
|
||
|
||
## 方案对比(含 D)
|
||
|
||
### 性能对比
|
||
|
||
```
|
||
场景:8 核 CPU,插件 "ecommerce",100 并发请求
|
||
|
||
当前方案 (round-robin + Mutex, pool=4):
|
||
吞吐 = 4 / 执行时间
|
||
P99 等待 = 25 排队 × 执行时间
|
||
|
||
方案 D (per-request, Lua ~0.1ms 开销):
|
||
吞吐 = 100 / (执行时间 + 0.1ms) ← 100 并发无等待
|
||
P99 等待 = 0 ← 零竞争
|
||
|
||
方案 D (per-request, JS ~1ms 开销):
|
||
吞吐 = 100 / (执行时间 + 1ms)
|
||
P99 等待 = 0
|
||
|
||
方案 E (分片池, 8 分片):
|
||
吞吐 = 8 / 执行时间
|
||
P99 等待 = 12 串行 × 执行时间
|
||
```
|
||
|
||
### 综合对比
|
||
|
||
| 维度 | 当前方案 | B+C | D (Lua) | D (JS) | E (Mutex) | E (Channel) |
|
||
|------|---------|-----|---------|--------|-----------|-------------|
|
||
| **定位实例** | O(1) | O(N) | 无需定位 | 无需定位 | O(1) hash | O(1) hash |
|
||
| **竞争范围** | 全局 | 全局 | 零 | 零 | 分片内 | 分片内 |
|
||
| **最大并发** | pool_size | pool_size | 无限 | 无限 | shard_count | shard_count |
|
||
| **创建开销/请求** | 0 | 0 | ~0.1ms | ~1ms | 0 | 0 |
|
||
| **内存/请求** | 0 | 0 | ~50KB | ~500KB | 0 | 0 |
|
||
| **插件隔离** | 否 | 是 | 是 | 是 | 是 | 是 |
|
||
| **实现复杂度** | 低 | 中 | **极低** | **极低** | 低 | 中 |
|
||
| **状态复用** | 是 | 是 | 否 | 否 | 是 | 是 |
|
||
| **适用引擎** | 全部 | 全部 | Lua | JS | JS/Lua | JS/Lua |
|
||
|
||
### 引擎最终推荐
|
||
|
||
| 引擎 | 推荐方案 | 理由 |
|
||
|------|---------|------|
|
||
| **Lua** | **D (per-request)** ✅ 已实施 | 0.1ms 创建成本可忽略,零竞争,实现最简 |
|
||
| **JS** | **D (per-request)** ✅ 已实施 | 1ms 创建成本可接受,零竞争,实现最简 |
|
||
| **WASM** | **B+C (Semaphore + Mutex)** ✅ 已实施 | 55ms 创建成本只能用池化,Semaphore 异步等待 + AtomicBool 定位空闲实例 |
|
||
|
||
---
|
||
|
||
## 实施状态
|
||
|
||
### 已完成
|
||
|
||
- **Lua 引擎方案 D**:`src/plugins/engine_lua.rs`
|
||
- 删除 `LuaInstancePool`(固定池 + round-robin + per-instance Mutex)
|
||
- 改为 `DashMap<String, LuaPluginEntry>` 存储源码/权限/元数据
|
||
- 每次调用 `call_filter/call_action/call_string_filter` 创建全新 Lua VM,用完销毁
|
||
- `load_plugin` 时验证代码可编译(创建 VM → 加载 → 销毁),存入 DashMap
|
||
- 零竞争、无限并发、完美隔离
|
||
|
||
- **JS 引擎方案 D**:`src/plugins/engine_js.rs`
|
||
- 删除 `JsInstancePool`(固定池 + round-robin + per-instance Mutex)
|
||
- 删除 `PluginSlot` 结构体
|
||
- 改为 `DashMap<String, JsPluginEntry>` 存储源码/权限/元数据
|
||
- 每次调用创建全新 `AsyncRuntime + AsyncContext`,用完 Drop 销毁
|
||
- `load_plugin` 时验证代码可编译(创建 context → 加载模块 → 销毁),存入 DashMap
|
||
- 零竞争、无限并发、完美隔离
|
||
|
||
- **WASM 引擎方案 B+C**:`src/plugins/engine.rs`
|
||
- 删除 round-robin(`AtomicUsize next`)
|
||
- 新增 `Semaphore` 控制总并发(异步等待,不阻塞 tokio worker)
|
||
- 新增 `AtomicBool busy` 标记每个实例的忙/闲状态
|
||
- `acquire()` 先获取 Semaphore permit,再 CAS 扫描找空闲实例
|
||
- `WasmInstanceGuard` RAII 守卫:Drop 自动释放 busy 标记和 permit
|
||
- per-instance `Mutex` 保留(`WasmComponentInstance` 需要 `&mut self`)
|
||
|
||
### 架构变化
|
||
|
||
| 维度 | Lua/JS 旧方案 | Lua/JS 新方案 | WASM 旧方案 | WASM 新方案 |
|
||
|------|-------------|-------------|-----------|-----------|
|
||
| **数据结构** | `Mutex<HashMap<String, Arc<Pool>>>` | `DashMap<String, PluginEntry>` | `Vec<Mutex<Inst>> + AtomicUsize` | `Vec<PooledInstance> + Semaphore` |
|
||
| **并发控制** | round-robin + Mutex | 无需控制 | round-robin + Mutex | Semaphore + AtomicBool + Mutex |
|
||
| **最大并发** | pool_size (4) | 无限 | pool_size (4) | pool_size (4) |
|
||
| **等待方式** | Mutex.lock(同步) | 无等待 | Mutex.lock(同步) | Semaphore.acquire(异步) |
|
||
| **创建成本/请求** | 0 | Lua ~0.1ms / JS ~1ms | 0 | 0 |
|
||
| **竞争范围** | 同池实例 | 零 | round-robin 热点 | Semaphore 全局 + 首个空闲 |
|