日均千万单物流订单系统 生产环境无损全链路压测落地操作手册

日均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. 前期准备:明确目标→梳理全链路→制定应急预案
  2. 无损底座搭建:影子库/影子表创建→流量染色全链路透传→隔离规则配置
  3. 压测平台搭建:分布式压测环境部署→压测脚本编写
  4. 1:1真实压测模型构建:场景模型→流量模型→数据模型
  5. 标准化压测执行:从测试环境→预发→生产小流量→生产全量压测
  6. 瓶颈定位与优化:TOP高频瓶颈排查方法+落地优化方案
  7. 精准容量规划:基于压测结果计算实例数→弹性扩缩容配置
  8. 常态化压测机制落地

第一部分 前期准备:压测成功的前提(压测前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:梳理全链路拓扑,形成链路清单

压测前必须搞清楚:一笔下单请求,到底经过了哪些服务、哪些中间件、哪些依赖接口,漏一个环节,压测结果就不准。

  1. 工具:用SkyWalking的「拓扑图」功能,自动生成全链路依赖关系,不用手动梳理
  2. 操作步骤
    1. 打开SkyWalking UI → 【拓扑图】→ 选择时间范围,筛选logistics-order服务
    2. 导出完整的链路拓扑图,标记出核心节点:网关→订单服务→用户/商品/地址/营销/仓储/库存/支付服务→MySQL/Redis/RocketMQ
    3. 形成《全链路接口清单》,包含每个服务的接口名称、请求方式、依赖关系、是否核心链路,示例:
      服务名称接口名称接口路径链路阶段是否核心
      订单服务创建订单/order/create下单核心链路
      库存服务预占库存/stock/preOccupy下单核心链路
      营销服务优惠计算/marketing/calc非核心链路
  3. 为什么必须做? 只有梳理清楚全链路,才能确保压测流量覆盖所有节点,不会出现「压测时依赖服务没扩容,导致瓶颈出现在下游,误以为是订单服务的问题」。
  4. 不做的隐患:漏测依赖服务,压测结果完全失真,大促时下游服务先崩,整个链路雪崩。
步骤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%以下

必须提前做的准备

  1. 压测平台必须有一键停止按钮,绝对不能分步停止施压机;
  2. 提前写好脏数据清理脚本,验证可用;
  3. 提前配置好所有非核心依赖的降级开关,验证可用;
  4. 压测前通知所有相关团队(运维、DBA、中间件、依赖服务团队),告知压测时间、范围,预留应急联系人。

第二部分 无损压测核心底座:影子库/影子表搭建(压测前2天完成)

这是生产环境无损压测的核心,决定了会不会产生脏数据,会不会影响真实用户,必须100%做对。

一、核心目标

实现压测流量与真实业务流量100%隔离,压测数据全部写入影子表,不碰真实业务数据,同时100%还原生产环境的数据库、中间件性能压力。

二、为什么选影子表方案?

对比行业主流方案,影子表是物流订单系统(分库分表+高并发)的最优解:

方案优点缺点适配性
影子表(同库)和真实表共用数据库实例,100%还原数据库CPU、IO、连接数压力;对业务代码零侵入;适配分库分表场景需要创建和真实表一一对应的影子表✅ 物流订单系统首选
影子库(独立实例)数据完全隔离,不影响真实库无法还原真实库的性能压力,压测结果失真❌ 不适合核心交易链路压测
测试账号+特殊标识操作简单会产生脏数据,占用真实表资源,影响真实用户❌ 生产环境严禁使用

三、详细操作步骤

步骤1:创建影子表,和真实表100%结构一致

影子表必须和真实表字段、索引、分区、分库分表规则、引擎、字符集完全一致,差一个索引,压测出来的SQL性能就和真实场景完全不符。

  1. 批量创建影子表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(仓库表)
    -- --------------------------
    
  2. 核心要求
    • 所有在下单链路中会写入的表,必须创建对应的影子表,一个都不能漏;
    • 分库分表的场景,每个真实库的每个真实分表,都必须对应一个影子表,比如8个订单库,每个库12个月的分表,就要创建8*12=96个订单主表影子表;
    • 影子表的索引必须和真实表完全一致,比如真实表有idx_user_id,影子表也必须有,不然压测出来的SQL性能是假的。
步骤2:配置影子路由规则,实现流量自动隔离

我们用Sharding-JDBC的影子库功能实现路由,因为你的订单系统已经用了Sharding-JDBC做分库分表,对业务代码零侵入,不用改一行业务代码,初级工程师只要改配置就行。

  1. 核心原理:Sharding-JDBC在JDBC层面拦截SQL,识别到是压测流量,就自动把SQL路由到影子表;是真实流量,就路由到真实表,完全隔离。
  2. 详细配置(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:
            # 这里保留你之前的分库分表规则,完全不用改
    
  3. 为什么这么配置?
    • 多规则兜底,哪怕一个规则失效,其他规则还能保证压测流量路由到影子表,避免脏数据;
    • 用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%不会产生脏数据,初级工程师按以下步骤执行:

  1. 准备一个压测专用用户ID:1000000000001(符合我们的前缀规则);
  2. 用这个用户ID,发起一笔下单请求;
  3. 验证步骤:
    • 第一步:查看订单服务日志,确认识别到了压测标记,isPressureTest=true
    • 第二步:查看真实订单表order_main_202601,确认没有这笔订单数据;
    • 第三步:查看影子订单表order_main_202601_shadow,确认这笔订单数据正常写入;
    • 第四步:查看下游库存、支付、营销服务的真实表,确认没有压测数据,影子表有数据;
  4. 只有所有环节都验证通过,确认零脏数据,才能进入下一步。

第三部分 压测平台搭建(压测前1天完成)

一、核心目标

搭建分布式压测平台,支持构造1:1真实流量、阶梯式加压、实时监控、一键停止,满足2万TPS的压测需求。

二、技术选型(初级工程师友好,大厂主流)

  • 压测工具:JMeter + 分布式压测(开源免费,生态完善,易上手)
  • 辅助平台:MeterSphere(开源压测平台,可视化操作,比原生JMeter更简单)
  • 施压机要求:单台4核8G,至少5台,避免压测机本身成为瓶颈

三、详细操作步骤

步骤1:分布式压测环境搭建
  1. 环境准备
    • 1台控制机(安装JMeter,用于编写脚本、控制施压机)
    • 5台施压机(安装和控制机完全相同版本的JDK17、JMeter5.6.3,关闭防火墙,和生产网络互通)
  2. 配置步骤
    1. 所有施压机修改jmeter.properties,关闭SSL,开启远程服务:
      server.rmi.ssl.disable=true
      server_port=1099
      
    2. 所有施压机启动jmeter-server服务:
      # 进入JMeter的bin目录
      cd /usr/local/apache-jmeter-5.6.3/bin
      # 后台启动服务
      nohup ./jmeter-server &
      
    3. 控制机修改jmeter.properties,配置施压机地址:
      remote_hosts=施压机1IP:1099,施压机2IP:1099,施压机3IP:1099,施压机4IP:1099,施压机5IP:1099
      server.rmi.ssl.disable=true
      
  3. 为什么用分布式压测?
    单台压测机的CPU、网络带宽有限,最多只能压5000TPS,要压2万TPS,必须用多台施压机分布式加压,不然压测机本身就会成为瓶颈,压出来的结果完全不准。
步骤2:压测脚本编写(JMeter)

核心是模拟真实用户的下单请求,初级工程师按以下结构编写:

  1. 线程组配置
    • 线程数:单台施压机设置400线程,5台施压机总共2000线程,满足2万TPS的需求
    • Ramp-Up时间:60s(阶梯式加压,避免瞬间把系统打垮)
    • 循环次数:永远(压测时手动控制停止)
  2. HTTP请求默认值
    • 配置网关地址、端口、请求头(Content-Type: application/jsonpressure-test: true
  3. 压测数据准备
    • 提前准备好压测专用的用户ID列表、SKU ID列表、地址ID列表,和生产真实数据分布一致(比如热点SKU占30%,普通SKU占70%)
    • 用JMeter的CSV Data Set Config组件,读取这些数据,实现每个请求的参数不一样,模拟真实用户
  4. 下单接口HTTP请求配置
    • 路径:/order/create
    • 请求方式:POST
    • 请求体:和真实下单请求完全一致的JSON参数,用CSV里的变量替换用户ID、SKU ID等
  5. 断言配置
    • 响应码断言:必须返回200
    • 响应内容断言:返回的code必须是200,订单号必须非空,确保下单成功
  6. 监听器配置
    • 汇总报告:查看TPS、RT、成功率、错误率
    • 查看结果树:调试时查看请求和响应详情,压测时关闭,避免影响施压机性能
步骤3:压测脚本验证
  1. 先在控制机本地运行脚本,单线程循环10次,确认所有请求都成功,数据都写入影子表,无脏数据;
  2. 远程启动1台施压机,小流量压测1分钟,确认施压机正常运行,压测数据正确;
  3. 所有验证通过后,再进行全量压测。

第四部分 1:1真实压测模型构建(压测结果准不准的核心)

很多人压测结果和生产不符,核心原因就是压测模型不对,和真实业务场景完全脱节。初级工程师必须严格按照以下要求构建模型,1:1还原生产。

一、场景模型:覆盖全业务场景,和生产占比完全一致

必须覆盖物流订单系统的所有核心场景,不能只压普通下单,每个场景的占比必须和生产完全一致,示例:

链路类型包含场景生产流量占比压测模型占比
核心链路普通订单下单、预售订单下单、订单支付、订单取消、履约发货、订单签收30%30%
查询链路订单详情查询、物流轨迹查询、收货地址查询、商品信息查询、优惠券查询65%65%
其他操作收货地址修改、订单备注修改、发票申请、退款申请5%5%
订单类型生产占比压测模型占比
普通快递订单80%80%
预售订单10%10%
逆向退款订单5%5%
大件冷链/同城急送/跨境订单5%5%

为什么必须这么做? 生产环境里,查询流量占比高达65%,会大量占用数据库连接、CPU、缓存资源,如果只压下单接口,测出来的TPS再高,大促时查询流量一上来,系统直接就崩了。

二、流量模型:还原生产的时序特征

  1. 阶梯式加压模型:从1000TPS→5000TPS→10000TPS→15000TPS→20000TPS,每个阶梯保持10分钟,观察系统在每个流量水位的性能表现,精准定位瓶颈点;
  2. 峰值突增模型:模拟大促零点,瞬间从日常266TPS突增到2万TPS,验证系统的弹性扩缩容能力,会不会出现雪崩;
  3. 长时间稳定性模型:峰值2万TPS持续压测2小时,验证系统的稳定性,会不会出现内存泄漏、GC频繁、连接池耗尽、磁盘IO打满等问题(短时间压测没问题,长时间跑可能会出问题)。

三、数据模型:和生产数据分布100%一致

压测用的数据,必须和生产真实数据的分布完全一致,不然测不出真实瓶颈,核心要求:

  1. 用户数据:压测用户ID数量≥100万,覆盖活跃用户、不活跃用户,和生产用户分布一致;
  2. 商品数据:包含热点爆品SKU(占下单量的30%)、普通SKU(70%),SKU的库存数量、分仓情况、重量体积和生产一致;
  3. 地址数据:覆盖全国所有省份、城市,一线城市、偏远地区的占比和生产一致;
  4. 订单数据:单笔订单的SKU数量,和生产一致(80%的订单是1-2个SKU,20%的订单是3个以上SKU)。

反例:如果压测用的都是单SKU订单,生产里很多是多SKU订单,多SKU订单要多次调用库存接口,锁竞争更激烈,性能更差,压测结果就会完全失真,大促时直接出问题。


第五部分 标准化压测执行流程(生产环境压测必须严格遵守)

绝对不能上来就生产全量压测,必须按以下流程逐步执行,避免出故障。

一、压测前检查清单(压测前1小时必须全部完成)

✅ 影子库/影子表全部创建完成,结构和真实表100%一致
✅ 小流量验证通过,压测流量100%写入影子表,零脏数据
✅ 压测标记全链路透传验证通过,无遗漏环节
✅ 压测脚本验证通过,所有请求正常,断言全部通过
✅ 分布式施压机全部正常启动,和控制机网络互通
✅ 全链路监控体系正常,所有核心指标可实时查看
✅ 应急预案全部准备完成,一键停止开关验证可用
✅ 所有相关团队已通知,应急联系人到位
✅ 非核心依赖的降级开关验证可用
✅ 压测时间段为业务低峰期(凌晨2-4点),对用户影响最小

二、分步执行流程

步骤1:测试环境压测(压测前3天完成)
  • 在测试环境,用同样的脚本、模型,跑一遍全量压测;
  • 验证脚本是否正常,有没有语法错误,接口是否能正常调用;
  • 初步定位测试环境的瓶颈,比如代码bug、SQL慢查询、接口逻辑问题,先优化掉,避免带到生产环境。
步骤2:预发环境压测(压测前2天完成)
  • 预发环境是和生产配置、数据完全一致的环境,先在预发环境跑全量压测;
  • 验证压测模型、脚本、影子规则是否正常,定位预发环境的瓶颈,优化完成后,再到生产环境压测。
步骤3:生产环境小流量灰度压测(压测当天凌晨2点开始)
  1. 先压100TPS,持续5分钟,验证:
    • 压测流量是否正常,有没有脏数据;
    • 真实用户的下单成功率、RT有没有受影响;
    • 系统、中间件指标是否正常。
  2. 没问题的话,逐步加到500TPS、1000TPS,每个阶梯持续5分钟,观察所有指标,确认无异常;
  3. 小流量压测完成后,确认零脏数据、真实用户无感知,再进行全量压测。
步骤4:生产环境全量阶梯式压测
  1. 按照预设的阶梯,从1000TPS→5000TPS→10000TPS→15000TPS→20000TPS,每个阶梯保持10分钟;
  2. 每个阶梯,都必须实时观察以下4类指标,出现异常立即停止加压:
    • 压测指标:TPS、RT(P99/P95/P50)、下单成功率、错误率;
    • 系统指标:所有服务的CPU、内存、JVM GC情况、线程池状态;
    • 中间件指标:MySQL的CPU、慢查询、连接数、锁等待;Redis的CPU、命中率、响应时间;RocketMQ的消息堆积、生产/消费TPS;
    • 真实用户指标:真实用户的下单成功率、RT,有没有受影响。
  3. 如果某个阶梯出现了瓶颈,比如RT飙升、成功率下降,就停止加压,保持当前流量,定位瓶颈,优化完成后,再继续加压。
步骤5:峰值稳定性压测
  • 达到目标峰值2万TPS后,持续压测2小时;
  • 重点观察系统的稳定性,有没有出现内存泄漏、GC频繁、连接池耗尽、中间件过载等问题;
  • 全程确保真实用户无感知,下单成功率≥99.99%。
步骤6:压测结束后清理工作
  1. 点击一键停止按钮,终止所有压测流量,确认所有施压机全部停止;
  2. 清理影子库/影子表里的压测数据,释放存储空间;
  3. 关闭压测相关的开关、配置,恢复系统到正常状态;
  4. 通知所有相关团队,压测结束,系统正常运行。

第六部分 物流订单系统TOP高频瓶颈定位与优化方案

结合你的案例,通过压测定位并解决了23个性能瓶颈,这里给你讲物流订单系统最常见的TOP5瓶颈,初级工程师照着步骤就能定位、就能优化。

瓶颈1:库存锁竞争激烈,库存预占接口RT飙升,TPS上不去(占比80%的大促瓶颈)

怎么定位?
  1. SkyWalking Trace瀑布图里,库存预占接口耗时占比最高,超过200ms;
  2. MySQL里执行show engine innodb status,看到大量的行锁等待;
  3. Arthas执行trace 库存服务全类名 preOccupyStock,发现库存更新的SQL执行时间很长,或者分布式锁等待时间很长。
根因

大促爆品SKU,大量用户同时下单,都要更新同一条库存记录,数据库行锁竞争激烈,或者分布式锁等待时间长,导致接口RT飙升,TPS上不去。

优化方案(落地就见效)
  1. 库存分片(最核心优化):把一个SKU的库存拆分成10个分片,比如SKU1001总库存10000,拆成10个分片,每个1000库存。下单时轮询获取分片,锁竞争从1条记录变成10条,降低90%的锁冲突;
  2. 缩小锁粒度:分布式锁只锁「库存更新」的核心代码块,不要锁整个方法,把远程调用、日志打印等逻辑移出锁范围,缩短锁持有时间;
  3. 乐观锁+分布式锁双重保障:用version版本号的乐观锁,避免数据库行锁等待,同时用分布式锁防止超卖;
  4. 热点SKU本地缓存预扣减:热点爆品的库存,提前缓存到服务本地,先本地预扣减,再异步更新到数据库,彻底降低数据库压力。
不优化的隐患

大促爆品下单时,库存接口大量超时,下单失败,甚至出现超卖,导致用户大规模投诉、资损风险。


瓶颈2:分库分表路由慢,订单写入/查询RT高

怎么定位?
  1. SkyWalking Trace里,订单写入/查询的SQL执行时间长,但SQL本身不复杂;
  2. Sharding-JDBC日志里,路由时间很长,扫描了所有8个库的所有12张表;
  3. MySQL慢查询日志里没有这条SQL,说明瓶颈不在数据库,在分库分表路由。
根因

订单查询/写入时,没有携带分片键user_id,Sharding-JDBC会扫描所有库所有表,路由时间极长;或者分片规则配置不合理,绑定表没配置,导致笛卡尔积路由。

优化方案
  1. 强制所有订单操作必须携带分片键user_id,避免全库表扫描,这是分库分表的核心红线;
  2. 配置绑定表order_mainorder_item配置为绑定表,分片键都是user_id,避免跨库关联查询;
  3. 优化分片算法:用MOD哈希分片,比INLINE表达式性能更高,路由更快;
  4. 单表数据量控制在1000万以内,超过的话增加分表数量。
不优化的隐患

订单接口RT越来越高,TPS上不去,大促时数据库连接池打满,系统雪崩。


瓶颈3:连接池打满(数据库/Redis/Feign),请求排队等待

怎么定位?
  1. SkyWalking里,接口RT很高,但方法内部执行时间很短,大部分时间在排队等待连接;
  2. 监控里,数据库连接池的活跃连接数达到了最大连接数,有大量等待线程;
  3. 日志里有大量的「连接池耗尽」「连接超时」报错。
根因

连接池的最大连接数配置太小,高峰期流量上来,连接不够用,请求排队等待;或者连接超时时间设置太长,空闲连接不释放,导致连接池耗尽。

优化方案
  1. 数据库连接池(HikariCP):按公式CPU核心数*2 + 磁盘数配置最大连接数,比如8核CPU,最大连接数设为20,不要配置太大,不然数据库扛不住;
  2. Feign连接池:配置最大连接数200,单路由最大连接数50,连接超时300ms,读取超时500ms,空闲连接60s自动释放;
  3. Redis连接池:配置最大连接数100,最小空闲连接20,空闲连接30s自动释放;
  4. 开启连接池监控,实时查看活跃连接数、等待数,提前预警。
不优化的隐患

高峰期连接池打满,请求排队超时,下单失败,服务雪崩。


瓶颈4:下单链路串行调用太多,RT太长

怎么定位?

SkyWalking Trace瀑布图里,下单链路的RPC调用是串行的,一个调用完了才调用下一个,总耗时是所有调用的耗时之和。比如先调用用户服务,再调用商品服务,再调用地址服务,再调用营销服务,串行4次调用总耗时200ms,并行的话只需要50ms。

根因

代码里把无依赖的RPC调用写成了串行,没有用并行调用,导致链路RT太长。

优化方案
  1. CompletableFuture把无依赖的RPC调用改成并行,比如用户、商品、地址、营销服务的调用,没有依赖,完全可以并行,把总耗时从200ms降到50ms;
  2. 有依赖的调用,尽量缩短串行长度,比如分仓匹配完成后,运费计算和优惠最终计算可以并行;
  3. 非核心的调用,改成异步MQ消息,不阻塞同步下单链路。
不优化的隐患

下单链路RT太长,P99超过500ms,用户体验差,高峰期超时率飙升,下单成功率下降。


瓶颈5:JVM频繁GC,STW时间长,服务卡顿

怎么定位?
  1. Prometheus监控里,Young GC每分钟超过10次,Full GC每小时超过1次,STW时间超过100ms;
  2. 接口RT忽高忽低,没有规律,GC的时候服务完全卡顿,请求超时;
  3. Arthas执行gc命令,看到堆内存使用率持续上涨,不会下降。
根因

代码里创建了大量的大对象、临时对象(比如循环里创建String对象、大集合没有释放),导致Young GC频繁;或者有内存泄漏,老年代内存越来越多,频繁Full GC。

优化方案
  1. 优化JVM参数:新生代设置为堆内存的50%,用G1收集器,设置最大停顿时间200ms,避免STW时间太长;
  2. 优化代码:避免循环里创建大量临时对象,大对象复用,用完的集合及时清空,避免内存泄漏;
  3. jmap dump堆内存,用MAT分析,找到内存泄漏的点,修复代码;
  4. 大促前,提前触发Full GC,避免大促期间出现Full GC。
不优化的隐患

高峰期频繁GC,服务卡顿,请求超时,下单成功率下降,甚至服务OOM宕机。


第七部分 精准容量规划落地

基于压测结果,做精准的容量规划,既能保障大促峰值稳定,又不浪费资源,对应你的案例:日常12台实例,大促扩容到30台,CPU利用率从22%提升到76%。

一、核心计算公式(初级工程师直接套用)

  1. 单实例最大承载TPS:压测时,单实例在CPU利用率70%时,能稳定承载的最大TPS。比如压测得出,单台订单服务实例最大承载TPS=1000。
  2. 日常需要的实例数日常峰值TPS / 单实例安全TPS(单实例最大TPS的30%)
    • 示例:日常峰值TPS=266,266/(1000*30%)≈1,为了3AZ容灾,每个AZ至少4台,总共12台,和你的案例一致。
    • 为什么用30%水位?日常要预留足够的冗余,应对突发流量、单AZ故障。
  3. 大促需要的实例数大促峰值TPS / 单实例安全TPS(单实例最大TPS的70%)
    • 示例:大促峰值TPS=20000,20000/(1000*70%)≈29,所以需要30台,和你的案例一致。
    • 为什么用70%水位?大促前已经提前扩容,预留30%的冗余,应对突发流量,同时最大化资源利用率。

二、详细操作步骤

  1. 基于压测结果,输出每个服务的《容量报表》,包含:单实例最大承载TPS、日常需要实例数、大促需要实例数、瓶颈点;
  2. 配置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分钟,避免频繁扩缩容
    
  3. 大促前预热扩容:大促前2小时,提前把实例数扩容到大促需要的80%(24台),避免大促开始时流量突增,扩容不及时导致系统被打垮;
  4. 成本优化:压测后,对资源利用率低的服务进行缩容,对瓶颈服务进行优化/扩容,平衡稳定与成本。

三、不做精准容量规划的隐患

  • 盲目扩容,日常就部署30台实例,CPU利用率只有22%,一年多花几十万服务器成本;
  • 扩容不足,大促峰值时系统扛不住,直接宕机,造成巨额营收损失。

第八部分 常态化压测机制落地

压测不是一次性的,必须常态化,才能持续保障系统稳定。

  1. 日常常态化压测:每周低峰期做一次小流量压测,验证系统性能有没有下降,新上线的代码有没有引入性能瓶颈;
  2. 版本发布压测:每次核心代码发布,都要做单接口压测,验证性能没有劣化,避免坏代码上线;
  3. 大促前全量压测:每次大促前2周,做全量全场景压测,所有场景100%覆盖,所有瓶颈优化完成,才能进入大促保障期;
  4. 压测结果闭环:每次压测后,输出《压测报告》,列出瓶颈点、优化方案、责任人、完成时间,跟踪优化落地,下次压测验证效果;
  5. 团队培训:把压测方法、瓶颈定位、优化方案整理成手册,团队全员学习,提升整体能力。

最终验收标准

做完以上所有步骤,必须达到以下标准,才算压测成功:

  1. 压测全程零脏数据,压测流量100%落到影子表,对真实用户无感知;
  2. 系统峰值支撑能力达到2万TPS,下单链路P99 RT≤300ms,下单成功率≥99.99%;
  3. 所有压测发现的瓶颈,都完成了优化,再次压测验证通过;
  4. 基于压测结果完成了精准容量规划,日常资源利用率≥30%,大促峰值CPU利用率≤75%;
  5. 建立了常态化压测机制,每次版本发布、大促前都做压测,保障系统长期稳定。

生产压测红线(绝对不能碰)

  1. 绝对不能在业务高峰期做压测,必须在低峰期(凌晨2-4点)执行;
  2. 绝对不能跳过小流量验证,直接全量压测,必须先确认零脏数据;
  3. 绝对不能没有应急预案就压测,必须有一键停止开关,出问题立即停止;
  4. 绝对不能不通知相关团队就压测,必须提前同步运维、DBA、依赖服务团队;
  5. 绝对不能压测完成后不清理数据,必须清理影子表数据,释放存储空间。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值