0%

面试题-线上监控的功能

页面整体

  • App选择(Android、iOS)
  • 版本选择(生产版本、测试版本)
  • 时间段(1天、1周、1个月,最多支持3个月)

对于选中的

  • App启动次数、网络请求次数(地域分布)
  • App崩溃次数,崩溃率(可选时间段)
  • ANR分析、卡顿分析
  • http请求错误率、CDN性能
  • 服务响应时间(平均0.4s 的水平)
  • Webview(响应最差的接口、http错误率最高的接口)
  • js错误类型
  • 交互信息:设备型号分布、操作系统版本、App版本(各个版本占比多少)

以上内容对照现有的APM系统界面编写

360开源的 ArgusAPM 线上监控

主要功能

ArgusAPM 主要支持如下性能指标:

  • 交互分析:分析 Activity 生命周期耗时
  • 网络请求分析:监控流量使用情况,发现并定位各种网络问题
  • 内存分析:全面监控内存的使用
  • 进程监控:针对多进程应用,统计进程启动情况,发现启动异常(耗电、存活率等)
  • 文件监控: 监控 App 私有文件大小/变化,避免私有文件过大导致的卡顿
  • 卡顿分析:监控并发现卡顿原因
  • ANR 分析: 捕获 ANR 异常

结构

主要注意有一个

数据采集

1、卡顿(block) 信息

通过使用 :

1
Looper.getMainLooper().setMessageLogging()

方法设置自定义的 Printer,监听消息的开始和消息的结束。

2、FPS

主要通过监听系统执行下一帧的回调来做到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//这个方法的使用方式
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback(){

@Override
public void doFrame(long frameTimeNanos) {

}
});

//在 ArgusAPM 中的使用:
public void doFrame(long frameTimeNanos) {
mFpsCount ++;
mFrameTimeNanos = frameTimeNanos;
if(isCanWork()) {
Choreographer.getInstance().postFrameCallback(this);
} else {
mFpsCount = 0;
}
}

在 doFrame 回调里面,每次都重新注册这个监听,当到了定时任务,就计算当前 mFpsCount 数量(总帧数),我们直到 FPS 无非就是每秒绘制的帧数,所以,我们可以计算出 FPS 的值:总帧数/时间。

3、Memory

内存收集,在 ArgusAPM 中是通过 getMemoryInfo 方法来获得的:

1
2
3
4
5
6
7

private MemoryInfo getMemoryInfo() {
// 注意:这里是耗时和耗CPU的操作,一定要谨慎调用
Debug.MemoryInfo info = new Debug.MemoryInfo();
Debug.getMemoryInfo(info);
return new MemoryInfo(ProcessUtils.getCurrentProcessName(), info.getTotalPss(), info.dalvikPss, info.nativePss, info.otherPss);
}

通过 Debug 类的相关功能,最后在Native层面实现。 Debug 这个类很有用,我们可以看下主要的方法,会有意想不到的收获。比如启动时等待调试就是调用的 waitForDebugger 方法;获取当前的虚拟机信息可以通过 getVmFeatureList 方法。此外,这个比较耗时和cpu,所以谨慎调用,需要合理降低采集的次数。

4、watchdog

WatchDogTask 做的事情和 blockTask 类似,都是卡顿检测,不过采用另外的思路,代码如下:

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
private Runnable runnable = new Runnable() {
@Override
public void run() {
if (null == mHandler) {
Log.e(TAG, "handler is null");
return;
}

mHandler.post(new Runnable() {
@Override
public void run() {
mTick++;
}
});

try {
Thread.sleep(DELAY_TIME);
} catch (InterruptedException e) {
e.printStackTrace();
}

if (TICK_INIT_VALUE == mTick) {
String stack = captureStacktrace();
saveWatchdogInfo(stack);
} else {
mTick = TICK_INIT_VALUE;
}

AsyncThreadTask.getInstance().executeDelayed(runnable, ArgusApmConfigManager.getInstance().getArgusApmConfigData().funcControl.getWatchDogIntervalTime());
}
};

主要思路是:往主线程post 一个任务,对一个变量 mTick 执行 ++ 操作,然后再当前线程中 sleep 一段时间,然后再去检测 mTick,假如主线程没有卡顿的话,那么 ++ 操作肯定会得到执行,这时候 mTick 就不会与初始值相等。如果相等就可以认为这个等待时间里面,主线程发生了卡顿,这个时候就采集数据,采集的主要是堆栈

