Java全链路压测案例

wen java案例 1

本文目录导读:

Java全链路压测案例

  1. 案例背景
  2. 压测架构设计
  3. 核心代码实现
  4. 压测执行步骤
  5. 最佳实践
  6. 完整压测代码仓库示例

下面是一个完整的Java全链路压测案例,包含架构设计、压测工具选择、代码实现和最佳实践。

案例背景

假设我们要对一个电商系统的下单流程进行全链路压测,链路包含:

用户请求 → Nginx → Spring Cloud Gateway → 用户服务 → 订单服务 → 库存服务 → 支付服务

压测架构设计

整体架构

压测架构:
  压测工具: [Apache JMeter/Gatling/自研压测平台]
  压测入口: Nginx集群(负载均衡)
  链路组件: [Gateway, 用户服务, 订单服务, 库存服务, 支付服务]
  数据支撑: [MySQL, Redis, MQ, 分布式缓存]
  监控系统: [Prometheus + Grafana + 日志中心]

核心配置示例

# application.yml 压测配置
spring:
  profiles: stress-test
server:
  tomcat:
    max-threads: 200
    min-spare-threads: 50
    accept-count: 100
  connection-timeout: 3000
stress:
  enabled: true
  mode: full-link
# JMeter压测配置
Thread Group:
  Number of Threads: 500
  Ramp-Up Period: 60
  Loop Count: 100
HTTP Request:
  Protocol: http
  Server: 10.0.0.100
  Port: 8080
  Method: POST
  Path: /api/order/create

核心代码实现

压测链路服务改造

// 用户服务 - 预留压测专用接口
@RestController
@RequestMapping("/api/user")
public class UserController {
    @PostMapping("/login")
    public Result<UserInfo> login(@RequestBody LoginRequest request) {
        // 正常业务逻辑
        return userService.login(request);
    }
    @PostMapping("/login/test")
    @StressTest (不落库,只做内存操作)
    public Result<UserInfo> loginTest(@RequestBody LoginRequest request) {
        // 压测专用,不操作数据库,只返回模拟数据
        return Result.success(mockUserInfo(request.getUsername()));
    }
}
// 订单服务 - 异步链路支持
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private InventoryService inventoryService;
    @Autowired
    private PaymentService paymentService;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public OrderResult createOrder(OrderRequest request) {
        // 判断是否为压测请求
        if (StressTestContext.isStressTest()) {
            return createMockOrder(request);
        }
        try {
            // 1. 创建订单
            Order order = buildOrder(request);
            orderMapper.insert(order);
            // 2. 扣减库存
            inventoryService.deductStock(order);
            // 3. 调用支付
            paymentService.pay(order);
            return OrderResult.success(order);
        } catch (Exception e) {
            log.error("创建订单失败", e);
            return OrderResult.fail(e.getMessage());
        }
    }
    // 压测专用:不落库,走内存队列
    private OrderResult createMockOrder(OrderRequest request) {
        Order order = buildOrder(request);
        order.setOrderNo("MOCK-" + System.currentTimeMillis());
        // 放内存队列,不落库
        mockOrderQueue.offer(order);
        return OrderResult.success(order);
    }
}
// 库存服务 - 缓存优先设计
@Service
@Slf4j
public class InventoryServiceImpl implements InventoryService {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    @Autowired
    private InventoryMapper inventoryMapper;
    @Override
    public Boolean deductStock(Order order) {
        // 压测请求直接返回成功
        if (StressTestContext.isStressTest()) {
            return true;
        }
        // 先查Redis
        String key = "stock:" + order.getProductId();
        Long stock = redisTemplate.opsForValue().decrement(key);
        if (stock < 0) {
            // 库存不足,回滚
            redisTemplate.opsForValue().increment(key);
            throw new BusinessException("库存不足");
        }
        // 异步同步到数据库
        asyncSyncStock(order.getProductId(), stock);
        return true;
    }
}
// 压测上下文工具类
@Component
public class StressTestContext {
    private static final ThreadLocal<Boolean> STRESS_TEST_FLAG = new ThreadLocal<>();
    private static final Queue<Order> MOCK_ORDER_QUEUE = new ConcurrentLinkedQueue<>();
    private static volatile boolean isStressTestMode = false;
    public static void setStressTest(boolean isStressTest) {
        STRESS_TEST_FLAG.set(isStressTest);
    }
    public static boolean isStressTest() {
        return Boolean.TRUE.equals(STRESS_TEST_FLAG.get()) || isStressTestMode;
    }
    public static void setStressTestMode(boolean mode) {
        isStressTestMode = mode;
    }
    public static void clear() {
        STRESS_TEST_FLAG.remove();
    }
}
// 过滤器 - 识别压测请求
@Component
public class StressTestFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, 
                        FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String stressTestFlag = httpRequest.getHeader("X-Stress-Test");
        if ("true".equals(stressTestFlag)) {
            StressTestContext.setStressTest(true);
            // 添加压测标记到MDC,便于日志追踪
            MDC.put("stressTest", "true");
        }
        try {
            chain.doFilter(request, response);
        } finally {
            StressTestContext.clear();
            MDC.clear();
        }
    }
}

