调优最容易犯的错是凭感觉动手:加缓存、调池子、换数据库,一周忙完发现瓶颈还在原地。这篇把我们压过一个订单系统(拆分后峰值 3000 QPS)的调优过程整理成分层方法论——每一步都是”先测量、再动手、后验证”,最后合成一份 checklist。
方法论:先测量
所有调优从数据开始,JHipster 生成的可观测设施(第 20 篇)就是为此准备的:
- Actuator + Micrometer:
/management/metrics看http.server.requests的 P95/P99、Hikari 池占用、JVM 内存; - 慢日志:生成的配置里打开耗时阈值日志,先抓出慢请求清单:
# application-prod.yml
server:
shutdown: graceful
management:
http:
server:
requests:
record-request-start-time: true
logging:
level:
org.springframework.web: INFO
# 慢 SQL 由 datasource proxy / hibernate session events 输出
- 链路追踪:拆分后的架构里先定位慢在哪一跳(第 20 篇的 traceId 贯穿)。
我们那次的问题清单长这样:GET /api/orders?page=0&size=20 P99 = 2.8s、库存同步接口偶发 5s+、首页首屏 6s。三类问题分别落在数据库、缓存、前端——分层各个击破。
后端:连接池
Hikari 是默认池,参数不多但都吃紧。诊断顺序:hikaricp.connections.pending > 0 说明池打满 → 先看是不是慢 SQL 占着连接不放(大概率),再考虑扩池。经验公式起点:池大小 = CPU 核数 * 2,加上压测修正:
spring:
datasource:
hikari:
maximum-pool-size: 24 # 按压测修正,不是越大越好
minimum-idle: 8
connection-timeout: 3000 # 快速失败优于排队堆积
max-lifetime: 1800000
坑:池打满的表象是超时,根因常是下面说的 N+1——先修 SQL 再动池子。
后端:Hibernate 批处理与 N+1
生成代码里最大的两个数据库杀手,一个一个抓。
N+1 检测:打开 Hibernate 统计或在测试里断言 SQL 条数(第 17 篇的 Testcontainers 测试顺手做):
// 列表页 20 订单,每单查一次客户 → 21 条 SQL
// 修复:fetch join 或 @EntityGraph
public interface OrderRepository extends JpaRepository<Order, Long> {
@EntityGraph(attributePaths = {"customer", "items"})
Page<Order> findByCustomerId(Long customerId, Pageable pageable);
}
批量写入:默认逐条 flush,saveAll 千条数据就是千次往返。开批量 + 保证有序 id:
spring:
jpa:
properties:
hibernate:
jdbc:
batch_size: 50
order_inserts: true
order_updates: true
// 序列主键要配 ID 生成器的 allocationSize 与批量对齐
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "orderSeq")
@SequenceGenerator(name = "orderSeq", sequenceName = "ord_order_seq", allocationSize = 50)
private Long id;
这两项做完,我们那个 P99 = 2.8s 的接口降到 340ms,没写一行”优化代码”——只是让 SQL 回到它该有的条数。
后端:缓存命中
JHipster 的二级缓存(第 10 篇讲了选型)上线后要看命中率而不是配了就算数:/management/metrics/hikarii 之外,ehcache/redis 的 hit ratio 走 Micrometer 暴露。我们抓到过两个典型问题:
- 缓存了频繁写的数据:库存余量每秒都在变,缓存命中率 30% 还引入脏读延迟——这种数据就不该进缓存,改为本地短 TTL + 事件失效;
- 微服务下用了本地缓存:拆分后各实例各存一份,失效不同步,出现”刷新后数据时有时无”。多实例一律切 Redis(第 10 篇的分布式缓存决策在这里兑现)。
后端:JVM 与容器内存对齐
容器里 JVM 的经典坑:默认堆按宿主机内存算,容器限额 2G 被 OOMKilled。Java 21 的容器感知已默认开启,但要显式对齐:
# 容器 limit 2Gi 时的启动参数
java -XX:MaxRAMPercentage=70.0 \
-XX:+UseG1GC -XX:MaxGCPauseMillis=200 \
-jar myapp.jar
MaxRAMPercentage 留出 30% 给元空间、线程栈、堆外——直接 -Xmx2g 顶满 limit 是被 OOMKilled 的标准姿势。GC 日志接第 20 篇的指标看板,暂停时间有回归立刻能发现。
GraalVM native:一个真实收益点。网关和无状态边缘服务用 ./mvnw -Pprod native 编译后,启动从 25s 降到 0.4s、RSS 从 900M 降到 180M——对弹性伸缩(K8s HPA 扩容冷启动)价值巨大。代价是构建慢、反射配置要维护,我们只在网关与 BFF 层用,业务服务仍 JIT。
前端:bundle 与首屏
Angular 21 的 Zoneless 默认减少了变更检测开销,但首屏的账还要自己算:
npm run build -- --configuration production
# 看 bundle 明细,圈出超预期的大块
我们抓到的问题与解法:
| 问题 | 现象 | 解法 |
|---|---|---|
| 图表库进了主 bundle | 首屏 2.1MB | dashboard 路由 loadComponent 懒加载,降到 850KB |
| 日期库全 locale 打包 | date-fns 拖 300KB | 按需 import locale(第 27 篇 i18n 呼应) |
| 实体页全量加载 | 路由模块没拆 | 生成路由本身就是 lazy 的,被我们早年手改坏了,恢复出厂 |
| 首轮渲染等待 API | 白屏 1.2s | 骨架屏 + 关键接口并行 |
OnPush + Zoneless 下注意:生成的组件已按信号(signal)写法组织,手写老代码迁过来时 ngZone 依赖要清干净,否则白瞎了 Zoneless 的收益。
数据库:索引与执行计划
慢 SQL 的终审法庭是执行计划:
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM ord_order
WHERE customer_id = $1 AND status = 'PAID'
ORDER BY created_at DESC LIMIT 20;
看到 Seq Scan + 大行数就是缺索引。加索引的正确姿势(呼应第 7 篇):走 Liquibase 变更集,且对大表用 PostgreSQL 的并发建索引避免锁表:
<changeSet id="20260824-01-idx-order-cust-status" author="charlie">
<sql dbms="postgresql">
CREATE INDEX CONCURRENTLY idx_order_cust_status
ON ord_order (customer_id, status, created_at DESC);
</sql>
</changeSet>
注意 CONCURRENTLY 不能在事务里跑,changeSet 要配 runInTransaction="false"。这条细节的完整展开见第 7 篇与第 29 篇的锁表事故。
压测验证
每轮优化用同一把尺子量,ab/wrk 两件套(工具详解见压测工具篇):
# 基线与优化后各跑一轮,固定场景
wrk -t8 -c200 -d60s -s order_list.lua https://staging.mycompany.com/api/orders
记录 P50/P95/P99、错误率、DB CPU、池占用四个数,优化前后的对比表贴进 PR。没有压测数字的性能 PR 一律不合——这是从”改完感觉快了”的玄学回到工程的唯一路径。
调优 checklist
| 层 | 动作 | 验证指标 |
|---|---|---|
| 测量 | 慢日志 + metrics + trace 基线 | 慢请求清单 Top10 |
| SQL | N+1 修复、批量写入 | SQL 条数、接口 P95 |
| 索引 | 执行计划审查 + Liquibase 加索引 | Seq Scan 消失 |
| 连接池 | pending 报警、池参数压测修正 | 池占用 < 80% |
| 缓存 | 命中率看板、失效策略复核 | hit ratio > 90%(读多写少数据) |
| JVM | 容器内存对齐、GC 看板 | 无 OOMKilled、P99 暂停 < 200ms |
| native | 边缘服务 GraalVM 化 | 启动 < 1s、RSS 降 70%+ |
| 前端 | bundle 分析、lazy route、locale 按需 | 首屏 < 1MB、LCP 达标 |
| 压测 | 同场景前后对比 | 数字进 PR |
小结
- 调优是测量驱动的循环:慢日志/指标/trace 给出清单,每项优化用压测数字验收。
- 生成代码的最大红利是两个杀手(N+1、缓存误用)都有标准修法:EntityGraph、批量参数、分布式缓存选对数据。
- 容器内存对齐(MaxRAMPercentage)与 GraalVM native(边缘服务)是回报确定的两个工程动作。
- 前端账要定期算:lazy route、locale 按需、首屏指标,Zoneless 的收益别被老代码拖累。
系列导航
- 上一篇:微服务拆分实战
- 下一篇:国际化与本地化
- 相关阅读:测量的基础设施来自第 20 篇可观测性;缓存选型见第 10 篇;索引变更的发布纪律见第 7 篇 Liquibase;上线前还要再压一轮,见第 28 篇生产 Checklist