第2184篇:LLM应用的容量规划——如何预测并准备好峰值流量
2026/4/30大约 7 分钟
第2184篇:LLM应用的容量规划——如何预测并准备好峰值流量
适读人群:负责AI系统稳定性和运维的工程师 | 阅读时长:约15分钟 | 核心价值:掌握LLM应用的容量规划方法论,提前准备峰值所需的资源
双十一前两周,我接到平台那边的通知:"AI助手要参与今年的活动,预计活动期间流量是平时的30倍。"
我当时的第一反应是:30倍流量,我们的LLM系统能扛住吗?
这个问题没有直觉上的答案,因为LLM系统的容量瓶颈和传统Web服务完全不同。传统服务加机器就能水平扩展,LLM系统的瓶颈可能在API限流、Token速率、上下文窗口缓存,甚至在后端GPU资源——你加多少台应用服务器都没用。
容量规划需要先搞清楚瓶颈在哪里。
LLM系统的容量瓶颈分析
LLM系统容量瓶颈层次:
第一层:API速率限制(最常见瓶颈)
OpenAI GPT-4:默认10,000 TPM(每分钟Token数)
可申请提高,但有上限
瓶颈表现:响应变慢→429错误→请求失败
解决方案:多账号、多供应商、请求队列
第二层:应用服务器并发
每个LLM请求需要持有HTTP连接直到流式响应结束
通常5-15秒,比传统请求长10倍
瓶颈表现:连接池耗尽、响应排队
解决方案:异步处理、连接池调优
第三层:向量数据库并发
RAG系统中,每次请求都要查向量库
向量数据库的并发能力通常比关系型DB低
瓶颈表现:检索延迟上升、超时
解决方案:连接池、读副本、查询缓存
第四层:缓存命中率
相似请求命中缓存可以绕过LLM调用
峰值时如果缓存失效,会导致LLM调用量激增
瓶颈表现:成本突增、LLM调用量暴增
解决方案:预热缓存、提高缓存容量容量规划的测量基础
/**
* 容量规划数据收集器
*
* 在正常运行期间收集足够的数据,才能做准确的容量预测
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class CapacityPlanningDataCollector {
private final MeterRegistry metrics;
private final PerformanceDataRepository perfRepo;
/**
* 记录每次LLM调用的关键指标
*
* 这些数据是容量规划的原材料
*/
public void recordLLMCallMetrics(LLMCallContext ctx, LLMResponse response) {
// 1. 延迟分布(包括首个Token延迟 TTFT 和总延迟)
metrics.timer("llm.call.duration",
"model", ctx.getModelId(),
"feature", ctx.getFeatureId())
.record(response.getTotalLatencyMs(), TimeUnit.MILLISECONDS);
metrics.timer("llm.call.ttft", // Time To First Token
"model", ctx.getModelId())
.record(response.getTtftMs(), TimeUnit.MILLISECONDS);
// 2. Token消耗(用于计算实际QPS vs Token速率的关系)
metrics.summary("llm.call.input.tokens",
"model", ctx.getModelId())
.record(response.getInputTokens());
metrics.summary("llm.call.output.tokens",
"model", ctx.getModelId())
.record(response.getOutputTokens());
// 3. 并发数追踪(关键:了解峰值并发)
metrics.gauge("llm.concurrent.requests",
getCurrentConcurrentRequests());
// 4. 持久化(用于历史分析)
perfRepo.save(LLMCallRecord.from(ctx, response));
}
/**
* 计算Token速率消耗模型
*
* 每QPS消耗多少TPM(Token Per Minute)?
* 这是计算API速率配额需求的关键
*/
public TokenRateModel computeTokenRateModel(LocalDate from, LocalDate to) {
List<LLMCallRecord> records = perfRepo.findByDateRange(from, to);
// 计算平均每次调用的Token数
double avgInputTokens = records.stream()
.mapToInt(LLMCallRecord::getInputTokens)
.average().orElse(0);
double avgOutputTokens = records.stream()
.mapToInt(LLMCallRecord::getOutputTokens)
.average().orElse(0);
double p99InputTokens = percentile(records.stream()
.mapToInt(LLMCallRecord::getInputTokens).toArray(), 99);
// Token速率 = QPS × 每次调用Token数
// 对于容量规划,用P99值更安全(考虑峰值情况)
double tpmPerQps_avg = (avgInputTokens + avgOutputTokens) * 60;
double tpmPerQps_p99 = (p99InputTokens + avgOutputTokens * 1.5) * 60;
return new TokenRateModel(
avgInputTokens, avgOutputTokens,
tpmPerQps_avg, tpmPerQps_p99);
}
}峰值预测模型
/**
* 峰值流量预测
*/
@Service
@RequiredArgsConstructor
public class PeakTrafficPredictor {
private final CapacityPlanningDataCollector dataCollector;
private final CalendarEventRepository calendarRepo;
/**
* 基于历史数据预测未来峰值
*/
public PeakPrediction predictPeak(
LocalDateTime targetDateTime,
String eventDescription,
double expectedTrafficMultiplier) {
// 获取当前基线指标
BaselineMetrics baseline = computeBaseline();
// 预测峰值QPS
double peakQPS = baseline.getAvgQPS() * expectedTrafficMultiplier;
double peakQPS_P99Buffer = peakQPS * 1.3; // 再留30%余量
// 预测所需Token速率
TokenRateModel rateModel = dataCollector.computeTokenRateModel(
LocalDate.now().minusMonths(1), LocalDate.now());
double requiredTPM = peakQPS_P99Buffer * rateModel.getTpmPerQps_p99();
// 预测并发数(根据平均延迟估算)
double avgLatencySeconds = baseline.getAvgLatencyMs() / 1000.0;
double requiredConcurrency = peakQPS_P99Buffer * avgLatencySeconds * 1.5;
// 计算所需API配额(考虑到多供应商分流)
double openaiTPMNeeded = requiredTPM * 0.6; // 主供应商承担60%
double anthropicTPMNeeded = requiredTPM * 0.4; // 备用供应商承担40%
return PeakPrediction.builder()
.targetDateTime(targetDateTime)
.eventDescription(eventDescription)
.trafficMultiplier(expectedTrafficMultiplier)
.predictedPeakQPS(peakQPS_P99Buffer)
.requiredTokensPerMinute(requiredTPM)
.requiredConcurrency((int) Math.ceil(requiredConcurrency))
.openaiTPMNeeded((long) openaiTPMNeeded)
.anthropicTPMNeeded((long) anthropicTPMNeeded)
.recommendedCacheCapacityGB(estimateCacheCapacity(peakQPS_P99Buffer))
.confidenceLevel(computeConfidence(baseline, expectedTrafficMultiplier))
.build();
}
/**
* 计算当前基线指标
*/
private BaselineMetrics computeBaseline() {
// 取最近2周(排除异常日)的平均值
LocalDate twoWeeksAgo = LocalDate.now().minusWeeks(2);
List<DailyMetrics> dailyMetrics = perfRepo.getDailyMetrics(
twoWeeksAgo, LocalDate.now());
// 排除异常日(超过3σ的数据点)
OptionalDouble avgQPS = dailyMetrics.stream()
.filter(d -> !isAnomaly(d, dailyMetrics))
.mapToDouble(DailyMetrics::getAvgQPS)
.average();
OptionalDouble avgLatency = dailyMetrics.stream()
.filter(d -> !isAnomaly(d, dailyMetrics))
.mapToDouble(DailyMetrics::getAvgLatencyMs)
.average();
return new BaselineMetrics(
avgQPS.orElse(10.0), // 默认10 QPS
avgLatency.orElse(3000.0)); // 默认3秒
}
}压测:验证容量规划是否准确
规划是预测,需要压测来验证:
/**
* LLM系统压力测试工具
*
* 模拟峰值流量,验证系统是否能承受
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class LLMLoadTestRunner {
private final AISystemEndpoint systemEndpoint;
private final LoadTestDataGenerator dataGenerator;
/**
* 运行阶梯式负载测试
* 从当前QPS逐步加压,找到系统崩溃点
*/
public LoadTestReport runStairStepTest(
double startQPS,
double maxQPS,
double stepSize,
Duration stepDuration) {
List<LoadStepResult> steps = new ArrayList<>();
double currentQPS = startQPS;
while (currentQPS <= maxQPS) {
log.info("负载测试阶梯: {}QPS", currentQPS);
LoadStepResult result = runLoadStep(currentQPS, stepDuration);
steps.add(result);
log.info("阶梯结果: qps={}, p50={:.0f}ms, p99={:.0f}ms, error={:.1f}%",
currentQPS, result.getP50LatencyMs(), result.getP99LatencyMs(),
result.getErrorRate() * 100);
// 错误率超过5%,认为系统到达容量极限
if (result.getErrorRate() > 0.05) {
log.warn("错误率超过5%,系统可能接近容量极限: {}QPS", currentQPS);
break;
}
// P99延迟超过15秒,用户体验不可接受
if (result.getP99LatencyMs() > 15000) {
log.warn("P99延迟过高({}ms),系统容量不足: {}QPS",
result.getP99LatencyMs(), currentQPS);
break;
}
currentQPS += stepSize;
}
// 找到安全容量上限
double safeCapacity = steps.stream()
.filter(s -> s.getErrorRate() < 0.01 && s.getP99LatencyMs() < 10000)
.mapToDouble(LoadStepResult::getQPS)
.max().orElse(0);
return new LoadTestReport(steps, safeCapacity, safeCapacity * 0.7);
// 建议工作点:安全容量的70%(留30%余量)
}
private LoadStepResult runLoadStep(double targetQPS, Duration duration) {
long startTime = System.currentTimeMillis();
long endTime = startTime + duration.toMillis();
List<Long> latencies = Collections.synchronizedList(new ArrayList<>());
AtomicInteger errors = new AtomicInteger(0);
AtomicInteger total = new AtomicInteger(0);
// 用线程池模拟并发请求
ScheduledExecutorService executor = Executors.newScheduledThreadPool(100);
long intervalNs = (long) (1_000_000_000 / targetQPS);
ScheduledFuture<?> task = executor.scheduleAtFixedRate(() -> {
if (System.currentTimeMillis() > endTime) return;
executor.submit(() -> {
long reqStart = System.currentTimeMillis();
try {
String testQuery = dataGenerator.getRandomQuery();
systemEndpoint.process(testQuery);
latencies.add(System.currentTimeMillis() - reqStart);
} catch (Exception e) {
errors.incrementAndGet();
} finally {
total.incrementAndGet();
}
});
}, 0, intervalNs, TimeUnit.NANOSECONDS);
// 等待测试结束
try {
Thread.sleep(duration.toMillis());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
task.cancel(false);
executor.shutdown();
return computeStepResult(targetQPS, latencies, errors.get(), total.get());
}
}峰值应对策略
/**
* 峰值期间的自动应对策略
*/
@Service
@RequiredArgsConstructor
public class PeakTrafficAdaptationService {
private final RequestQueueManager queueManager;
private final ModelRoutingEngine routingEngine;
private final PromptOptimizer promptOptimizer;
/**
* 在检测到流量峰值时自动启用峰值模式
*/
@EventListener
public void onPeakDetected(PeakTrafficEvent event) {
log.info("峰值流量检测到,当前QPS={},启用峰值应对策略", event.getCurrentQPS());
// 策略1:启用请求排队(避免直接拒绝请求)
queueManager.enableQueueMode(
event.getCurrentQPS() / event.getCapacity());
// 策略2:强制路由到快速/便宜的模型(牺牲部分质量换速度)
routingEngine.enablePeakMode(true);
// 策略3:激进的Prompt压缩(减少Token消耗)
promptOptimizer.enableAggressiveCompression(true);
// 策略4:扩大缓存命中范围(相似度阈值降低,更多请求走缓存)
cacheService.relaxSimilarityThreshold(0.75); // 默认0.90
}
@EventListener
public void onPeakEnded(PeakEndedEvent event) {
log.info("峰值流量结束,恢复正常模式");
queueManager.disableQueueMode();
routingEngine.enablePeakMode(false);
promptOptimizer.enableAggressiveCompression(false);
cacheService.restoreDefaultThreshold();
}
}核心洞察:容量规划的本质是理解你的瓶颈
那次双十一,我们提前三周做了容量规划,提前申请了OpenAI的速率配额提升,调整了连接池参数,预热了缓存。活动期间,流量峰值达到了平时的27倍,系统稳定撑住了。
回顾整个过程,有几个关键认知:
LLM系统的容量瓶颈和传统服务不同。传统Web服务瓶颈通常是CPU和内存,可以无限加机器扩展。LLM系统的瓶颈往往是供应商的速率配额——加机器没用,要提前申请配额。
延迟估算要用P99,不用均值。均值3秒的接口,在高并发下P99可能是20秒。容量规划要按P99来准备,不然峰值时大量请求会超时。
压测必须用真实数据。用"你好"这样的简短问题压测,结果没有参考价值。真实流量的Token分布、并发模式才是准确预测的基础。
提前联系供应商。对于可预期的活动峰值,提前告知OpenAI/Anthropic,申请临时配额提升。等流量来了再申请已经来不及了。