JMeter压测脚本配置

<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.0" properties="5.0">
  <hashTree>
    <TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="电商系统全链路压测">
      <elementProp name="TestPlan.user_defined_variables" elementType="Arguments">
        <collectionProp name="Arguments.arguments">
          <elementProp name="SERVER" elementType="Argument">
            <stringProp name="Argument.name">SERVER</stringProp>
            <stringProp name="Argument.value">10.0.0.100</stringProp>
          </elementProp>
          <elementProp name="PORT" elementType="Argument">
            <stringProp name="Argument.name">PORT</stringProp>
            <stringProp name="Argument.value">8080</stringProp>
          </elementProp>
        </collectionProp>
      </elementProp>
      <hashTree>
        <!-- 线程组配置 -->
        <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="全链路压力测试">
          <stringProp name="ThreadGroup.num_threads">500</stringProp>
          <stringProp name="ThreadGroup.ramp_time">60</stringProp>
          <stringProp name="ThreadGroup.duration">300</stringProp>
          <boolProp name="ThreadGroup.scheduler">true</boolProp>
          <elementProp name="ThreadGroup.main_controller" elementType="LoopController">
            <stringProp name="LoopController.loops">-1</stringProp>
          </elementProp>
          <hashTree>
            <!-- 登录请求 -->
            <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="用户登录">
              <stringProp name="HTTPSampler.domain">${SERVER}</stringProp>
              <stringProp name="HTTPSampler.port">${PORT}</stringProp>
              <stringProp name="HTTPSampler.method">POST</stringProp>
              <stringProp name="HTTPSampler.path">/api/user/login</stringProp>
              <stringProp name="HTTPSampler.connect_timeout">5000</stringProp>
              <stringProp name="HTTPSampler.response_timeout">10000</stringProp>
              <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
                <collectionProp name="Arguments.arguments">
                  <elementProp name="request" elementType="HTTPArgument">
                    <stringProp name="Argument.name">request</stringProp>
                    <stringProp name="Argument.value">{"username":"testUser${__Random(1,1000)}","password":"123456"}</stringProp>
                    <stringProp name="Argument.metadata">=</stringProp>
                  </elementProp>
                </collectionProp>
              </elementProp>
            </HTTPSamplerProxy>
            <!-- 创建订单请求 -->
            <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="创建订单">
              <stringProp name="HTTPSampler.domain">${SERVER}</stringProp>
              <stringProp name="HTTPSampler.port">${PORT}</stringProp>
              <stringProp name="HTTPSampler.method">POST</stringProp>
              <stringProp name="HTTPSampler.path">/api/order/create</stringProp>
              <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
                <collectionProp name="Arguments.arguments">
                  <elementProp name="token" elementType="HTTPArgument">
                    <stringProp name="Argument.name">token</stringProp>
                    <stringProp name="Argument.value">${login_token}</stringProp>
                  </elementProp>
                  <elementProp name="request" elementType="HTTPArgument">
                    <stringProp name="Argument.name">request</stringProp>
                    <stringProp name="Argument.value">{"productId":1001,"quantity":1,"userId":"testUser"}</stringProp>
                  </elementProp>
                </collectionProp>
              </elementProp>
            </HTTPSamplerProxy>
            <!-- 进行链路检查 -->
            <ResponseAssertion guiclass="ResponseAssertionGui" testclass="ResponseAssertion" testname="响应断言">
              <collectionProp name="Asserion.test_strings">
                <stringProp name="49586">success</stringProp>
              </collectionProp>
              <stringProp name="Assertion.test_field">Assertion.response_message</stringProp>
            </ResponseAssertion>
          </hashTree>
        </ThreadGroup>
      </hashTree>
    </TestPlan>
  </hashTree>
</jmeterTestPlan>

监控与性能分析

// 性能指标收集器
@Aspect
@Component
public class PerformanceMonitor {
    private static final MeterRegistry meterRegistry = new SimpleMeterRegistry();
    @Pointcut("execution(* com.example.service.*.*(..))")
    public void serviceLayer() {}
    @Around("serviceLayer()")
    public Object monitor(ProceedingJoinPoint joinPoint) throws Throwable {
        Timer.Sample sample = Timer.start(meterRegistry);
        String methodName = joinPoint.getSignature().getName();
        try {
            Object result = joinPoint.proceed();
            // 记录成功率
            meterRegistry.counter("method_success_count", "method", methodName).increment();
            return result;
        } catch (Exception e) {
            // 记录失败
            meterRegistry.counter("method_failure_count", "method", methodName, "error", e.getMessage()).increment();
            throw e;
        } finally {
            sample.stop(Timer.builder("method_execution_time")
                      .tag("method", methodName)
                      .register(meterRegistry));
        }
    }
}

压测数据准备

