260104-JVM面试题(RT毛刺)

JVM RT 毛刺面试题完全指南

目录


一、问题背景

1.1 什么是 RT 毛刺

RT(Response Time)毛刺是指接口响应时间出现短暂的尖峰波动,表现为 P99/P95 延迟突然升高,但平均 RT 可能变化不大。

核心特征

  • ✅ 持续时间短:几秒到几分钟
  • ✅ 偶发性:不是持续高延迟
  • ✅ 影响大:P99/P95 显著升高
  • ❌ 平均 RT 可能正常

典型表现

1
2
3
4
5
正常情况:
P50: 50ms, P95: 100ms, P99: 150ms

毛刺情况:
P50: 55ms, P95: 500ms, P99: 2000ms ← P99 飙升 13 倍

1.2 RT 毛刺的影响

🔥 直接影响

  1. 用户体验恶化

    • 部分请求超时
    • 页面加载缓慢
    • 用户流失率增加
  2. SLA 不达标

    • P99 RT 超过承诺值
    • 可用性下降
    • 可能触发赔偿条款
  3. 雪崩风险

    • 调用方重试加剧压力
    • 级联故障扩散
    • 整个链路性能下降
  4. 告警疲劳

    • 频繁误报
    • On-Call 人员疲惫
    • 真实问题被忽略

1.3 常见症状

🔍 监控指标异常

  1. RT 分布异常

    1
    2
    3
    4
    P50:  正常(50ms)
    P95: 突增(100ms → 500ms)
    P99: 暴增(150ms → 2000ms)
    P999: 极端(10000ms+)
  2. 伴随指标变化

    • CPU 使用率突增
    • GC 频率增加
    • 线程池队列堆积
    • 连接池耗尽
  3. 错误率上升

    • 超时错误增加
    • 熔断器打开
    • 降级策略触发

二、排查思路

2.1 总体流程

采用四步法系统化排查:

1
2
3
确认现象 → 定位根因 → 深入分析 → 针对性修复
↓ ↓ ↓ ↓
监控大盘 GC/非GC MAT/火焰图 参数/代码/架构

每一步的核心目标

阶段 目标 关键动作
确认现象 明确问题范围 监控大盘、排除外部依赖
定位根因 确定是否 GC 引起 jstat、GC 日志分析
深入分析 找到具体代码路径 MAT、火焰图、jstack
修复 解决问题 参数调优、代码优化、架构改进

2.2 核心原则

排查原则

  1. 先外后内

    • 先排除外部依赖(DB、缓存、网络)
    • 再分析应用内部问题
  2. 数据驱动

    • 基于监控数据判断,不凭感觉
    • 保留现场证据(GC 日志、线程 dump)
  3. 分层排查

    • 从宏观到微观
    • 从现象到根因
  4. 闭环思维

    • 修复后必须压测验证
    • 灰度发布,避免线上盲调

三、确认现象阶段

3.1 监控大盘观察

首先通过监控大盘确认 RT 毛刺的具体表现。

关键指标

1. RT 百分位分布

1
2
3
4
# Prometheus 查询
histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m]))
histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))
histogram_quantile(0.50, rate(http_request_duration_seconds_bucket[5m]))

分析要点

  • P99/P95 是否突增
  • P50 是否正常(区分全局问题和局部问题)
  • 毛刺持续时间

2. QPS 变化

1
2
# Prometheus 查询
rate(http_requests_total[5m])

判断标准

  • RT 毛刺时 QPS 下跌 → 可能是处理能力下降
  • RT 毛刺时 QPS 正常 → 可能是单次请求变慢
  • RT 毛刺时 QPS 突增 → 可能是流量激增

3. 错误率

1
2
# Prometheus 查询
rate(http_requests_total{status=~"5.."}[5m]) / rate(http_requests_total[5m]) * 100

关注点

  • 超时错误(504)
  • 服务端错误(500/502/503)
  • 熔断器状态

3.2 排除外部依赖

确认是否为应用内部问题,还是外部依赖导致。

检查清单

1. 数据库

1
2
3
4
5
6
7
-- MySQL 慢查询
SHOW PROCESSLIST;
SELECT * FROM information_schema.processlist WHERE TIME > 1;

-- 连接池状态
SHOW STATUS LIKE 'Threads_connected';
SHOW STATUS LIKE 'Threads_running';

