HTTP/2协议下流量大量增加问题剖析
Posted 喜马拉雅技术博客
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了HTTP/2协议下流量大量增加问题剖析相关的知识,希望对你有一定的参考价值。
背景
由于HTTP/2协议在多路复用、二进制分帧、头部压缩以及Server Push等各方面的好处,各大互联网公司都陆续将网络协议升级到HTTP/2。喜马拉雅从今年上半年开始启动了升级协议的工作,首期打算升级部分和业务非强相关的上报日志域名。在我们打开该域名协议升级开关后,高峰期这个域名的出口流量居然接近成倍上升,于是紧急关闭了协议升级。
问题
通过运维提供的流量数据,发现流量的上升以及回降趋势基本符合协议开和关的时间点,这样可以确认是协议升级带来的问题。运维提供的信息还有网络出口流量上升但入口流量变化不大以及存在部分用户1s内返回大量408的问题。查询上报日志域名在客户端APM网络监控中的错误情况,那段时间出现了大量的ConnectionShutdownException异常。
初步分析
分析nginx返回408的原因
问题发生时运维监控到部分用户存在大量返回408的情况,并且每次都是返回128次结束,其中128次正好符合HTTP/2中设置的单连接最大并发流数上限。我们怀疑可能是大量请求导致了流量的升高。查询nginx408是在client_body_timeout参数超时情况下发生,同时该域名用于上报端上落盘文件日志。结合以上特点,可能是端上文件日志上报逻辑出现问题,且在HTTP/1.1下没有触发,在HTTP/2下才有触发条件。
很快我们重现了该问题,原因在于上报文件时,某些情况下会发生另外一个线程同时删除了文件,造成上传时发生了FileNotFoundException。OkHttp的RetryAndFollowUpInterceptor在HTTP/2环境下处理该种非网络异常时发生无限重试的bug,导致1s内在h2连接上请求128次,达到最大流数从而发生ConnectionShutdownException断开连接。后续我们修改了日志上报逻辑以及加固OkHttp的异常处理方式,确保不会再次出现该种问题。
分析出口流量变大的原因
新版本上线后,我们打开协议升级开关,流量问题依然存在(408问题已经没有了)。验证排除了各种客户端可能会出现的问题,怀疑可能是手机系统在处理HTTP/2的会话复用出现问题。使用WireShark分别抓取不同版本手机TLS握手情况,在android 8.0、7.0、6.0上每次HTTP/2请求居然都会重复回传证书,喜马证书大小在2.8kb左右,这样会大大增加出口流量(因为手边的测试机为android q,导致一直没有发现该问题)。
同时我们抓取了这些手机上安装的淘宝、网易云音乐、今日头条、京东等APP,发现存在同样的情况。
在分析问题之前我们先简单了解一下会话复用,TLS的会话复用提供两种机制Session Identifier和Session Ticket。Session Identifier(会话标识符),是 TLS 握手中生成的 Session ID。服务端可以存储SessionId协商后信息,同时客户端保存SessionId,并且客户端在后续的请求中附带上SessionId,如果服务端能够找到匹配的SessionId,就可以节省协商的步骤。SessionId机制存在一些弱点,例如:1)在集群机器中,机器之间没有同步Session信息,如果客户端两次请求没有到达同一台机器就无法匹配信息;2)服务端不好控制存储的SessionId对应信息的有效时间,如果设置的很长就会占用大量的服务端资源。SessionTicket是通过只有服务端知道的安全秘钥加密会话信息,且将会话信息保存在客户端,在下次请求的发生时,ClientHello会带上对应的SessionTicket,只要服务端能够成功解密,就可以快速完成本次握手过程。
这样无论对于网络传输流量的消耗以及建立连接的时长都有非常大的好处。现在问题就落在为什么在这些手机上SessionTicket机制没有生效?
深入分析
分析NOT RESUMABLE问题
目前Android中使用的不是OpenSSL库,而是谷歌在此基础上修改过后的BoringSSL库。BoringSSL关于会话缓存分为内存缓存和文件缓存。可以通过以下API设置文件缓存的路径。
网络请求后,在对应的文件路径下能看到不同域名的会话缓存。对比其他手机中能够正常会话复用的文件,发现不能会话复用的文件中存在“NOT RESUMABLE”提示。
同时我们使用了相同的解析方式解析该文件,得出的结果也为不合法session,目前我们基本可以确认这些手机中会话不能复用的特征为会话缓存文件中存在“NOT RESUMABLE”。
搜索了BoringSSL的全部代码后,发现“NOT RESUMABLE”位于此处设置。
由于BoringSSL底层代码逻辑过于复杂,不能通过代码的分析得出真实的触发原因。我们想到的办法只能重新编译ssl库替换手机系统ssl库,然后通过打日志的形式进一步获取信息。分析了OkHttp中tls握手的过程,发现只需要将BoringSSL编译成AAR文件(具体的编译过程略坑),然后导入项目替换网络库中的相关类文件,同时在Application中加载对应的动态库,即可使用我们自己编译ssl库。
当使用最新代码编译出的库替换后,在原本会话复用失效的手机上也能够复用,说明在最新版本代码谷歌已经修复这种问题。比较粗暴的解决方式为直接在APP中引入新ssl库,但这样会增加包体积大小,同时可能有一些未知的问题存在。通过代码版本回退的方式,最终发现如果当代码中不存在这个类ConscryptFileDescriptorSocket,则就会出现会话复用失败的问题。查看了那个时间点的提交记录,谷歌确实重构了BoringSSL。
目前我们已经知道问题位于BoringSSL库中且新版本代码已经修复该问题,剩下的我们只需要编译不同版本代码深入分析即可。
分析FalseStart带来的问题
TLS握手实现使用以下的case by case的代码逻辑实现,具体可以查看源码:
https://boringssl.googlesource.com/boringssl/+/refs/heads/master/ssl/handshake_client.cc
,最终在握手成功后会通过callback通知上层Java代码获取session缓存。
在介绍具体原因之前我们需要先了解TLS中的FalseStart概念。TLS False Start 是指客户端在发送 Change Cipher Spec Finished 同时发送应用数据(如 HTTP 请求),服务端在 TLS 握手完成时直接返回应用数据(如 HTTP 响应)。这样,应用数据的发送实际上并未等到握手全部完成,故谓之抢跑。具体过程如下图所示(来自网络图片)
通过 Wireshark 抓包可以清楚地看到 False Start 带来的好处(服务端的 ChangeCipherSpec 出现在 158 号包中,但在之前的 155 号包中,客户端已经发出了请求,相当于 TLS 握手只消耗了一个 RTT)
对于是否能够支持FalseStart在源码中有清晰介绍,主要包括是否支持前向安全性(Forward Secrecy)的加密算法以及APLN协议是否支持。
通过以上了解我们知道FalseStart就是个抢跑的策略且需要两个条件。
解决思路
搞清楚了问题出现的原因后,为了及时验证想法是否正确,只需要禁用FalseStart就可以了。我们打开协议升级开关,稍后这台机器的出口流量持续上升,几分钟后我们关闭该nginx机器上的前向安全算法的配置,这样会导致FalseStart失败,同时该机器上的出口流量也处于持续下降的状态。但是FalseStart会增加TLS的握手效率,不能关闭。
FalseStart除了前向安全算法这个必要条件外,还有个APLN协议是否支持的限定。APLN是HTTP/2升级的协商协议。如果在第一次请求的过程中,我们关闭APLN协议支持,触发生成合法的session缓存,随后我们打开APLN支持就可以在后续的过程中使用该session。开关APLN协议的代码如下所示:
同时我们调大了BoringSSL中默认的内存缓存以及文件缓存大小。
总结
以上修改上线后,我们重新打开了协议升级开关,出口流量相对之前未修改的情况下下降了60%,同时在APM上观察到该域名的https建立连接时长相对之前下降明显。
以上是关于HTTP/2协议下流量大量增加问题剖析的主要内容,如果未能解决你的问题,请参考以下文章