日均2300万单物流订单系统 生产环境无损全链路压测落地操作手册
手册前置说明
一、先给初级Java工程师讲透:为什么必须做这件事?
1. 核心背景量化
你负责的是日均2300万单、日常峰值1300TPS、大促峰值1.5万TPS的物流订单系统,核心诉求是支撑日常10倍以上峰值(≥2万TPS),同时兼顾资源利用率。
先明确一个核心误区:中小厂常用的「测试环境单接口压测」,在大厂高并发场景下完全无效,核心痛点如下:
| 中小厂压测方式 | 致命缺陷(物流订单场景) |
|---|---|
| 测试环境压测 | 测试环境配置、数据量、流量模型和生产完全不一致:生产单表千万级数据,测试环境仅几万条,测不出分库分表、索引、锁竞争的真实瓶颈;依赖服务多为Mock,测不出全链路性能问题 |
| 生产环境用测试账号压测 | 会产生脏数据,污染真实订单、库存、支付数据,有资损风险;流量隔离不彻底,占用真实业务资源,影响正常用户 |
| 仅压下单接口 | 生产环境是混合流量:下单仅占20%,订单查询、物流轨迹查询、地址校验等流量占70%+,仅压下单接口的结果和真实场景完全不符 |
| 一次性压测 | 仅大促前压一次,日常版本迭代不压测,新代码引入的性能劣化无法提前发现,大促必出问题 |
2. 不做生产无损全链路压测的致命隐患
- 大促峰值流量到来时,系统扛不住直接宕机,宕机1分钟直接损失1.6万笔订单,对应百万级营收损失、用户大规模投诉、品牌口碑受损;
- 压测结果不准,盲目扩容导致资源严重浪费,一年多花数十万服务器成本;或扩容不足,大促峰值直接雪崩;
- 隐藏的性能瓶颈(如库存锁竞争、分库分表路由慢、连接池打满)无法提前发现,线上出故障后定位耗时几小时,故障时长被无限拉长;
- 击穿99.99%可用性SLA,全年允许宕机时长仅52分钟,一次大促故障就会直接超标。
3. 核心概念大白话扫盲(先看懂再操作)
| 术语 | 大白话解释 | 物流订单场景核心要求 |
|---|---|---|
| 全链路压测 | 模拟真实用户的完整下单流程,覆盖从网关→订单→所有依赖服务→数据库/缓存/MQ的全链路,不是只压单个接口 | 覆盖下单、支付、取消、履约4条核心链路,120+依赖接口 |
| 无损压测 | 压测流量和真实业务流量100%隔离,不产生脏数据,对真实用户的请求无任何影响 | 零脏数据、真实用户下单成功率波动≤0.01%、RT波动≤50ms |
| 影子库/影子表 | 和生产真实库表结构100%一致的库/表,专门存储压测流量产生的数据,和真实业务数据完全隔离 | 真实表order_main_202601 → 影子表order_main_202601_shadow,同库实例部署,真实还原数据库压力 |
| 流量染色 | 给压测流量打上专属标记(如请求头加pressure-test: true),全链路透传,让所有服务、中间件能识别出这是压测流量,路由到影子表 | 标记从网关透传到所有服务、RPC、MQ,无一处遗漏 |
| TPS | 每秒处理的请求数,核心性能指标 | 目标峰值≥2万TPS(日常10倍) |
| RT | 接口响应时间,重点看P99 RT(99%的请求都能在这个时间内完成) | 下单链路P99 RT≤300ms |
| 阶梯式加压 | 从低TPS逐步加到目标峰值,每个水位保持一段时间,精准定位瓶颈点 | 1000TPS→5000TPS→10000TPS→15000TPS→20000TPS |
4. 手册整体执行框架
严格遵循大厂标准压测流程,环环相扣,初级工程师可1:1复制执行:
- 前期准备:明确目标→梳理全链路→制定应急预案
- 无损底座搭建:影子库/影子表创建→流量染色全链路透传→隔离规则配置
- 压测平台搭建:分布式压测环境部署→压测脚本编写
- 1:1真实压测模型构建:场景模型→流量模型→数据模型
- 标准化压测执行:从测试环境→预发→生产小流量→生产全量压测
- 瓶颈定位与优化:TOP高频瓶颈排查方法+落地优化方案
- 精准容量规划:基于压测结果计算实例数→弹性扩缩容配置
- 常态化压测机制落地
第一部分 前期准备:压测成功的前提(压测前3天完成)
一、核心目标
明确可量化的压测目标,梳理清楚全链路拓扑,制定风险兜底预案,避免压测过程中手忙脚乱出故障。
二、详细操作步骤
步骤1:制定可量化的压测目标(必须精准,不能模糊)
所有目标必须和业务强绑定,初级工程师直接套用以下物流订单场景标准:
| 目标类型 | 量化指标 | 说明 |
|---|---|---|
| 性能目标 | 峰值TPS≥20000(日常10倍);下单链路P99 RT≤300ms;下单成功率≥99.99%;错误率≤0.01% | 核心红线,达不到就必须优化 |
| 业务覆盖目标 | 覆盖4条核心链路(下单、支付、取消、履约);120+依赖接口;8种订单类型(普通/预售/逆向/大件冷链/同城急送等) | 避免漏测场景,导致压测结果失真 |
| 安全目标 | 压测流量100%隔离,零脏数据;对真实用户请求影响≤0.01%(无感知) | 生产压测的底线,绝对不能突破 |
| 资源目标 | 峰值流量下,服务器CPU利用率≤75%;数据库CPU≤80%;Redis CPU≤70% | 预留冗余,应对突发流量 |
步骤2:梳理全链路拓扑,形成链路清单
压测前必须搞清楚:一笔下单请求,到底经过了哪些服务、哪些中间件、哪些依赖接口,漏一个环节,压测结果就不准。
- 工具:用SkyWalking的「拓扑图」功能,自动生成全链路依赖关系,不用手动梳理
- 操作步骤:
- 打开SkyWalking UI → 【拓扑图】→ 选择时间范围,筛选
logistics-order服务 - 导出完整的链路拓扑图,标记出核心节点:网关→订单服务→用户/商品/地址/营销/仓储/库存/支付服务→MySQL/Redis/RocketMQ
- 形成《全链路接口清单》,包含每个服务的接口名称、请求方式、依赖关系、是否核心链路,示例:
服务名称 接口名称 接口路径 链路阶段 是否核心 订单服务 创建订单 /order/create 下单核心链路 是 库存服务 预占库存 /stock/preOccupy 下单核心链路 是 营销服务 优惠计算 /marketing/calc 非核心链路 否
- 打开SkyWalking UI → 【拓扑图】→ 选择时间范围,筛选
- 为什么必须做? 只有梳理清楚全链路,才能确保压测流量覆盖所有节点,不会出现「压测时依赖服务没扩容,导致瓶颈出现在下游,误以为是订单服务的问题」。
- 不做的隐患:漏测依赖服务,压测结果完全失真,大促时下游服务先崩,整个链路雪崩。
步骤3:制定风险应急预案与回滚方案(生产压测的生命线)
生产环境压测,必须先想好「出问题了怎么办」,绝对不能上来就压。初级工程师直接套用以下标准化预案,每个预案必须明确「触发条件、操作人、操作步骤、验证标准」。
| 预案类型 | 触发条件 | 操作步骤 | 验证标准 |
|---|---|---|---|
| 紧急停止预案 | 任何异常情况,比如真实用户RT飙升、成功率下降 | 1. 压测平台点击「一键停止」按钮,1s内终止所有压测流量;2. 通知所有相关团队;3. 观察系统指标是否恢复正常 | 压测流量完全停止,系统指标5分钟内恢复到压测前水平 |
| 性能异常预案 | 真实用户下单P99 RT>500ms,或下单成功率<99.9% | 1. 立即停止压测;2. 开启非核心依赖的降级开关;3. 扩容瓶颈服务实例 | 真实用户下单指标1分钟内恢复正常 |
| 脏数据预案 | 发现压测数据写入了真实表 | 1. 立即停止压测;2. 执行提前准备好的脏数据清理脚本;3. 排查影子规则失效原因,修复后再小流量验证 | 真实表脏数据完全清理,无业务影响,影子规则修复验证通过 |
| 中间件过载预案 | MySQL/Redis CPU>80%,或出现慢查询、锁等待 | 1. 停止加压,保持当前流量;2. 若持续过载,立即停止压测;3. 扩容中间件集群,优化慢SQL | 中间件CPU利用率降到70%以下,无慢查询、锁等待 |
| 服务雪崩预案 | 某个服务异常率>50%,或实例宕机 | 1. 立即停止压测;2. 触发服务熔断降级;3. 重启/扩容故障服务 | 服务恢复正常,异常率降到0.1%以下 |
必须提前做的准备:
- 压测平台必须有一键停止按钮,绝对不能分步停止施压机;
- 提前写好脏数据清理脚本,验证可用;
- 提前配置好所有非核心依赖的降级开关,验证可用;
- 压测前通知所有相关团队(运维、DBA、中间件、依赖服务团队),告知压测时间、范围,预留应急联系人。
第二部分 无损压测核心底座:影子库/影子表搭建(压测前2天完成)
这是生产环境无损压测的核心,决定了会不会产生脏数据,会不会影响真实用户,必须100%做对。
一、核心目标
实现压测流量与真实业务流量100%隔离,压测数据全部写入影子表,不碰真实业务数据,同时100%还原生产环境的数据库、中间件性能压力。
二、为什么选影子表方案?
对比行业主流方案,影子表是物流订单系统(分库分表+高并发)的最优解:
| 方案 | 优点 | 缺点 | 适配性 |
|---|---|---|---|
| 影子表(同库) | 和真实表共用数据库实例,100%还原数据库CPU、IO、连接数压力;对业务代码零侵入;适配分库分表场景 | 需要创建和真实表一一对应的影子表 | ✅ 物流订单系统首选 |
| 影子库(独立实例) | 数据完全隔离,不影响真实库 | 无法还原真实库的性能压力,压测结果失真 | ❌ 不适合核心交易链路压测 |
| 测试账号+特殊标识 | 操作简单 | 会产生脏数据,占用真实表资源,影响真实用户 | ❌ 生产环境严禁使用 |
三、详细操作步骤
步骤1:创建影子表,和真实表100%结构一致
影子表必须和真实表字段、索引、分区、分库分表规则、引擎、字符集完全一致,差一个索引,压测出来的SQL性能就和真实场景完全不符。
- 批量创建影子表SQL模板(直接复制执行,初级工程师不用改逻辑):
-- -------------------------- -- 1. 订单主表影子表创建(按月分表,每个月的真实表对应一个影子表) -- 真实表:order_main_202601 → 影子表:order_main_202601_shadow -- -------------------------- CREATE TABLE `order_main_202601_shadow` ( `order_id` bigint NOT NULL COMMENT '压测订单ID', `user_id` bigint NOT NULL COMMENT '压测用户ID(分片键)', `request_id` varchar(64) NOT NULL COMMENT '幂等请求ID', `order_status` tinyint NOT NULL DEFAULT '0' COMMENT '订单状态', `total_amount` decimal(12,2) NOT NULL COMMENT '订单总金额', `pay_amount` decimal(12,2) NOT NULL COMMENT '实付金额', `freight_amount` decimal(12,2) DEFAULT '0.00' COMMENT '运费', `discount_amount` decimal(12,2) DEFAULT '0.00' COMMENT '优惠金额', `address_id` bigint NOT NULL COMMENT '收货地址ID', `warehouse_id` bigint NOT NULL COMMENT '发货仓ID', `pay_order_id` bigint DEFAULT NULL COMMENT '支付单ID', `pay_status` tinyint DEFAULT '0' COMMENT '支付状态', `estimate_arrive_time` datetime DEFAULT NULL COMMENT '预计送达时间', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `is_deleted` tinyint NOT NULL DEFAULT '0' COMMENT '是否删除', PRIMARY KEY (`order_id`), KEY `idx_user_id` (`user_id`), KEY `idx_request_id` (`request_id`), KEY `idx_create_time` (`create_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单主表影子表'; -- -------------------------- -- 2. 订单明细表影子表创建,和真实表结构完全一致 -- -------------------------- CREATE TABLE `order_item_202601_shadow` ( `id` bigint NOT NULL AUTO_INCREMENT, `order_id` bigint NOT NULL COMMENT '订单ID', `user_id` bigint NOT NULL COMMENT '用户ID(分片键)', `sku_id` bigint NOT NULL COMMENT '商品SKU ID', `sku_name` varchar(256) NOT NULL COMMENT 'SKU名称', `sku_num` int NOT NULL COMMENT '购买数量', `sku_price` decimal(12,2) NOT NULL COMMENT 'SKU单价', `warehouse_id` bigint NOT NULL COMMENT '发货仓ID', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_order_id` (`order_id`), KEY `idx_user_id` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单明细表影子表'; -- -------------------------- -- 3. 其他核心表影子表创建,规则完全一致:表名加_shadow后缀,结构100%和真实表一致 -- 必须创建的影子表:stock_sku_shadow(库存表)、pay_order_shadow(支付单表)、marketing_coupon_shadow(优惠券表)、warehouse_info_shadow(仓库表) -- -------------------------- - 核心要求:
- 所有在下单链路中会写入的表,必须创建对应的影子表,一个都不能漏;
- 分库分表的场景,每个真实库的每个真实分表,都必须对应一个影子表,比如8个订单库,每个库12个月的分表,就要创建8*12=96个订单主表影子表;
- 影子表的索引必须和真实表完全一致,比如真实表有
idx_user_id,影子表也必须有,不然压测出来的SQL性能是假的。
步骤2:配置影子路由规则,实现流量自动隔离
我们用Sharding-JDBC的影子库功能实现路由,因为你的订单系统已经用了Sharding-JDBC做分库分表,对业务代码零侵入,不用改一行业务代码,初级工程师只要改配置就行。
- 核心原理:Sharding-JDBC在JDBC层面拦截SQL,识别到是压测流量,就自动把SQL路由到影子表;是真实流量,就路由到真实表,完全隔离。
- 详细配置(application.yml,直接复制到订单服务、库存服务、支付服务等所有核心服务):
spring: shardingsphere: # 影子功能总开关,压测时开启,平时关闭 shadow: enabled: true # 影子表配置:所有需要走影子规则的表名 tables: - order_main - order_item - stock_sku - pay_order - marketing_coupon - address_info # 核心:影子流量匹配规则,怎么识别压测流量 column: # 规则1:用户ID以1000000000开头的压测专用用户,走影子表(最核心规则) - operation: insert # 匹配插入操作 column: user_id # 匹配的字段名 value: ^1000000000.*$ # 匹配规则:user_id以1000000000开头 regex: true # 开启正则匹配 # 规则2:订单号以999999开头的压测订单,走影子表 - operation: insert column: order_id value: ^999999.*$ regex: true # 规则3:请求头带pressure-test=true的压测请求,走影子表 - operation: insert column: pressure_test_flag value: true regex: false # 原有分库分表配置保持不变,不用修改 datasource: # 这里保留你之前的8个订单库的配置,完全不用改 rules: sharding: # 这里保留你之前的分库分表规则,完全不用改 - 为什么这么配置?
- 多规则兜底,哪怕一个规则失效,其他规则还能保证压测流量路由到影子表,避免脏数据;
- 用user_id作为核心匹配规则,因为订单系统的所有表都有user_id分片键,全链路都能匹配到;
- 零代码侵入,不用改业务代码,只改配置,不会污染业务逻辑,初级工程师不会写错。
步骤3:压测标记全链路透传(最容易踩坑的环节)
压测流量的标记,必须从网关开始,透传到所有服务、所有RPC调用、所有MQ消息、所有异步线程,只要有一个环节没透传,下游服务就识别不出压测流量,就会把数据写到真实表里,产生脏数据。
以下是全链路透传的标准化代码,初级工程师直接复制到对应模块即可:
1. 网关层:压测标记识别与注入
给压测流量打标,放到请求头里,透传给下游所有服务:
// 网关全局过滤器,优先级最高,在路由之前执行
@Component
public class PressureTestMarkFilter implements GlobalFilter, Ordered {
// 压测标记的请求头key
private static final String PRESSURE_TEST_HEADER = "pressure-test";
// 压测专用用户ID前缀,和影子规则里的配置一致
private static final String PRESSURE_USER_PREFIX = "1000000000";
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
// 1. 识别压测流量:两种情况都判定为压测流量
// 情况1:请求头里主动带了pressure-test=true
String headerFlag = request.getHeaders().getFirst(PRESSURE_TEST_HEADER);
// 情况2:用户ID是压测专用用户(以1000000000开头)
String userId = request.getHeaders().getFirst("user-id");
boolean isPressureTest = "true".equals(headerFlag) || (userId != null && userId.startsWith(PRESSURE_USER_PREFIX));
// 2. 给请求头加上压测标记,强制透传给下游服务
ServerHttpRequest.Builder requestBuilder = request.mutate();
if (isPressureTest) {
requestBuilder.header(PRESSURE_TEST_HEADER, "true");
}
// 3. 继续执行过滤器链
return chain.filter(exchange.mutate().request(requestBuilder.build()).build());
}
@Override
public int getOrder() {
// 优先级最高,在所有路由、鉴权过滤器之前执行
return Ordered.HIGHEST_PRECEDENCE;
}
}
2. 服务层:上下文工具类,存储压测标记
用ThreadLocal存储压测标记,服务内所有方法都能获取到,同时处理异步线程的上下文传递:
// 全链路上下文工具类,所有服务都要引入
@Component
public class TraceContextHolder {
// 压测标记的key
private static final String PRESSURE_TEST_KEY = "pressure_test";
// 用InheritableThreadLocal,解决异步线程上下文传递问题
private static final ThreadLocal<Map<String, Object>> CONTEXT = new InheritableThreadLocal<>();
// 设置压测标记
public static void setPressureTestFlag(boolean flag) {
getContext().put(PRESSURE_TEST_KEY, flag);
}
// 获取压测标记
public static boolean isPressureTest() {
Map<String, Object> context = CONTEXT.get();
if (context == null) {
return false;
}
return Boolean.TRUE.equals(context.get(PRESSURE_TEST_KEY));
}
// 清除上下文,避免线程池复用导致的上下文错乱
public static void clear() {
CONTEXT.remove();
}
private static Map<String, Object> getContext() {
Map<String, Object> context = CONTEXT.get();
if (context == null) {
context = new HashMap<>();
CONTEXT.set(context);
}
return context;
}
}
3. Web层:拦截器,从请求头获取压测标记,设置到上下文
// Spring MVC拦截器,每个服务都要配置
@Component
public class PressureTestInterceptor implements HandlerInterceptor {
private static final String PRESSURE_TEST_HEADER = "pressure-test";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 从请求头获取压测标记,设置到上下文
String pressureTestFlag = request.getHeader(PRESSURE_TEST_HEADER);
TraceContextHolder.setPressureTestFlag("true".equals(pressureTestFlag));
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
// 请求结束后,清除上下文,避免线程池复用错乱
TraceContextHolder.clear();
}
}
// 注册拦截器
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private PressureTestInterceptor pressureTestInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(pressureTestInterceptor).addPathPatterns("/**");
}
}
4. RPC层:Feign拦截器,透传压测标记到下游服务
// Feign调用拦截器,所有服务都要配置
@Component
public class FeignPressureTestInterceptor implements RequestInterceptor {
private static final String PRESSURE_TEST_HEADER = "pressure-test";
@Override
public void apply(RequestTemplate template) {
// 从上下文获取压测标记,放到Feign请求头,透传给下游服务
boolean isPressureTest = TraceContextHolder.isPressureTest();
template.header(PRESSURE_TEST_HEADER, String.valueOf(isPressureTest));
}
}
5. MQ层:消息发送/消费时,透传压测标记
// 1. 消息发送时,把压测标记放到消息属性里
@Component
public class OrderMessageSender {
@Autowired
private RocketMQTemplate rocketMQTemplate;
public void sendOrderCreateEvent(OrderCreateEvent event) {
// 构建消息,把压测标记放到消息属性中
Message<OrderCreateEvent> message = MessageBuilder.withPayload(event)
.setHeader("pressure-test", TraceContextHolder.isPressureTest())
.build();
// 发送消息
rocketMQTemplate.send("order_create_success_topic", message);
}
}
// 2. 消息消费时,获取压测标记,设置到上下文
@RocketMQMessageListener(topic = "order_create_success_topic", consumerGroup = "order_create_consumer_group")
@Component
public class OrderCreateMessageConsumer implements RocketMQListener<MessageExt> {
@Override
public void onMessage(MessageExt message) {
// 从消息属性中获取压测标记
String pressureTestFlag = message.getProperty("pressure-test");
boolean isPressureTest = "true".equals(pressureTestFlag);
// 设置到上下文
TraceContextHolder.setPressureTestFlag(isPressureTest);
try {
// 处理业务逻辑
OrderCreateEvent event = JSONUtil.toBean(new String(message.getBody()), OrderCreateEvent.class);
handleOrderCreateEvent(event);
} finally {
// 消费完成后,清除上下文
TraceContextHolder.clear();
}
}
private void handleOrderCreateEvent(OrderCreateEvent event) {
// 你的业务逻辑
}
}
步骤4:影子规则验证(必须做,不验证绝对不能全量压测)
配置完成后,必须做小流量验证,确保100%不会产生脏数据,初级工程师按以下步骤执行:
- 准备一个压测专用用户ID:1000000000001(符合我们的前缀规则);
- 用这个用户ID,发起一笔下单请求;
- 验证步骤:
- 第一步:查看订单服务日志,确认识别到了压测标记,
isPressureTest=true; - 第二步:查看真实订单表
order_main_202601,确认没有这笔订单数据; - 第三步:查看影子订单表
order_main_202601_shadow,确认这笔订单数据正常写入; - 第四步:查看下游库存、支付、营销服务的真实表,确认没有压测数据,影子表有数据;
- 第一步:查看订单服务日志,确认识别到了压测标记,
- 只有所有环节都验证通过,确认零脏数据,才能进入下一步。
第三部分 压测平台搭建(压测前1天完成)
一、核心目标
搭建分布式压测平台,支持构造1:1真实流量、阶梯式加压、实时监控、一键停止,满足2万TPS的压测需求。
二、技术选型(初级工程师友好,大厂主流)
- 压测工具:JMeter + 分布式压测(开源免费,生态完善,易上手)
- 辅助平台:MeterSphere(开源压测平台,可视化操作,比原生JMeter更简单)
- 施压机要求:单台4核8G,至少5台,避免压测机本身成为瓶颈
三、详细操作步骤
步骤1:分布式压测环境搭建
- 环境准备:
- 1台控制机(安装JMeter,用于编写脚本、控制施压机)
- 5台施压机(安装和控制机完全相同版本的JDK17、JMeter5.6.3,关闭防火墙,和生产网络互通)
- 配置步骤:
- 所有施压机修改
jmeter.properties,关闭SSL,开启远程服务:server.rmi.ssl.disable=true server_port=1099 - 所有施压机启动jmeter-server服务:
# 进入JMeter的bin目录 cd /usr/local/apache-jmeter-5.6.3/bin # 后台启动服务 nohup ./jmeter-server & - 控制机修改
jmeter.properties,配置施压机地址:remote_hosts=施压机1IP:1099,施压机2IP:1099,施压机3IP:1099,施压机4IP:1099,施压机5IP:1099 server.rmi.ssl.disable=true
- 所有施压机修改
- 为什么用分布式压测?
单台压测机的CPU、网络带宽有限,最多只能压5000TPS,要压2万TPS,必须用多台施压机分布式加压,不然压测机本身就会成为瓶颈,压出来的结果完全不准。
步骤2:压测脚本编写(JMeter)
核心是模拟真实用户的下单请求,初级工程师按以下结构编写:
- 线程组配置:
- 线程数:单台施压机设置400线程,5台施压机总共2000线程,满足2万TPS的需求
- Ramp-Up时间:60s(阶梯式加压,避免瞬间把系统打垮)
- 循环次数:永远(压测时手动控制停止)
- HTTP请求默认值:
- 配置网关地址、端口、请求头(
Content-Type: application/json、pressure-test: true)
- 配置网关地址、端口、请求头(
- 压测数据准备:
- 提前准备好压测专用的用户ID列表、SKU ID列表、地址ID列表,和生产真实数据分布一致(比如热点SKU占30%,普通SKU占70%)
- 用JMeter的CSV Data Set Config组件,读取这些数据,实现每个请求的参数不一样,模拟真实用户
- 下单接口HTTP请求配置:
- 路径:
/order/create - 请求方式:POST
- 请求体:和真实下单请求完全一致的JSON参数,用CSV里的变量替换用户ID、SKU ID等
- 路径:
- 断言配置:
- 响应码断言:必须返回200
- 响应内容断言:返回的code必须是200,订单号必须非空,确保下单成功
- 监听器配置:
- 汇总报告:查看TPS、RT、成功率、错误率
- 查看结果树:调试时查看请求和响应详情,压测时关闭,避免影响施压机性能
步骤3:压测脚本验证
- 先在控制机本地运行脚本,单线程循环10次,确认所有请求都成功,数据都写入影子表,无脏数据;
- 远程启动1台施压机,小流量压测1分钟,确认施压机正常运行,压测数据正确;
- 所有验证通过后,再进行全量压测。
第四部分 1:1真实压测模型构建(压测结果准不准的核心)
很多人压测结果和生产不符,核心原因就是压测模型不对,和真实业务场景完全脱节。初级工程师必须严格按照以下要求构建模型,1:1还原生产。
一、场景模型:覆盖全业务场景,和生产占比完全一致
必须覆盖物流订单系统的所有核心场景,不能只压普通下单,每个场景的占比必须和生产完全一致,示例:
| 链路类型 | 包含场景 | 生产流量占比 | 压测模型占比 |
|---|---|---|---|
| 核心链路 | 普通订单下单、预售订单下单、订单支付、订单取消、履约发货、订单签收 | 30% | 30% |
| 查询链路 | 订单详情查询、物流轨迹查询、收货地址查询、商品信息查询、优惠券查询 | 65% | 65% |
| 其他操作 | 收货地址修改、订单备注修改、发票申请、退款申请 | 5% | 5% |
| 订单类型 | 生产占比 | 压测模型占比 |
|---|---|---|
| 普通快递订单 | 80% | 80% |
| 预售订单 | 10% | 10% |
| 逆向退款订单 | 5% | 5% |
| 大件冷链/同城急送/跨境订单 | 5% | 5% |
为什么必须这么做? 生产环境里,查询流量占比高达65%,会大量占用数据库连接、CPU、缓存资源,如果只压下单接口,测出来的TPS再高,大促时查询流量一上来,系统直接就崩了。
二、流量模型:还原生产的时序特征
- 阶梯式加压模型:从1000TPS→5000TPS→10000TPS→15000TPS→20000TPS,每个阶梯保持10分钟,观察系统在每个流量水位的性能表现,精准定位瓶颈点;
- 峰值突增模型:模拟大促零点,瞬间从日常266TPS突增到2万TPS,验证系统的弹性扩缩容能力,会不会出现雪崩;
- 长时间稳定性模型:峰值2万TPS持续压测2小时,验证系统的稳定性,会不会出现内存泄漏、GC频繁、连接池耗尽、磁盘IO打满等问题(短时间压测没问题,长时间跑可能会出问题)。
三、数据模型:和生产数据分布100%一致
压测用的数据,必须和生产真实数据的分布完全一致,不然测不出真实瓶颈,核心要求:
- 用户数据:压测用户ID数量≥100万,覆盖活跃用户、不活跃用户,和生产用户分布一致;
- 商品数据:包含热点爆品SKU(占下单量的30%)、普通SKU(70%),SKU的库存数量、分仓情况、重量体积和生产一致;
- 地址数据:覆盖全国所有省份、城市,一线城市、偏远地区的占比和生产一致;
- 订单数据:单笔订单的SKU数量,和生产一致(80%的订单是1-2个SKU,20%的订单是3个以上SKU)。
反例:如果压测用的都是单SKU订单,生产里很多是多SKU订单,多SKU订单要多次调用库存接口,锁竞争更激烈,性能更差,压测结果就会完全失真,大促时直接出问题。
第五部分 标准化压测执行流程(生产环境压测必须严格遵守)
绝对不能上来就生产全量压测,必须按以下流程逐步执行,避免出故障。
一、压测前检查清单(压测前1小时必须全部完成)
✅ 影子库/影子表全部创建完成,结构和真实表100%一致
✅ 小流量验证通过,压测流量100%写入影子表,零脏数据
✅ 压测标记全链路透传验证通过,无遗漏环节
✅ 压测脚本验证通过,所有请求正常,断言全部通过
✅ 分布式施压机全部正常启动,和控制机网络互通
✅ 全链路监控体系正常,所有核心指标可实时查看
✅ 应急预案全部准备完成,一键停止开关验证可用
✅ 所有相关团队已通知,应急联系人到位
✅ 非核心依赖的降级开关验证可用
✅ 压测时间段为业务低峰期(凌晨2-4点),对用户影响最小
二、分步执行流程
步骤1:测试环境压测(压测前3天完成)
- 在测试环境,用同样的脚本、模型,跑一遍全量压测;
- 验证脚本是否正常,有没有语法错误,接口是否能正常调用;
- 初步定位测试环境的瓶颈,比如代码bug、SQL慢查询、接口逻辑问题,先优化掉,避免带到生产环境。
步骤2:预发环境压测(压测前2天完成)
- 预发环境是和生产配置、数据完全一致的环境,先在预发环境跑全量压测;
- 验证压测模型、脚本、影子规则是否正常,定位预发环境的瓶颈,优化完成后,再到生产环境压测。
步骤3:生产环境小流量灰度压测(压测当天凌晨2点开始)
- 先压100TPS,持续5分钟,验证:
- 压测流量是否正常,有没有脏数据;
- 真实用户的下单成功率、RT有没有受影响;
- 系统、中间件指标是否正常。
- 没问题的话,逐步加到500TPS、1000TPS,每个阶梯持续5分钟,观察所有指标,确认无异常;
- 小流量压测完成后,确认零脏数据、真实用户无感知,再进行全量压测。
步骤4:生产环境全量阶梯式压测
- 按照预设的阶梯,从1000TPS→5000TPS→10000TPS→15000TPS→20000TPS,每个阶梯保持10分钟;
- 每个阶梯,都必须实时观察以下4类指标,出现异常立即停止加压:
- 压测指标:TPS、RT(P99/P95/P50)、下单成功率、错误率;
- 系统指标:所有服务的CPU、内存、JVM GC情况、线程池状态;
- 中间件指标:MySQL的CPU、慢查询、连接数、锁等待;Redis的CPU、命中率、响应时间;RocketMQ的消息堆积、生产/消费TPS;
- 真实用户指标:真实用户的下单成功率、RT,有没有受影响。
- 如果某个阶梯出现了瓶颈,比如RT飙升、成功率下降,就停止加压,保持当前流量,定位瓶颈,优化完成后,再继续加压。
步骤5:峰值稳定性压测
- 达到目标峰值2万TPS后,持续压测2小时;
- 重点观察系统的稳定性,有没有出现内存泄漏、GC频繁、连接池耗尽、中间件过载等问题;
- 全程确保真实用户无感知,下单成功率≥99.99%。
步骤6:压测结束后清理工作
- 点击一键停止按钮,终止所有压测流量,确认所有施压机全部停止;
- 清理影子库/影子表里的压测数据,释放存储空间;
- 关闭压测相关的开关、配置,恢复系统到正常状态;
- 通知所有相关团队,压测结束,系统正常运行。
第六部分 物流订单系统TOP高频瓶颈定位与优化方案
结合你的案例,通过压测定位并解决了23个性能瓶颈,这里给你讲物流订单系统最常见的TOP5瓶颈,初级工程师照着步骤就能定位、就能优化。
瓶颈1:库存锁竞争激烈,库存预占接口RT飙升,TPS上不去(占比80%的大促瓶颈)
怎么定位?
- SkyWalking Trace瀑布图里,库存预占接口耗时占比最高,超过200ms;
- MySQL里执行
show engine innodb status,看到大量的行锁等待; - Arthas执行
trace 库存服务全类名 preOccupyStock,发现库存更新的SQL执行时间很长,或者分布式锁等待时间很长。
根因
大促爆品SKU,大量用户同时下单,都要更新同一条库存记录,数据库行锁竞争激烈,或者分布式锁等待时间长,导致接口RT飙升,TPS上不去。
优化方案(落地就见效)
- 库存分片(最核心优化):把一个SKU的库存拆分成10个分片,比如SKU1001总库存10000,拆成10个分片,每个1000库存。下单时轮询获取分片,锁竞争从1条记录变成10条,降低90%的锁冲突;
- 缩小锁粒度:分布式锁只锁「库存更新」的核心代码块,不要锁整个方法,把远程调用、日志打印等逻辑移出锁范围,缩短锁持有时间;
- 乐观锁+分布式锁双重保障:用version版本号的乐观锁,避免数据库行锁等待,同时用分布式锁防止超卖;
- 热点SKU本地缓存预扣减:热点爆品的库存,提前缓存到服务本地,先本地预扣减,再异步更新到数据库,彻底降低数据库压力。
不优化的隐患
大促爆品下单时,库存接口大量超时,下单失败,甚至出现超卖,导致用户大规模投诉、资损风险。
瓶颈2:分库分表路由慢,订单写入/查询RT高
怎么定位?
- SkyWalking Trace里,订单写入/查询的SQL执行时间长,但SQL本身不复杂;
- Sharding-JDBC日志里,路由时间很长,扫描了所有8个库的所有12张表;
- MySQL慢查询日志里没有这条SQL,说明瓶颈不在数据库,在分库分表路由。
根因
订单查询/写入时,没有携带分片键user_id,Sharding-JDBC会扫描所有库所有表,路由时间极长;或者分片规则配置不合理,绑定表没配置,导致笛卡尔积路由。
优化方案
- 强制所有订单操作必须携带分片键
user_id,避免全库表扫描,这是分库分表的核心红线; - 配置绑定表:
order_main和order_item配置为绑定表,分片键都是user_id,避免跨库关联查询; - 优化分片算法:用MOD哈希分片,比INLINE表达式性能更高,路由更快;
- 单表数据量控制在1000万以内,超过的话增加分表数量。
不优化的隐患
订单接口RT越来越高,TPS上不去,大促时数据库连接池打满,系统雪崩。
瓶颈3:连接池打满(数据库/Redis/Feign),请求排队等待
怎么定位?
- SkyWalking里,接口RT很高,但方法内部执行时间很短,大部分时间在排队等待连接;
- 监控里,数据库连接池的活跃连接数达到了最大连接数,有大量等待线程;
- 日志里有大量的「连接池耗尽」「连接超时」报错。
根因
连接池的最大连接数配置太小,高峰期流量上来,连接不够用,请求排队等待;或者连接超时时间设置太长,空闲连接不释放,导致连接池耗尽。
优化方案
- 数据库连接池(HikariCP):按公式
CPU核心数*2 + 磁盘数配置最大连接数,比如8核CPU,最大连接数设为20,不要配置太大,不然数据库扛不住; - Feign连接池:配置最大连接数200,单路由最大连接数50,连接超时300ms,读取超时500ms,空闲连接60s自动释放;
- Redis连接池:配置最大连接数100,最小空闲连接20,空闲连接30s自动释放;
- 开启连接池监控,实时查看活跃连接数、等待数,提前预警。
不优化的隐患
高峰期连接池打满,请求排队超时,下单失败,服务雪崩。
瓶颈4:下单链路串行调用太多,RT太长
怎么定位?
SkyWalking Trace瀑布图里,下单链路的RPC调用是串行的,一个调用完了才调用下一个,总耗时是所有调用的耗时之和。比如先调用用户服务,再调用商品服务,再调用地址服务,再调用营销服务,串行4次调用总耗时200ms,并行的话只需要50ms。
根因
代码里把无依赖的RPC调用写成了串行,没有用并行调用,导致链路RT太长。
优化方案
- 用
CompletableFuture把无依赖的RPC调用改成并行,比如用户、商品、地址、营销服务的调用,没有依赖,完全可以并行,把总耗时从200ms降到50ms; - 有依赖的调用,尽量缩短串行长度,比如分仓匹配完成后,运费计算和优惠最终计算可以并行;
- 非核心的调用,改成异步MQ消息,不阻塞同步下单链路。
不优化的隐患
下单链路RT太长,P99超过500ms,用户体验差,高峰期超时率飙升,下单成功率下降。
瓶颈5:JVM频繁GC,STW时间长,服务卡顿
怎么定位?
- Prometheus监控里,Young GC每分钟超过10次,Full GC每小时超过1次,STW时间超过100ms;
- 接口RT忽高忽低,没有规律,GC的时候服务完全卡顿,请求超时;
- Arthas执行
gc命令,看到堆内存使用率持续上涨,不会下降。
根因
代码里创建了大量的大对象、临时对象(比如循环里创建String对象、大集合没有释放),导致Young GC频繁;或者有内存泄漏,老年代内存越来越多,频繁Full GC。
优化方案
- 优化JVM参数:新生代设置为堆内存的50%,用G1收集器,设置最大停顿时间200ms,避免STW时间太长;
- 优化代码:避免循环里创建大量临时对象,大对象复用,用完的集合及时清空,避免内存泄漏;
- 用
jmap dump堆内存,用MAT分析,找到内存泄漏的点,修复代码; - 大促前,提前触发Full GC,避免大促期间出现Full GC。
不优化的隐患
高峰期频繁GC,服务卡顿,请求超时,下单成功率下降,甚至服务OOM宕机。
第七部分 精准容量规划落地
基于压测结果,做精准的容量规划,既能保障大促峰值稳定,又不浪费资源,对应你的案例:日常12台实例,大促扩容到30台,CPU利用率从22%提升到76%。
一、核心计算公式(初级工程师直接套用)
- 单实例最大承载TPS:压测时,单实例在CPU利用率70%时,能稳定承载的最大TPS。比如压测得出,单台订单服务实例最大承载TPS=1000。
- 日常需要的实例数:
日常峰值TPS / 单实例安全TPS(单实例最大TPS的30%)- 示例:日常峰值TPS=266,266/(1000*30%)≈1,为了3AZ容灾,每个AZ至少4台,总共12台,和你的案例一致。
- 为什么用30%水位?日常要预留足够的冗余,应对突发流量、单AZ故障。
- 大促需要的实例数:
大促峰值TPS / 单实例安全TPS(单实例最大TPS的70%)- 示例:大促峰值TPS=20000,20000/(1000*70%)≈29,所以需要30台,和你的案例一致。
- 为什么用70%水位?大促前已经提前扩容,预留30%的冗余,应对突发流量,同时最大化资源利用率。
二、详细操作步骤
- 基于压测结果,输出每个服务的《容量报表》,包含:单实例最大承载TPS、日常需要实例数、大促需要实例数、瓶颈点;
- 配置K8s HPA弹性扩缩容,基于压测结果设置阈值,示例:
apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: logistics-order-hpa spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: logistics-order minReplicas: 12 # 日常最小实例数 maxReplicas: 30 # 大促最大实例数 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 70 # CPU利用率70%触发扩容 - type: Resource resource: name: memory target: type: Utilization averageUtilization: 80 behavior: scaleUp: stabilizationWindowSeconds: 30 # 扩容冷却30s,快速扩容 scaleDown: stabilizationWindowSeconds: 300 # 缩容冷却5分钟,避免频繁扩缩容 - 大促前预热扩容:大促前2小时,提前把实例数扩容到大促需要的80%(24台),避免大促开始时流量突增,扩容不及时导致系统被打垮;
- 成本优化:压测后,对资源利用率低的服务进行缩容,对瓶颈服务进行优化/扩容,平衡稳定与成本。
三、不做精准容量规划的隐患
- 盲目扩容,日常就部署30台实例,CPU利用率只有22%,一年多花几十万服务器成本;
- 扩容不足,大促峰值时系统扛不住,直接宕机,造成巨额营收损失。
第八部分 常态化压测机制落地
压测不是一次性的,必须常态化,才能持续保障系统稳定。
- 日常常态化压测:每周低峰期做一次小流量压测,验证系统性能有没有下降,新上线的代码有没有引入性能瓶颈;
- 版本发布压测:每次核心代码发布,都要做单接口压测,验证性能没有劣化,避免坏代码上线;
- 大促前全量压测:每次大促前2周,做全量全场景压测,所有场景100%覆盖,所有瓶颈优化完成,才能进入大促保障期;
- 压测结果闭环:每次压测后,输出《压测报告》,列出瓶颈点、优化方案、责任人、完成时间,跟踪优化落地,下次压测验证效果;
- 团队培训:把压测方法、瓶颈定位、优化方案整理成手册,团队全员学习,提升整体能力。
最终验收标准
做完以上所有步骤,必须达到以下标准,才算压测成功:
- 压测全程零脏数据,压测流量100%落到影子表,对真实用户无感知;
- 系统峰值支撑能力达到2万TPS,下单链路P99 RT≤300ms,下单成功率≥99.99%;
- 所有压测发现的瓶颈,都完成了优化,再次压测验证通过;
- 基于压测结果完成了精准容量规划,日常资源利用率≥30%,大促峰值CPU利用率≤75%;
- 建立了常态化压测机制,每次版本发布、大促前都做压测,保障系统长期稳定。
生产压测红线(绝对不能碰)
- 绝对不能在业务高峰期做压测,必须在低峰期(凌晨2-4点)执行;
- 绝对不能跳过小流量验证,直接全量压测,必须先确认零脏数据;
- 绝对不能没有应急预案就压测,必须有一键停止开关,出问题立即停止;
- 绝对不能不通知相关团队就压测,必须提前同步运维、DBA、依赖服务团队;
- 绝对不能压测完成后不清理数据,必须清理影子表数据,释放存储空间。

325

被折叠的 条评论
为什么被折叠?



