第17章 性能监控平台搭建:自定义性能采集SDK与Grafana+Prometheus可视化
性能优化这件事,最怕的就是「盲人摸象」。你修了一个内存泄漏,结果CPU又飙上去了;你压了帧率,流量又爆了。没有数据,你根本不知道问题出在哪。
所以这一章,我们来聊聊怎么搭建一套属于自己的性能监控平台。说白了,就是给App装上「心电图」和「体温计」,再把这些数据扔到Grafana上,让你一眼看出问题。
17.1 自定义性能采集SDK的设计思路
我刚开始做性能监控时,犯过一个错:把采集逻辑直接写在业务代码里。结果上线后,采集本身反而成了性能瓶颈。嗯,这里要注意——采集SDK必须独立、轻量、低侵入。
一个合格的性能采集SDK,至少包含四个模块:
- 数据采集器:负责从系统API或框架中捞数据
- 数据聚合器:把原始数据做初步加工,比如算平均值、去噪
- 数据缓存器:防止频繁上报,先存本地
- 数据上报器:按策略把数据推送到服务端
我个人习惯把采集器设计成插件式。每个指标一个独立模块,互不干扰。这样哪天想加个「网络延迟采集」,直接插一个模块就行,不用动原有代码。
核心原则:采集SDK本身不能成为性能负担。如果采集导致卡顿,那还不如不采。
17.2 CPU、内存、FPS、流量采集实战
17.2.1 CPU采集
CPU采集其实不复杂。Android系统会在/proc/stat和/proc/[pid]/stat里暴露CPU使用情况。我一般用RandomAccessFile去读,注意别在主线程操作。
public class CpuCollector {
public float getCpuUsage() {
try {
RandomAccessFile reader = new RandomAccessFile("/proc/stat", "r");
String line = reader.readLine();
// 解析cpu总时间
String[] fields = line.split("\\s+");
long totalCpu = 0;
for (int i = 1; i < fields.length; i++) {
totalCpu += Long.parseLong(fields[i]);
}
reader.close();
// 再读一次,计算差值
Thread.sleep(100);
// ... 省略差值计算逻辑
return usage;
} catch (Exception e) {
return -1f;
}
}
}
我在项目中遇到过一个问题:某些国产ROM对/proc的读取权限做了限制。所以建议加个降级策略——如果读不到,就改用Debug.getCpuUsage()。
17.2.2 内存采集
内存采集相对简单。用Runtime.getRuntime()可以拿到Java堆内存信息。但要注意,这只能看到堆内存,看不到Native内存。
如果你想看更细的,可以用Debug.MemoryInfo。它能拿到dalvik、native、other等细分项。我一般这样写:
public MemorySnapshot collectMemory() {
MemorySnapshot snapshot = new MemorySnapshot();
Runtime runtime = Runtime.getRuntime();
snapshot.javaHeapUsed = (runtime.totalMemory() - runtime.freeMemory()) / 1024;
snapshot.javaHeapMax = runtime.maxMemory() / 1024;
Debug.MemoryInfo info = new Debug.MemoryInfo();
Debug.getMemoryInfo(info);
snapshot.nativeHeapUsed = info.getTotalPss() / 1024;
return snapshot;
}
避坑指南:我曾经在线上发现Debug.getMemoryInfo()在某些低端机上耗时超过50ms。建议加个采样频率控制,比如每5秒采一次,别每帧都采。
17.2.3 FPS采集
FPS采集,说白了就是监听Choreographer的回调。Android 4.1之后,系统每帧都会回调Choreographer.FrameCallback。我们只需要计算两次回调的时间间隔。
public class FpsCollector {
private long lastFrameTime = 0;
private int frameCount = 0;
private float fps = 0f;
public void start() {
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
if (lastFrameTime == 0) {
lastFrameTime = frameTimeNanos;
} else {
long diff = (frameTimeNanos - lastFrameTime) / 1000000;
if (diff > 0) {
fps = 1000f / diff;
}
lastFrameTime = frameTimeNanos;
}
Choreographer.getInstance().postFrameCallback(this);
}
});
}
}
你想想看,如果连续几帧的间隔都超过16ms,那用户肯定感觉到卡顿了。所以除了FPS,我还会额外统计「掉帧次数」——也就是单帧耗时超过16ms的次数。
17.2.4 流量采集
流量采集用TrafficStats类。它能拿到手机启动以来的总流量。我们只需要定时采样,然后算差值。
public class TrafficCollector {
private long lastRxBytes = 0;
private long lastTxBytes = 0;
public TrafficSnapshot collect() {
long currentRx = TrafficStats.getTotalRxBytes();
long currentTx = TrafficStats.getTotalTxBytes();
TrafficSnapshot snapshot = new TrafficSnapshot();
snapshot.rxSpeed = (currentRx - lastRxBytes) / 1024f; // KB/s
snapshot.txSpeed = (currentTx - lastTxBytes) / 1024f;
lastRxBytes = currentRx;
lastTxBytes = currentTx;
return snapshot;
}
}
注意:TrafficStats从Android 8.0开始,对非自己UID的流量读取做了限制。如果你需要统计其他App的流量,得用NetworkStatsManager,但需要PACKAGE_USAGE_STATS权限。
17.3 数据上报策略
数据采了,怎么上报?这是个大学问。我见过有人每秒钟往服务器发一次请求,结果服务器被打挂了,用户流量也爆了。
我的策略是三个字:攒、压、推。
- 攒:把多次采集的数据攒成一个批次。比如每30秒攒一批。
- 压:用Gzip压缩数据体,能省60%以上的流量。
- 推:只在WiFi下上报大包,移动网络下只上报关键指标。
具体实现上,我一般用WorkManager来做定时上报。它自带电量优化和网络状态感知。
public class ReportWorker extends Worker {
@NonNull
@Override
public Result doWork() {
List<MetricData> batch = db.metricDao().getUnreportedBatch(30);
if (batch.isEmpty()) return Result.success();
byte[] compressed = compress(serialize(batch));
boolean success = api.report(compressed);
if (success) {
db.metricDao().markReported(batch);
}
return success ? Result.success() : Result.retry();
}
}
关键点:上报失败一定要重试,但别无限重试。我一般设置最多重试3次,超过就丢弃。毕竟性能数据是时序数据,丢了旧的,新的还会来。
17.4 Grafana + Prometheus 可视化
数据上报到服务端之后,怎么展示?我推荐用Prometheus + Grafana的组合。Prometheus负责存时序数据,Grafana负责画图。
服务端需要做一个「桥接层」:接收App上报的数据,转换成Prometheus的metrics格式。Prometheus定期来拉取。
Grafana的配置其实不复杂。你只需要:
- 添加Prometheus作为数据源
- 创建Dashboard,添加Panel
- 每个Panel写一条PromQL查询语句
比如查CPU使用率的PromQL:
avg(android_cpu_usage{app="com.example.app"}) by (device_id)
查FPS掉帧率:
rate(android_fps_drop_count[5m]) / rate(android_fps_total_count[5m])
我个人习惯在Grafana上建三个Dashboard:
| Dashboard名称 | 展示内容 | 刷新频率 |
|---|---|---|
| 实时监控 | CPU、内存、FPS、流量的实时曲线 | 10秒 |
| 日报看板 | 各指标的平均值、P99、最大值 | 1小时 |
| 异常告警 | 掉帧率>10%、内存超限等异常事件 | 实时 |
17.5 整体架构图
下面这张图,是我自己项目里用的性能监控架构。你看一眼就能明白数据是怎么从App流到Grafana的。
17.6 一些实战经验
最后,分享几个我在实战中踩过的坑:
- 采集频率别太高:CPU和内存每5秒采一次就够了,FPS可以每帧都采,但上报时要做聚合。
- 注意电量消耗:我曾经因为采集太频繁,导致App耗电量增加了15%。后来加了「仅在充电时高频采集」的策略。
- 数据要带时间戳:每条数据都必须带上采集时的设备时间戳。否则在Grafana上画图时,时间轴会乱。
- 做好降级:如果服务端挂了,本地缓存要能撑至少2小时的数据。我一般用Room数据库存,上限5000条,超过就丢弃最旧的。
一个小技巧:在Grafana的Dashboard上,把「异常阈值」用红色虚线标出来。这样你一眼就能看到哪些指标越界了,不用盯着数字看。
好了,这一章的内容就到这里。性能监控平台搭建好了,你就能像医生看心电图一样,随时掌握App的健康状况。数据不会骗人,它会告诉你哪里该优化,哪里已经OK了。
公众号:蓝海资料掘金营,微信deep3321