一個 HTTP 逾時問題
最近有同僚反映我們的 app 在網絡正常的情況下偶爾會出現請求逾時。
我的第一反應是某個服務挂掉了(因為最近服務端再搞重構),就回報給了服務層。
但是服務層的同僚排查下來發現 api 層并沒有産生異常日志,應該不是服務本身或者依賴的中台服務挂掉了。
定位
想起來 NSURLSession 有個預設的單個Host最大連接配接數,超過之後會進入排隊,可能導緻後續服務逾時。
Objective-C
/* The maximum number of simultanous persistent connections per host */
@property NSInteger HTTPMaximumConnectionsPerHost;
利用 Xcode 的調試模式來看一下,結果驚掉下巴,随便點了幾下就建立了這麼多連接配接
說好的 The default value is 6 in macOS, or 4 in iOS 的呢,難道是Xcode的調試面闆有問題嗎?再次用 Wireshark 抓包确認一下,得到的結果是确實每次請求都會建立一個連接配接(主要展現在端口,TLS 協定)。
下面貼 2 個連接配接的截圖
端口 53929
端口 53930
這樣肯定是有問題的:
1 HTTP 1.1 預設開啟了 Keep-Alive 屬性(這點在之前的 Charles 抓包中也有證明),為什麼連結沒有複用。
2NSURLSession 的預設單個 Host 最大連接配接數在 iOS 上為 4,為什麼會這麼多。
直接看代碼吧,我們現在項目裡有兩套網絡請求的架構,一套是Objective-C的基于AFNetworking的老代碼,一套是為了向Swift遷移新實作,一番折騰,在以前的基礎庫裡找到了這段看似平平無奇的代碼:
Objective-C
AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] initWithBaseURL:[NSURL URLWithString:@""]];
[manager POST:api parameters:params progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {...}
這段代碼的實作裡
AFHTTPSessionManager
每次都是新建立出來的,雖然系統預設開啟了 Keep-Alive ,但是 TCP 通道并不會複用,相當于一個披着 HTTP 1.1 外衣的 1.0連接配接,這就回答了第一個疑問。
至于第二個,我重新翻看了官方文檔
This limit is per session, so if you use multiple sessions, your app as a whole may exceed this limit. Additionally, depending on your connection to the Internet, a session may use a lower limit than the one you specify.
意思就是這個最大連接配接數隻是一個
NRLSession
的限制。
修複
修複很簡單,我們把上面的 Manager 修改成了屬性,保證了唯一性。完了使用 Xcode 的 Network 工具再看一下。嗯,看起來是複用了。
到這裡,解決了一個連接配接不複用的問題。分别觀察了兩個版本的代碼一段時間,修改後問題似乎不再出現了,舊版本的話用 Wireshark 抓到了詳細的逾時的時候的資料包:
看上去問題是去服務端握手的時候服務端沒回應,回報給服務端同僚,一開始以為是單個 Linux 主機有個理論上的最大連接配接數限制 65536之類的問題,後來覺得不可能。最終我們猜測是服務端之前設定了單個 api 的最大連接配接數,或者說防火牆那邊會有一段時間内的單個 ip 的最大連接配接數限制造成的。這個問題的根本應該是在服務端,但是用戶端修複了的連接配接方式會緩解這個問題。
驗證
修改代碼上線後,我們觀察了 2 個版本,線上沒有再報類似的問題了。請求逾時的錯誤率也稍有下降。嗯,最棒的是因為複用了連接配接 TCP 慢啟動,TLS 握手都省了,我們請求時間也縮短了。相當于被動的做了優化
修複前
修複後
代碼真不是随便寫寫,一不小心就埋雷。