第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的配置其实不复杂。你只需要:

  1. 添加Prometheus作为数据源
  2. 创建Dashboard,添加Panel
  3. 每个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的。

性能监控平台整体架构 Android App CPU采集器 内存采集器 FPS采集器 流量采集器 数据聚合 + 本地缓存 Gzip上报 服务端桥接层 HTTP接收接口 数据解压 & 解析 格式转换 暴露 /metrics 端点 拉取 Prometheus 时序数据库 告警规则 查询 Grafana Dashboard 告警通知 数据流:App采集 → 压缩上报 → 桥接层转换 → Prometheus存储 → Grafana展示 App端 服务端 存储 可视化

17.6 一些实战经验

最后,分享几个我在实战中踩过的坑:

  • 采集频率别太高:CPU和内存每5秒采一次就够了,FPS可以每帧都采,但上报时要做聚合。
  • 注意电量消耗:我曾经因为采集太频繁,导致App耗电量增加了15%。后来加了「仅在充电时高频采集」的策略。
  • 数据要带时间戳:每条数据都必须带上采集时的设备时间戳。否则在Grafana上画图时,时间轴会乱。
  • 做好降级:如果服务端挂了,本地缓存要能撑至少2小时的数据。我一般用Room数据库存,上限5000条,超过就丢弃最旧的。

一个小技巧:在Grafana的Dashboard上,把「异常阈值」用红色虚线标出来。这样你一眼就能看到哪些指标越界了,不用盯着数字看。

好了,这一章的内容就到这里。性能监控平台搭建好了,你就能像医生看心电图一样,随时掌握App的健康状况。数据不会骗人,它会告诉你哪里该优化,哪里已经OK了。


公众号:蓝海资料掘金营,微信deep3321