AI应用的全链路压测:找出系统在极限压力下的真实瓶颈
AI应用的全链路压测:找出系统在极限压力下的真实瓶颈
date: 2026-10-12 tags: [全链路压测, 性能测试, Spring AI, Java, Gatling]
一、真实故事:双十一前的噩梦,损失300万
2024年11月11日,凌晨0点整。
刘刚盯着大屏幕上的监控告警,手机屏幕亮起又暗下,不停地振动。
他是某中型电商平台的后端负责人,他们的AI商品推荐服务已经平稳运行了8个月。大促前,他们专门做了接口压测——单接口QPS可以支撑2000,是平时峰值的5倍。他觉得稳了。
0:00:38,第一条告警:AI推荐服务响应超时。
0:01:15,AI推荐调用量暴增1800%,错误率飙升到67%。
0:02:03,推荐服务完全宕机。商品详情页的"猜你喜欢"全部变成空白。
0:03:30,大量用户因为没有个性化推荐,商品发现路径断掉,开始大量跳出。
到早上8点,故障持续了7小时,保守估算:直接损失300万,间接损失(用户流失)估计还要翻倍。
故障复盘时,真相大白:
他们只做了单接口压测,没有做全链路压测。
问题根本不在AI推荐接口本身。而是:
- 大促期间,用户行为日志的写入量暴增,占满了数据库连接池
- 用户画像服务依赖的Redis集群,在高并发下出现热点Key问题
- AI推荐依赖用户画像,画像服务卡住,推荐服务连锁超时
- 全局超时时间设置过短(3秒),大量请求在完成前被强制中断,产生雪崩
单接口压测无法发现这些链路间的相互影响。
这就是全链路压测存在的意义。
二、全链路压测与单接口压测的本质区别
2.1 核心差异
| 维度 | 单接口压测 | 全链路压测 |
|---|---|---|
| 流量模型 | 人工构造 | 真实用户行为比例 |
| 服务依赖 | 不考虑 | 完整考虑 |
| 数据状态 | 固定测试数据 | 接近真实状态 |
| 发现问题 | 接口本身的容量上限 | 系统整体的真实瓶颈 |
| 复杂度 | 低 | 高 |
| 执行成本 | 低 | 高 |
| 准确性 | 低(乐观估计) | 高(接近真实) |
核心结论:单接口压测告诉你"某个环节能跑多快",全链路压测告诉你"用户真实使用时系统能支撑多少人"。两者缺一不可,但全链路压测是大促前必须要做的。
三、压测方案设计:业务场景建模
3.1 用户行为漏斗分析
在设计压测脚本之前,先分析真实用户的访问路径分布。
// 从生产日志中分析用户行为分布
@Service
public class UserBehaviorAnalyzer {
public BehaviorProfile analyzeProdTraffic(LocalDate startDate, LocalDate endDate) {
// 从日志平台获取各接口调用量
Map<String, Long> apiCallCounts = logAnalyticsService.getApiCallCounts(
startDate, endDate
);
long totalSessions = sessionMetrics.getTotalSessions(startDate, endDate);
// 计算各步骤的转化率
BehaviorProfile profile = new BehaviorProfile();
profile.setTotalSessions(totalSessions);
// 典型电商+AI推荐场景的流量比例
profile.addStep("home_page", 1.00); // 100%用户到首页
profile.addStep("search", 0.45); // 45%用户搜索
profile.addStep("category_browse", 0.55); // 55%浏览分类
profile.addStep("product_detail", 0.68); // 68%查看商品详情
profile.addStep("ai_recommend", 0.60); // 60%触发AI推荐
profile.addStep("add_to_cart", 0.25); // 25%加购
profile.addStep("checkout", 0.15); // 15%下单
profile.addStep("order_ai_review", 0.15); // 100%订单经过AI审核
profile.addStep("payment", 0.12); // 12%支付
return profile;
}
}3.2 大促流量预测模型
@Service
public class LoadPredictionService {
public PressureTestPlan buildPromotionTestPlan(PeakConfig config) {
// 大促流量通常是平时峰值的N倍
double dailyPeakQPS = metricsService.getDailyPeakQPS(); // 平时峰值
double promotionMultiplier = config.getExpectedMultiplier(); // 预期倍数(如5倍)
double targetQPS = dailyPeakQPS * promotionMultiplier;
// 大促流量形态:有明显的尖刺
List<LoadPhase> phases = List.of(
LoadPhase.rampUp("预热阶段", Duration.ofMinutes(5),
targetQPS * 0.1, targetQPS * 0.3),
LoadPhase.steadyState("稳定压测", Duration.ofMinutes(10),
targetQPS * 0.3),
LoadPhase.spike("模拟0点尖刺", Duration.ofMinutes(2),
targetQPS * 0.3, targetQPS), // 从30%瞬间到100%
LoadPhase.steadyState("高压维持", Duration.ofMinutes(15),
targetQPS),
LoadPhase.coolDown("降压", Duration.ofMinutes(5),
targetQPS, targetQPS * 0.1)
);
return PressureTestPlan.builder()
.targetQPS(targetQPS)
.phases(phases)
.successCriteria(SuccessCriteria.builder()
.maxErrorRate(0.01) // 错误率<1%
.maxP99LatencyMs(2000) // P99延迟<2秒
.maxP999LatencyMs(5000) // P999延迟<5秒
.minThroughput(targetQPS * 0.95) // 吞吐量不低于目标的95%
.build())
.build();
}
}四、Gatling全链路压测脚本
4.1 Gatling脚本架构
// build.sbt
libraryDependencies ++= Seq(
"io.gatling.highcharts" % "gatling-charts-highcharts" % "3.10.3" % "test",
"io.gatling" % "gatling-test-framework" % "3.10.3" % "test"
)// ECommerceFullChainSimulation.scala
package simulations
import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._
import scala.util.Random
class ECommerceFullChainSimulation extends Simulation {
// 基础配置
val httpProtocol = http
.baseUrl(System.getProperty("baseUrl", "https://test-api.example.com"))
.acceptHeader("application/json")
.contentTypeHeader("application/json")
.header("X-Test-Flag", "pressure-test") // 标记为压测流量(影子流量)
// 测试数据(从CSV加载,与生产数据类似但不是生产数据)
val userFeeder = csv("test_users.csv").random
val productFeeder = csv("test_products.csv").random
val searchKeywordFeeder = csv("search_keywords.csv").random
// ============ 场景1:普通浏览+AI推荐(主流程,60%用户)===========
val browseWithRecommendScenario = scenario("浏览+AI推荐")
.feed(userFeeder)
.exec(homePageRequest) // 首页
.pause(1, 3) // 停留1-3秒
.exec(categoryBrowseRequest) // 分类浏览
.pause(2, 5)
.exec(productDetailRequest) // 商品详情
.exec(aiRecommendRequest) // AI推荐(关键链路!)
.pause(3, 8)
.doIfOrElse(session => Random.nextDouble() < 0.3) { // 30%加购
exec(addToCartRequest)
.doIfOrElse(session => Random.nextDouble() < 0.5) { // 50%下单
exec(checkoutRequest)
.exec(orderAIReviewRequest) // AI订单审核
.exec(paymentRequest)
} {
exec(abandonCartRequest)
}
} {
exec(leaveDetailRequest)
}
// ============ 场景2:搜索+AI推荐(25%用户)=====================
val searchWithRecommendScenario = scenario("搜索+AI推荐")
.feed(userFeeder)
.feed(searchKeywordFeeder)
.exec(homePageRequest)
.pause(1, 2)
.exec(searchRequest) // 搜索(触发AI搜索增强)
.pause(2, 4)
.repeat(3) { // 平均看3个搜索结果
exec(productDetailRequest)
.exec(aiRecommendRequest)
.pause(1, 3)
}
// ============ 场景3:纯浏览不购买(15%用户)====================
val browsOnlyScenario = scenario("纯浏览")
.feed(userFeeder)
.exec(homePageRequest)
.pause(2, 5)
.repeat(5) {
exec(categoryBrowseRequest)
.pause(1, 3)
}
// 注入策略(模拟大促流量形态)
setUp(
browseWithRecommendScenario.inject(
rampUsersPerSec(10).to(500).during(5.minutes), // 预热
constantUsersPerSec(500).during(10.minutes), // 稳压
rampUsersPerSec(500).to(1500).during(2.minutes), // 尖刺
constantUsersPerSec(1500).during(15.minutes), // 高压
rampUsersPerSec(1500).to(50).during(5.minutes) // 降压
).protocols(httpProtocol),
searchWithRecommendScenario.inject(
rampUsersPerSec(4).to(200).during(5.minutes),
constantUsersPerSec(200).during(25.minutes),
rampUsersPerSec(200).to(20).during(5.minutes)
).protocols(httpProtocol),
browsOnlyScenario.inject(
rampUsersPerSec(2).to(100).during(5.minutes),
constantUsersPerSec(100).during(25.minutes),
rampUsersPerSec(100).to(10).during(5.minutes)
).protocols(httpProtocol)
).assertions(
global.responseTime.percentile(99).lt(2000), // P99 < 2秒
global.successfulRequests.percent.gte(99), // 成功率 > 99%
forAll.responseTime.max.lt(10000) // 最大响应时间 < 10秒
)
// ============ HTTP请求定义 ==================================
val homePageRequest = http("首页")
.get("/api/home")
.queryParam("userId", "#{userId}")
.check(status.is(200))
val aiRecommendRequest = http("AI推荐")
.get("/api/recommend")
.queryParam("userId", "#{userId}")
.queryParam("productId", "#{productId}")
.queryParam("scene", "product_detail")
.check(status.is(200))
.check(jsonPath("$.recommendations").exists)
.check(jsonPath("$.recommendations[*]").count.gte(3)) // 至少3条推荐
val searchRequest = http("搜索")
.post("/api/search")
.body(StringBody("""{"keyword": "#{keyword}", "userId": "#{userId}"}"""))
.check(status.is(200))
.check(jsonPath("$.results").exists)
val orderAIReviewRequest = http("AI订单审核")
.post("/api/order/review")
.body(StringBody("""{"orderId": "#{orderId}", "userId": "#{userId}"}"""))
.check(status.is(200))
.check(jsonPath("$.reviewResult").exists)
// AI审核可能需要更长时间,单独设置超时
.requestTimeout(5.seconds)
}4.2 AI调用的异步压测处理
// AI异步接口的压测(提交任务+轮询结果)
val asyncAIRecommendFlow = exec(
http("提交AI推荐任务")
.post("/api/recommend/async")
.body(StringBody("""{"userId": "#{userId}", "context": "#{context}"}"""))
.check(status.is(202))
.check(jsonPath("$.taskId").saveAs("taskId"))
).pause(500.milliseconds) // 等500ms再轮询
.asLongAsDuring(
session => session("taskStatus").asOption[String].getOrElse("PENDING") != "DONE",
10.seconds // 最多等10秒
) {
exec(
http("查询AI推荐结果")
.get("/api/recommend/result/#{taskId}")
.check(status.in(200, 202))
.check(jsonPath("$.status").saveAs("taskStatus"))
.check(
jsonPath("$.result").saveAs("recommendResult").optional
)
).pause(300.milliseconds) // 每300ms轮询一次
}五、影子环境:不影响生产数据的压测方案
5.1 影子环境架构
关键设计决策:影子环境的LLM用Mock而非真实LLM
原因:
- 真实LLM费用高(压测几万次调用,成本不可控)
- 压测目的是找出基础设施瓶颈,而非LLM本身
- LLM的延迟固定为P50/P95值,使结果更稳定
// 影子LLM:模拟真实LLM的延迟特征
@Service
@Profile("shadow")
public class MockLLMClient implements ChatClient {
private static final Random random = new Random();
// 基于生产统计的延迟分布(按百分比区间模拟)
private static final int[] LATENCY_DISTRIBUTION = {
// 百分位 -> 延迟(ms)
// P50 = 800ms, P90 = 1500ms, P99 = 3000ms
800, 800, 800, 800, 800, // 0-49%: ~800ms
1000, 1100, 1200, 1300, 1500, // 50-89%: 1000-1500ms
2000, 2500, 3000, 4000, 5000 // 90-99%: 2000-5000ms
};
@Override
public String call(String prompt) {
// 模拟LLM延迟
int percentile = random.nextInt(100);
int latencyIndex = percentile / 7; // 映射到数组
int baseLatency = LATENCY_DISTRIBUTION[Math.min(latencyIndex, LATENCY_DISTRIBUTION.length - 1)];
int jitter = random.nextInt(100); // 抖动
try {
Thread.sleep(baseLatency + jitter);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// 返回固定格式的Mock响应(符合接口契约)
return generateMockResponse(prompt);
}
private String generateMockResponse(String prompt) {
// 根据prompt类型返回合适的Mock数据
if (prompt.contains("推荐") || prompt.contains("recommend")) {
return """
{
"recommendations": [
{"productId": "P001", "score": 0.95},
{"productId": "P002", "score": 0.87},
{"productId": "P003", "score": 0.82}
],
"reasoning": "基于用户偏好"
}
""";
}
if (prompt.contains("审核") || prompt.contains("review")) {
return """
{
"reviewResult": "APPROVED",
"riskScore": 0.1,
"flags": []
}
""";
}
return """{"result": "mock_response", "confidence": 0.9}""";
}
}5.2 影子数据管理
@Service
@Slf4j
public class ShadowDataSyncService {
// 每天凌晨从生产同步脱敏数据到影子环境
@Scheduled(cron = "0 0 1 * * ?")
public void syncShadowData() {
log.info("开始同步影子数据...");
// 1. 用户数据脱敏同步
syncUsers();
// 2. 商品数据同步(无需脱敏)
syncProducts();
// 3. 订单数据脱敏同步(只取最近30天)
syncOrders();
// 4. 用户画像向量同步
syncUserProfiles();
log.info("影子数据同步完成");
}
private void syncUsers() {
// 分批处理,避免大事务
int batchSize = 1000;
long totalUsers = prodUserRepo.count();
for (long offset = 0; offset < totalUsers; offset += batchSize) {
List<User> prodUsers = prodUserRepo.findAll(
PageRequest.of((int)(offset / batchSize), batchSize)
).getContent();
List<User> shadowUsers = prodUsers.stream()
.map(this::desensitizeUser)
.toList();
shadowUserRepo.saveAll(shadowUsers);
}
}
private User desensitizeUser(User prodUser) {
User shadow = new User();
shadow.setId(prodUser.getId());
shadow.setUsername("test_user_" + prodUser.getId()); // 脱敏用户名
shadow.setPhone(maskPhone(prodUser.getPhone())); // 手机号脱敏
shadow.setEmail(maskEmail(prodUser.getEmail())); // 邮箱脱敏
shadow.setAge(prodUser.getAge()); // 年龄保留(统计特征)
shadow.setCity(prodUser.getCity()); // 城市保留(地域特征)
shadow.setRegisteredAt(prodUser.getRegisteredAt());
return shadow;
}
private String maskPhone(String phone) {
if (phone == null || phone.length() < 11) return "13800138000";
return phone.substring(0, 3) + "****" + phone.substring(7);
}
}六、流量录制:从生产流量生成压测脚本
6.1 Nginx流量录制
# nginx.conf 开启流量录制
http {
# 将生产流量镜像到录制服务
location /api/ {
mirror /record;
proxy_pass http://backend_servers;
}
location = /record {
internal;
# 只在开启录制模式时镜像
set_by_lua_block $should_record {
local flag = ngx.shared.config:get("traffic_record_enabled")
return flag == "1" and "1" or "0"
}
if ($should_record = "0") {
return 200;
}
proxy_pass http://traffic-recorder:8080;
proxy_pass_request_body on;
}
}// 流量录制服务
@RestController
@Slf4j
public class TrafficRecorderController {
private final TrafficRecordRepository recordRepo;
@PostMapping("/**")
public void recordTraffic(
HttpServletRequest request,
@RequestBody(required = false) String body) {
try {
TrafficRecord record = TrafficRecord.builder()
.method(request.getMethod())
.uri(request.getRequestURI())
.queryString(request.getQueryString())
.headers(extractHeaders(request))
.body(desensitizeBody(body)) // 脱敏请求体
.userId(extractUserId(request))
.recordedAt(Instant.now())
.build();
recordRepo.save(record);
} catch (Exception e) {
log.error("流量录制失败", e);
// 录制失败不影响正常流程
}
}
// 从录制的流量生成Gatling脚本
@PostMapping("/generate-script")
public String generateGatlingScript(
@RequestParam LocalDateTime startTime,
@RequestParam LocalDateTime endTime,
@RequestParam double samplingRate) {
List<TrafficRecord> records = recordRepo.findByRecordedAtBetween(startTime, endTime);
// 采样
List<TrafficRecord> sampled = sampleRecords(records, samplingRate);
// 生成Gatling脚本
return gatlingScriptGenerator.generate(sampled);
}
}七、压测观测体系:全链路指标采集
7.1 三层监控体系
7.2 Spring Boot Actuator + Micrometer配置
// 自定义压测关键指标
@Configuration
public class PressureTestMetricsConfig {
@Bean
public MeterBinder aiCallMetrics(ChatClient chatClient) {
return registry -> {
// AI调用队列深度(反映积压情况)
Gauge.builder("ai.call.queue.depth",
aiCallQueue, Queue::size)
.description("AI调用队列积压深度")
.register(registry);
// LLM响应时间分布
Timer.builder("ai.llm.response.time")
.description("LLM API响应时间")
.publishPercentiles(0.5, 0.9, 0.95, 0.99, 0.999)
.publishPercentileHistogram()
.register(registry);
// 降级触发次数
Counter.builder("ai.fallback.triggered")
.description("AI降级触发次数")
.register(registry);
};
}
}// 压测期间的实时监控面板
@RestController
@RequestMapping("/api/pressure-test/monitor")
public class PressureTestMonitorController {
private final MeterRegistry meterRegistry;
// 实时指标(供Grafana拉取)
@GetMapping("/realtime-metrics")
public Map<String, Object> getRealtimeMetrics() {
Map<String, Object> metrics = new LinkedHashMap<>();
// AI相关指标
metrics.put("ai_recommend_success_rate",
getSuccessRate("http.server.requests", "ai-recommend"));
metrics.put("ai_recommend_p99_latency_ms",
getPercentile("ai.llm.response.time", 0.99));
metrics.put("ai_call_queue_depth",
getGaugeValue("ai.call.queue.depth"));
metrics.put("ai_fallback_rate",
getFallbackRate());
// 基础设施指标
metrics.put("jvm_heap_used_bytes",
getGaugeValue("jvm.memory.used", "heap"));
metrics.put("db_connection_pool_active",
getGaugeValue("hikaricp.connections.active"));
metrics.put("redis_memory_used_bytes",
getGaugeValue("redis.memory.used"));
metrics.put("timestamp", Instant.now().toEpochMilli());
return metrics;
}
// 压测结论摘要(给非技术管理层看的)
@GetMapping("/summary")
public PressureTestSummary getSummary() {
double currentQPS = getThroughput();
double errorRate = getErrorRate();
double p99Latency = getPercentile("http.server.requests", 0.99);
double cpuUsage = getCPUUsage();
double memoryUsage = getMemoryUsage();
// 综合评分
boolean passQPS = currentQPS >= targetQPS * 0.95;
boolean passErrorRate = errorRate < 0.01;
boolean passLatency = p99Latency < 2000;
boolean passResources = cpuUsage < 80 && memoryUsage < 85;
boolean overallPass = passQPS && passErrorRate && passLatency && passResources;
return PressureTestSummary.builder()
.overallResult(overallPass ? "通过" : "不通过")
.currentQPS(currentQPS)
.targetQPS(targetQPS)
.errorRate(errorRate)
.p99LatencyMs(p99Latency)
.cpuUsagePercent(cpuUsage)
.memoryUsagePercent(memoryUsage)
.bottleneck(findBottleneck())
.recommendation(generateRecommendation(passQPS, passErrorRate, passLatency, passResources))
.build();
}
private String findBottleneck() {
// 找出最可能的瓶颈
double cpuUsage = getCPUUsage();
double memoryUsage = getMemoryUsage();
double dbPoolUsage = getGaugeValue("hikaricp.connections.active") /
getGaugeValue("hikaricp.connections.max");
double aiQueueDepth = getGaugeValue("ai.call.queue.depth");
if (aiQueueDepth > 100) return "AI调用队列积压,LLM处理能力不足";
if (dbPoolUsage > 0.9) return "数据库连接池耗尽,需扩容或优化SQL";
if (cpuUsage > 80) return "CPU成为瓶颈,考虑水平扩容";
if (memoryUsage > 85) return "内存压力过高,检查内存泄漏或调大Heap";
return "暂未发现明显瓶颈";
}
}八、瓶颈定位:找出AI应用中的最慢环节
8.1 链路追踪集成
// 全链路追踪配置
@Configuration
public class TracingConfig {
@Bean
public Tracer tracer() {
return Tracing.newBuilder()
.localServiceName("ai-recommend-service")
.spanReporter(AsyncReporter.create(
OkHttpSender.create("http://zipkin:9411/api/v2/spans")
))
.build()
.tracer();
}
}// AI服务关键路径埋点
@Service
@Slf4j
public class InstrumentedAIService {
private final Tracer tracer;
public RecommendResult recommend(String userId, String productId) {
Span span = tracer.nextSpan()
.name("ai-recommend-full-chain")
.tag("userId", userId)
.start();
try (Tracer.SpanInScope ws = tracer.withSpan(span)) {
// 步骤1:获取用户画像
Span profileSpan = tracer.nextSpan().name("get-user-profile").start();
UserProfile profile;
try (Tracer.SpanInScope ignored = tracer.withSpan(profileSpan)) {
profile = userProfileService.getProfile(userId);
} finally {
profileSpan.finish();
}
// 步骤2:向量检索候选商品
Span vectorSpan = tracer.nextSpan().name("vector-search").start();
List<String> candidates;
try (Tracer.SpanInScope ignored = tracer.withSpan(vectorSpan)) {
candidates = vectorSearchService.search(profile.getVector(), 50);
} finally {
vectorSpan.finish();
}
// 步骤3:LLM精排
Span llmSpan = tracer.nextSpan().name("llm-ranking")
.tag("candidateCount", String.valueOf(candidates.size()))
.start();
List<RankedProduct> ranked;
try (Tracer.SpanInScope ignored = tracer.withSpan(llmSpan)) {
ranked = llmRankingService.rank(profile, candidates, productId);
} finally {
llmSpan.finish();
}
span.tag("resultCount", String.valueOf(ranked.size()));
return new RecommendResult(ranked);
} finally {
span.finish();
}
}
}8.2 瓶颈分析工具
// 压测期间自动分析瓶颈
@Service
@Slf4j
public class BottleneckAnalyzer {
// 分析Zipkin中的Trace数据,找出耗时最长的环节
public BottleneckReport analyze(String serviceId, Duration window) {
List<Span> slowSpans = zipkinApiClient.querySlowSpans(
serviceId, window,
Duration.ofMillis(1000) // P90以上的Span
);
// 统计各Span的耗时分布
Map<String, SpanStats> spanStatsByName = slowSpans.stream()
.collect(Collectors.groupingBy(
Span::getName,
Collectors.collectingAndThen(
Collectors.toList(),
spans -> calculateStats(spans)
)
));
// 找出最慢的环节
List<BottleneckItem> bottlenecks = spanStatsByName.entrySet().stream()
.sorted(Comparator.comparingLong(
e -> -e.getValue().getP99DurationMs()
))
.limit(5)
.map(e -> BottleneckItem.builder()
.spanName(e.getKey())
.avgDurationMs(e.getValue().getAvgDurationMs())
.p99DurationMs(e.getValue().getP99DurationMs())
.callCount(e.getValue().getCallCount())
.percentOfTotalTime(
(double) e.getValue().getAvgDurationMs() / getTotalAvgMs() * 100
)
.build())
.toList();
return BottleneckReport.builder()
.serviceId(serviceId)
.analysisPeriod(window)
.bottlenecks(bottlenecks)
.topBottleneck(bottlenecks.isEmpty() ? null : bottlenecks.get(0))
.recommendation(generateRecommendation(bottlenecks))
.build();
}
private String generateRecommendation(List<BottleneckItem> bottlenecks) {
if (bottlenecks.isEmpty()) return "未发现明显瓶颈";
BottleneckItem top = bottlenecks.get(0);
if (top.getSpanName().contains("llm")) {
return String.format(
"LLM调用是主要瓶颈(P99=%dms,占总时间%.0f%%)。" +
"建议:1)减少LLM调用次数;2)增加LLM结果缓存;3)并行调用替代串行。",
top.getP99DurationMs(), top.getPercentOfTotalTime()
);
}
if (top.getSpanName().contains("vector") || top.getSpanName().contains("search")) {
return String.format(
"向量检索是主要瓶颈(P99=%dms)。" +
"建议:1)检查向量索引类型(HNSW vs IVF);2)增加向量数据库内存;3)减少检索TopK数量。",
top.getP99DurationMs()
);
}
if (top.getSpanName().contains("db") || top.getSpanName().contains("sql")) {
return String.format(
"数据库查询是主要瓶颈(P99=%dms)。" +
"建议:1)检查慢SQL;2)添加索引;3)增加查询缓存;4)考虑读写分离。",
top.getP99DurationMs()
);
}
return "建议详细查看Trace详情,分析 " + top.getSpanName() + " 的具体耗时来源";
}
}刘刚团队压测发现的瓶颈(修复后):
| 瓶颈环节 | 发现前P99 | 修复方案 | 修复后P99 |
|---|---|---|---|
| 用户画像Redis热点Key | 3200ms | 分片+本地缓存 | 45ms |
| LLM调用串行 | 2800ms | 并行调用+缓存 | 900ms |
| 数据库连接池不足 | 1500ms | 连接池扩到50 | 120ms |
| 行为日志写入阻塞 | 800ms | 改为异步写入 | 10ms |
九、限流验证:确认限流在压力下正常工作
9.1 限流策略配置
// AI接口的分层限流
@Configuration
public class RateLimitConfig {
@Bean
public RateLimiterRegistry rateLimiterRegistry() {
RateLimiterConfig aiCallConfig = RateLimiterConfig.custom()
.limitForPeriod(1000) // 每秒最多1000次
.limitRefreshPeriod(Duration.ofSeconds(1))
.timeoutDuration(Duration.ofMillis(200)) // 等待获取许可的最长时间
.build();
RateLimiterConfig userLevelConfig = RateLimiterConfig.custom()
.limitForPeriod(10) // 每用户每秒最多10次
.limitRefreshPeriod(Duration.ofSeconds(1))
.timeoutDuration(Duration.ofMillis(100))
.build();
return RateLimiterRegistry.of(Map.of(
"ai-recommend", aiCallConfig,
"user-level", userLevelConfig
));
}
}// 限流拦截器
@Component
@Slf4j
public class AIRateLimitInterceptor implements HandlerInterceptor {
private final RateLimiterRegistry registry;
private final RedisTemplate<String, Long> redisTemplate;
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
String userId = extractUserId(request);
// 1. 全局限流
RateLimiter globalLimiter = registry.rateLimiter("ai-recommend");
if (!globalLimiter.acquirePermission()) {
log.warn("全局限流触发");
response.setStatus(429);
response.getWriter().write("""
{"error": "服务繁忙,请稍后重试", "code": "RATE_LIMITED"}
""");
return false;
}
// 2. 用户级限流(防止单用户刷接口)
String userKey = "rate_limit:user:" + userId;
Long count = redisTemplate.opsForValue().increment(userKey);
if (count == 1) {
redisTemplate.expire(userKey, Duration.ofSeconds(1));
}
if (count > 10) { // 每用户每秒10次上限
log.warn("用户级限流触发: userId={}", userId);
response.setStatus(429);
response.getWriter().write("""
{"error": "请求过于频繁", "code": "USER_RATE_LIMITED"}
""");
return false;
}
return true;
}
}9.2 限流压测验证脚本
// 专门验证限流的Gatling场景
val rateLimitValidationScenario = scenario("限流验证")
.exec(session => session.set("startTime", System.currentTimeMillis()))
.during(30.seconds) {
exec(
http("高频AI推荐请求")
.get("/api/recommend")
.queryParam("userId", "test-user-001")
.check(
// 同时接受200和429
status.in(200, 429)
)
).pause(10.milliseconds) // 10ms间隔,QPS约为100/用户
}
.exec(session => {
// 验证限流是否生效:应该有一定比例的429
println(s"压测完成,请查看Grafana中429:200的比例")
session
})
setUp(
// 50个用户同时以高频请求,验证限流
rateLimitValidationScenario.inject(
atOnceUsers(50)
)
).assertions(
// 限流生效:应该有400以上的响应被限流(429)
global.responseTime.percentile(99).lt(500)
// 注:此处不检查成功率,因为预期有429
)十、压测报告:向管理层展示压测结论
10.1 自动生成管理层报告
@Service
public class PressureTestReportGenerator {
public PressureTestReport generateExecutiveSummary(
String testRunId,
PressureTestPlan plan,
List<MetricSnapshot> metrics) {
// 1. 总体结论
boolean passed = evaluateOverall(metrics, plan.getSuccessCriteria());
// 2. 关键指标对比(目标 vs 实际)
List<MetricComparison> keyMetrics = List.of(
MetricComparison.of("最大并发用户数",
plan.getTargetConcurrency(),
getMaxConcurrency(metrics)),
MetricComparison.of("AI推荐成功率",
"≥99%",
String.format("%.2f%%", getSuccessRate(metrics, "ai-recommend") * 100)),
MetricComparison.of("P99响应时间",
"≤2000ms",
getP99(metrics) + "ms"),
MetricComparison.of("系统吞吐量(QPS)",
String.valueOf(plan.getTargetQPS()),
String.valueOf(getActualQPS(metrics))),
MetricComparison.of("最大CPU使用率",
"≤80%",
getMaxCPU(metrics) + "%"),
MetricComparison.of("AI降级触发次数",
"0",
String.valueOf(getFallbackCount(metrics)))
);
// 3. 发现的问题
List<FindingItem> findings = analyzeFindings(metrics);
// 4. 风险评估
RiskLevel riskLevel = assessRisk(passed, findings);
// 5. 建议
List<String> recommendations = generateRecommendations(findings);
return PressureTestReport.builder()
.testRunId(testRunId)
.testDate(LocalDate.now())
.overallResult(passed ? "通过" : "不通过")
.riskLevel(riskLevel)
.keyMetrics(keyMetrics)
.findings(findings)
.recommendations(recommendations)
.nextSteps(buildNextSteps(passed, riskLevel))
.conclusion(buildConclusion(passed, riskLevel, findings))
.build();
}
private String buildConclusion(boolean passed, RiskLevel risk, List<FindingItem> findings) {
if (passed && risk == RiskLevel.LOW) {
return "系统已完成全链路压测,在模拟大促峰值流量下表现稳定," +
"各项指标均满足预设标准。建议按计划执行大促。";
}
if (!passed) {
long criticalCount = findings.stream()
.filter(f -> f.getSeverity() == Severity.CRITICAL)
.count();
return String.format("压测发现%d个严重问题需要修复后重新压测," +
"当前状态不建议进行大促活动。", criticalCount);
}
return "系统整体通过压测,但存在少量优化建议,可以在大促后跟进处理。";
}
}10.2 报告模板输出
// 生成Markdown格式的报告(可转换为Word/PDF)
public String renderMarkdownReport(PressureTestReport report) {
return """
# 全链路压测报告
**测试时间**:%s
**测试环境**:影子生产环境
**压测工具**:Gatling 3.10
## 一、总体结论
| 项目 | 结果 |
|------|------|
| 压测结论 | **%s** |
| 风险等级 | %s |
| 建议操作 | %s |
## 二、关键指标
| 指标 | 目标 | 实际 | 是否达标 |
|------|------|------|---------|
%s
## 三、发现的问题
%s
## 四、优化建议
%s
## 五、下一步计划
%s
---
*本报告由压测系统自动生成,数据来源:Grafana + Zipkin + Gatling Report*
""".formatted(
report.getTestDate(),
report.getOverallResult(),
report.getRiskLevel().getDisplayName(),
report.getNextSteps().get(0),
renderMetricsTable(report.getKeyMetrics()),
renderFindings(report.getFindings()),
renderRecommendations(report.getRecommendations()),
renderNextSteps(report.getNextSteps())
);
}十一、刘刚团队的最终成果(压测+修复后)
再次大促(2025年双十二)的结果:
十二、FAQ
Q:全链路压测一定要在生产环境做吗?
A:不建议。标准做法是影子环境:与生产环境同配置、同规模、脱敏数据、Mock外部依赖(如LLM)。只有影子环境无法复现的问题,才考虑生产只读流量录制回放。
Q:Gatling和JMeter怎么选?
A:AI应用优先选Gatling。理由:Gatling的异步模式更适合模拟AI应用的长耗时请求(异步任务模式),生成的HTML报告质量更高,Scala DSL表达力更强。JMeter的UI适合非技术人员,但对异步接口支持较弱。
Q:压测时Mock了LLM,结果有代表性吗?
A:有代表性,但有局限性。Mock LLM的目的是测试基础设施瓶颈(数据库、缓存、网络、JVM)。LLM本身的性能需要单独测试(调用LLM API的QPS上限、Token吞吐量),两者结合才完整。
Q:压测发现数据库连接池耗尽,应该直接扩连接池吗?
A:不要直接扩。连接池耗尽通常是症状而非原因。先查:是SQL太慢导致连接被长时间占用?还是连接泄漏?还是真的并发量超过了DB处理能力?弄清原因再决定方案(加索引/缓存/扩容/分库分表)。
Q:压测脚本的维护成本太高怎么办?
A:用流量录制替代手写脚本。每次大促前2周,开启1-2小时的流量录制,自动生成Gatling脚本。维护成本接近零,且脚本始终反映最新的用户行为。
Q:AI应用限流应该限在哪一层?
A:多层限流:1)网关层:全局QPS控制(Nginx/Kong);2)应用层:用户级别限流(Sentinel/Resilience4j);3)AI调用层:LLM API的速率限制。三层防护,任意一层触发都要有降级逻辑,不能直接报500。
总结
全链路压测是AI应用大促前的必做功课:
- 场景建模:基于真实用户行为漏斗,而非想象的流量
- Gatling脚本:模拟真实用户行为序列,含AI调用和异步等待
- 影子环境:Mock LLM降成本,脱敏数据保合规
- 三层监控:业务+应用+基础设施全覆盖
- 链路追踪:Zipkin找出最慢的环节
- 限流验证:确认限流配置在真实压力下生效
- 管理层报告:结论清晰,数据说话
刘刚团队用一次血淋淋的教训换来的经验:每一次大促前做全链路压测,比出了问题后救火便宜100倍。
