APP網(wǎng)絡(luò)監(jiān)控-技術(shù)分享

1.背景

隨著業(yè)務(wù)的發(fā)展,用戶對于網(wǎng)絡(luò)的依賴場景會越來越多,隨之而來遇到的各種異常網(wǎng)絡(luò)場景也越來越多。

1、為了保證網(wǎng)絡(luò)的接口的持續(xù)健康、及時發(fā)現(xiàn)問題,為網(wǎng)絡(luò)性能優(yōu)化提供數(shù)據(jù)基礎(chǔ)。

2、同時以報表的形式直觀的去展現(xiàn)現(xiàn)有網(wǎng)絡(luò)質(zhì)量。

3、也為了更好的支撐后續(xù)業(yè)務(wù)的發(fā)展,就需要我們?nèi)ゴ罱ㄆ鹣鄳?yīng)的網(wǎng)絡(luò)監(jiān)控體系。

二、目標(biāo)

提供一套完整的網(wǎng)絡(luò)采集、監(jiān)控和預(yù)警的可視化機制,用于線上接口可用性和健康度觀察,并提供一系列排查問題的輔助信息。

通過儀表盤可視化呈現(xiàn)網(wǎng)絡(luò)質(zhì)量,包括但不限于以下能力:

  • 總體請求成功率
  • 過濾單個請求成功率、失敗錯誤碼
  • 查看接口請求詳情
  • 接口訪問平均耗時、時長分布
  • 通過 traceId 實現(xiàn)從客戶端請求的發(fā)起到最終具體服務(wù)器的處理返回全鏈路追蹤。

三、總體方案

網(wǎng)絡(luò)接口監(jiān)控架構(gòu).png

不管是 iOS 還是 Android ,兩者最終要做的目標(biāo)方案如上圖。這里從下到上分別闡述每個部分的功能:

網(wǎng)絡(luò)基礎(chǔ)庫:針對平臺特性,這里有 iOS 的 AFnetworking、Alamofire 和 Android 的 okhttp3、okhttp4 ,其實現(xiàn)原理應(yīng)該都差不多,都是針對底層的網(wǎng)絡(luò)api進行進一步的封裝,提高接口的易用性。

攔截器:主要是針對各個基礎(chǔ)網(wǎng)絡(luò)庫進行接口攔截。根據(jù)平臺不同,iOS 主要使用 NSProtocol + Hook,Android 使用 Aspect 。

網(wǎng)絡(luò)封裝庫:一般開發(fā)過程都會針對基礎(chǔ)的網(wǎng)絡(luò)庫再做二次封裝,加入一些策略、緩存、安全校驗等管理,使其更加貼合業(yè)務(wù)和快速接入使用。

插件/功能模塊:以插件化的形式提供額外的網(wǎng)絡(luò)功能

統(tǒng)計模塊:將從業(yè)務(wù)開始調(diào)用到回調(diào)給業(yè)務(wù)方的各個環(huán)節(jié)的耗時及狀態(tài)值,變成統(tǒng)計數(shù)據(jù)匯報到APM。
網(wǎng)絡(luò)診斷模塊:對關(guān)鍵業(yè)務(wù)進行診斷,包括dns解析、ping、弱網(wǎng)檢測等,輸出診斷報告并上報到APM。
重試模塊:根據(jù)策略進行重試,包括 ip 重試、https 降級重試、原 url 重試等。
httpdns模塊:提供 httpdns 能力,解決域名劫持問題。
上傳模塊:提供上傳能力,包括斷點續(xù)傳、分片上傳以及包體大小、上傳耗時等信息監(jiān)控。
下載模塊:提供下載能力,包括大文件下載、斷點續(xù)傳以及包體大小、下載耗時等信息監(jiān)控。
mock 模塊:提供 mock 能力,主要用于測試和后臺接口還沒有準(zhǔn)備好的情況下使用。
對外接口層:這一層直接對接上層業(yè)務(wù)。

四、具體實現(xiàn)

1)請求方式

iOS 常用的第三方網(wǎng)絡(luò) AFNetworking、Alamofire 基本都是基于 NSURLConnection 或者是 NSURLSession 的封裝,其中 NSURLConnection 是比較舊的使用方式了,而 NSURLSession 則是比較新的也是比較被推薦的使用方式。

2)底層原理

在使用 NSURLConnection 和 NSURLSession 進行網(wǎng)絡(luò)請求的時候,實際上走的都是更底層的 URL Loading System,URL Loading System 使用標(biāo)準(zhǔn)協(xié)議 https 或者自定義協(xié)議訪問標(biāo)識資源,本身支持 http,https,文件,ftp 和數(shù)據(jù)協(xié)議。

