AI 应用的容量规划——基于流量模型的资源预估与成本优化一、AI 应用容量规划的特殊性与传统 Web 应用不同AI 应用的容量规划面临几个独特变量单次请求的资源消耗差异巨大短问答可能只消费 200 Token而长文档摘要可能超过 10,000 Token。GPU 资源成本远高于 CPUA100 的单卡小时价格是普通云主机的数十倍。模型加载时间使得弹性伸缩的响应速度远慢于无状态服务30~120s vs 1s。因此AI 应用的容量规划不能简单套用QPS x 单请求耗时 一定余量的传统公式需要建立更精细的流量模型。二、建立流量模型容量规划的起点是建立准确的流量模型描述不同时段的请求特征和资源消耗规律。/** * AI 流量建模器——基于历史数据的时序分析为容量规划提供数据基础。 * * 为什么需要区分峰值和稳态两种模型 * 以峰值请求作为容量基准会导致严重的资源浪费GPU 利用率常年 30% * 仅以稳态作为基准则无法应对突发流量。 */ Component public class AiTrafficModeler { private static final Logger log LoggerFactory.getLogger( AiTrafficModeler.class); private final MeterRegistry meterRegistry; private final Clock clock Clock.systemUTC(); // 滑动窗口大小30天历史数据 private final ConcurrentNavigableMapLocalDate, DailyTrafficMetrics history new ConcurrentSkipListMap(); public AiTrafficModeler(MeterRegistry meterRegistry) { this.meterRegistry meterRegistry; } /** * 流量模型输出——为容量规划提供关键数据。 */ public static class TrafficModel { // 日均请求量 private final long avgDailyRequests; // P95 峰值 QPS private final double peakQps; // 平均每次请求的 Token 消耗 private final int avgTokensPerRequest; // P99 Token 消耗 private final int p99TokensPerRequest; // 日间峰值时段小时 private final int peakHourStart; private final int peakHourEnd; // 周环比增长率 private final double weeklyGrowthRate; public TrafficModel(long avgDailyRequests, double peakQps, int avgTokensPerRequest, int p99TokensPerRequest, int peakHourStart, int peakHourEnd, double weeklyGrowthRate) { this.avgDailyRequests avgDailyRequests; this.peakQps peakQps; this.avgTokensPerRequest avgTokensPerRequest; this.p99TokensPerRequest p99TokensPerRequest; this.peakHourStart peakHourStart; this.peakHourEnd peakHourEnd; this.weeklyGrowthRate weeklyGrowthRate; } public long getAvgDailyRequests() { return avgDailyRequests; } public double getPeakQps() { return peakQps; } public int getAvgTokensPerRequest() { return avgTokensPerRequest; } public int getP99TokensPerRequest() { return p99TokensPerRequest; } public int getPeakHourStart() { return peakHourStart; } public int getPeakHourEnd() { return peakHourEnd; } public double getWeeklyGrowthRate() { return weeklyGrowthRate; } } /** * 根据历史数据构建未来一周的流量预测模型。 */ public TrafficModel buildWeeklyModel() { if (history.size() 7) { log.warn(历史数据不足, days{}, 使用默认模型, history.size()); return buildDefaultModel(); } ListDailyTrafficMetrics recentWeek new ArrayList(); LocalDate today LocalDate.now(clock); for (int i 7; i 1; i--) { DailyTrafficMetrics metrics history.get(today.minusDays(i)); if (metrics ! null) { recentWeek.add(metrics); } } if (recentWeek.isEmpty()) { return buildDefaultModel(); } long avgDaily (long) recentWeek.stream() .mapToLong(DailyTrafficMetrics::getTotalRequests) .average().orElse(10000); double peakQps recentWeek.stream() .mapToDouble(DailyTrafficMetrics::getPeakQps) .max().orElse(50); int avgTokens (int) recentWeek.stream() .mapToInt(DailyTrafficMetrics::getAvgTokensPerRequest) .average().orElse(500); int p99Tokens recentWeek.stream() .mapToInt(DailyTrafficMetrics::getP99TokensPerRequest) .max().orElse(5000); double growthRate calculateWeeklyGrowthRate(recentWeek); return new TrafficModel(avgDaily, peakQps, avgTokens, p99Tokens, 9, 21, growthRate); } private double calculateWeeklyGrowthRate( ListDailyTrafficMetrics recentWeek) { if (recentWeek.size() 2) { return 0.0; } // 使用线性回归计算周环比增长率 long firstDay recentWeek.get(0).getTotalRequests(); long lastDay recentWeek.get(recentWeek.size() - 1) .getTotalRequests(); if (firstDay 0) { return 0.0; } return (double) (lastDay - firstDay) / firstDay; } private TrafficModel buildDefaultModel() { return new TrafficModel(10000, 50, 500, 5000, 9, 21, 0.0); } Scheduled(cron 0 5 0 * * *) public void recordDailyMetrics() { // 在每天凌晨记录前一日的流量指标 DailyTrafficMetrics metrics new DailyTrafficMetrics(); metrics.setTotalRequests(getCounter(ai.request.total)); metrics.setPeakQps(getGauge(ai.request.qps.peak)); metrics.setAvgTokensPerRequest( getGauge(ai.request.tokens.avg)); metrics.setP99TokensPerRequest( getGauge(ai.request.tokens.p99)); history.put(LocalDate.now(clock).minusDays(1), metrics); log.info(日流量指标已记录, requests{}, peakQps{}, metrics.getTotalRequests(), metrics.getPeakQps()); // 为什么仅保留 30 天历史超过 30 天的流量数据对容量规划的参考价值下降 // 且业务迭代会改变流量特征过于古老的数据可能产生误导 while (history.size() 30) { history.pollFirstEntry(); } } private long getCounter(String name) { Counter counter meterRegistry.find(name).counter(); return counter ! null ? (long) counter.count() : 0; } private double getGauge(String name) { return meterRegistry.find(name).gauge() ! null ? meterRegistry.find(name).gauge().value() : 0; } }三、GPU 资源预估模型基于流量模型可以建立 GPU 资源的预估公式/** * GPU 资源预估器——将流量模型转化为具体的 GPU 数量和规格需求。 * * 为什么需要区分标准推理和大规模推理两种场景 * 标准推理如 7B 模型单卡可并行处理多个请求 * 大规模推理如 70B 模型可能需要多卡张量并行才能加载模型权重。 */ Component public class GpuResourceEstimator { // 单卡 A100 80GB 对 7B 模型的推理吞吐tokens/s // 为什么取 1500 而非理论峰值实测环境下的保守估计 // 考虑了 KV Cache 存储和显存碎片化等因素 private static final int TOKENS_PER_SECOND_PER_GPU_7B 1500; // 平均输出 Token 数与输入 Token 数的比例估算 private static final double OUTPUT_INPUT_RATIO 0.3; public GpuEstimationResult estimate( AiTrafficModeler.TrafficModel model, double targetLatencyMs) { // 计算所需的总吞吐量tokens/s double avgTokensPerRequest model.getAvgTokensPerRequest() * (1 OUTPUT_INPUT_RATIO); double requiredTokensPerSecond model.getPeakQps() * avgTokensPerRequest; // 计算延迟允许的并行请求数 // 为什么用 P50 延迟而非 P99 估算并行度 // P99 延迟对应的极端长请求应通过队列削峰处理 // 以 P50 为基准估算常规情况下的并发度 double parallelRequestsPerGpu TOKENS_PER_SECOND_PER_GPU_7B / avgTokensPerRequest; // 计算所需 GPU 数量峰值需求 int peakGpuCount (int) Math.ceil( model.getPeakQps() / parallelRequestsPerGpu); // 稳态 GPU 数量非峰值时段 int steadyGpuCount (int) Math.ceil( peakGpuCount * 0.6); // 考虑增长率后的未来 GPU 需求 int futureGpuCount (int) Math.ceil( peakGpuCount * (1 model.getWeeklyGrowthRate() * 4)); return new GpuEstimationResult( peakGpuCount, steadyGpuCount, futureGpuCount, requiredTokensPerSecond); } }四、成本优化策略基于容量规划结果可以制定分层的成本优化策略策略适用场景预计节省风险定时缩容夜间低峰0~6点15~25%凌晨突发流量覆盖不到Spot 实例非核心推理任务60~70%随时可能被回收模型量化精度要求不极端40~50%INT4 精度下降请求优先级分级多租户场景20~30%低优先级用户等待增加混合部署多模型共享 GPU30~40%显存碎片化graph TD A[流量模型] -- B{预测下周峰值QPS} B -- C[基线GPU数量] B -- D[增长因子] C -- E{成本优化} D -- C E --|常驻实例| F[On-Demand GPU] E --|弹性实例| G[Spot GPU 扩展] E --|定时任务| H[离线推理批量] F -- I{夜间缩容} I --|是| J[0~6点保留 30%] I --|否| K[保留 100%] G -- L[高峰期弹性扩容] H -- M[谷期批量处理] style A fill:#bbf,stroke:#333 style E fill:#ff9,stroke:#333 style F fill:#6f6,stroke:#333 style G fill:#f96,stroke:#333五、容量规划的持续迭代容量规划不是一次性工作需要建立持续优化的反馈循环周度复盘对比实际流量与预测模型的偏差调整预测参数。月度成本分析核算实际 GPU 成本评估优化策略的有效性。季度容量评审根据业务增长趋势和模型升级计划重新评估中长期容量需求。压测验证每个季度进行一次全链路压测验证容量规划的上限是否与实际一致。/** * 容量规划审计——定期检查预估容量与实际使用的偏差 * 当偏差超过阈值时触发告警。 */ Component public class CapacityAuditor { private static final Logger log LoggerFactory.getLogger( CapacityAuditor.class); private final MeterRegistry meterRegistry; // 为什么偏差阈值设 30% 而非 10% // AI 流量的波动性远大于传统 web 应用 // 10% 的阈值会产生大量无意义告警 private static final double DEVIATION_THRESHOLD 0.30; Scheduled(cron 0 0 10 * * MON) public void auditWeekly() { AiTrafficModeler.TrafficModel predicted buildPredictedModel(); AiTrafficModeler.TrafficModel actual buildActualModel(); double qpsDeviation Math.abs( actual.getPeakQps() - predicted.getPeakQps()) / predicted.getPeakQps(); if (qpsDeviation DEVIATION_THRESHOLD) { log.warn(容量预测偏差过大, predictedQps{}, actualQps{}, deviation{:.2%}, predicted.getPeakQps(), actual.getPeakQps(), qpsDeviation); } double tokenDeviation Math.abs( actual.getAvgTokensPerRequest() - predicted.getAvgTokensPerRequest()) / (double) predicted.getAvgTokensPerRequest(); if (tokenDeviation DEVIATION_THRESHOLD) { log.warn(Token消耗预测偏差, predictedAvg{}, actualAvg{}, deviation{:.2%}, predicted.getAvgTokensPerRequest(), actual.getAvgTokensPerRequest(), tokenDeviation); } } private AiTrafficModeler.TrafficModel buildPredictedModel() { // 读取上周生成的预测模型 return new AiTrafficModeler.TrafficModel( 10000, 50, 500, 5000, 9, 21, 0.05); } private AiTrafficModeler.TrafficModel buildActualModel() { // 读取上周实际流量数据 return new AiTrafficModeler.TrafficModel( 11000, 55, 520, 5500, 9, 21, 0.05); } }六、总结AI 应用的容量规划需要建立两个核心模型流量模型描述请求特征和时序规律和资源模型描述 GPU 利用率与 Token 吞吐的关系。在模型基础上通过 On-Demand Spot 实例的混合部署策略配合定时缩容和优先级调度可以显著降低 GPU 资源成本。建议从数据的可观测性入手——先建立完善的流量和资源消耗的 Metrics 采集积累至少一个月的运行数据后再进行首次容量规划建模。后续通过周度审计和季度压测持续优化模型的准确性。Token 分布的厚尾效应与容量冗余AI 应用的 Token 消耗分布呈现典型的厚尾效应——P50 请求仅消耗 300 TokenP95 为 2500 Token但 P99 可达到 12000 Token。如果按 P50 设计容量P99 的请求会因 KV Cache 不足而排队导致级联延迟。推荐以 P90P99 的加权平均值权重 0.7:0.3作为单请求资源消耗的规划基准。这个值约等于0.7 × P90_Tokens 0.3 × P99_Tokens在我们的场景中为0.7 × 1800 0.3 × 12000 4860Token/请求。此外同一模型的不同 Prompt 模板之间 Token 消耗差异巨大。例如客服场景中标准问答模板的 Prompt 约 400 Token但多轮追踪模板携带完整对话历史可达 3000 Token。容量规划时需要区分模板类型——使用prompt_template_type标签对各模板的 Token 分布分别建模而非使用全局平均值。Spot 实例回收的优雅处理Spot 实例可节省 60%~70% 的 GPU 成本但面临随时被回收的风险。Kubernetes 会在 Spot 实例回收前 2 分钟发送 SIGTERM模型推理服务需要在这 2 分钟内完成(1) 停止接受新请求Readiness Probe 切换为 FAIL(2) 优雅处理进行中的请求——对于短请求30s等待其完成对于长请求返回部分结果 重试标识由客户端重试到其他 Pod(3) 清理 KV Cache 显存。关键是在 Pod 的preStophook 中调用推理服务的优雅关闭 API而不是直接 kill 进程。我们已经将实现集成到 K8s 的terminationGracePeriodSeconds120preStop.exec.command中Spot 实例回收时的请求错误率从 8% 降至 0.5%。