七、应用性能分析工具
7.1 Java诊断工具全家桶
# Arthas(阿里开源,必学)
# 官方:https://arthas.aliyun.com
# 快速入门
java -jar arthas-boot.jar
# 选择Java进程
# 常用命令
dashboard # 实时面板(CPU、内存、线程)
thread # 查看线程
thread -n 5 # 查看最繁忙的5个线程
jvm # 查看JVM信息
sysprop # 查看系统属性
sysenv # 查看环境变量
# 方法监控
watch com.example.service.OrderService findById '{params,returnObj,throwExp}' -x 2
# 参数说明:{params}参数, {returnObj}返回值, {throwExp}异常
# 方法执行耗时
trace com.example.service.OrderService findById -n 5
# 方法调用统计
monitor -c 5 com.example.service.OrderService findById
# 热更新代码
jad com.example.service.OrderService # 反编译
mc -c 327a647b /tmp/OrderService.java # 编译
redefine /tmp/com/example/service/OrderService.class # 热更新
# 火焰图
profiler start
profiler stop --format html
7.2 火焰图分析
# 生成CPU火焰图
# 使用perf生成
perf record -F 99 -p <PID> -g -- sleep 60
perf script > out.perf
# 使用FlameGraph工具生成
stackcollapse-perf.pl out.perf > out.folded
flamegraph.pl out.folded > cpu-flamegraph.svg
# 使用async-profiler(推荐)
./profiler.sh -d 60 -f /tmp/flamegraph.html <PID>
火焰图解读:
X轴:采样数量(宽度越大,CPU占用越高)
Y轴:调用栈深度
颜色:随机,无特殊含义
山顶平顶:说明CPU在此函数中执行时间较长
宽度突变:说明该分支是热点路径
7.3 阿里Java诊断工具Arthas深度使用
// 场景1:定位慢方法
// 使用trace跟踪方法调用链
trace com.example.service.OrderService findById
// 输出示例:
// +--[1.234s] com.example.service.OrderService:findById()
// +--[0.001ms] com.example.dao.OrderMapper:selectById()
// +--[1.200s] com.example.service.InventoryService:checkStock()
// | +--[1.190s] com.example.client.InventoryClient:callRemote()
// +--[0.023ms] com.example.dao.OrderMapper:updateStatus()
// 场景2:监控接口QPS和响应时间
monitor -c 5 com.example.controller.OrderController *
// 场景3:查看方法参数和返回值(线上调试)
watch com.example.service.OrderService createOrder '{params[0], returnObj}' -x 3
// 场景4:定位ClassLoader问题
sc -d com.example.service.OrderService
// 场景5:查看Spring容器中的Bean
tt -t org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter invokeHandlerMethod
7.4 性能剖析工具对比
八、全链路压测
8.1 压测方法论
压测类型:
- 基准测试: 单机单接口基线
- 负载测试: 逐步加压至预期目标
- 压力测试: 寻找系统拐点
- 稳定性测试: 长时间运行验证
- 峰值测试: 模拟突发流量
- 容量规划: 确定资源与吞吐量关系
压测流程:
1. 明确目标: 预期QPS、响应时间
2. 准备数据: 脱敏生产数据、构造测试数据
3. 环境隔离: 独立压测环境或流量染色
4. 脚本开发: JMeter/Gatling脚本
5. 执行压测: 从低到高逐步加压
6. 监控分析: 实时观察系统指标
7. 瓶颈定位: 找到性能拐点
8. 优化迭代: 针对性优化
8.2 JMeter压测脚本示例
// JMeter BeanShell预处理脚本
import java.util.UUID;
// 生成唯一订单ID
vars.put("orderId", UUID.randomUUID().toString());
// 生成随机用户ID
vars.put("userId", String.valueOf(ThreadLocalRandom.current().nextInt(1, 100000)));
// 生成测试数据
String json = "{\"userId\":\"" + vars.get("userId") +
"\",\"orderId\":\"" + vars.get("orderId") +
"\",\"amount\":" + ThreadLocalRandom.current().nextInt(100, 10000) + "}";
vars.put("requestBody", json);
8.3 压测结果分析
# JMeter聚合报告关键指标
# Samples: 请求数
# Average: 平均响应时间
# Min: 最小响应时间
# Max: 最大响应时间
# Std. Dev: 标准差(波动越大性能越不稳定)
# Error %: 错误率(应<0.01%)
# Throughput: 吞吐量(QPS/TPS)
# 性能拐点判断
# 当响应时间随着并发数增加出现非线性增长时,说明达到瓶颈
# 常见拐点:CPU>80%、GC频繁、连接池满、数据库连接池满
九、混沌工程
混沌工程通过主动注入故障,验证系统的韧性。
9.1 故障注入工具
ChaosBlade(阿里开源):
安装: curl -L -O https://github.com/chaosblade-io/chaosblade/releases/download/v1.7.2/chaosblade-1.7.2-linux-amd64.tar.gz
CPU满载: blade create cpu load --cpu-percent 80
CPU释放: blade destroy <UID>
内存占用: blade create mem load --mem-percent 80
内存释放: blade destroy <UID>
网络延迟: blade create network delay --time 3000 --offset 1000 --interface eth0
网络丢包: blade create network loss --percent 30 --interface eth0
磁盘IO: blade create disk fill --path /tmp --size 1024
Java异常: blade create jvm throwCustomException --classname com.example.service --methodname getUser
9.2 混沌实验设计
实验场景:
基础设施故障:
- 宕机: 随机kill一个节点
- 网络分区: 隔离部分节点
- 时钟偏移: NTP不同步
应用层故障:
- 慢响应: 增加方法延迟
- 异常抛出: 随机抛出业务异常
- 依赖不可用: 关闭外部服务
资源耗尽:
- CPU满负载
- 内存OOM
- 磁盘写满
验收标准:
- 故障恢复时间 < 5分钟
- 错误率 < 1%
- 无数据丢失
- 熔断降级生效
十、经典故障案例复盘
10.1 案例一:Full GC频繁导致服务不可用
现象:
服务每30秒发生一次Full GC,每次停顿3-5秒
请求超时率上升,RT从50ms飙升到10秒
排查过程:
# 1. 查看GC日志
jstat -gcutil <PID> 1000
# 输出:
# S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
# 0.00 98.22 85.33 99.98 95.12 91.23 1250 15.23 150 450.32 465.55
# 老年代占用99.98%,频繁Full GC
# 2. 查看堆内存对象分布
jmap -histo:live <PID> | head -20
# 发现:
# 1: 0x00000006c0a8a0a0 30000000 1200000000 [C
# 2: 0x00000007c0b8b0b0 25000000 1000000000 java.lang.String
# 3. dump堆内存分析
jmap -dump:live,format=b,file=heap.hprof <PID>
# MAT分析发现大量String对象未被释放
根因:
// 问题代码:日志打印了大对象
public void processLargeData() {
String largeData = readLargeFile(); // 100MB数据
// 生产环境日志级别是INFO,但对象已经创建
log.debug("Processing data: {}", largeData);
}
// 解决方案
public void processLargeData() {
if (log.isDebugEnabled()) {
String largeData = readLargeFile();
log.debug("Processing data: {}", largeData);
}
}
10.2 案例二:数据库死锁导致订单服务不可用
现象:
订单创建成功率从99.99%下降到80%
数据库CPU飙升到100%
排查过程:
-- 1. 查看死锁信息
SHOW ENGINE INNODB STATUS\G
-- 死锁日志:
-- *** (1) TRANSACTION:
-- TRANSACTION 12345, ACTIVE 2 sec starting index read
-- mysql tables in use 1, locked 1
-- LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s)
-- UPDATE orders SET status = 'PAID' WHERE id = 1001
--
-- *** (2) TRANSACTION:
-- TRANSACTION 12346, ACTIVE 2 sec updating or deleting
-- mysql tables in use 1, locked 1
-- 3 lock struct(s), heap size 1136, 2 row lock(s)
-- UPDATE orders SET status = 'SHIPPED' WHERE id = 1001
--
-- *** WE ROLL BACK TRANSACTION (2)
-- 2. 分析事务隔离级别
SELECT @@transaction_isolation;
-- REPEATABLE-READ
-- 3. 查看索引使用情况
EXPLAIN UPDATE orders SET status = 'PAID' WHERE id = 1001
-- type: ALL(全表扫描!)
根因:
id字段没有索引,UPDATE操作锁全表
两个并发事务同时更新不同数据,但由于锁全表导致死锁
解决方案:
-- 1. 创建索引
ALTER TABLE orders ADD INDEX idx_id (id);
-- 2. 优化事务范围
-- 事务中只保留必要的操作,减少持锁时间
-- 3. 使用乐观锁
ALTER TABLE orders ADD COLUMN version INT DEFAULT 0;
UPDATE orders
SET status = 'PAID', version = version + 1
WHERE id = 1001 AND version = 1;