CHARLIE SAYS

查理如是说
DATE 2026-08-24
THEME
SERIES / JHIPSTER / P-241 · JHipster 项目开发

JHipster 开发 26:性能调优实战

调优最容易犯的错是凭感觉动手:加缓存、调池子、换数据库,一周忙完发现瓶颈还在原地。这篇把我们压过一个订单系统(拆分后峰值 3000 QPS)的调优过程整理成分层方法论——每一步都是”先测量、再动手、后验证”,最后合成一份 checklist。

方法论:先测量

所有调优从数据开始,JHipster 生成的可观测设施(第 20 篇)就是为此准备的:

  • Actuator + Micrometer/management/metricshttp.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.1MBdashboard 路由 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
SQLN+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 的收益别被老代码拖累。

系列导航

← 设计模式 026:访问者(Visitor) 目录 算法 027:图:AOE & 关键路径 →
← 返回文章列表