监控指标

  • DB RT P99
  • 连接池使用率
  • 慢查询数量

2. 缓存(Redis)

1
2
3
4
5
6
# Redis 慢查询
SLOWLOG GET 10

# Redis 监控
INFO stats # 查看 rejected_connections
INFO clients # 查看 connected_clients

监控指标

  • Redis RT P99
  • 连接数
  • 内存使用率

3. 网络

1
2
3
4
5
6
7
8
9
# 网络延迟
ping <target>

# TCP 连接状态
netstat -an | grep ESTABLISHED | wc -l
ss -s # 查看 socket 统计

# 网络丢包
mtr <target>

监控指标

  • 网络 RT
  • TCP 重传率
  • 连接建立时间

4. 下游服务

1
2
3
4
5
# 微服务调用链
# SkyWalking / Zipkin / Jaeger

# 检查下游服务健康状态
curl http://downstream-service/health

监控指标

  • 下游服务 RT
  • 调用成功率
  • 熔断器状态

判断方法

1
2
如果外部依赖 RT 正常 → 应用内部问题
如果外部依赖 RT 异常 → 先解决外部依赖问题

3.3 关联指标分析

综合分析多个指标,初步判断问题类型。

关联分析矩阵

RT 毛刺 CPU GC QPS 可能原因
➡️ Full GC Stop-The-World
➡️ ➡️ Young GC 频繁
➡️ ➡️ ⬇️ 线程池/连接池耗尽
➡️ ⬆️ 流量突增
➡️ ➡️ ➡️ 锁竞争、I/O 阻塞

符号说明

  • ✅:突增
  • ➡️:正常
  • ⬇️:下跌
  • ⬆️:上涨

四、定位根因阶段

4.1 GC 引起的 RT 毛刺

如果 RT 毛刺伴随 GC 频次增加,重点分析 GC 情况。

1. 查看 GC 频率

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 实时监控 GC 情况
jstat -gcutil <PID> 1000

# 输出示例
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 45.23 67.89 89.12 92.34 88.56 1234 12.345 56 45.678 58.023
↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑
S0/S1 Survivor 使用率
E Eden 使用率
O Old Gen 使用率
M Metaspace 使用率
YGC Young GC 次数
YGCT Young GC 总耗时
FGC Full GC 次数
FGCT Full GC 总耗时
GCT GC 总耗时

判断标准

  • YGC 每秒多次 → Young GC 频繁
  • FGC 每分钟多次 → Full GC 频繁
  • FGCT/GCT > 10% → GC 占用过多 CPU

2. 分析 GC 日志

开启 GC 日志

1
2
3
4
5
# JDK 8
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/path/to/gc.log

# JDK 11+
-Xlog:gc*:file=/path/to/gc.log:time,uptime:filecount=5,filesize=10M

Full GC 日志分析

1
2
3
4
5
6
7
8
9
10
11
12
13
# 正常的 Full GC
2026-01-04T10:00:00.000+0800: [Full GC (Ergonomics)
[PSYoungGen: 524288K->0K(1048576K)]
[ParOldGen: 3145728K->524288K(4194304K)] # 老年代从 3G 降到 512M
3670016K->524288K(5242880K),
2.345 secs]

# 异常的 Full GC(回收效果差)
2026-01-04T10:05:00.000+0800: [Full GC (Ergonomics)
[PSYoungGen: 524288K->0K(1048576K)]
[ParOldGen: 3670016K->3407872K(4194304K)] # 老年代从 3.5G 只降到 3.2G
4194304K->3407872K(5242880K),
5.678 secs] # 耗时过长

异常特征

  • ❌ Full GC 后老年代回收很少(< 20%)
  • ❌ Full GC 耗时过长(> 3 秒)
  • ❌ Full GC 频率过高(几分钟一次)

3. Full GC 频繁的原因

常见原因

原因 说明 验证方法
老年代使用率高 对象晋升过快 jstat 查看 O 列
Metaspace 溢出 动态类生成过多 jstat 查看 M 列
显式 System.gc() 代码主动触发 GC 日志查找 “System.gc()”
内存泄漏 对象无法回收 MAT 分析 Heap Dump
堆大小不足 物理内存限制 jmap -heap 查看堆配置

排查步骤