可以通過繼承 NSURLProtocol 實現(xiàn)一個自定義的 Protocol,然后調(diào)用 registerClass:方法注冊到 URL Loading System 中去,這樣 NSURLConnection、NSURLSession 或者是 NSURLDownload 在使用 NSURLRequest 初始化一個連接的時候,URL Loading System 就會

將按照注冊時的相反順序詢問每個注冊的類,詢問到第一個 +canInitWithRequest: 方法返回 YES 的時候則使用該類去處理請求。

  • NSURConnection 中,直接調(diào)用 registerClass:方法注冊我們自己的協(xié)議即可。
  • NSURLSession 中,如果是通過 [NSURLSession sharedSession] 初始化創(chuàng)建網(wǎng)絡(luò)請求,調(diào)用 registerClass:即可,如果是通過 configurantion 來初始化,則通過修改 configuration 的 protocolClasses 屬性,把自定義類插入到該數(shù)組的前面,確保我們的自定義的協(xié)議能夠優(yōu)先處理到網(wǎng)絡(luò)請求。

可以看到 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)實現(xiàn)步驟

利用 Objc 運行時 hook 掉 NSURLSessionConfiguration 的 defaultSessionConfiguration 屬性和 ephemeralSessionConfiguration 屬性設(shè)置,然后修改 configuration 的 protocolClassess 屬性,插入我們自定義的 Protocol
在自定義的 NSURLProtocol 之類中實現(xiàn)如下方法:
+ canInitWithRequest: 在這里判斷當(dāng)前網(wǎng)絡(luò)請求是否需要監(jiān)控,如果不需要直接 return NO 即可。

      + canonicalRequestForRequest:   生成一個新的 request 請求,同時標(biāo)識該請求已經(jīng)處理過,防止死循環(huán)。

      - startLoading  將新的請求發(fā)送出去,設(shè)置對應(yīng)的回調(diào)代理。

      - stopLoading   停止網(wǎng)絡(luò)請求。

 3. 處理請求回調(diào),實現(xiàn)需要進行處理的回調(diào)方法,處理完成后通過 self.client.urlProtocol 將回調(diào)方法傳回至原來的 delegate。

 4. 至此,我們就完成了發(fā)送、接收等一系列操作,并且完美的將回調(diào)轉(zhuǎn)發(fā)回了原來的代理方,剩下的就是我們在回調(diào)中收集完了請求的各種信息就好。

流程圖如下:


網(wǎng)絡(luò)攔截前后對比圖.png
4)可能存在的問題及優(yōu)化

流程并不復(fù)雜,從上圖可以看到,使用了網(wǎng)絡(luò)攔截之后的流程圖比原本的多了一個 custom protocol(DLURLProtocol)和 custom session。custom potocol 用于攔截網(wǎng)絡(luò)請求,custom session 用于發(fā)起新的請求。

這里可能會存在兩個問題:

每個請求都會新創(chuàng)建一個 NSURLSession,對于網(wǎng)絡(luò)請求這種很頻繁的操作來說不是很友好;
新創(chuàng)建的 NSURLSession 如何確保超時、緩存、認(rèn)證、cookies 等策略跟原始的 NSURLSession 保持一致,如果不一致是否會影響到既有的網(wǎng)絡(luò)請求?

五、風(fēng)險評估

針對可能存在的問題做相關(guān)梳理和驗證~

關(guān)于第一點:每個請求都會創(chuàng)建一個 NSURLSession 這個很好解決,使用一個單例即可,從蘋果的官方Demo CustomHTTPProtocol 中可以看到 Demux 這個類,通過閱讀源碼知道,該類的存在除了最大化復(fù)用 Session 之外,還將請求的發(fā)起和回調(diào)都放到了這個類進行處理,確保請求發(fā)起和回調(diào)都是在同一個線程和 Runloop Mode,至于為什么要這么做,文檔中沒有找到明確說明,不過后面踩坑的時候才發(fā)現(xiàn),如果不這么做的話,在回調(diào)里面很容易就會遇到崩潰的情況,盡管你什么都沒有做。

至于第二點:新創(chuàng)建的 NSURLSession 是否會影響到原來的網(wǎng)絡(luò)請求策略?

思考:

