gitbook/Android开发高手课/docs/78920.md
2022-09-03 22:05:03 +08:00

16 KiB
Raw Permalink Blame History

17 | 网络优化(下):大数据下网络该如何监控?

通过上一期的学习,我们对如何打造一个高质量的网络已经有了一个整体的认识。但是这就足够了吗?回想一下,一个网络请求从手机到后台服务器,会涉及基站、光纤、路由器这些硬件设施,也会跟运营商和服务器机房有关。

不论是基站故障、光纤被挖断、运营商挟持还是我们的机房、CDN服务商出现故障都有可能会引起用户网络出现问题。你有没有感觉线上经常突发各种千奇百怪的网络问题很多公司的运维人员每天过得胆战心惊、疲于奔命。

“善良”的故障过了一段时间之后莫名其妙就好了,“顽固”的故障难以定位也难以解决。这些故障究竟是如何产生的?为什么突然就恢复了?它们影响了多少用户、哪些用户?想要解决这些问题离不开高质量的网络,而高质量的网络又离不开强大的监控。今天我们就一起来看看网络该如何监控吧。

移动端监控

对于移动端来说我们可能会有各种各样的网络请求。即使使用了OkHttp网络库也可能会有一些开发人员或者第三方组件使用了系统的网络库。那应该如何统一的监控客户端的所有的网络请求呢

1. 如何监控网络

第一种方法:插桩。

为了兼容性考虑我首先想到的还是插桩。360开源的性能监控工具ArgusAPM就是利用Aspect切换插桩实现监控系统和OkHttp网络库的请求。

系统网络库的插桩实现可以参考TraceNetTrafficMonitor,主要利用Aspect的切面功能关于OkHttp的拦截可以参考OkHttp3Aspect它会更加简单一些因为OkHttp本身就有代理机制。

@Pointcut("call(public okhttp3.OkHttpClient build())")
public void build() {
}

@Around("build()")
public Object aroundBuild(ProceedingJoinPoint joinPoint) throws Throwable {
    Object target = joinPoint.getTarget();
    if (target instanceof OkHttpClient.Builder &&    Client.isTaskRunning(ApmTask.TASK_NET)) {
        OkHttpClient.Builder builder = (OkHttpClient.Builder) target;
        builder.addInterceptor(new NetWorkInterceptor());
    }
    return joinPoint.proceed();
}

插桩的方法看起来很好但是并不全面。如果使用的不是系统和OkHttp网络库又或者使用了Native代码的网络请求都无法监控到。

第二种方法Native Hook。

跟I/O监控一样这个时候我们想到了强大的Native Hook。网络相关的我们一般会Hook下面几个方法

  • 连接相关connect。

  • 发送数据相关send和sendto。

  • 接收数据相关recv和recvfrom。

Android在不同版本Socket的逻辑会有那么一些差异以Android 7.0为例Socket建连的堆栈如下

java.net.PlainSocketImpl.socketConnect(Native Method)
java.net.AbstractPlainSocketImpl.doConnect
java.net.AbstractPlainSocketImpl.connectToAddress
java.net.AbstractPlainSocketImpl.connect
java.net.SocksSocketImpl.connect
java.net.Socket.connect
com.android.okhttp.internal.Platform.connectSocket
com.android.okhttp.Connection.connectSocket
com.android.okhttp.Connection.connect

“socketConnect”方法对应的Native方法定义在PlainSocketImpl.c,查看makefile可以知道它们会编译在libopenjdk.so中。不过在Android 8.0整个调用流程又完全改变了。为了兼容性考虑我们直接PLT Hook内存的所有so但是需要排除掉Socket函数本身所在的libc.so。

