1.背景
隨著業(yè)務的發(fā)展轮听,用戶對于網(wǎng)絡的依賴場景會越來越多卷谈,隨之而來遇到的各種異常網(wǎng)絡場景也越來越多杯拐。
1、為了保證網(wǎng)絡的接口的持續(xù)健康世蔗、及時發(fā)現(xiàn)問題端逼,為網(wǎng)絡性能優(yōu)化提供數(shù)據(jù)基礎。
2污淋、同時以報表的形式直觀的去展現(xiàn)現(xiàn)有網(wǎng)絡質量顶滩。
3、也為了更好的支撐后續(xù)業(yè)務的發(fā)展寸爆,就需要我們?nèi)ゴ罱ㄆ鹣鄳木W(wǎng)絡監(jiān)控體系礁鲁。
二盐欺、目標
提供一套完整的網(wǎng)絡采集、監(jiān)控和預警的可視化機制仅醇,用于線上接口可用性和健康度觀察冗美,并提供一系列排查問題的輔助信息。
通過儀表盤可視化呈現(xiàn)網(wǎng)絡質量析二,包括但不限于以下能力:
- 總體請求成功率
- 過濾單個請求成功率墩衙、失敗錯誤碼
- 查看接口請求詳情
- 接口訪問平均耗時、時長分布
- 通過 traceId 實現(xiàn)從客戶端請求的發(fā)起到最終具體服務器的處理返回全鏈路追蹤甲抖。
三漆改、總體方案
不管是 iOS 還是 Android ,兩者最終要做的目標方案如上圖准谚。這里從下到上分別闡述每個部分的功能:
網(wǎng)絡基礎庫:針對平臺特性挫剑,這里有 iOS 的 AFnetworking、Alamofire 和 Android 的 okhttp3柱衔、okhttp4 樊破,其實現(xiàn)原理應該都差不多,都是針對底層的網(wǎng)絡api進行進一步的封裝唆铐,提高接口的易用性哲戚。
攔截器:主要是針對各個基礎網(wǎng)絡庫進行接口攔截。根據(jù)平臺不同艾岂,iOS 主要使用 NSProtocol + Hook顺少,Android 使用 Aspect 。
網(wǎng)絡封裝庫:一般開發(fā)過程都會針對基礎的網(wǎng)絡庫再做二次封裝王浴,加入一些策略脆炎、緩存、安全校驗等管理氓辣,使其更加貼合業(yè)務和快速接入使用秒裕。
插件/功能模塊:以插件化的形式提供額外的網(wǎng)絡功能
統(tǒng)計模塊:將從業(yè)務開始調用到回調給業(yè)務方的各個環(huán)節(jié)的耗時及狀態(tài)值,變成統(tǒng)計數(shù)據(jù)匯報到APM钞啸。
網(wǎng)絡診斷模塊:對關鍵業(yè)務進行診斷几蜻,包括dns解析、ping体斩、弱網(wǎng)檢測等梭稚,輸出診斷報告并上報到APM。
重試模塊:根據(jù)策略進行重試硕勿,包括 ip 重試哨毁、https 降級重試、原 url 重試等源武。
httpdns模塊:提供 httpdns 能力扼褪,解決域名劫持問題想幻。
上傳模塊:提供上傳能力,包括斷點續(xù)傳话浇、分片上傳以及包體大小脏毯、上傳耗時等信息監(jiān)控。
下載模塊:提供下載能力幔崖,包括大文件下載食店、斷點續(xù)傳以及包體大小、下載耗時等信息監(jiān)控赏寇。
mock 模塊:提供 mock 能力吉嫩,主要用于測試和后臺接口還沒有準備好的情況下使用。
對外接口層:這一層直接對接上層業(yè)務嗅定。
四自娩、具體實現(xiàn)
1)請求方式
iOS 常用的第三方網(wǎng)絡 AFNetworking、Alamofire 基本都是基于 NSURLConnection 或者是 NSURLSession 的封裝渠退,其中 NSURLConnection 是比較舊的使用方式了忙迁,而 NSURLSession 則是比較新的也是比較被推薦的使用方式。
2)底層原理
在使用 NSURLConnection 和 NSURLSession 進行網(wǎng)絡請求的時候碎乃,實際上走的都是更底層的 URL Loading System姊扔,URL Loading System 使用標準協(xié)議 https 或者自定義協(xié)議訪問標識資源,本身支持 http梅誓,https恰梢,文件,ftp 和數(shù)據(jù)協(xié)議证九。
可以通過繼承 NSURLProtocol 實現(xiàn)一個自定義的 Protocol删豺,然后調用 registerClass:方法注冊到 URL Loading System 中去,這樣 NSURLConnection愧怜、NSURLSession 或者是 NSURLDownload 在使用 NSURLRequest 初始化一個連接的時候,URL Loading System 就會
將按照注冊時的相反順序詢問每個注冊的類妈拌,詢問到第一個 +canInitWithRequest: 方法返回 YES 的時候則使用該類去處理請求拥坛。
- NSURConnection 中,直接調用 registerClass:方法注冊我們自己的協(xié)議即可尘分。
- NSURLSession 中猜惋,如果是通過 [NSURLSession sharedSession] 初始化創(chuàng)建網(wǎng)絡請求,調用 registerClass:即可培愁,如果是通過 configurantion 來初始化著摔,則通過修改 configuration 的 protocolClasses 屬性,把自定義類插入到該數(shù)組的前面定续,確保我們的自定義的協(xié)議能夠優(yōu)先處理到網(wǎng)絡請求谍咆。
可以看到 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 屬性設置,然后修改 configuration 的 protocolClassess 屬性摹察,插入我們自定義的 Protocol
在自定義的 NSURLProtocol 之類中實現(xiàn)如下方法:
+ canInitWithRequest: 在這里判斷當前網(wǎng)絡請求是否需要監(jiān)控恩掷,如果不需要直接 return NO 即可。
+ canonicalRequestForRequest: 生成一個新的 request 請求供嚎,同時標識該請求已經(jīng)處理過黄娘,防止死循環(huán)。
- startLoading 將新的請求發(fā)送出去克滴,設置對應的回調代理逼争。
- stopLoading 停止網(wǎng)絡請求。
3. 處理請求回調劝赔,實現(xiàn)需要進行處理的回調方法誓焦,處理完成后通過 self.client.urlProtocol 將回調方法傳回至原來的 delegate。
4. 至此望忆,我們就完成了發(fā)送罩阵、接收等一系列操作,并且完美的將回調轉發(fā)回了原來的代理方启摄,剩下的就是我們在回調中收集完了請求的各種信息就好稿壁。
流程圖如下:
4)可能存在的問題及優(yōu)化
流程并不復雜,從上圖可以看到歉备,使用了網(wǎng)絡攔截之后的流程圖比原本的多了一個 custom protocol(DLURLProtocol)和 custom session傅是。custom potocol 用于攔截網(wǎng)絡請求,custom session 用于發(fā)起新的請求蕾羊。
這里可能會存在兩個問題:
每個請求都會新創(chuàng)建一個 NSURLSession喧笔,對于網(wǎng)絡請求這種很頻繁的操作來說不是很友好;
新創(chuàng)建的 NSURLSession 如何確保超時龟再、緩存书闸、認證、cookies 等策略跟原始的 NSURLSession 保持一致利凑,如果不一致是否會影響到既有的網(wǎng)絡請求浆劲?
五、風險評估
針對可能存在的問題做相關梳理和驗證~
關于第一點:每個請求都會創(chuàng)建一個 NSURLSession 這個很好解決哀澈,使用一個單例即可牌借,從蘋果的官方Demo CustomHTTPProtocol 中可以看到 Demux 這個類,通過閱讀源碼知道割按,該類的存在除了最大化復用 Session 之外膨报,還將請求的發(fā)起和回調都放到了這個類進行處理,確保請求發(fā)起和回調都是在同一個線程和 Runloop Mode,至于為什么要這么做现柠,文檔中沒有找到明確說明院领,不過后面踩坑的時候才發(fā)現(xiàn),如果不這么做的話晒旅,在回調里面很容易就會遇到崩潰的情況栅盲,盡管你什么都沒有做。
至于第二點:新創(chuàng)建的 NSURLSession 是否會影響到原來的網(wǎng)絡請求策略废恋?
思考:
根據(jù)蘋果提供的Demo CustomHTTPProtocol 中可以看到谈秫,同樣也是通過新創(chuàng)建一個 NSURLSession 發(fā)起請求的,那么它難道不會出現(xiàn)超時鱼鼓、緩存拟烫、認證等參數(shù)和原始請求不一致的情況么?
從邏輯上來說迄本,要么就是要獲取原始請求的 session硕淑,拿到對應的超時、緩存嘉赎、認證等配置信息再發(fā)起請求置媳;要么就是 Demux 中新創(chuàng)建的 session 對于請求發(fā)起方來說是透明的,這種透明包括不影響任何原始請求的參數(shù)配置公条!
針對以上猜想做相關驗證:
5.1拇囊、超時驗證:
驗證1:原始請求設置超時為 5s,Demux 設置超時時間為 60s靶橱,手機網(wǎng)絡設置為100% lost
驗證結果: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: 調用發(fā)設置超時時間為 60s寥袭,Demux 設置超時時間為 5s,手機網(wǎng)絡設置為 100% lost
驗證結果: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}`
結論:Demux 中新創(chuàng)建的 NSURLSession 超時時間設置不影響到請求發(fā)起方关霸。
5.2传黄、緩存驗證:
驗證1:原始請求設置為不使用緩存 NSURLRequestReloadIgnoringLocalCacheData,Demux 中設置為使用緩存 NSURLRequestReturnCacheDataElseLoad 队寇;通過 charles 抓包確認在收到 completed 的時候是否有真正發(fā)起請求膘掰。
驗證結果:每次點擊開始請求按鈕的時候, charles 都能抓到請求數(shù)據(jù)包佳遣,且 response code 為 200炭序。
驗證2:原始請求設置為使用緩存 NSURLRequestReturnCacheDataElseLoad,Demux 中設置為不使用緩存 NSURLRequestReturnCacheDataElseLoad苍日;通過 charles 抓包確認在收到 completed 的時候是否有真正發(fā)起請求。
驗證結果:卸載App窗声,首次點擊請求按鈕的時候可以在 charles 中抓到請求數(shù)據(jù)包相恃;后面再次點擊的時候就沒有抓到相關請求數(shù)據(jù)包了,但卻返回到了 completed 回調笨觅,且 response code 為 200
結論:Demux 中新創(chuàng)建的 NSURLSession 緩存設置不影響到請求發(fā)起方拦耐。
5.3耕腾、認證策略驗證:
驗證1:由于條件限制,我們這里只做單向驗證杀糯,即驗證服務器證書扫俺。在請求方的回調 URLSession: didReceiveChallenge: completionHandler: 回調里面對服務器證書與本地正式的校驗,校驗通過則返回 completionHandler(NSURLSessionAuthChallengeUseCredential,credential);固翰;然后在 DLURLProtocol 的 URLSession: didReceiveChallenge: completionHandler: 回調中直接設置為校驗不通過
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, NULL);
驗證結果:觸發(fā)請求方的超時設置狼纬!
結論:會影響到請求方,盡管在請求方的 URLSession: didReceiveChallenge: completionHandler:回調里面調用了認證通過的completionHandler骂际,一樣會觸發(fā)超時操作疗琉!
規(guī)避方法:網(wǎng)絡監(jiān)控所需要的信息采集不涉及到認證這塊,可以直接將回調拋給請求方歉铝,由請求發(fā)起方進行處理盈简。
5.4、耗時驗證:
驗證1:相隔10ms太示,異步輪流發(fā)起請求分別請求 www.baidu.com 柠贤、www.sina.com.cn 和 www.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
結論:除去網(wǎng)絡波動影響,耗時基本相近呀非。
詳細日志:(存放在百度網(wǎng)盤上面的網(wǎng)絡監(jiān)控文件夾)