美文网首页ios安卓
APP网络监控-技术分享

APP网络监控-技术分享

作者: coder_feng | 来源:发表于2021-07-12 16:09 被阅读0次

    1.背景

    随着业务的发展,用户对于网络的依赖场景会越来越多,随之而来遇到的各种异常网络场景也越来越多。
    
    1、为了保证网络的接口的持续健康、及时发现问题,为网络性能优化提供数据基础。
    
    2、同时以报表的形式直观的去展现现有网络质量。
    
    3、也为了更好的支撑后续业务的发展,就需要我们去搭建起相应的网络监控体系。
    

    二、目标

    提供一套完整的网络采集、监控和预警的可视化机制,用于线上接口可用性和健康度观察,并提供一系列排查问题的辅助信息。

    通过仪表盘可视化呈现网络质量,包括但不限于以下能力:

    • 总体请求成功率
    • 过滤单个请求成功率、失败错误码
    • 查看接口请求详情
    • 接口访问平均耗时、时长分布
    • 通过 traceId 实现从客户端请求的发起到最终具体服务器的处理返回全链路追踪。

    三、总体方案

    网络接口监控架构.png

    不管是 iOS 还是 Android ,两者最终要做的目标方案如上图。这里从下到上分别阐述每个部分的功能:

    网络基础库:针对平台特性,这里有 iOS 的 AFnetworking、Alamofire 和 Android 的 okhttp3、okhttp4 ,其实现原理应该都差不多,都是针对底层的网络api进行进一步的封装,提高接口的易用性。

    拦截器:主要是针对各个基础网络库进行接口拦截。根据平台不同,iOS 主要使用 NSProtocol + Hook,Android 使用 Aspect 。

    网络封装库:一般开发过程都会针对基础的网络库再做二次封装,加入一些策略、缓存、安全校验等管理,使其更加贴合业务和快速接入使用。

    插件/功能模块:以插件化的形式提供额外的网络功能

    统计模块:将从业务开始调用到回调给业务方的各个环节的耗时及状态值,变成统计数据汇报到APM。
    网络诊断模块:对关键业务进行诊断,包括dns解析、ping、弱网检测等,输出诊断报告并上报到APM。
    重试模块:根据策略进行重试,包括 ip 重试、https 降级重试、原 url 重试等。
    httpdns模块:提供 httpdns 能力,解决域名劫持问题。
    上传模块:提供上传能力,包括断点续传、分片上传以及包体大小、上传耗时等信息监控。
    下载模块:提供下载能力,包括大文件下载、断点续传以及包体大小、下载耗时等信息监控。
    mock 模块:提供 mock 能力,主要用于测试和后台接口还没有准备好的情况下使用。
    对外接口层:这一层直接对接上层业务。

    四、具体实现

    1)请求方式

    iOS 常用的第三方网络 AFNetworking、Alamofire 基本都是基于 NSURLConnection 或者是 NSURLSession 的封装,其中 NSURLConnection 是比较旧的使用方式了,而 NSURLSession 则是比较新的也是比较被推荐的使用方式。

    2)底层原理

    在使用 NSURLConnection 和 NSURLSession 进行网络请求的时候,实际上走的都是更底层的 URL Loading System,URL Loading System 使用标准协议 https 或者自定义协议访问标识资源,本身支持 http,https,文件,ftp 和数据协议。

    可以通过继承 NSURLProtocol 实现一个自定义的 Protocol,然后调用 registerClass:方法注册到 URL Loading System 中去,这样 NSURLConnection、NSURLSession 或者是 NSURLDownload 在使用 NSURLRequest 初始化一个连接的时候,URL Loading System 就会

    将按照注册时的相反顺序询问每个注册的类,询问到第一个 +canInitWithRequest: 方法返回 YES 的时候则使用该类去处理请求。

    • NSURConnection 中,直接调用 registerClass:方法注册我们自己的协议即可。
    • NSURLSession 中,如果是通过 [NSURLSession sharedSession] 初始化创建网络请求,调用 registerClass:即可,如果是通过 configurantion 来初始化,则通过修改 configuration 的 protocolClasses 属性,把自定义类插入到该数组的前面,确保我们的自定义的协议能够优先处理到网络请求。

    可以看到 OHHTTPStubs 开源库在注册子类的时候也是这样处理的

    + (void)setEnabled:(BOOL)enable forSessionConfiguration:(NSURLSessionConfiguration*)sessionConfig
    {
        // Runtime check to make sure the API is available on this version
        if ([sessionConfig respondsToSelector:@selector(protocolClasses)]
            && [sessionConfig respondsToSelector:@selector(setProtocolClasses:)])
        {
            NSMutableArray * urlProtocolClasses = [NSMutableArray arrayWithArray:sessionConfig.protocolClasses];
            Class protoCls = HTTPStubsProtocol.class;
            if (enable && ![urlProtocolClasses containsObject:protoCls])
            {
                // 将自己的 NSURLProtocol 插入到 protocolClasses 的第一个,进行拦截
                [urlProtocolClasses insertObject:protoCls atIndex:0];
            }
            else if (!enable && [urlProtocolClasses containsObject:protoCls])
            {
                // 拦截完成后移除
                [urlProtocolClasses removeObject:protoCls];
            }
            sessionConfig.protocolClasses = urlProtocolClasses;
        }
        else
        {
            NSLog(@"[OHHTTPStubs] %@ is only available when running on iOS7+/OSX9+. "
                  @"Use conditions like 'if ([NSURLSessionConfiguration class])' to only call "
                  @"this method if the user is running iOS7+/OSX9+.", NSStringFromSelector(_cmd));
        }
    }
    
    3)实现步骤

    利用 Objc 运行时 hook 掉 NSURLSessionConfiguration 的 defaultSessionConfiguration 属性和 ephemeralSessionConfiguration 属性设置,然后修改 configuration 的 protocolClassess 属性,插入我们自定义的 Protocol
    在自定义的 NSURLProtocol 之类中实现如下方法:
    + canInitWithRequest: 在这里判断当前网络请求是否需要监控,如果不需要直接 return NO 即可。

          + canonicalRequestForRequest:   生成一个新的 request 请求,同时标识该请求已经处理过,防止死循环。
    
          - startLoading  将新的请求发送出去,设置对应的回调代理。
    
          - stopLoading   停止网络请求。
    
     3. 处理请求回调,实现需要进行处理的回调方法,处理完成后通过 self.client.urlProtocol 将回调方法传回至原来的 delegate。
    
     4. 至此,我们就完成了发送、接收等一系列操作,并且完美的将回调转发回了原来的代理方,剩下的就是我们在回调中收集完了请求的各种信息就好。
    

    流程图如下:


    网络拦截前后对比图.png
    4)可能存在的问题及优化

    流程并不复杂,从上图可以看到,使用了网络拦截之后的流程图比原本的多了一个 custom protocol(DLURLProtocol)和 custom session。custom potocol 用于拦截网络请求,custom session 用于发起新的请求。

    这里可能会存在两个问题:

    每个请求都会新创建一个 NSURLSession,对于网络请求这种很频繁的操作来说不是很友好;
    新创建的 NSURLSession 如何确保超时、缓存、认证、cookies 等策略跟原始的 NSURLSession 保持一致,如果不一致是否会影响到既有的网络请求?

    五、风险评估

    针对可能存在的问题做相关梳理和验证~

    关于第一点:每个请求都会创建一个 NSURLSession 这个很好解决,使用一个单例即可,从苹果的官方Demo CustomHTTPProtocol 中可以看到 Demux 这个类,通过阅读源码知道,该类的存在除了最大化复用 Session 之外,还将请求的发起和回调都放到了这个类进行处理,确保请求发起和回调都是在同一个线程和 Runloop Mode,至于为什么要这么做,文档中没有找到明确说明,不过后面踩坑的时候才发现,如果不这么做的话,在回调里面很容易就会遇到崩溃的情况,尽管你什么都没有做。

    至于第二点:新创建的 NSURLSession 是否会影响到原来的网络请求策略?

    思考:

    根据苹果提供的Demo CustomHTTPProtocol 中可以看到,同样也是通过新创建一个 NSURLSession 发起请求的,那么它难道不会出现超时、缓存、认证等参数和原始请求不一致的情况么?

    从逻辑上来说,要么就是要获取原始请求的 session,拿到对应的超时、缓存、认证等配置信息再发起请求;要么就是 Demux 中新创建的 session 对于请求发起方来说是透明的,这种透明包括不影响任何原始请求的参数配置!

    针对以上猜想做相关验证:

    5.1、超时验证:

    验证1:原始请求设置超时为 5s,Demux 设置超时时间为 60s,手机网络设置为100% lost

    验证结果:5s 触发超时

    `2021``-``03``-``28` `18``:``44``:``11.307007``+``0800` `NSURLProtocolTest[``36460``:``8443172``] start a request...`
    
    `2021``-``03``-``28` `18``:``44``:``16.381879``+``0800` `NSURLProtocolTest[``36460``:``8443172``] Task <CDCBC81D-E0C5-4A52-BDBF-5912AC0BCA32>.<``1``> finished with error [-``1001``] Error Domain=NSURLErrorDomain Code=-``1001` `"The request timed out."` `UserInfo={_kCFStreamErrorCodeKey=-``2102``, NSUnderlyingError=``0x2836d19e0` `{Error Domain=kCFErrorDomainCFNetwork Code=-``1001` `"(null)"` `UserInfo={_kCFStreamErrorCodeKey=-``2102``, _kCFStreamErrorDomainKey=``4``}}, _NSURLErrorFailingURLSessionTaskErrorKey=LocalDataTask <CDCBC81D-E0C5-4A52-BDBF-5912AC0BCA32>.<``1``>, _NSURLErrorRelatedURLSessionTaskErrorKey=(`
    
    `"LocalDataTask <CDCBC81D-E0C5-4A52-BDBF-5912AC0BCA32>.<1>"`
    
    `), NSLocalizedDescription=The request timed out., NSErrorFailingURLStringKey=https:``//www.baidu.com, NSErrorFailingURLKey=[https://www.baidu.com](https://www.baidu.com/), _kCFStreamErrorDomainKey=4}`
    
    
    • 验证2: 调用发设置超时时间为 60s,Demux 设置超时时间为 5s,手机网络设置为 100% lost

    验证结果:60s 触发超时

    | 
    
    `2021``-``03``-``28` `19``:``02``:``29.918506``+``0800` `NSURLProtocolTest[``36473``:``8448954``] start a request...`
    
    `2021``-``03``-``28` `19``:``03``:``29.869946``+``0800` `NSURLProtocolTest[``36473``:``8448954``] Task <A533832C-C1E2-48B4-9C73-50447B930141>.<``1``> finished with error [-``1001``] Error Domain=NSURLErrorDomain Code=-``1001` `"The request timed out."` `UserInfo={_kCFStreamErrorCodeKey=-``2102``, NSUnderlyingError=``0x283d144b0` `{Error Domain=kCFErrorDomainCFNetwork Code=-``1001` `"(null)"` `UserInfo={_kCFStreamErrorCodeKey=-``2102``, _kCFStreamErrorDomainKey=``4``}}, _NSURLErrorFailingURLSessionTaskErrorKey=LocalDataTask <A533832C-C1E2-48B4-9C73-50447B930141>.<``1``>, _NSURLErrorRelatedURLSessionTaskErrorKey=(`
    
    `"LocalDataTask <A533832C-C1E2-48B4-9C73-50447B930141>.<1>"`
    
    `), NSLocalizedDescription=The request timed out., NSErrorFailingURLStringKey=https:``//www.baidu.com, NSErrorFailingURLKey=[https://www.baidu.com](https://www.baidu.com/), _kCFStreamErrorDomainKey=4}`
    
    

    结论:Demux 中新创建的 NSURLSession 超时时间设置不影响到请求发起方。

    5.2、缓存验证:

    验证1:原始请求设置为不使用缓存 NSURLRequestReloadIgnoringLocalCacheData,Demux 中设置为使用缓存 NSURLRequestReturnCacheDataElseLoad ;通过 charles 抓包确认在收到 completed 的时候是否有真正发起请求。

    验证结果:每次点击开始请求按钮的时候, charles 都能抓到请求数据包,且 response code 为 200。

    验证2:原始请求设置为使用缓存 NSURLRequestReturnCacheDataElseLoad,Demux 中设置为不使用缓存 NSURLRequestReturnCacheDataElseLoad;通过 charles 抓包确认在收到 completed 的时候是否有真正发起请求。

    验证结果:卸载App,首次点击请求按钮的时候可以在 charles 中抓到请求数据包;后面再次点击的时候就没有抓到相关请求数据包了,但却返回到了 completed 回调,且 response code 为 200

    结论:Demux 中新创建的 NSURLSession 缓存设置不影响到请求发起方。

    Snip20210712_6.png
    5.3、认证策略验证:

    验证1:由于条件限制,我们这里只做单向验证,即验证服务器证书。在请求方的回调 URLSession: didReceiveChallenge: completionHandler: 回调里面对服务器证书与本地正式的校验,校验通过则返回 completionHandler(NSURLSessionAuthChallengeUseCredential,credential);;然后在 DLURLProtocol 的 URLSession: didReceiveChallenge: completionHandler: 回调中直接设置为校验不通过

    completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, NULL);

    验证结果:触发请求方的超时设置!

    结论:会影响到请求方,尽管在请求方的 URLSession: didReceiveChallenge: completionHandler:回调里面调用了认证通过的completionHandler,一样会触发超时操作!

    规避方法:网络监控所需要的信息采集不涉及到认证这块,可以直接将回调抛给请求方,由请求发起方进行处理。

    5.4、耗时验证:

    验证1:相隔10ms,异步轮流发起请求分别请求 www.baidu.comwww.sina.com.cnwww.taobao.com 这几个域名,加起来总共请求 100 次,然后计算使用 NSProtocol 和不使用 NSProtocol 的平均耗时。

    验证结果:

    //有接入:
    2021-05-11 00:36:39.112549+0800 NSURLProtocolTest[90129:29124516] baidu, count:26 , avgDuration:324.019181
     
    2021-05-11 00:36:39.112666+0800 NSURLProtocolTest[90129:29124516] sina, count:46  avgDuration:553.305805
     
    2021-05-11 00:36:39.115587+0800 NSURLProtocolTest[90129:29124516] taobao, count:28  avgDuration:300.874954
    
    //无接入:
     
    2021-05-11 00:29:52.958785+0800 NSURLProtocolTest[90066:29117542] baidu, count:35 , avgDuration:306.175676
     
    2021-05-11 00:29:52.958941+0800 NSURLProtocolTest[90066:29117542] sina, count:29  avgDuration:321.528200
     
    2021-05-11 00:29:52.959113+0800 NSURLProtocolTest[90066:29117542] taobao, count:36  avgDuration:297.670796
    

    结论:除去网络波动影响,耗时基本相近。
    详细日志:(存放在百度网盘上面的网络监控文件夹)

    六、参考链接

    iOS 中的网络调试

    爱奇艺全链路自动化监控平台的探索与实践

    深度理解 NSURLProtocol

    移动端APM网络监控与优化实践

    URL Session Programming Guide

    CustomHTTPProtocol

    相关文章

      网友评论

        本文标题:APP网络监控-技术分享

        本文链接:https://www.haomeiwen.com/subject/qgwlpltx.html