1
2
3
4
5
6
7
8
9
10
11
# 1. 查看堆使用情况
jmap -heap <PID>

# 2. 查看 Metaspace 使用
jstat -gc <PID> 1000 | awk '{print $7, $8}' # MC, MU

# 3. 搜索显式 GC 调用
grep "System.gc()" GC_LOG_FILE

# 4. 生成 Heap Dump 分析
jcmd <PID> GC.heap_dump /tmp/heapdump.hprof

4. Young GC 频繁的原因

常见原因

原因 说明 解决方案
新生代过小 对象快速填满 Eden 增大 -Xmn
短生命周期对象过多 大量临时对象 优化代码,减少 new
对象过早晋升 Survivor 区不足 增大 SurvivorRatio

排查步骤

1
2
3
4
5
# 查看 Young GC 频率
jstat -gcutil <PID> 1000

# 如果 YGC 列快速增长,说明 Young GC 频繁
# 结合 E(Eden)和 S0/S1(Survivor)使用率分析

4.2 非 GC 引起的 RT 毛刺

如果 RT 毛刺不伴随 GC 异常,需要从其他角度分析。

1. 线程池/连接池耗尽

症状

  • RT 毛刺时 QPS 下跌
  • 线程池队列长度突增
  • 连接池活跃连接数达到上限

排查方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 暴露线程池指标
@Component
public class ThreadPoolMetrics {

@Autowired
private MeterRegistry meterRegistry;

@Autowired
private ThreadPoolExecutor threadPoolExecutor;

@PostConstruct
public void init() {
// 活跃线程数
Gauge.builder("threadpool.active.count",
threadPoolExecutor::getActiveCount)
.register(meterRegistry);

// 队列长度
Gauge.builder("threadpool.queue.size",
() -> threadPoolExecutor.getQueue().size())
.register(meterRegistry);

// 拒绝任务数
Gauge.builder("threadpool.rejected.count",
() -> ((ThreadPoolExecutor.Statistics) threadPoolExecutor).getRejectedCount())
.register(meterRegistry);
}
}

监控指标

  • 线程池活跃线程数
  • 线程池队列长度
  • 连接池活跃连接数
  • 连接池等待队列长度

2. 锁竞争

症状

  • 线程状态为 BLOCKED
  • CPU 使用率正常
  • RT 毛刺偶发

排查方法

1
2
3
4
5
6
7
8
9
10
11
# 查看线程状态
jstack <PID> | grep "java.lang.Thread.State" | sort | uniq -c

# 输出示例
85 RUNNABLE
10 BLOCKED ← 关注 BLOCKED 线程
5 WAITING
50 TIMED_WAITING

# 查看锁竞争详情
jstack <PID> | grep "waiting to lock" -B 5 -A 10

输出示例

1
2
3
4
5
"http-nio-8080-exec-10" #42 daemon prio=5 os_prio=0 tid=0x00007f8b4c001000 nid=0x303a waiting for monitor entry [0x00007f8b2c5fe000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.example.service.OrderService.updateStock(OrderService.java:89)
- waiting to lock <0x00000000f5e8a123> (a com.example.model.Product)
at com.example.controller.OrderController.createOrder(OrderController.java:45)

3. I/O 阻塞

症状

  • Wait CPU 占比高
  • 网络/磁盘 I/O 等待时间长
  • 线程状态为 WAITING/TIMED_WAITING

排查方法

1
2
3
4
5
6
7
8
9
10
11
12
13
# 查看 I/O 等待
vmstat 1 5

# 输出示例
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
2 5 0 102400 51200 204800 0 0 100 200 5000 8000 20 5 60 15 0

wa = 15%(I/O 等待)

# 查看网络连接
netstat -an | grep ESTABLISHED | wc -l
ss -s

五、深入分析阶段

5.1 Heap Dump 分析

如果怀疑内存泄漏或对象分配异常,使用 MAT 分析 Heap Dump。

生成 Heap Dump

1
2
3
4
5
6
7
8
9
# 方法 1:jcmd(推荐)
jcmd <PID> GC.heap_dump /tmp/heapdump.hprof

# 方法 2:Arthas
java -jar arthas-boot.jar
heapdump --live /tmp/heapdump_live.hprof

# 方法 3:OOM 时自动生成
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/dumps/

