【腾讯经验】闪现社区App网络优化

转载自腾讯闪现社区App工程师技术分享

腾讯游戏社区App(原名:闪现一下)自上线以来,网络模块从App平台层下沉到C++层,C++层网络组件经历过多轮优化、打磨,整体的网络请求耗时和成功率获得了非常可观的性能提升,现将一些方案和心得进行总结。

游戏社区的网络请求主要为App内部的api请求,这类型请求的特点是数据量相对较小、请求集中、并发量高且不可缓存等,原有的App网络框架有如下问题:

Android使用OkHttp3作为基础库,iOS使用NSURLSessionDataTask,所有使用都是基于上述组件进行封装,但是他们均不支持Quic协议。

OkHttp3如果没有做深度定制,在高并发和弱网环境下性能一般;NSURLSessionDataTask是一个黑盒子,无法深度的改造和定制。

如果要引入第三方Quic的支持,需要双端都写一套Bridge接入到主工程,在项目初期我们就基于这套方法接入了TQuic,从数据上看TQuic有较大的提升,但是Android/iOS双端都需要写一套降级重试策略,而且双端的数据始终对不上,网络请求耗时数据双端差异较大,难以保证体验的一致性。

Flutter通过MethodChannel调用到终端的网络组件进行网络请求,需要经过多次线程切换,效率太低。

网络组件位于App主工程内部,一些涉及到网络请求的C++公共组件要么通过Bridge进行请求,要么再写一套底层网络组件。

一、新的网络组件

我们项目中,在整个跨平台技术规划的背景下,我们决定实现一个C++的跨平台开发框架:Crossing,网络引擎作为第一期内部组件:Lighting。

Lighting网络组件内部全部使用C++实现,底层网络请求实现使用了TQuic和Curl,TQuic主要用来进行quic协议请求;Curl则是Curl + Mbedtls + NgHttp2支持Http2请求,主要是用来做Quic兜底实现。所以网络请求耗时的优化主要是由于Lighting网络组件 + TQuic带来的提升,整体Lighting网络组件架构如下:

image.png

主要分为Engine + Core + External层,External层为第三方基础的网络组件库,比如引入TQuic SDK和Curl,编译成静态库使用;Core层则是对TQuic和Curl的封装,封装成对应的UrlConnection实现;Engine层则是网络框架,提供了同步接口和异步接口调用、线程池管理、调度器、请求策略、降级、缓存等一系列功能,主要供业务方直接使用。

image.png

Lighting内部实现参考了OkHttp3的责任链模式,所有的网络请求通过一系列的拦截器来实现:

image.png

通过责任链模式,可以解耦请求过程中各个阶段,并且允许外部自定义拦截器进行控制,使整个网络请求流程更加清晰。如是否走Quic请求,或者quic连接失败自动降级到Http请求,这个过程就在RetryStartegyInterceptor里面实现。

除了自定义拦截器外,同时Lighting支持外部传入自定义DnsProvider、线程池、代理选择器组件,对使用方更加透明且可定制化。

最后网络请求结束,也会返回一系列请求过程中的性能数据给调用方,调用方可以对比自身的耗时数据。

二、 统一的Quic请求策略 + 降级策略

Google的Cronet网络库,在初始化的时候需要传入一系列QuicHint(Host和Port的组合)来表明哪些Host支持Quic请求,在底层进行网络请求的时候根据Url的Host是否命中预设的QuicHint,如果有命中便优先进行Quic协议的请求,但不会保证一定会走到Quic,因为Cronet内部会有一套竞速策略来抉择最后的请求是Http还是Quic。这种情况下调用方很难控制,这里不再展开讲述。

Lighting也沿用了相似的策略,但是更加灵活,每个请求都可以单独设置是否强制走Quic,如果设置了不管服务端是否支持都会先进行Quic请求;同时也允许设置QuicHint,即使没有设置强制Quic,也会主动判断Host是否命中QuicHint,继而尝试Quic请求。

最后允许业务方设置是否进行自动降级,由于服务端网关Quic有时候并不稳定:

image.png

或者用户机型网络原因(如路由器设置qos值比较低、特殊机型ROM不允许UDP)导致Quic失败:

image.png

同时之前所有降级策略都放在不同的平台的App侧,Android/iOS各一套,需要双端约定降级策略来保证体验的一致性,但是实现起来还是会有多方面不一致的情况。所以需要有一套Quic降级重试机制:当Quic失败,还能切换为Http保证接口能够成功调用,基于现网经验,把降级策略下沉到网络组件内部,实现了一套统一的降级策略,提升了Android和iOS的双端一致性。

