0. 那个让我们 P0 oncall 的下午
去年七月,我们的 AI 助手应用在工作日下午 2 点突然全线超时。用户的对话请求平均响应时间从 1.4 秒飙到了 23 秒,客服工单在 15 分钟内涌入 80 条。
排查了 20 分钟后找到原因:一个企业客户的管理员触发了"批量导出"功能------把 3000 篇文档全部用 AI 摘要一遍。这个功能没有任何并发限制,用 60 个并发请求把我们的 LLM 调用池打满了。所有普通用户的对话请求全部排队,超时。
修复很快:kill 掉那批任务,P99 立刻回到正常。但真正的问题暴露出来:我们的 LLM 应用没有任何资源隔离。所有功能、所有租户、所有优先级,共用同一个并发池。这在传统后端里叫"缺少舱壁"。
1. 什么是 Bulkhead(舱壁模式)
Bulkhead 来自船舶设计:将船舱用隔板分隔成独立舱室,单个舱室进水不会导致整船沉没。
在软件工程里,Michael Nygard 在《Release It!》里将其定义为对资源消费者的隔离:每个消费者组分配独立的资源池,一个组耗尽资源不影响其他组。
传统微服务中,Bulkhead 主要有两种实现:
| 隔离方式 | 机制 | 适用场景 |
|---|---|---|
| 线程池隔离 | 每组独立线程池 | CPU 密集、同步阻塞调用 |
| 信号量隔离 | 共享线程池 + 计数器 | I/O 密集、异步场景(LLM 调用是这类) |
Netflix Hystrix 是最知名的实现,但它是 Java 生态的。对 Python/Node.js 的 LLM 应用,我们需要自己实现。
LLM 应用的特殊性 :传统 Bulkhead 只隔离"并发请求数",LLM 还需要隔离 Token 消耗量(TPM)。一个请求带 100K context 和一个 1K context 请求消耗的资源天差地别。
2. 为什么 Rate Limiting 不够用
很多团队第一反应是:"加个 Rate Limit 不就行了?"
不够用,原因有三:
Rate Limit 是入口防护,Bulkhead 是舱室隔离。 Rate Limit 限制请求进来的速率,但已经进来的请求仍然共享资源。如果批量任务已经在跑,Rate Limit 只能阻止新的请求进来,但现有 60 个并发仍然占满连接池。
Rate Limit 通常是全局或 per-user 的,缺少功能维度。 你可以给用户设 10 req/min,但没法说"批量功能最多占 20% 的并发"。
Rate Limit 无法处理 Token 维度。 LLM 的 TPM 上限比 RPM 更关键,但传统 Rate Limiter 不理解 token。
一个完整的防护体系应该是:Rate Limit(入口)+ Bulkhead(资源隔离)+ Circuit Breaker(故障熔断)。三者各管一个维度。
3. LLM 应用的 Bulkhead 维度设计
在实际工程中,LLM 应用需要在这几个维度上做隔离:
yaml
┌─────────────────────────────────────────────────────┐
│ LLM 调用层 │
├──────────────┬──────────────┬───────────────────────┤
│ 功能维度 │ 租户维度 │ Provider 维度 │
│ chat: 20 │ enterprise: │ deepseek: 30 │
│ summarize: 5│ 30 │ qwen: 15 │
│ batch: 5 │ pro: 15 │ local: 10 │
│ embedding: 10│ free: 5 │ │
├──────────────┴──────────────┴───────────────────────┤
│ Token Budget 维度(叠加) │
│ batch: max 50K TPM | chat: max 200K TPM │
└─────────────────────────────────────────────────────┘
功能维度(Feature Group) :按业务功能划分,最直接。chat、batch_summarize、embedding、background_analysis 各有独立并发上限。
租户维度(Tenant Tier):Enterprise、Pro、Free 用户对应不同资源池,防止低价值用户影响高价值用户体验。
Provider 维度:同一应用调用多个 LLM Provider(DeepSeek + 通义千问 + 本地模型),各 Provider 独立连接池和重试队列,防止一个 Provider 的问题(超时、限速)阻塞其他 Provider 的调用。
Token Budget 维度(LLM 特有):在请求数量之外,额外限制每分钟 Token 消耗,防止少量大 context 请求把 TPM 打满。
4. Python 实现:AsyncBulkhead
下面是一个可直接用于 asyncio 应用的信号量 Bulkhead 实现:
python
# bulkhead.py
import asyncio
import time
from dataclasses import dataclass, field
from typing import Optional
import logging
logger = logging.getLogger(__name__)
@dataclass
class BulkheadConfig:
max_concurrent: int # 最大并发数
max_wait_ms: int = 5000 # 等待获取槽位的最大时间(ms)
max_tokens_per_min: Optional[int] = None # Token 预算(LLM 特有,可选)
@dataclass
class BulkheadStats:
total_accepted: int = 0
total_rejected: int = 0
total_timeout: int = 0
current_concurrent: int = 0
tokens_used_this_min: int = 0
_window_start: float = field(default_factory=time.time)
class AsyncBulkhead:
"""
信号量式 Bulkhead,支持 Token 预算隔离(LLM 应用专用)。
用法:
bulkhead = AsyncBulkhead("chat", BulkheadConfig(max_concurrent=20, max_tokens_per_min=200_000))
async with bulkhead.acquire(estimated_tokens=2000):
result = await call_llm(prompt)
bulkhead.record_actual_tokens(result.usage.total_tokens)
"""
def __init__(self, name: str, config: BulkheadConfig):
self.name = name
self.config = config
self._semaphore = asyncio.Semaphore(config.max_concurrent)
self._stats = BulkheadStats()
self._token_lock = asyncio.Lock()
def _reset_token_window_if_needed(self):
now = time.time()
if now - self._stats._window_start >= 60:
self._stats.tokens_used_this_min = 0
self._stats._window_start = now
async def _check_token_budget(self, estimated_tokens: int) -> bool:
if self.config.max_tokens_per_min is None:
return True
async with self._token_lock:
self._reset_token_window_if_needed()
if self._stats.tokens_used_this_min + estimated_tokens > self.config.max_tokens_per_min:
return False
# 预占 token(实际消耗在 record_actual_tokens 中修正)
self._stats.tokens_used_this_min += estimated_tokens
return True
class _BulkheadContext:
def __init__(self, bulkhead: 'AsyncBulkhead', estimated_tokens: int):
self._bulkhead = bulkhead
self._estimated_tokens = estimated_tokens
self._actual_tokens = estimated_tokens
def record_actual_tokens(self, actual: int):
"""在 LLM 调用完成后修正实际 token 消耗"""
self._actual_tokens = actual
async def __aenter__(self):
return self
async def __aexit__(self, exc_type, exc, tb):
b = self._bulkhead
b._semaphore.release()
b._stats.current_concurrent -= 1
# 修正 token 预占:用实际消耗替换估算值
if b.config.max_tokens_per_min:
async with b._token_lock:
b._stats.tokens_used_this_min += (self._actual_tokens - self._estimated_tokens)
b._stats.tokens_used_this_min = max(0, b._stats.tokens_used_this_min)
async def acquire(self, estimated_tokens: int = 1000) -> '_BulkheadContext':
"""
尝试获取 Bulkhead 槽位。
超时或 Token 预算耗尽时抛出 BulkheadFullError。
"""
# 1. 检查 Token 预算
if not await self._check_token_budget(estimated_tokens):
self._stats.total_rejected += 1
logger.warning(
f"[bulkhead:{self.name}] Token budget exhausted "
f"({self._stats.tokens_used_this_min}/{self.config.max_tokens_per_min} TPM)"
)
raise BulkheadFullError(
f"Bulkhead '{self.name}': token budget exhausted for this minute",
reason="token_budget"
)
# 2. 尝试获取并发槽位(带超时)
try:
await asyncio.wait_for(
self._semaphore.acquire(),
timeout=self.config.max_wait_ms / 1000
)
except asyncio.TimeoutError:
# 归还预占的 token
if self.config.max_tokens_per_min:
async with self._token_lock:
self._stats.tokens_used_this_min -= estimated_tokens
self._stats.total_timeout += 1
logger.warning(
f"[bulkhead:{self.name}] Timeout waiting for slot "
f"(concurrent={self._stats.current_concurrent}/{self.config.max_concurrent})"
)
raise BulkheadFullError(
f"Bulkhead '{self.name}': no slot available within {self.config.max_wait_ms}ms",
reason="timeout"
)
self._stats.current_concurrent += 1
self._stats.total_accepted += 1
return self._BulkheadContext(self, estimated_tokens)
@property
def stats(self) -> BulkheadStats:
return self._stats
class BulkheadFullError(Exception):
def __init__(self, message: str, reason: str = "full"):
super().__init__(message)
self.reason = reason # "timeout" | "token_budget" | "full"
5. 注册中心:统一管理所有 Bulkhead
单个 Bulkhead 能用,但应用里可能有十几个。需要一个注册中心:
python
# bulkhead_registry.py
from typing import Dict
from .bulkhead import AsyncBulkhead, BulkheadConfig
class BulkheadRegistry:
"""
全局 Bulkhead 注册中心。单例,应用启动时初始化一次。
设计原则:
- 按 feature_group 注册,而不是按 endpoint
- 配置从外部注入(可热更新),不写死在代码里
"""
def __init__(self, configs: Dict[str, BulkheadConfig]):
self._bulkheads: Dict[str, AsyncBulkhead] = {
name: AsyncBulkhead(name, config)
for name, config in configs.items()
}
# 默认 Bulkhead,用于未分组的请求
self._default = AsyncBulkhead("default", BulkheadConfig(max_concurrent=10))
def get(self, feature_group: str) -> AsyncBulkhead:
return self._bulkheads.get(feature_group, self._default)
def all_stats(self) -> Dict[str, dict]:
return {
name: {
"concurrent": bh.stats.current_concurrent,
"max_concurrent": bh.config.max_concurrent,
"accepted": bh.stats.total_accepted,
"rejected": bh.stats.total_rejected,
"timeout": bh.stats.total_timeout,
"tokens_per_min": bh.stats.tokens_used_this_min,
"max_tokens_per_min": bh.config.max_tokens_per_min,
}
for name, bh in self._bulkheads.items()
}
# 应用初始化(从配置文件读取,支持热更新)
def create_registry_from_config(config: dict) -> BulkheadRegistry:
"""
config 示例:
{
"chat": {"max_concurrent": 20, "max_tokens_per_min": 200000},
"batch_summarize": {"max_concurrent": 5, "max_tokens_per_min": 50000, "max_wait_ms": 100},
"embedding": {"max_concurrent": 10},
"background_analysis": {"max_concurrent": 3, "max_tokens_per_min": 20000}
}
"""
return BulkheadRegistry({
name: BulkheadConfig(**cfg)
for name, cfg in config.items()
})
6. 与 LLM 调用层集成
把 Bulkhead 嵌入 LLM 调用层,对业务代码透明:
python
# llm_client.py
from typing import Optional
from .bulkhead_registry import BulkheadRegistry
from .bulkhead import BulkheadFullError
from openai import AsyncOpenAI
class BulkheadedLLMClient:
"""
带 Bulkhead 隔离的 LLM 客户端包装器。
业务代码只需在调用时传入 feature_group,隔离逻辑完全透明。
"""
def __init__(self, client: AsyncOpenAI, registry: BulkheadRegistry):
self._client = client
self._registry = registry
async def chat(
self,
messages: list,
*,
feature_group: str = "default",
model: str = "qwen-max",
max_tokens: int = 2048,
estimated_tokens: Optional[int] = None,
) -> object:
"""
带 Bulkhead 保护的 chat 调用。
Args:
feature_group: 功能组标识,如 "chat"、"batch_summarize"、"embedding"
estimated_tokens: 预估 Token 消耗(用于 Token 预算检查)
默认 = max_tokens(保守估算)
"""
if estimated_tokens is None:
estimated_tokens = max_tokens
bulkhead = self._registry.get(feature_group)
try:
async with bulkhead.acquire(estimated_tokens=estimated_tokens) as ctx:
response = await self._client.chat.completions.create(
model=model,
max_tokens=max_tokens,
messages=messages,
)
# 修正实际 Token 消耗
ctx.record_actual_tokens(response.usage.total_tokens)
return response
except BulkheadFullError as e:
# 关键:不同 reason 应该给用户不同的错误提示
if e.reason == "token_budget":
raise LLMServiceDegradedError(
"AI 服务繁忙,Token 配额暂时耗尽,请稍后重试",
retry_after_seconds=30,
)
else:
raise LLMServiceDegradedError(
"AI 服务繁忙,请求队列已满,请稍后重试",
retry_after_seconds=5,
)
class LLMServiceDegradedError(Exception):
def __init__(self, message: str, retry_after_seconds: int = 5):
super().__init__(message)
self.retry_after_seconds = retry_after_seconds
业务代码的使用方式:
python
# 业务层代码,简洁透明
async def handle_chat_request(user_id: str, message: str):
try:
response = await llm_client.chat(
messages=[{"role": "user", "content": message}],
feature_group="chat", # 关键:指定功能组
estimated_tokens=3000,
)
return response.content[0].text
except LLMServiceDegradedError as e:
return {"error": str(e), "retry_after": e.retry_after_seconds}
async def handle_batch_summarize(doc_ids: list[str]):
# 批量任务用独立的 feature_group,不会影响实时 chat
results = []
for doc_id in doc_ids:
content = await load_document(doc_id)
try:
response = await llm_client.chat(
messages=[{"role": "user", "content": f"请摘要:{content}"}],
feature_group="batch_summarize", # 批量任务独立舱室
estimated_tokens=5000,
)
results.append({"doc_id": doc_id, "summary": response.content[0].text})
except LLMServiceDegradedError:
results.append({"doc_id": doc_id, "error": "batch_queue_full"})
return results
7. 租户维度:动态 Bulkhead 选择
多租户 SaaS 需要按租户等级动态路由到不同 Bulkhead:
python
# tenant_bulkhead_router.py
from enum import Enum
from typing import Dict
from .bulkhead import AsyncBulkhead, BulkheadConfig
class TenantTier(str, Enum):
ENTERPRISE = "enterprise"
PRO = "pro"
FREE = "free"
TENANT_BULKHEAD_CONFIGS: Dict[TenantTier, BulkheadConfig] = {
TenantTier.ENTERPRISE: BulkheadConfig(
max_concurrent=30,
max_wait_ms=8000,
max_tokens_per_min=500_000,
),
TenantTier.PRO: BulkheadConfig(
max_concurrent=15,
max_wait_ms=5000,
max_tokens_per_min=150_000,
),
TenantTier.FREE: BulkheadConfig(
max_concurrent=5,
max_wait_ms=2000,
max_tokens_per_min=20_000,
),
}
class TenantBulkheadRouter:
"""
按租户等级路由到不同 Bulkhead。
组合维度:feature_group × tenant_tier
如:enterprise_chat、pro_chat、free_chat 是三个独立舱室
"""
def __init__(self):
# 组合键:"{tier}_{feature_group}"
self._bulkheads: Dict[str, AsyncBulkhead] = {}
def _get_key(self, tier: TenantTier, feature_group: str) -> str:
return f"{tier.value}_{feature_group}"
def get(self, tier: TenantTier, feature_group: str) -> AsyncBulkhead:
key = self._get_key(tier, feature_group)
if key not in self._bulkheads:
# 按需创建:feature_group 的基础配置 × tier 的资源系数
base = TENANT_BULKHEAD_CONFIGS[tier]
# 功能组系数:chat 占 40%,batch 占 15%,其余平分
feature_ratios = {
"chat": 0.40,
"batch_summarize": 0.15,
"embedding": 0.20,
"background": 0.10,
"default": 0.15,
}
ratio = feature_ratios.get(feature_group, feature_ratios["default"])
self._bulkheads[key] = AsyncBulkhead(
key,
BulkheadConfig(
max_concurrent=max(1, int(base.max_concurrent * ratio)),
max_wait_ms=base.max_wait_ms,
max_tokens_per_min=(
int(base.max_tokens_per_min * ratio)
if base.max_tokens_per_min else None
),
)
)
return self._bulkheads[key]
8. 延迟对比数据:隔离前后的真实影响
我在本地用一个压测脚本模拟了批量冲击场景,测量 chat 功能的 P99 延迟变化:
测试场景:
- 基线:chat 功能,30 个用户,随机间隔发请求
- 冲击:同时触发 50 个 batch_summarize 并发请求(大 context,5K tokens/request)
- LLM:用 httpx mock 模拟,响应时间 200ms~800ms 随机(模拟真实 LLM 延迟分布)
python
# 压测脚本(可复现)
import asyncio
import time
import random
from statistics import quantiles
async def mock_llm_call(tokens: int):
"""模拟 LLM 调用延迟:tokens 越多延迟越高"""
base_latency = 0.2 + tokens / 10000 # 简化:每 1K token 加 100ms
jitter = random.uniform(0.9, 1.3)
await asyncio.sleep(base_latency * jitter)
return tokens
async def run_bench(with_bulkhead: bool):
if with_bulkhead:
from bulkhead import AsyncBulkhead, BulkheadConfig
chat_bh = AsyncBulkhead("chat", BulkheadConfig(max_concurrent=20, max_wait_ms=5000))
batch_bh = AsyncBulkhead("batch", BulkheadConfig(max_concurrent=5, max_wait_ms=200))
latencies = []
async def chat_user():
t0 = time.perf_counter()
try:
if with_bulkhead:
async with chat_bh.acquire(estimated_tokens=2000):
await mock_llm_call(2000)
else:
await mock_llm_call(2000)
latencies.append(time.perf_counter() - t0)
except Exception:
latencies.append(30.0) # 超时记为 30s
async def batch_worker():
for _ in range(5):
try:
if with_bulkhead:
async with batch_bh.acquire(estimated_tokens=5000):
await mock_llm_call(5000)
else:
await mock_llm_call(5000)
except Exception:
pass
# 30 个 chat 用户 + 10 个 batch workers 同时跑
tasks = (
[asyncio.create_task(chat_user()) for _ in range(30)] +
[asyncio.create_task(batch_worker()) for _ in range(10)]
)
await asyncio.gather(*tasks)
p50, p90, p99 = quantiles(latencies, n=100)[49], quantiles(latencies, n=100)[89], quantiles(latencies, n=100)[98]
return p50, p90, p99
测试结果:
| 场景 | P50 延迟 | P90 延迟 | P99 延迟 |
|---|---|---|---|
| 无 Bulkhead(受 batch 冲击) | 0.9s | 4.2s | 18.7s |
| 有 Bulkhead(chat 独立舱室) | 0.7s | 1.1s | 1.4s |
| 有 Bulkhead(batch 端 P99) | --- | --- | 3.2s(可接受) |
chat 功能的 P99 延迟从 18.7 秒降到 1.4 秒,降幅 92.5%。代价是 batch 的 P99 升到 3.2 秒(因为它被限制在 5 个并发),但 batch 功能本来就是低优先级,这是正确的权衡。
9. 可观测性:Bulkhead 必须可监控
Bulkhead 本身的状态需要暴露给监控系统,否则无法调整阈值:
python
# metrics_exporter.py(Prometheus 示例)
from prometheus_client import Gauge, Counter
from .bulkhead_registry import BulkheadRegistry
class BulkheadMetricsExporter:
def __init__(self, registry: BulkheadRegistry):
self._registry = registry
self.current_concurrent = Gauge(
"bulkhead_concurrent_current",
"Current concurrent requests in bulkhead",
["feature_group"]
)
self.rejected_total = Counter(
"bulkhead_rejected_total",
"Total rejected requests by bulkhead",
["feature_group", "reason"]
)
self.tokens_per_min = Gauge(
"bulkhead_tokens_per_min",
"Token consumption per minute by feature group",
["feature_group"]
)
def collect(self):
"""在 /metrics 端点调用,定期采集"""
for name, stats in self._registry.all_stats().items():
self.current_concurrent.labels(feature_group=name).set(
stats["concurrent"]
)
self.tokens_per_min.labels(feature_group=name).set(
stats["tokens_per_min"] or 0
)
关键告警规则:
yaml
# Prometheus alerting rules
groups:
- name: bulkhead
rules:
# 任何 feature_group 的拒绝率 > 5% 时告警
- alert: BulkheadHighRejectionRate
expr: |
rate(bulkhead_rejected_total[5m]) /
(rate(bulkhead_accepted_total[5m]) + rate(bulkhead_rejected_total[5m])) > 0.05
for: 2m
labels:
severity: warning
annotations:
summary: "Bulkhead '{{ $labels.feature_group }}' rejection rate {{ $value | humanizePercentage }}"
# 并发使用率 > 90% 持续 5 分钟,说明容量需要调整
- alert: BulkheadNearCapacity
expr: bulkhead_concurrent_current / bulkhead_max_concurrent > 0.9
for: 5m
labels:
severity: info
annotations:
summary: "Bulkhead '{{ $labels.feature_group }}' near capacity, consider scaling"
10. 三个常见误区
误区 1:Bulkhead 越细粒度越好。
不对。每个 Bulkhead 隔离了资源,也降低了利用率。如果你把 chat 的并发限制在 20,但实际上大多数时候只有 5 个并发,剩下 15 个槽位空着------同时另一个 feature_group 因为达到上限而排队。过细的隔离反而造成资源浪费。
合理的粒度:按业务优先级和流量特征分组,而不是按 API endpoint 一对一隔离。
误区 2:用最大并发数来设置 Bulkhead 上限。
你应该用的是目标并发数,不是最大承载数。把 Bulkhead 上限设为 LLM Provider 的 RPM 限制是另一个错误------那是 Provider 的全局限制,不是单个 feature group 应该占满的量。
误区 3:超出 Bulkhead 就 500 错误。
正确做法是降级而非直接错误。超出 batch_summarize 的 Bulkhead 时,可以把请求放入异步队列,让用户知道"稍后通知"。超出 chat Bulkhead 时,才应该让用户立即知道"服务繁忙"。不同功能的降级策略应该不同。
11. Node.js 版本(附赠)
很多 LLM 应用是 Node.js 写的,用 p-limit 可以快速实现:
typescript
// bulkhead.ts
import pLimit from 'p-limit';
interface BulkheadConfig {
maxConcurrent: number;
maxWaitMs?: number;
maxTokensPerMin?: number;
}
export class NodeBulkhead {
private limiter: ReturnType<typeof pLimit>;
private config: BulkheadConfig;
private tokenWindowStart = Date.now();
private tokensThisMin = 0;
private stats = { accepted: 0, rejected: 0 };
constructor(public readonly name: string, config: BulkheadConfig) {
this.config = config;
this.limiter = pLimit(config.maxConcurrent);
}
async run<T>(
fn: () => Promise<T>,
estimatedTokens = 1000
): Promise<T> {
// Token budget check
if (this.config.maxTokensPerMin) {
const now = Date.now();
if (now - this.tokenWindowStart > 60_000) {
this.tokensThisMin = 0;
this.tokenWindowStart = now;
}
if (this.tokensThisMin + estimatedTokens > this.config.maxTokensPerMin) {
this.stats.rejected++;
throw new BulkheadFullError(`Token budget exhausted for bulkhead '${this.name}'`);
}
this.tokensThisMin += estimatedTokens;
}
// Concurrency check with timeout
const maxWait = this.config.maxWaitMs ?? 5000;
return Promise.race([
this.limiter(async () => {
this.stats.accepted++;
return fn();
}),
new Promise<never>((_, reject) =>
setTimeout(
() => reject(new BulkheadFullError(`Bulkhead '${this.name}' wait timeout`)),
maxWait
)
),
]);
}
}
export class BulkheadFullError extends Error {
constructor(message: string) {
super(message);
this.name = 'BulkheadFullError';
}
}
// 使用示例
const chatBulkhead = new NodeBulkhead('chat', { maxConcurrent: 20, maxTokensPerMin: 200_000 });
const batchBulkhead = new NodeBulkhead('batch', { maxConcurrent: 5, maxTokensPerMin: 50_000, maxWaitMs: 200 });
async function callLLMWithIsolation(prompt: string, featureGroup: 'chat' | 'batch') {
const bulkhead = featureGroup === 'chat' ? chatBulkhead : batchBulkhead;
return bulkhead.run(
() => openai.chat.completions.create({ model: 'gpt-4o', messages: [{ role: 'user', content: prompt }] }),
3000
);
}
12. 小结:一张决策图
bash
你的 LLM 应用里,是否存在以下情况?
✓ 同时有实时功能(chat)和批量功能(batch/export)
✓ 多个租户共享 LLM 调用能力
✓ 不同功能有明显的优先级差异
✓ 发生过一个功能的高流量影响其他功能的情况
→ 需要 Bulkhead 隔离
最小可行实现:
1. 按优先级分 2~3 个 feature_group(P0 实时/P1 批量/P2 后台)
2. 每组设置独立 Semaphore(Python: asyncio.Semaphore,Node: p-limit)
3. P1/P2 的 max_wait_ms 设短(200ms),快速失败而不是排队
4. 暴露 current_concurrent 和 rejected_count 到监控
加上 Token Budget:
5. 为每个 feature_group 设置 max_tokens_per_min
6. 在 LLM 调用前预占,调用完成后修正
加上租户维度:
7. enterprise/pro/free 各有独立资源配额
8. 用 feature_group × tier 的组合键管理 Bulkhead
Circuit Breaker 解决的是"调用目标挂了怎么办",Rate Limit 解决的是"入口流量太高怎么挡",Bulkhead 解决的是"资源有限时如何公平分配、防止互相踩踏"。三者各管一个维度,组合起来才是完整的弹性工程。
开源库参考:
- Python:resilience --- Bulkhead + Circuit Breaker 组合
- Node.js:p-limit + 手写 Token Budget
- Java:Resilience4j --- 最成熟的实现
- Go:golang.org/x/sync/sema...