MAT 分析步骤

  1. 打开 Heap Dump

    1
    File → Open Heap Dump → 选择 .hprof 文件
  2. 查看 Leak Suspects Report

    • MAT 自动标注可能的泄漏点
  3. 支配树分析

    1
    Actions → Histogram → 按 Retained Heap 排序
  4. GC Roots 分析

    1
    右键对象 → Path to GC Roots → exclude weak/soft references
  5. 定位泄漏代码

    • 通过调用栈信息定位到具体类和方法

5.2 火焰图分析

使用 async-profiler 生成火焰图,直观看到 CPU 热点和对象分配热点。

安装 async-profiler

1
2
wget https://github.com/async-profiler/async-profiler/releases/download/v2.9/async-profiler-2.9-linux-x64.tar.gz
tar -xzf async-profiler-2.9-linux-x64.tar.gz

生成 CPU 火焰图

1
2
3
4
5
6
7
cd async-profiler-2.9-linux-x64

# 采集 30 秒 CPU 数据
./profiler.sh -d 30 -f /tmp/cpu_flame.html <PID>

# 生成 SVG 格式
./profiler.sh -d 30 -f /tmp/cpu_flame.svg <PID>

生成分配火焰图

1
2
# 采集 30 秒对象分配数据
./profiler.sh -d 30 -e alloc -f /tmp/alloc_flame.html <PID>

火焰图解读

1
2
3
4
5
6
7
8
9
10
11
12
13
每个方块代表一个方法:
- 宽度:CPU 时间或分配量占比(越宽越热点)
- 高度:调用栈深度
- 颜色:随机分配(便于区分)

从上往下看:
- 顶层:入口方法
- 底层:叶子方法(实际执行代码)

从下往上看:
- 找到最宽的方块(热点)
- 向上追溯调用链
- 定位到具体业务代码

优势

  • ✅ 直观展示热点方法
  • ✅ 快速定位瓶颈代码
  • ✅ 支持离线分析
  • ✅ 开销极低(< 1%)

5.3 代码路径定位

根据分析结果,定位到具体的代码路径。

常见热点代码模式

1. 循环内创建对象

1
2
3
4
5
6
7
8
9
10
11
12
13
// ❌ 错误:循环内创建大量临时对象
for (int i = 0; i < 10000; i++) {
String key = "prefix_" + i; // 每次循环创建新字符串
Map<String, Object> map = new HashMap<>(); // 每次循环创建新 Map
map.put(key, value);
}

// ✅ 正确:复用对象
Map<String, Object> map = new HashMap<>(10000); // 预分配容量
for (int i = 0; i < 10000; i++) {
String key = "prefix_" + i;
map.put(key, value);
}

2. 复杂正则表达式

1
2
3
4
5
6
7
8
9
// ❌ 错误:回溯爆炸
String regex = "(a+)+b";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher("aaaaaaaaaaaaaaaaX");
matcher.matches(); // 可能耗时极长

// ✅ 正确:优化正则
String regex = "a+b";
Pattern pattern = Pattern.compile(regex);

3. 同步阻塞调用

1
2
3
4
5
6
7
8
9
// ❌ 错误:同步 HTTP 调用
public Response callExternalService(Request request) {
return httpClient.execute(request); // 阻塞等待
}

// ✅ 正确:异步调用
public CompletableFuture<Response> callExternalService(Request request) {
return httpClient.executeAsync(request); // 异步非阻塞
}

六、修复方案

6.1 参数层优化

调整 JVM 参数优化 GC 行为。

1. 合理设置堆大小

1
2
3
4
5
6
7
8
9
# 基础配置
-Xms4g # 初始堆大小
-Xmx4g # 最大堆大小(建议与 -Xms 相同,避免动态调整)

# G1 GC 配置
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200 # 目标暂停时间
-XX:G1HeapRegionSize=16m # Region 大小
-XX:InitiatingHeapOccupancyPercent=45 # 触发并发标记的堆占比

调优建议

  • ✅ -Xms 和 -Xmx 设置为相同值,避免堆动态调整
  • ✅ MaxGCPauseMillis 不宜过小(会导致频繁 GC)
  • ✅ 根据实际负载调整 IHOP

2. 调整新生代比例

1
2
3
4
5
6
7
8
# 方法 1:固定新生代大小
-Xmn1g # 新生代 1GB

