1.3 机器准备:CVM 规格与 JVM 调优

订单中心第一台生产机器是 8 核 16G 的 CVM,JVM 参数照着网上的模板抄,堆给 8G,老年代占一半。压测时发现一个奇怪现象:单机 QPS 到 800 就上不去,CPU 才 40%,GC 日志倒是刷得飞起。这一节讲我们怎么把机器和 JVM 调到匹配的。

场景:下单链路的内存画像

下单链路的内存特征很典型:短对象多、峰值集中。每个请求产生订单对象、明细对象、序列化后的 JSON 字符串、MyBatis 的 ResultSetHandler 中间结果,这些东西朝生夕灭。大促瞬间一千个并发请求同时进来,年轻代瞬间被填满,Minor GC 频率拉高,如果年轻代太小,存活对象被过早推进老年代,Full GC 就来了。

我们的画像数据:日常 Minor GC 每 3 秒一次,大促时每 300 毫秒一次,Full GC 日常一天两次,压测时十分钟三次。这个频率显然不健康。

方案:规格与参数的匹配

先定机器规格。订单中心是典型的「中等 CPU、较大内存、吃磁盘 IO」的服务,对照 CVM 的规格族:

场景 推荐规格 堆配置 备注
日常 8 万单 8 核 16G 8G 堆 年轻代 3G
大促 50 万单 16 核 32G 12G 堆 两台做负载均衡
对账批处理 8 核 16G 10G 堆 IO 优化型实例

机器是一次性投入,选规格宁可留三分余量。新用户特惠期拿的价格大概是平时的六折,我们一次买了两年:腾讯云云服务器特惠。

再定 JVM 参数。调完之后的参数和对应的理由:

参数 值 理由
-Xms / -Xmx 相同值 避免堆动态伸缩引发停顿
-Xmn 3G 下单短对象多,年轻代给足
-XX:+UseG1GC 开启 16G 堆下 G1 停顿更可控
-XX:MaxGCPauseMillis 100ms 目标停顿,配合下单超时 800ms
-XX:+HeapDumpOnOutOfMemoryError 开启 留现场

配置:启动脚本

参数调整落在启动脚本里,用环境变量注入方便不同环境切换:

JAVA_OPTS="-Xms8g -Xmx8g -Xmn3g -XX:+UseG1GC"
JAVA_OPTS="$JAVA_OPTS -XX:MaxGCPauseMillis=100"
JAVA_OPTS="$JAVA_OPTS -XX:+HeapDumpOnOutOfMemoryError"
JAVA_OPTS="$JAVA_OPTS -XX:HeapDumpPath=/data/logs/heapdump.hprof"
JAVA_OPTS="$JAVA_OPTS -Xlog:gc*:file=/data/logs/gc.log:time,uptime"
nohup java $JAVA_OPTS -jar order-center.jar > /dev/null 2>&1 &

GC 日志和 heapdump 都落到 /data/logs,这个目录同时是 CLS 采集器监听的位置,日志直接进 CLS 而不留存本地磁盘。机器上的日志配置见后面的章节,这里先埋个线。

踩坑:三次调优经历

第一次是堆不是越大越好。最早给了 12G 堆,心想内存大总没错,结果 G1 的混合回收停顿随堆增长,下单超时反而变多。调到 8G 堆加 3G 年轻代,停顿从 200ms 降到 60ms。大对象(批量对账的 List)走 G1 的 Humongous 分配,超过_region 一半就特殊处理,堆太大反而让回收成本上升。

第二次是元空间被忽视。一次发布后服务起不来,OOM 在 MetaSpace。原因是 MyBatis 动态 SQL 生成大量代理类,默认元空间无限增长但机器内存有限,当时是 ClassLoader 泄漏。加了 -XX:MaxMetaspaceSize=256m 后立显报错,也好排查。

第三次是 swap 的坑。机器内存吃紧时系统开始用 swap,JVM 的停顿会飙到秒级。后来在 CVM 控制台看到监控曲线才发现。处理办法是关掉 swap(业务内存可控的前提下),并给机器加内存告警,内存使用超过 85% 就提前扩容,不等 OOM。