根據(jù)蘋果提供的Demo CustomHTTPProtocol 中可以看到,同樣也是通過新創(chuàng)建一個 NSURLSession 發(fā)起請求的,那么它難道不會出現(xiàn)超時、緩存、認(rèn)證等參數(shù)和原始請求不一致的情況么?

從邏輯上來說,要么就是要獲取原始請求的 session,拿到對應(yīng)的超時、緩存、認(rèn)證等配置信息再發(fā)起請求;要么就是 Demux 中新創(chuàng)建的 session 對于請求發(fā)起方來說是透明的,這種透明包括不影響任何原始請求的參數(shù)配置!

針對以上猜想做相關(guān)驗證:

5.1、超時驗證:

驗證1:原始請求設(shè)置超時為 5s,Demux 設(shè)置超時時間為 60s,手機網(wǎng)絡(luò)設(shè)置為100% lost

驗證結(jié)果:5s 觸發(fā)超時

`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: 調(diào)用發(fā)設(shè)置超時時間為 60s,Demux 設(shè)置超時時間為 5s,手機網(wǎng)絡(luò)設(shè)置為 100% lost

驗證結(jié)果:60s 觸發(fā)超時

| 

`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}`

結(jié)論:Demux 中新創(chuàng)建的 NSURLSession 超時時間設(shè)置不影響到請求發(fā)起方。

5.2、緩存驗證:

驗證1:原始請求設(shè)置為不使用緩存 NSURLRequestReloadIgnoringLocalCacheData,Demux 中設(shè)置為使用緩存 NSURLRequestReturnCacheDataElseLoad ;通過 charles 抓包確認(rèn)在收到 completed 的時候是否有真正發(fā)起請求。

驗證結(jié)果:每次點擊開始請求按鈕的時候, charles 都能抓到請求數(shù)據(jù)包,且 response code 為 200。

驗證2:原始請求設(shè)置為使用緩存 NSURLRequestReturnCacheDataElseLoad,Demux 中設(shè)置為不使用緩存 NSURLRequestReturnCacheDataElseLoad;通過 charles 抓包確認(rèn)在收到 completed 的時候是否有真正發(fā)起請求。

驗證結(jié)果:卸載App,首次點擊請求按鈕的時候可以在 charles 中抓到請求數(shù)據(jù)包;后面再次點擊的時候就沒有抓到相關(guān)請求數(shù)據(jù)包了,但卻返回到了 completed 回調(diào),且 response code 為 200

結(jié)論:Demux 中新創(chuàng)建的 NSURLSession 緩存設(shè)置不影響到請求發(fā)起方。

Snip20210712_6.png
5.3、認(rèn)證策略驗證:

驗證1:由于條件限制,我們這里只做單向驗證,即驗證服務(wù)器證書。在請求方的回調(diào) URLSession: didReceiveChallenge: completionHandler: 回調(diào)里面對服務(wù)器證書與本地正式的校驗,校驗通過則返回 completionHandler(NSURLSessionAuthChallengeUseCredential,credential);;然后在 DLURLProtocol 的 URLSession: didReceiveChallenge: completionHandler: 回調(diào)中直接設(shè)置為校驗不通過

completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, NULL);

驗證結(jié)果:觸發(fā)請求方的超時設(shè)置!

結(jié)論:會影響到請求方,盡管在請求方的 URLSession: didReceiveChallenge: completionHandler:回調(diào)里面調(diào)用了認(rèn)證通過的completionHandler,一樣會觸發(fā)超時操作!

規(guī)避方法:網(wǎng)絡(luò)監(jiān)控所需要的信息采集不涉及到認(rèn)證這塊,可以直接將回調(diào)拋給請求方,由請求發(fā)起方進行處理。

5.4、耗時驗證:

驗證1:相隔10ms,異步輪流發(fā)起請求分別請求 www.baidu.com 、www.sina.com.cnwww.taobao.com 這幾個域名,加起來總共請求 100 次,然后計算使用 NSProtocol 和不使用 NSProtocol 的平均耗時。

驗證結(jié)果:

//有接入:
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

結(jié)論:除去網(wǎng)絡(luò)波動影響,耗時基本相近。
詳細(xì)日志:(存放在百度網(wǎng)盤上面的網(wǎng)絡(luò)監(jiān)控文件夾)

六、參考鏈接

iOS 中的網(wǎng)絡(luò)調(diào)試

愛奇藝全鏈路自動化監(jiān)控平臺的探索與實踐

深度理解 NSURLProtocol

移動端APM網(wǎng)絡(luò)監(jiān)控與優(yōu)化實踐

URL Session Programming Guide

CustomHTTPProtocol

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容