# 方法 2:设置比例
-XX:NewRatio=2 # 老年代:新生代 = 2:1

# 方法 3:设置 Survivor 比例
-XX:SurvivorRatio=8 # Eden:Survivor = 8:1

调优建议

  • ✅ 新生代占堆的 1/3 ~ 1/2
  • ✅ 如果 Young GC 频繁,适当增大新生代
  • ✅ 如果对象过早晋升,增大 Survivor 区

3. 选用合适的 GC 算法

GC 算法选型

GC 算法 适用场景 优势 劣势
G1 堆 4GB-32GB 可预测停顿时间 配置复杂
ZGC 堆 > 32GB 超低停顿(< 10ms) JDK 11+
Parallel 吞吐量优先 简单高效 停顿时间长
CMS 低延迟(已废弃) 并发收集 碎片化、浮动垃圾

推荐配置

1
2
3
4
5
6
7
8
9
10
# G1 GC(JDK 8+ 推荐)
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:G1ReservePercent=10
-XX:ParallelGCThreads=8
-XX:ConcGCThreads=2

# ZGC(JDK 11+,大堆推荐)
-XX:+UseZGC
-XX:ConcGCThreads=2

6.2 代码层优化

优化代码减少对象分配和 CPU 消耗。

1. 减少临时对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ❌ 错误:频繁创建临时对象
public String buildMessage(User user) {
StringBuilder sb = new StringBuilder();
sb.append("Hello, ");
sb.append(user.getName());
sb.append("! Your age is ");
sb.append(user.getAge());
sb.append(".");
return sb.toString();
}

// ✅ 正确:使用 String.format 或文本块
public String buildMessage(User user) {
return String.format("Hello, %s! Your age is %d.",
user.getName(), user.getAge());
}

// ✅ 更好:JDK 15+ 文本块
public String buildMessage(User user) {
return """
Hello, %s! Your age is %d.
""".formatted(user.getName(), user.getAge());
}

2. 避免循环内 new

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// ❌ 错误:循环内创建对象
List<Result> results = new ArrayList<>();
for (Data data : dataList) {
Result result = new Result(); // 每次循环创建新对象
result.setId(data.getId());
result.setName(data.getName());
results.add(result);
}

// ✅ 正确:使用 Stream + Map
List<Result> results = dataList.stream()
.map(data -> new Result(data.getId(), data.getName()))
.collect(Collectors.toList());

// ✅ 更好:对象池(高频场景)
private static final ObjectPool<Result> resultPool =
new GenericObjectPool<>(new ResultFactory());

List<Result> results = dataList.stream()
.map(data -> {
Result result = resultPool.borrowObject();
result.setId(data.getId());
result.setName(data.getName());
return result;
})
.collect(Collectors.toList());

// 使用后归还
results.forEach(resultPool::returnObject);

3. 慎用对象池

对象池适用场景

  • ✅ 对象创建成本高(如数据库连接)
  • ✅ 对象频繁创建和销毁
  • ✅ 对象无状态或状态可重置

对象池不适用场景

  • ❌ 对象创建成本低(如 String、Integer)
  • ❌ 对象有复杂状态
  • ❌ 并发访问频繁(池本身成为瓶颈)

示例

1
2
3
4
5
6
7
8
// ✅ 适合使用对象池:数据库连接
HikariDataSource dataSource = new HikariDataSource(config);

// ❌ 不适合使用对象池:普通 DTO
// 直接 new 即可,JVM 逃逸分析会优化
DTO dto = new DTO();
dto.setId(1);
dto.setName("test");

6.3 架构层改进

从系统架构层面优化性能。

1. 引入缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// ✅ 使用 Caffeine 本地缓存
private static final Cache<String, Product> productCache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build();

public Product getProduct(Long id) {
return productCache.get(id.toString(), key ->
productRepository.findById(Long.parseLong(key))
);
}

// ✅ 使用 Redis 分布式缓存
@Autowired
private RedisTemplate<String, Product> redisTemplate;

public Product getProduct(Long id) {
String key = "product:" + id;
Product product = redisTemplate.opsForValue().get(key);

if (product == null) {
product = productRepository.findById(id);
redisTemplate.opsForValue().set(key, product, 10, TimeUnit.MINUTES);
}

return product;
}

