APP网络监控-技术分享

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容

  • .前言 1.什么是APM? APM(Application Performance Management & Mo...
    Jackxu_q阅读 1,532评论 0 2
  • iOS网络相关类介绍 网络请求地址对象——NSURL url 介绍url,统一资源定位符,也被称为网址,因特网上标...
    小芳姑娘2012阅读 411评论 3 0
  • 目标 TCP建立连接时间 DNS时间 SSL/TLS时间 响应总时间 请求头、请求body、响应头、响应body大...
    mtry阅读 3,113评论 1 5
  • iOS网络架构讨论梳理整理中。。。 其实如果没有APIManager这一层是没法使用delegate的,毕竟多个单...
    yhtang阅读 5,160评论 1 23
  • 在开发中,有时候我们需要获取这些信息: 手机是否联网当前网络是WiFi还是蜂窝那么我总结一下具体的使用场景有哪些?...
    水落斜阳阅读 960评论 0 2