5、ANR

发生 ANR 时都会在 data/anr 下产生 trace 文件,因此 anr 就是以 trace 文件为核心。ArgusAPM 提供了两种思路: 1)通过Fileobserver 的方式监听 data/anr 这个目录;2)定时采样方式(隔一段时间就保存一下现场)。

但是这里需要注意,如果没有权限就拿不到 trace 文件,这里并没有解决方案。

6、Net

有个 gradle 的 plugin,通过这个 gradle ,在编译 APK 的时候,读取所有的 class 文件,如果发现 Class 文件是 Okhttp 的时候,在方法里面,拿到 interceptors 字段,添加自己的 Interceptor ,这样就完成了 用 ASM 写入一段字节码,这样就可以采集 Okhttp 信息。

7、Activity

Activity 的启动常规方式不好采集,ArgusAPM 采用 Hook 的方式,这里采用的是 Hook Instrumentation 这个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private static void hookInstrumentation() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, NoSuchFieldException {
Log.e(TAG, " hookInstrumentation: ");
Class<?> c = Class.forName("android.app.ActivityThread");
Method currentActivityThread = c.getDeclaredMethod("currentActivityThread");
boolean acc = currentActivityThread.isAccessible();
if (!acc) {
currentActivityThread.setAccessible(true);
}
Object o = currentActivityThread.invoke(null);
if (!acc) {
currentActivityThread.setAccessible(acc);
}
Field f = c.getDeclaredField("mInstrumentation");
acc = f.isAccessible();
if (!acc) {
f.setAccessible(true);
}
Instrumentation currentInstrumentation = (Instrumentation) f.get(o);
Instrumentation ins = new ApmInstrumentation(currentInstrumentation);
f.set(o, ins);
if (!acc) {
f.setAccessible(acc);
}
}

hook 当前currentActivityThread 的 mInstrumentation。每次执行 Activity 的任何生命周期都会先调用到 Instrumentation 。在后续可以通过 Hook 方式将自定义的 Instrumentation 代理系统原来的 Instrumentation,这样,就能统计 Activity 中的每个 onXXX 方法执行的耗时,而没有嵌入任何代码。

8、Appstart

ArgusAPM 中采用的是 application 的启动到第一个 Activity 的创建结束,因为已经 Hook 了 Instrumentation ,因此在 Instrumentation 的 callApplicationOnCreate 方法执行时,就记录下 Application 启动的时间,然后 callActivityOnCreate 记录下第一个 Activity 的启动即可获得冷启动的启动时间。

总结

除了net 和 Activity 之外,其他的采集并不难,只是细节会非常多,所以需要很精细化的控制,降低对 App 的影响。

以上内容参考自简书上的博客csdn上的博客

关于线上监控,还可以参考爱奇艺的 xCrash框架,介绍如下:

xCarash源码解析

xCrash 整体分为两部分: 运行于崩溃的App进程楼内的部分、以及独立进程的部分(称为dumper)

  • App 进程内的部分分为 Java 部分 和 Native 部分,前者主要用于 Java 层的崩溃捕捉,后者用于在Native 负责信号捕捉
  • Dumper 独立进程是纯 Native 实现,主要用于负责凤奎进程中线程相关数据的收集和控制。

Java层的 Exception 捕捉

在 Java 层主要是还是通过 Java 自身提供的方法来捕捉,只需要自己自定义 UncaughtExceptionHandler 即可,在 xCrash 中定义了一个 JavaCrashHandler 类专门用来干这个事情,之后,在里面实现了注册捕捉:

1
Thread.setDefaultUncaughtExceptionHandler(this);

针对 ANR 的捕捉

ANR 捕捉主要是用 AnrHandler 实现,主要就是针对 /data/anr 路径的监听,但是,这只适合低版本的手机,在高版本(Android 版本 5.0 以上)的手机上,应用已经访问不到 /data/anr 目录了。xCrash 是怎么实现呢?实际上它捕获了 SIGQUIT 信号,其原理是:Android App 发生 ANR 时, AMS 会向 App 发送 SIGQUIT 信号!

当接在 Native 层收到 SIGQUIT 信号时,就开始 dump 现场信息。

Native 层的 Exception 捕捉

在Native 崩溃发生时,生成 tombstone 文件(与官方的格式兼容),方便查看

以上内容参考自简书的博客、以及爱奇艺技术产品团队

谢谢你的鼓励