一个 HTTP 超时问题
最近有同事反映我们的 app 在网络正常的情况下偶尔会出现请求超时。
我的第一反应是某个服务挂掉了(因为最近服务端再搞重构),就反馈给了服务层。
但是服务层的同事排查下来发现 api 层并没有产生异常日志,应该不是服务本身或者依赖的中台服务挂掉了。
定位
想起来 NSURLSession 有个默认的单个Host最大连接数,超过之后会进入排队,可能导致后续服务超时。
Objective-C
/* The maximum number of simultanous persistent connections per host */
@property NSInteger HTTPMaximumConnectionsPerHost;
利用 Xcode 的调试模式来看一下,结果惊掉下巴,随便点了几下就建立了这么多连接
1说好的 The default value is 6 in macOS, or 4 in iOS 的呢,难道是Xcode的调试面板有问题吗?再次用 Wireshark 抓包确认一下,得到的结果是确实每次请求都会新建一个连接(主要体现在端口,TLS 协议)。
下面贴 2 个连接的截图
端口 53929
1端口 53930
1这样肯定是有问题的:
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 工具再看一下。嗯,看起来是复用了。
1到这里,解决了一个连接不复用的问题。分别观察了两个版本的代码一段时间,修改后问题似乎不再出现了,旧版本的话用 Wireshark 抓到了详细的超时的时候的数据包:
1看上去问题是去服务端握手的时候服务端没回应,反馈给服务端同事,一开始以为是单个 Linux 主机有个理论上的最大连接数限制 65536之类的问题,后来觉得不可能。最终我们猜测是服务端之前设置了单个 api 的最大连接数,或者说防火墙那边会有一段时间内的单个 ip 的最大连接数限制造成的。这个问题的根本应该是在服务端,但是客户端修复了的连接方式会缓解这个问题。
验证
修改代码上线后,我们观察了 2 个版本,线上没有再报类似的问题了。请求超时的错误率也稍有下降。嗯,最棒的是因为复用了连接 TCP 慢启动,TLS 握手都省了,我们请求时间也缩短了。相当于被动的做了优化
修复前
1修复后
1代码真不是随便写写,一不小心就埋雷。
其他
我们还发现了 iOS 上关于 TCP 挥手的行为的异常,我们跟踪下来,发现了和其他一些疑似使用原生网络框架的连接,断开连接的时候都不是典型的四次挥手
如图,客户端发送了 FIN ACK 之后并没有等到服务端回复 ACK 和 FIN ACK,直接关闭了连接,之后这个通道再收到任何信息都会回复 RST 通知服务端关闭连接,一开始以为是异常,后来跟踪下来发现并不是,可能是 iOS 内部实现做了优化吧。
网友评论