微服务性能测试策略——服务级压测 vs 链路压测的选型与实践
微服务性能测试策略——服务级压测 vs 链路压测的选型与实践
适读人群:微服务架构开发工程师、测试架构师 | 阅读时长:约14分钟 | 核心价值:理解微服务环境下的压测策略选型,掌握服务级压测和全链路压测的场景、方法和结论对比
一次错误的压测策略差点造成双十一事故
2020年某电商的双十一备战。团队有20多个微服务,我们决定逐个压测每个服务,得到了每个服务的TPS上限:
用户服务:3200 TPS
商品服务:4800 TPS
库存服务:2100 TPS ← 最低
订单服务:2800 TPS
支付服务:1800 TPS ← 第二低我们的结论是:系统整体可以支撑1800 TPS(取最低值)。
但这个结论是错的。
双十一当天,当真实流量到800 TPS时,系统就开始报错了。
原因出在三个地方:
第一,我们压测每个服务时用的是Mock数据,Mock了所有下游依赖。但真实下单时,库存服务不是独立跑的,它同时要处理:订单服务的调用、商品服务的调用、定时任务的调用。三路同时进来,实际承载能力只有800 TPS。
第二,微服务之间的网络调用有额外延迟。订单服务调用库存服务,正常情况下网络延迟是5ms,但在高并发时服务网格的sidecar有额外的排队延迟,实际延迟变成了40ms,整条链路的P99大幅增加。
第三,超时配置问题在单服务压测时被掩盖了。订单服务调用库存服务的超时设置是200ms,在高并发时库存服务P99本身就到了180ms,加上网络延迟,超时频繁触发,导致订单失败率飙升。
这次经历让我得出结论:微服务的性能测试,服务级压测是必要条件,但绝对不是充分条件。
服务级压测 vs 全链路压测
服务级压测(Service-level Test)
定义:对单个微服务进行独立压测,通常Mock掉所有外部依赖。
适用场景:
- 服务初期开发完成后的功能验证
- 某个服务内部做了重大重构后的回归
- 定位某个服务本身的性能瓶颈
- CI/CD中的快速性能回归
优势:
- 场景可控,排除干扰
- 速度快,可以在CI中跑
- 问题定位精确,找到的问题一定是该服务本身的问题
局限性:
- Mock了依赖,无法反映真实调用链路的性能
- 无法发现服务间超时配置不合理
- 无法测试下游服务降级对上游的影响
全链路压测(End-to-End Test)
定义:从入口(通常是网关或前端API)发起请求,穿透整条调用链路,所有中间服务均使用真实实现。
适用场景:
- 大促前的整体容量评估
- 架构重大变更后的整体验证
- 发现跨服务的性能问题
优势:
- 最接近真实流量
- 能发现链路级别的问题(超时配置、服务降级、链路放大效应)
- 结论最可靠
局限性:
- 准备成本高(测试环境、数据准备)
- 速度慢,不适合CI快速回归
- 问题定位复杂(知道链路慢,但不知道哪个服务慢)
两种策略的选型建议
日常开发阶段:服务级压测(CI集成,每次合并自动跑)
↓
版本发布前1周:全链路压测(完整的端到端性能验证)
↓
大促前1个月:全链路 + 专项压测(针对预期瓶颈的专项测试)
↓
大促前1周:全链路压测演练(模拟大促流量模型)服务级压测实践
以Spring Boot微服务为例,使用k6进行服务级压测:
// inventory_service_test.js
// 对库存服务进行独立压测
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Counter, Trend } from 'k6/metrics';
const deductSuccess = new Counter('deduct_success');
const deductFailed = new Counter('deduct_failed');
const deductLatency = new Trend('deduct_latency_ms', true);
export const options = {
scenarios: {
deduct_stock: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '1m', target: 50 },
{ duration: '3m', target: 50 },
{ duration: '1m', target: 100 },
{ duration: '3m', target: 100 },
{ duration: '1m', target: 0 },
],
},
},
thresholds: {
'http_req_duration': ['p(99)<200'],
'deduct_latency_ms': ['p(99)<200'],
'http_req_failed': ['rate<0.005'],
},
};
// 直接打到库存服务,不经过网关
const INVENTORY_SERVICE = __ENV.SERVICE_URL || 'http://inventory-service:8080';
export default function() {
const productId = Math.floor(Math.random() * 10000) + 1;
const start = Date.now();
const res = http.post(
`${INVENTORY_SERVICE}/api/inventory/deduct`,
JSON.stringify({
productId: productId,
quantity: 1,
orderId: `bench_${__VU}_${__ITER}`,
}),
{
headers: {
'Content-Type': 'application/json',
'X-Service-Call': 'benchmark', // 标识这是压测请求
},
}
);
deductLatency.add(Date.now() - start);
const success = check(res, {
'status 200': (r) => r.status === 200,
'business success': (r) => {
try { return r.json('code') === 200; }
catch(e) { return false; }
},
});
if (success) {
deductSuccess.add(1);
} else {
deductFailed.add(1);
}
sleep(0.1); // 100ms间隔,模拟内部服务调用
}服务级压测的Mock策略
压测时Mock下游服务,需要让Mock足够真实:
// WireMock模拟下游服务
@ExtendWith(WireMockExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class InventoryServicePerfTest {
@RegisterExtension
static WireMockExtension orderServiceMock = WireMockExtension.newInstance()
.options(wireMockConfig().port(8081))
.build();
@BeforeEach
void setupMocks() {
// Mock订单服务,模拟真实响应时间(10-50ms)
orderServiceMock.stubFor(
post(urlPathEqualTo("/api/order/callback"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody("{\"code\":200}")
.withFixedDelay(20) // 模拟20ms的下游延迟
)
);
}
}关键:Mock的响应延迟要和真实下游服务的P50延迟接近,不能Mock成0ms,那样会低估整体链路耗时。
全链路压测的链路追踪集成
全链路压测时,需要能定位到哪个服务是慢的。标准方案是在压测请求里注入traceId,然后通过链路追踪系统(Jaeger/Zipkin/SkyWalking)找到对应的链路。
// k6全链路压测脚本
import http from 'k6/http';
import { check } from 'k6';
export default function() {
// 生成唯一的压测traceId,方便后续在链路追踪系统里查找
const traceId = `bench-${Date.now()}-${__VU}-${__ITER}`;
const res = http.post(
'https://api-gateway.example.com/api/order/create',
JSON.stringify({
productId: Math.floor(Math.random() * 10000) + 1,
quantity: 1,
}),
{
headers: {
'Content-Type': 'application/json',
'X-B3-TraceId': traceId,
'X-B3-SpanId': traceId.substring(0, 16),
'X-Benchmark': 'true',
},
}
);
if (res.status !== 200) {
// 打印traceId,方便在Jaeger里查询这条请求的完整链路
console.error(`Failed request traceId: ${traceId}, status: ${res.status}`);
}
}链路追踪查询:在Jaeger中搜索operation=POST /api/order/create,按duration > 1s过滤,找出耗时最长的几条请求,逐个查看链路,确定哪个服务消耗了最多时间。
踩坑实录
坑1:服务级压测Mock了数据库连接池竞争
现象: 订单服务服务级压测TPS是1500,但全链路压测时TPS只有600。排查发现全链路压测时数据库连接池被多个服务竞争,而服务级压测时只有一个服务使用DB。
原因: 全链路下,订单服务、库存服务、商品服务可能都连着同一个数据库实例(测试环境资源有限),服务级压测时每个服务独占数据库,自然表现好。
解法: 在计算全链路容量时,要考虑数据库共享的情况。或者全链路压测时用独立的数据库实例,接近生产的DB分配方式。
坑2:服务间调用超时设置在服务级压测里发现不了
现象: 全链路压测时,偶发2%的订单创建失败,错误信息是"调用库存服务超时"。但库存服务的服务级压测一切正常,P99是180ms。
原因: 订单服务调用库存服务的超时设置是200ms,但在全链路压测时,库存服务P99是180ms,加上网络延迟(5ms)和服务网格开销(15ms),实际P99是200ms,刚好在超时边界,导致偶发超时。服务级压测是直接HTTP调用,没有服务网格,延迟更低。
解法: 超时设置要留足余量:超时 = 下游服务P99 × 1.5 + 预期网络延迟。这里应该设为300ms以上。
坑3:全链路压测时测试数据写入了生产数据库
现象: 全链路压测创建的大量测试订单出现在生产数据库里,运营人员发现了大量异常订单。
原因: 全链路压测用的是生产环境的入口,但没有做好测试数据的隔离。
解法: 全链路压测必须使用影子库方案,或者在测试入口打标(如Header里加X-Benchmark: true),让服务识别后将数据写入独立的影子表。这就是下一篇要讲的全链路压测完整方案。
总结
微服务性能测试策略是双层的:服务级压测保证每个服务个体健康,全链路压测验证整体系统能力。
用一句话记住:服务级压测是找每个人的体能极限,全链路压测是看整个接力队的成绩。两者互相补充,缺一不可。