hook_plt_method_all_lib("libc.so", "connect", (hook_func) &create_hook);
hook_plt_method_all_lib("libc.so, "send", (hook_func) &send_hook);
hook_plt_method_all_lib("libc.so", "recvfrom", (hook_func) &recvfrom_hook);
...

这种做法不好的地方在于会把系统的Local Socket也同时接管了需要在代码中增加过滤条件。在今天的Sample中我给你提供了一套简单的实现。其实无论是哪一种Hook如果熟练掌握之后你会发现它并不困难。我们需要耐心地寻找梳理清楚整个调用流程。

第三种方法:统一网络库。

尽管拿到了所有的网络调用想想会有哪些使用场景呢模拟网络数据、统计应用流量或者是单独代理WebView的网络请求。

一般来说,我们不会非常关心第三方的网络请求情况,而对于我们应用自身的网络请求,最好的监控方法还是统一网络库。不过我们可以通过插桩和Hook这两个方法监控应用中有哪些地方使用了其他的网络库而不是默认的统一网络库。

在上一期内容中,我说过“网络质量监控”应该是客户端网络库中一个非常重要的模块,它也会跟大网络平台的接入服务共同协作。通过统一网络库的方式,的确无法监控到第三方的网络请求。不过我们可以通过其他方式拿到应用的整体流量使用情况,下面我们一起来看看。

2. 如何监控流量

应用流量监控的方法非常简单一般通过TrafficStats类。TrafficState是Android API 8加入的接口用于获取整个手机或者某个UID从开机算起的网络流量。至于如何使用你可以参考Facebook一个历史比较久远的开源库network-connection-class

getMobileRxBytes()        //从开机开始Mobile网络接收的字节总数不包括Wifi
getTotalRxBytes()         //从开机开始所有网络接收的字节总数包括Wifi
getMobileTxBytes()        //从开机开始Mobile网络发送的字节总数不包括Wifi
getTotalTxBytes()         //从开机开始所有网络发送的字节总数包括Wifi

它的实现原理其实也非常简单就是利用Linux内核的统计接口。具体来说是下面两个proc接口。

// stats接口提供各个uid在各个网络接口wlan0, ppp0等的流量信息
/proc/net/xt_qtaguid/stats
// iface_stat_fmt接口提供各个接口的汇总流量信息
proc/net/xt_qtaguid/iface_stat_fmt

TrafficStats的工作原理是读取proc并将目标UID下面所有网络接口的流量相加。但如果我们不使用TrafficStats接口而是自己解析proc文件呢那我们可以得到不同网络接口下的流量从而计算出WiFi、2G/3G/4G、VPN、热点共享、WiFi P2P等不同网络状态下的流量。

不过非常遗憾的是Android 7.0之后系统已经不让我们直接去读取stats文件防止开发者可以拿到其他应用的流量信息因此只能通过TrafficStats拿到自己应用的流量信息。

除了流量信息,通过/proc/net我们还可以拿到大量网络相关的信息例如网络信号强度、电平强度等。Android手机跟iPhone都有一个网络测试模式感兴趣的同学可以尝试一下。

  • iPhone打开拨号界面输入“*3001#12345#*”,然后按拨号键。

  • Android手机打开拨号界面输入“*#*#4636#*#*”,然后按拨号键(可进入工程测试模式,部分版本可能不支持)。

为什么系统可以判断此时的WiFi“已连接但无法访问互联网”回想一下专栏第15期我给你留的课后作业

iPhone的无线网络助理、小米和一加的自适应WLAN它们在检测WiFi不稳定时会自动切换到移动网络。那请你思考一下它们是如何实现侦测如何区分是应用后台服务器出问题还是WiFi本身有问题呢

我看了一下同学们的回复大部分同学认为需要访问一个公网IP的方式。其实对于手机厂商来说根据不需要它在底层可以拿到的信息有很多。

  • 网卡驱动层信息。如射频参数可以用来判断WiFi的信号强度网卡数据包队列长度可以用来判断网络是否拥塞。

  • 协议栈信息。主要是获取数据包发送、接收、时延和丢包等信息。

如果一个WiFi发送过数据包但是没有收到任何的ACK回包这个时候就可以初步判断当前的WiFi是有问题的。这样系统可以知道当前WiFi大概率是有问题的它并不关心是不是因为我们后台服务器出问题导致的。

大网络平台监控

前面我讲了一些应用网络请求和流量的监控方法,但是还没真正回答应该如何去打造一套强大的网络监控体系。跟网络优化一样,网络监控不是客户端可以单独完成的,它也是整个大网络平台的一个重要组成部分。

不过首先我们需要在客观上承认这件事情做起来并不容易,因为网络问题会存在下面这些特点:

  • 实时性。部分网络问题过时不候,可能很快就丢失现场。

  • 复杂性。可能跟国家、地区、运营商、版本、系统、机型、CDN都有关不仅维度多数据量也巨大。

  • 链路长。整个请求链条非常长,客户端故障、网链障络、服务故障都有可能。

因此所谓的网络监控,并不能保证可以明确找到故障的原因。而我们目标是希望快速发现问题,尽可能拿到更多的辅助信息,协助我们更容易地排查问题。

下面我分别从客户端与接入层的角度出发,一起来看看哪些信息可以帮助我们更好地发现问题和解决问题。

1. 客户端监控

客户端的监控使用统网络库的方式,你可以想想我们需要关心哪些内容:

  • 时延。一般我们比较关心每次请求的DNS时间、建连时间、首包时间、总时间等会有类似1秒快开率、2秒快开率这些指标。

  • 维度。网络类型、国家、省份、城市、运营商、系统、客户端版本、机型、请求域名等,这些维度主要用于分析问题。

  • 错误。DNS失败、连接失败、超时、返回错误码等会有DNS失败率、连接失败率、网络访问的失败率这些指标。

通过这些数据我们也可以汇总出应用的网络访问大图。例如在国内无论我们去到哪里都会问有没有WiFiWiFi的占比会超过50%。这其实远远比海外高在印度WiFi的占比仅仅只有15%左右。

同样的我们分版本、分国家、分运营商、分域名等各种各样的维度,来监控我们的时延和错误这些访问指标。

由于维度太多每个维度的取值范围也很广如果是实时计算整个数据量会非常非常大。对于客户端的上报数据微信可以做到分钟级别的监控报警。不过为了运算简单我们会抛弃UV只计算每一分钟部分维度的PV。

2. 接入层监控

客户端监控的数据会比接入层更加丰富,因为有可能会出现部分数据还没到达接入层就已经被打回,例如运营商劫持的情况。

但是接入层的数据监控还是非常有必要的,主要的原因是:

  • 实时性。客户端如果使用秒级的实时上报,对用户性能影响会比较大。服务端就不会存在这个问题,它很容易可以做到秒级的监控。

  • 可靠性。如果出现某些网络问题,客户端的数据上报通道可能也会受到影响,客户端的数据不完全可靠。

那接入层应该关心哪些数据呢?一般来说,我们会比较关心服务的入口和出口流量、服务端的处理时延、错误率等。

3. 监控报警

无论是客户端还是接入层的监控,它们都是分层的。

  • 实时监控。秒级或者分钟级别的实时监控的信息会相比少一些例如只有访问量PV、错误率没有去拆分几百个上千个维度也没有独立访问用户数UV实时监控的目的是最快速度发现问题。

  • 离线监控。小时或者天级别的监控我们可以拓展出全部的维度来做监控,它的目的是在监控的同时,可以更好地圈出问题的范围。

下面是一个简单根据客户端、国家以及运营商维度分析的示例。当然更多的时候是某一个服务出现问题,这个时候通过分域名或者错误码就可以很容易的找到原因。

那在监控的同时如何实现准确的自动化报警呢?这同样也是业界的一个难题,它的难度在于如果规则过于苛刻,可能会出现漏报;如果过于宽松,可能会出现太多的误报。

业界一般存在两种报警的算法,一套是基于规则,例如失败率与历史数据相比暴涨、流量暴跌等。另一种是基于时间序列算法或者神经网络的智能化报警,使用者不需要录入任何规则,只需有足够长的历史数据,就可以实现自动报警。智能化报警目前准确性也存在一些问题,在智能化基础上面添加少量规则可能会是更好的选择。

如果我们收到一个线上的网络报警,通过接入层和客户端的监控报表,也会有了一个大致的判断。那怎么样才能确定问题的最终原因?我们是否可以拿到用户完整的网络日志?甚至远程地诊断用户的网络情况?关于“网络日志和远程诊断,如何快速定位网络问题”,我会把它单独成篇放在专栏第二模块里,再来讲讲这个话题。

总结

监控、监控又是监控,很多性能优化工作其实都是“三分靠优化,七分靠监控”。

为什么监控这么重要呢?对于大公司来说,每一个项目参与人员可能成百上千人。并且大公司要的不是今天或者这个版本可以做好一些事情,而是希望保证每天每个版本都能持续保持应用的高质量。另一方面有了完善的分析和监控的平台,我们可以把复杂的事情简单化,把一些看起来“高不可攀”的优化工作,变成人人都可以做。

最后多谈两句我的感受,我们在工作的时候,希望你可以看得更远,从更高的角度去思考问题。多想想如果我能做好这件事情,怎么保证其他人不会犯错,或者让所有人都可以做得更好。

课后作业

对于网络问题,你尝试过哪些监控方法?有没有令你印象深刻的网络故障,最终又是通过什么方式解决的呢?欢迎留言跟我和其他同学一起讨论。

今天我们练习的Sample是通过PLT Hook代理Socket相关的几个重要函数这次还增加了一个一次性Hook所有已经加载Library的方法。

int hook_plt_method_all_lib(const char* exclueLibname, const char* name, hook_func hook) {
  if (refresh_shared_libs()) {
    // Could not properly refresh the cache of shared library data
    return -1;
  }
  int failures = 0;
  for (auto const& lib : allSharedLibs()) {
      if (strcmp(lib.first.c_str(), exclueLibname) != 0) {
        failures += hook_plt_method(lib.first.c_str(), name, hook);
      }
  }
  return failures;
}

希望你通过这几次课后练习可以学会将Hook技术应用到实践当中。

欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。最后别忘了在评论区提交今天的作业,我也为认真完成作业的同学准备了丰厚的“学习加油礼包”,期待与你一起切磋进步哦。