2. 异步处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// ✅ 异步发送邮件
@Autowired
private AsyncTaskExecutor taskExecutor;

public void registerUser(User user) {
userRepository.save(user);

// 异步发送欢迎邮件
taskExecutor.execute(() -> {
emailService.sendWelcomeEmail(user.getEmail());
});
}

// ✅ 异步报表导出
@PostMapping("/export")
public String exportReport(@RequestParam String queryId) {
CompletableFuture.runAsync(() -> {
// 后台生成报表
Report report = reportGenerator.generate(queryId);

// 通知用户下载
notificationService.sendDownloadLink(queryId, report.getFileUrl());
});

return "导出任务已提交,完成后将通知您";
}

3. 限流保护

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ✅ Sentinel 限流
@SentinelResource(value = "queryProduct",
blockHandler = "handleBlock")
public Product queryProduct(Long id) {
return productService.getProduct(id);
}

public Product handleBlock(Long id, BlockException ex) {
log.warn("请求被限流: {}", id);
return getDefaultProduct(); // 返回默认值
}

// ✅ Guava RateLimiter
private static final RateLimiter rateLimiter = RateLimiter.create(1000); // 1000 QPS

public Product queryProduct(Long id) {
if (!rateLimiter.tryAcquire(1, TimeUnit.SECONDS)) {
throw new ServiceException("请求过于频繁,请稍后重试");
}
return productService.getProduct(id);
}

七、验证与预防

7.1 压测验证

所有调优必须经过压测验证,确保效果。

压测工具

工具 优势 适用场景
JMeter 功能强大,插件丰富 复杂场景压测
wrk 高性能,简单易用 HTTP 接口压测
ab Apache 自带,轻量 简单压测
Gatling Scala 编写,报告精美 CI/CD 集成

wrk 压测示例

1
2
3
4
5
6
7
8
9
10
11
12
# 基础压测
wrk -t12 -c400 -d30s http://localhost:8080/api/product/1

# 输出示例
Running 30s test @ http://localhost:8080/api/product/1
12 threads and 400 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 50.23ms 15.67ms 200.45ms 85.23%
Req/Sec 678.90 45.67 890.12 92.34%
243567 requests in 30.01s, 50.23MB read
Requests/sec: 8115.67
Transfer/sec: 1.67MB

关键指标

  • Latency Avg:平均延迟
  • Latency Stdev:延迟标准差(越小越稳定)
  • Latency Max:最大延迟(关注 P99)
  • Req/Sec:每秒请求数(QPS)

压测对比

1
2
3
4
5
6
7
8
优化前:
P50: 50ms, P95: 100ms, P99: 500ms, QPS: 5000

优化后:
P50: 45ms, P95: 80ms, P99: 150ms, QPS: 8000

提升:
P99 降低 70%,QPS 提升 60%

7.2 灰度发布

采用渐进式发布,降低风险。

发布流程

1
2
3
4
5
第 1 天:10% 节点 → 观察核心指标
↓ 无异常
第 2-3 天:30% 节点 → 继续观察
↓ 无异常
第 4-7 天:100% 节点 → 全量发布

观察指标

指标 正常范围 告警阈值
RT P99 < 200ms > 500ms
RT P95 < 100ms > 300ms
QPS 波动 < 20% 下降 > 30%
错误率 < 0.1% > 1%
GC 频率 FGC < 10 次/天 FGC > 1 次/小时

回滚预案

  • 如果 RT P99 再次出现毛刺 → 立即回滚
  • 如果错误率显著增加 → 立即回滚
  • 保留旧版本镜像至少 7 天

7.3 监控告警

建立完善的监控告警体系,提前发现潜在问题。

Prometheus 告警规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
groups:
- name: rt-alerts
rules:
# RT P99 告警
- alert: HighRTP99
expr: histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m])) > 0.5
for: 3m
labels:
severity: warning
annotations:
summary: "{{ $labels.instance }} RT P99 超过 500ms"
description: "当前 RT P99: {{ $value }}s"

# RT 毛刺检测(标准差过大)
- alert: RTJitter
expr: stddev(rate(http_request_duration_seconds_sum[5m]) / rate(http_request_duration_seconds_count[5m])[10m:]) > 0.1
for: 5m
labels:
severity: warning
annotations:
summary: "{{ $labels.instance }} RT 波动过大"
description: "RT 标准差: {{ $value }}s"