image.png

三、 网络基础组件选择(TQuic + Curl)

QUIC为Google于2013年开发的基于UDP的多路并发传输协议,主要优势在于减少TCP三次握手及TLS握手,同时因为UDP特性在高丢包环境下依旧能正常工作。在使用情景复杂的移动网络环境下能有效降低延时以及提高请求成功率。

主要的优势在于(相比Http):

  1. 减少握手次数,TCP本身需要三次握手,TLS1.1需要3次,TLS1.2,需要1-2次,TLS1.3才能达到0RTT,而QUIC协议层只需要1-2 RTT,而再次连接为0RTT;
  2. 网络抖动时,TCP会产生阻塞,同时无法多路复用,而QUIC流与流之间是独立的,丢包后不会互相影响;
  3. TCP协议需要设备支持,比如当前主流移动设备均不支持TLS1.3,老设备甚至不支持TLS1.2,QUIC可以单独不是,升级及兼容更方便。
    为了优化体验,我们决定在客户端引入Quic的支持,这里我们直接使用STGW团队提供的TQuic SDK,tquic-sdk是一个跨平台网络组件,帮助移动端便捷接入QUIC协议,降低QUIC协议使用成本。
image.png

对于Http的支持,我们使用了Curl + Mbedtls + NgHttp2组合,Curl是非常成熟且值得信赖的网络库,并且得益于Multi Api的支持,性能非常优秀。

image.png

注:经过测试,实验室环境下curl性能比OkHttp3稍差,但是由于我们所有网络请求都已经切quic,curl只是起兜底作用,所以已经满足使用。

四、 Flutter网络请求ffi优化

网络Engine下沉到C++还有一个好处就是可以直接提供ffi接口供Flutter调用,ffi提供了Dart直接调用C++的能力,性能比Platform Channel优几十甚至上百倍(不涉及到线程转换的情况下);即使考虑到网络请求涉及到线程切换,FFI效率一样比Platform Channel高,调用原理如下所示:

image.png

如上图所示,这是由于Platform Channel涉及了3次线程切换,而ffi只有一次,而且不会阻塞平台UI线程。我们把ffi和MethodChannel的网络请求接口耗时进行上报并对比,数据如下:

image.png

FFI接口耗时仅为5ms,而MethodChannel接口耗时高达25ms,我们统计了最近3天的数据:

image.png

可以看出,接口耗时(总耗时 - 真实耗时)性能提升3-4倍,总耗时降低30%。

五、 采用新的网络组件前后对比

我们统计了基于TQuic的新网络组件耗时和原App的请求耗时(H2),在都是请求成功且没有降级的情况下的数据,这是摘取了灯塔上连续3天的数据对比:

Android

image.png

Android上平均耗时降低15% ~ 20%,优化前/优化后为全路径的总耗时,包括网络组件真实的网络耗时 + 回包校验 + PB解包(反序列化)等耗时,因为新的网络组件是通过OkHttp3的Interceptor机制接入,所以把两者的总耗时也上报上来,可以看出数据基本一致。

iOS

image.png

iOS上平均耗时降低20% ~ 30%,iOS只统计了网络请求前后的路径耗时,没有更细区分真实请求的耗时。

成功率

image.png

iOS整体成功率提升~2%,Android上有自动降级机制,成功率导致偏高,数据不太准确,这里不再列出。

六、 总结

网络库下沉到C++层后,可以深度利用TQuic带来的性能优势,网络请求速度获得了较为可观的收益;同时极大提升了Android/iOS双端表现的一致性;并且解决了Flutter网络请求通过Platform Channel的性能问题;最后也为Crossing整个跨平台开发框架奠定坚实的基础,得以把更多模块(上报、账号等)下沉到C++层。

本站文章资源均来源自网络,除非特别声明,否则均不代表站方观点,并仅供查阅,不作为任何参考依据!
如有侵权请及时跟我们联系,本站将及时删除!
如遇版权问题,请查看 本站版权声明
THE END
分享
二维码
海报
【腾讯经验】闪现社区App网络优化
游戏社区的网络请求主要为App内部的api请求,这类型请求的特点是数据量相对较小、请求集中、并发量高且不可缓存等,原有的App网络框架有如下问题:
<<上一篇
下一篇>>