Skip to content
第 21 章 架构 ⏱ 13 分钟阅读

第 21 章:性能调优 ​

学习目标 ​

  • 掌握性能调优的整体方法论
  • 学会用压测找瓶颈
  • 实战 CPU/内存/磁盘/网络调优
  • 避免过早优化、调优无依据的坑

一、性能调优方法论 ​

text
1. 设定目标(QPS、延迟、错误率)
2. 压测摸底(单接口 / 全链路)
3. 定位瓶颈(CPU / 内存 / IO / DB / 缓存)
4. 实施优化(代码 / 配置 / 架构)
5. 验证效果(再压测、A/B)

⚠️ 坑 1:"我觉得这里慢" → 改了一通 → 没效果。先压测,看指标,再调优。

二、JMH 微基准测试 ​

java
// 测试字符串拼接
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Thread)
public class StringConcatBenchmark {

    @Benchmark
    public String concatPlus() {
        return "a" + "b" + "c";
    }

    @Benchmark
    public String concatBuilder() {
        return new StringBuilder().append("a").append("b").append("c").toString();
    }
}
bash
mvn clean install
java -jar target/benchmarks.jar StringConcatBenchmark

三、Arthas 在线诊断 ​

bash
# 1. 启动 Arthas
java -jar arthas-boot.jar

# 2. 查看最慢的 3 个方法
dashboard

# 3. 跟踪方法调用
trace com.example.OrderService createOrder '#cost > 100'

# 4. 查看方法入参
watch com.example.OrderService createOrder '{params, returnObj, throwExp}' -x 2

# 5. 反编译查看
jad com.example.OrderService createOrder

# 6. 热更新代码(临时生效)
redefine /tmp/OrderService.class
text
常用命令
├── dashboard: 系统信息
├── thread: 线程状态
├── jvm: JVM 信息
├── trace: 方法追踪
├── watch: 观察入参返回值
├── jad: 反编译
└── redefine: 热更新

四、CPU 飙高排查 ​

bash
# 1. 找到高 CPU 进程
top -Hp <pid>

# 2. 找到高 CPU 线程(十进制 PID)
printf '%x' <pid>

# 3. 打印线程栈
jstack <pid> | grep -A 30 <hex_pid>
java
// 典型场景:死循环
public void hotLoop() {
    while (true) {       // CPU 100%
        doSomething();
    }
}

// 修复:加退出条件
public void hotLoop() {
    while (!Thread.currentThread().isInterrupted()) {
        doSomething();
    }
}
java
// 频繁 Full GC
public void createObjects() {
    for (int i = 0; i < 10000; i++) {
        list.add(new Order());   // 大量临时对象
    }
}
// 修复:对象池 / 减少分配

五、内存泄漏排查 ​

java
// 常见 1:静态集合
public class Cache {
    private static final Map<String, Object> cache = new HashMap<>();
    // 永不释放,类卸载时才释放
}

// 修复:用 WeakHashMap 或 Caffeine 带过期
java
// 常见 2:线程局部变量未清理
public void handleRequest() {
    ThreadLocal<User> tl = new ThreadLocal<>();
    tl.set(user);
    // 业务
    // ❌ 忘记 remove
}
// 修复:try-finally remove
bash
# 导出堆 dump
jmap -dump:format=b,file=heap.hprof <pid>

# 用 MAT / VisualVM 分析
# 重点看:Retained Heap 大的对象

⚠️ 坑 2:-Xmx 配太大(16G),堆 dump 文件 10G+,分析工具崩溃。生产不超过 8G。

六、数据库慢查询 ​

sql
-- 1. 开启慢查询日志
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;        -- 1 秒

-- 2. 查看慢查询
SELECT * FROM mysql.slow_log ORDER BY start_time DESC LIMIT 10;
java
// 3. EXPLAIN 分析
// 加索引
@Query("SELECT o FROM Order o WHERE o.userId = :userId AND o.status = :status")
@Index(name = "idx_user_status", columns = ["userId", "status"])
List<Order> findByUserAndStatus(@Param("userId") Long userId, @Param("status") String status);
sql
-- 4. 避免 SELECT *
SELECT * FROM order WHERE id = 1;       -- ❌
SELECT id, user_id, amount FROM order WHERE id = 1;  -- ✅

七、缓存设计 ​

java
// 1. 缓存热点数据
@Cacheable(value = "product", key = "#id")
public Product getProduct(Long id) {
    return productRepository.findById(id).orElseThrow();
}

// 2. 缓存预热
@PostConstruct
public void warmup() {
    List<Product> hotProducts = productRepository.findByHot();
    hotProducts.forEach(p -> cache.put("product:" + p.id(), p));
}

// 3. 防止缓存穿透
public Product getProduct(Long id) {
    return cache.get("product:" + id, () -> {
        return productRepository.findById(id).orElse(new Product());  // 空值也缓存
    });
}
java
// 4. 防止缓存雪崩
@Cacheable(value = "product", key = "#id", unless = "#result == null")
// TTL 加随机值
public Product getProduct(Long id) {
    Product p = productRepository.findById(id).orElse(null);
    redisTemplate.opsForValue()
        .set("product:" + id, p, Duration.ofSeconds(300 + ThreadLocalRandom.current().nextInt(60)));
    return p;
}

⚠️ 坑 3:同一个 key 多个请求无缓存,100 个请求同时打到 DB(缓存击穿)。加分布式锁或用 synchronized 限流。

八、连接池调优 ​

yaml
# HikariCP
spring:
  datasource:
    hikari:
      maximum-pool-size: 20              # 公式:2 * CPU + 有效磁盘数
      minimum-idle: 5
      connection-timeout: 30000
      idle-timeout: 600000
      max-lifetime: 1800000
      leak-detection-threshold: 60000    # 60s 没归还报警
java
// 监控连接池
@Bean
public HikariDataSource dataSource() {
    HikariDataSource ds = new HikariDataSource();
    ds.setMetricRegistry(meterRegistry);  // 暴露指标
    return ds;
}

九、Tomcat 调优 ​

yaml
server:
  tomcat:
    threads:
      max: 200                       # 业务线程,默认 200
      min-spare: 10
    accept-count: 100                # 等待队列
    max-connections: 10000
    connection-timeout: 5000
java
// 异步 servlet
@WebServlet(urlPatterns = "/api/order", asyncSupported = true)
public class OrderServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        AsyncContext ctx = req.startAsync();
        ctx.start(() -> {
            // 业务,工作线程
            try {
                resp.getWriter().write("ok");
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                ctx.complete();
            }
        });
    }
}

十、压测验证 ​

java
// JMeter 脚本或 wrk
wrk -t4 -c100 -d30s http://localhost:8080/api/order
text
压测结果
├── QPS: 1000 → 5000
├── P99: 200ms → 50ms
├── 错误率: 0.01% → 0.001%
└── CPU: 80% → 60%

本章小结 ​

阶段工具
性能测试JMeter、wrk、JMH
在线诊断Arthas、async-profiler
内存分析MAT、VisualVM
慢查询慢查询日志、EXPLAIN
优化点收益
缓存10x
异步5x
数据库5x
JVM2x

动手练习 ​

  1. JMH 基准:对比 ArrayList vs LinkedList,看场景差异
  2. Arthas 实战:用一个有性能问题的 demo,定位慢方法
  3. 缓存压测:无缓存 vs 加缓存,对比 QPS 提升
  4. 数据库优化:分析慢查询,加索引,验证效果

下一章:第 22 章:JVM 调优 →

本站基于 VitePress 构建 · 由 StackHub 团队维护