# GC 频率告警
- alert: FrequentFullGC
expr: increase(jvm_gc_collection_seconds_count{gc="G1 Old Generation"}[1h]) > 10
for: 30m
labels:
severity: critical
annotations:
summary: "{{ $labels.instance }} Full GC 过于频繁"
description: "过去 1 小时 Full GC 次数: {{ $value }}"

Grafana 看板

创建 RT 监控看板,包含:

  • RT 百分位分布(P50/P95/P99/P999)
  • QPS 趋势
  • 错误率
  • GC 频率
  • 线程池/连接池状态

八、面试回答模板

8.1 标准回答框架

面试官问:“遇到 RT 毛刺,你如何排查?”

回答框架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
我会按照「确认现象 → 定位根因 → 深入分析 → 针对性修复」四步法来处理:

第一步【确认现象】:
首先通过监控大盘观察 RT 毛刺的具体表现,重点关注 P99/P95 延迟突增情
况。同时查看关联指标:CPU 使用率、GC 频次、QPS、错误率等,初步判断
问题类型。然后排除外部依赖干扰,检查 DB、缓存、网络、下游服务的 RT
是否正常,确认是应用内部问题还是外部依赖导致。

第二步【定位根因】:
如果 RT 毛刺伴随 GC 频次增加,通过 jstat -gcutil 或 GC 日志确认是
Young GC 还是 Full GC 引起。
- 如果是 Full GC 频繁:重点检查老年代使用率、Metaspace、是否有显式
System.gc() 调用、是否存在内存泄漏。
- 如果是 Young GC 频繁:检查新生代大小是否过小、短生命周期对象是否
过多。
如果 RT 毛刺不伴随 GC 异常,则从线程池/连接池耗尽、锁竞争、I/O 阻塞
等角度分析。

第三步【深入分析】:
使用 jmap + MAT 分析堆内存,定位内存泄漏或对象分配异常。或者使用
async-profiler 抓取分配火焰图,直观看到热点代码路径。对于锁竞争问
题,使用 jstack 查看线程状态和锁等待情况。

第四步【针对性修复】:
- 参数层:合理设置堆大小(-Xms/-Xmx)、新生代比例(-Xmn)、选用合适
的 GC 算法(G1/ZGC)。
- 代码层:减少临时对象创建、避免循环内 new、慎用对象池、优化复杂正
则、改用异步处理。
- 架构层:引入缓存(Caffeine/Redis)、异步处理(CompletableFuture)、
限流保护(Sentinel/RateLimiter),从源头降低压力。

最后,所有调优必须经过压测验证(使用 wrk/JMeter),确认效果后再通
过灰度发布(10% → 30% → 100%)上线,避免线上盲调。同时配置监控告
警(RT P99 > 500ms 持续 3 分钟触发告警),形成工程化闭环。

8.2 加分项

展示深度思考

  1. 提到具体工具链

    1
    2
    3
    "我们团队使用 Prometheus + Grafana 监控 RT 指标,
    配合 Arthas 在线诊断,async-profiler 生成火焰图,
    MAT 离线分析 Heap Dump,形成完整的诊断链路。"
  2. 提到实际案例

    1
    2
    3
    4
    "我之前遇到过订单查询接口的 RT 毛刺问题,原因是
    Full GC 频繁。通过分析发现是缓存失效导致大量对象
    短时间内创建并晋升到老年代。通过增大缓存 TTL 和
    调整 G1 的 IHOP 参数解决,P99 从 2s 降到 150ms。"
  3. 提到性能权衡

    1
    2
    3
    4
    "在修复时要注意性能影响,比如:
    - 增大堆可以减少 GC 频率,但会增加 Full GC 停顿时间
    - 对象池可以减少对象创建,但增加了管理复杂度
    - 需要在稳定性和性能之间找到平衡点"
  4. 提到预防措施

    1
    2
    3
    4
    5
    "除了事后处理,我们更注重事前预防:
    - Code Review 强制检查循环内的对象创建
    - CI 集成 SonarQube 检测复杂度和潜在性能问题
    - 压测时专门监控 RT 百分位分布
    - 定期(每季度)进行性能专项排查"

参考资料

#
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×