-- 创建压测用户表
CREATE TABLE stress_test_user (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(50),
    password VARCHAR(50),
    is_stress BOOLEAN DEFAULT TRUE,
    created_time TIMESTAMP
);
-- 创建压测商品表
CREATE TABLE stress_test_product (
    id BIGINT PRIMARY KEY,
    product_name VARCHAR(100),
    price DECIMAL(10,2),
    stock INT,
    is_stress BOOLEAN DEFAULT TRUE
);
-- 初始化数据脚本
DELIMITER $$
CREATE PROCEDURE init_stress_data()
BEGIN
    DECLARE i INT DEFAULT 1;
    WHILE i <= 10000 DO
        INSERT INTO stress_test_user (username, password, is_stress)
        VALUES (CONCAT('stress_', i), MD5('123456'), TRUE);
        SET i = i + 1;
    END WHILE;
END$$
DELIMITER ;
CALL init_stress_data();

压测执行步骤

环境准备

# 启动压测环境
docker-compose -f stress-test-environment.yml up -d
# 初始化数据
mysql -h localhost -u root -p < init_data.sql
# 启动服务(去掉本地依赖)
nohup java -jar user-service.jar --spring.profiles.active=stress &
nohup java -jar order-service.jar --spring.profiles.active=stress &

执行压测

# 使用JMeter命令执行
jmeter -n -t full-link-stress-test.jmx -l results.jtl -j stress-log.log -e -o report
# 使用Gatling执行
gatling.sh -s fullLink.StressTestSimulation
# 或使用自研压测平台
curl -X POST http://stress-platform:8080/api/stress/start \
  -H "Content-Type: application/json" \
  -d '{"scenarioId": 1, "concurrency": 500, "duration": 300}'

监控指标采集

// 压测监控数据
public class StressMetrics {
    // 吞吐量
    private double requestsPerSecond;
    // 平均响应时间
    private double averageResponseTime;
    // P99响应时间
    private double p99ResponseTime;
    // 错误率
    private double errorRate;
    // CPU使用率
    private double cpuUsage;
    // 内存使用率
    private double memoryUsage;
    // 线程池状态
    private int activeThreads;
    private int queueSize;
}

最佳实践

压测隔离策略

隔离策略:
  数据库隔离: 使用独立压测数据库或压测专用表
  缓存隔离: 使用独立Redis实例
  MQ隔离: 使用独立Topic
  流量控制: 在网关层识别压测流量

常见问题处理

@Component
public class StressTestConfig {
    // 动态调整连接池
    @EventListener
    public void handleStressStart(StressStartEvent event) {
        // 调整线程池
        threadPoolConfig.setCorePoolSize(event.getConcurrency());
        threadPoolConfig.setMaxPoolSize(event.getConcurrency() * 2);
        // 调整数据库连接池
        hikariConfig.setMaximumPoolSize(event.getConcurrency());
        // 调整MQ消费者并发数
        rocketMQConsumer.setConsumeThreadNumber(event.getConcurrency());
    }
    // 压测结束恢复配置
    @EventListener
    public void handleStressEnd(StressStopEvent event) {
        threadPoolConfig.restore();
        hikariConfig.restore();
        rocketMQConsumer.restore();
    }
}

压测报告模板

# 压测报告 - 电商系统全链路压力测试
- 测试日期: 2024-01-15
- 测试环境: 生产环境3节点集群
- 压测工具: Apache JMeter 5.5
## 测试结果
### 总体指标
| 指标 | 结果 |
|------|------|
| 并发用户数 | 500 |
| 最大TPS | 980.5 |
| 平均响应时间 | 850ms |
| P95响应时间 | 1.2s |
| P99响应时间 | 2.1s |
| 错误率 | 1.2% |
### 系统资源
| 服务 | CPU使用率 | 内存使用率 | 磁盘I/O |
|------|-----------|------------|---------|
| Gateway | 65% | 70% | 45% |
| 用户服务 | 45% | 55% | 30% |
| 订单服务 | 78% | 82% | 65% |
| 库存服务 | 52% | 60% | 40% |
### 性能瓶颈分析
1. 订单服务MySQL连接池达到上限
2. 库存服务Redis读写偏高
3. Gateway线程数不足
### 优化建议
1. 增加数据库连接池到200
2. Redis主从改为集群架构
3. Gateway增加线程池和灰度控制

完整压测代码仓库示例

project/
├── pom.xml
├── src/
│   └── main/
│       ├── java/
│       │   └── com/example/
│       │       ├── controller/
│       │       ├── service/
│       │       ├── config/
│       │       ├── filter/
│       │       └── monitor/
│       └── resources/
│           ├── application.yml
│           └── stress-test.jmx
├── stress-environment/
│   └── docker-compose.yml
└── scripts/
    ├── build.sh
    ├── deploy.sh
    └── run-stress-test.sh

这个案例涵盖了Java全链路压测的完整流程,包括架构设计、代码实现、压测执行和监控分析,根据实际业务需求,可以调整具体的技术栈和配置。

抱歉,评论功能暂时关闭!