另外一个小经验:压测一定要在和生产同规格的机器上做。我们在 4 核测试机上压出的 2000 QPS,换到生产 8 核机只有 700,因为测试机的 IO 和网络带宽限制完全不同。云服务器的网络收发包配额也是隐形天花板,小规格实例的 PPS 上限会先于 CPU 到达。

进阶实战:压测与容量的配比

JVM 与机器调好之后,下一个问题是「这台机器到底能扛多少」。我们的做法是定期做小规模压测,把单机容量标出来,再按流量峰值倒推需要的实例数。压测的数据直接决定 CVM 的采购计划,避免拍脑袋买多或买少。

压测档位 单机 QPS 实例数 峰值余量
日常 8 万单 800 4 台 3 倍
大促 50 万单 2000 8 台 2 倍
春晚级红包雨 4000 16 台 1.5 倍

压测时还要盯一个隐形指标:机器的网络收发包 PPS。小规格实例的 PPS 配额比 CPU 先打满的现象在高并发小报文场景特别明显,此时加 CPU 没用,只能升规格。另外监控 CVM 控制台的网卡带宽利用率,超过 60% 就要规划带宽升级,不要等打满再处理。

速查清单

机器与 JVM 调优的四条纪律:

纪律 说明
看日志再调参 GC 日志与监控先于参数调整
堆匹配对象画像 短对象多则年轻代给足
停顿匹配超时 GC 停须小于接口超时的十分之一
关闭 swap 内存告警替代 swap 兜底

小结

机器与 JVM 调优的核心是匹配而非豪华:堆大小匹配对象画像,GC 停顿匹配接口超时,规格匹配流量峰值并留三分余量。调优顺序永远是先看 GC 日志再调参数,别盲抄模板。下一节把技术栈版本和云产品的对应对齐。

常见误解:JVM 调优的三个陷阱

第一个陷阱是「调优就是调参数」。大部分性能问题不是 JVM 参数不对,而是代码本身的问题。一次线上延迟突然升高,团队花了一下午调 G1 参数毫无效果,最后定位到是一个 SQL 漏了索引。此后我们立了规矩:调 JVM 参数之前先看三样东西——慢 SQL 排行、接口耗时分布、GC 日志,确认是 GC 问题再动手。

陷阱 正确姿势
盲目调参 先定位再调
堆越大越好 匹配对象画像
加机器解决一切 先找瓶颈点

第二个陷阱是「堆越大越好」。这个观点在前面的踩坑里提过半次,值得展开。大堆的代价是回收停顿长、Humongous 对象多、Full GC 一旦发生就是灾难级停顿。订单系统的甜点区在 8 到 16G 堆,再大不如横向加实例。实例多还有一个好处:单实例故障影响面小,灰度发布也能更从容。

第三个陷阱是「加机器是解决性能问题的通用答案」。如果瓶颈是行锁或者慢 SQL,加机器毫无帮助,反而掩盖问题让根因更难查。我们的纪律是先定位瓶颈类型:CPU 型、IO 型、锁型、网络型分别对应不同解法,加实例只在 CPU 型且单实例接近瓶颈时才是正确答案。

团队协作:运行时的观测约定

机器与 JVM 调优的成果需要被看见,我们因此定了一套观测约定:每台实例启动后十秒内,关键指标必须出现在监控大盘上——GC 频率、堆使用率、线程池水位、接口 P99。约定实现简单但效果显著:新实例扩出去后,看一眼大盘就知道它是否健康接入了流量,而不是等到用户投诉。

观测项 阈值 告警
Full GC 频次 每小时超过 2 次 P2
堆使用率 超过 85% P2
线程池等待 队列超过 100 P1
接口 P99 超过 800ms P2

约定里还包括「启动自检」:实例启动时把 JVM 参数写入日志,打上构建版本号。排查问题时第一眼就能确认机器跑的是不是预期的配置。这个习惯救过一次急:某次线上异常,发现有一台机器被手工用旧 jar 启动了,版本号一对便知。可观测性不是高大上的概念,就是约定的累积。

延伸阅读