导读: 2021年10月21日,「QCon 全球软件开发大会」在上海举办,网易智企技术 VP 陈功作为出品人发起了「AI 时代下的融合通信技术」专场,邀请到网易云信、网易音视频实验室、网易云音乐的技术专家与大家一起分享融合通信技术趋势和演进方向、视频通信关键技术探索及实践、音频 AI 算法在 RTC 中的实践、网易云音乐网络库跨平台化实践等话题。
我们会针对四个演讲专题逐一进行介绍与分享,本期是我们的第四期,网易云音乐网络库跨平台化实践。
嘉宾介绍:陈松茂,2020年底加入网易云音乐,一站式网络解决方案技术负责人,目前从事跨平台网络解决方案相关研究,旨在降低多端网络工作的研发成本,以及多应用间的接入成本,用较低的成本获得持续可观的性能及能效提升。曾就职于阿里,长期从事于 Chromium 相关技术研究和应用,拥有丰富的浏览器开发和内核升级经验。
前言
在做网络优化的过程中,由于系统网络库的差异,你不得不把同一类型的优化,在各端进行适配,你感觉工作量在成倍增长。
正因为多种适配版本的存在,一致性无法完全保证,另外由于系统网络库提供的数据采集能力不同,你很难采集到完全对齐的数据,这意味着服务端需要做一些兼容。
随着移动互联网的崛起,PC 在变得小众化,但这不代表 PC 不需要对应的网络优化或监控服务,但你确实无暇顾及。大厂可以不惜代价,筹建庞大的团队深入协议栈进行重新开发优化,但你希望只投入有限的资源,就可以换来可观的网络性能提升。
不知道这些场景是否有戳中你的痛点,或者你曾经也被困扰过?如果我告诉你,接下来的分享能帮你解决上述所有的问题,你是否有兴趣了解下?如果我告诉你,你可能什么都不需要做,就能轻松让网络的响应时间缩短 30%~40%,你是否更有兴趣了?
本次演讲主题为:网易云音乐网络库跨平台化实践。本文聚焦跨平台化,主要涉及工程化相关思路,分四个部分,分别是背景介绍、方案设计、落地实践及后续规划.
背景介绍
下图是当前云音乐网络库在各端的架构,可以看到上层的网络策略和监控服务等等在各端都有独立的实现,这会带来如下问题:
- 重复造轮子:所有的网络策略、质量监控,每个端都有实现。当你需要加一个新的策略的时候,每个端都得实现一遍,这是严重的人员浪费。
- 缺乏一致性:网络库及人的差异,导致轮子的标准和完成度不一致。
- 资源不对等:移动端研发比较多,但 PC 端研发就很少,这导致 PC 端很多网络策略和质量监控都是残缺的。
- 深度优化难:对网络库来说,要深层次的优化,需要对网络模块有充分的理解。但是这当下,我们可以在 Android 上进入深度的挖掘,但是不可能四个端里面都有做网络的同学来做专门的优化。这导致每个端只做一些浅层次的优化,不会做深层次的挖掘,因为没有人。
通过前面的分析,大家可以清楚的看到,其实每个端都实现一套,是不靠谱的。结合当下面临的一些痛点,自然而然产生了一个共同的诉求:让所有的端都共享一套网络解决方案。再进一步,是不是所有的 App 都能共享一套网络解决方案。
解决上述问题最主要的做法是整体方案跨平台化,但也面临着不少挑战。
- 首先是跨平台性。功能层面上是不是只有策略需要下沉,策略下沉以后,三方 SDK 是否能下沉?另外跨平台方案有很多,可以仅仅是移动端的跨平台化,也可以是移动端加桌面端都跨平台化。
- 其次是能力复用。使用 C++ 重写的网络策略能不能在不同场景下复用,除了重构业务,是不是可以提炼一些框架?这些框架是不是能复用?另外后续新的需求出来了,是不是能在现有的基础上很轻易的扩展上去?
- 最后是后期推广。一开始建立这个项目的时候就明确后期需要进行推广,不仅仅提供给网易云音乐使用。所以,其它 App 在接入网络库时成本怎么样?是不是能很简单的接入?这决定着后期能否顺利推广。另外,每个 App 都有各自的特点,比如网易云音乐有免流功能,但其它 App 可能不需要免流,换句话说,不同的 App 可以根据实际情况进行定制,这也是我们面对的挑战。
针对上述挑战进行的方案设计,是整个分享的核心。
方案设计
跨平台设计思路
一开始设计思路很简单,下图中右侧绿色部分是用 C++ 重写的包含核心逻辑的 SDK,在这个基础上,由 Android、iOS 分别接入这个 SDK,底下的网络库不变。这个改造成本很低,但接入成本较高,移动端接入 SDK 的时候,需要对我们提供的 SDK 进行二次包装。于是我们就在想,能不能接入的时候不要搞五六个 SDK,合并成一个 SDK?
接下来就有另外一个方案,我们想把上层的绿色部分全部合并到一个 SDK,这需要在现有的网络库之上抽象出网络代理层,基于网络代理,上面做各种策略和业务逻辑封装,这样才能把所有逻辑合并到一个 SDK 里面,这个做完了以后整体接入成本就会大大降低。但网络代理层很厚,随着时间的迁移,底下的网络库会变,上面的网络代理层需要不断适配,这样适配工作就会没完没了。
更进一步思考,能不能让网络库统一掉?我们把上面的网络代理层抽掉,选用一个通用的跨平台网络库进行替代,在这基础之上,封装我们的策略和服务,这样整体链路就跨平台化了,这是一个思路。
最后小结一下,跨平台的方法有很多,在网易云音乐,我们选择了最彻底的一种,即整个网络解决方案包括网络库、上层策略以及一些服务全部跨平台化。
跨平台网络库设计
上面讲了跨平台最关键的点就是选择一个合适的跨平台网络库,我们选择了 Cronet,它跨五端,完全能支持云音乐的场景;协议层面支持传统的 HTTP、HTTP/2,QUIC;另外普及程度上,Google 系全部用了 Cronet,国内百度、微博、网易传媒,已经接入了官方的 Cronet,他们在官方 Cronet 基础上,各端做了自己的策略包括一些监控服务。头条系、蘑菇街改造比较彻底,在 Cronet 基础上进行二次定制开发,形成了自己的网络库;自从 Chromium 开源以后,国内所有浏览器都是基于这个项目做的,并在不断的演进中,非常活跃;最后一点是开源协议,Chromium 主要使用的是 BSD 协议,也就是说我们基于 Cronet 做的修改,不需要强制开源,这对商业公司来说,很关键,也是我们选择 Cronet 的重要考量。
下图是整个跨平台网络库的整体架构。
最底下分别是 OS、Base 和 Net;我们在 Net 的基础上封装了一层 Common API,作为胶水层隔离 Net,并扩展一些自己的基础能力,包括 BI、FunctionBridge、插拔服务等;在通用能力基础上又做了一层组件,包括网络策略、APM 监控、HTTPDNS 服务等等;最后接口层,我们对外暴露了组件 API,方便 App 调用。
我们可以通过 Cronet API 进行网络收发,并在这个基础上新增了部分扩展 API(比如原先的 Cronet API 不支持超时[包括建连和包间超时]设置;对于成功和失败的请求,也不支持返回具体的 IP;另外,对于 HTTPDNS 支持也不是很方便)。
对于 App 来说,除了看到 Cronet,还能看到一个组件系统。App 通过统一配置,把内部的组件按需启用(比如这里默认会启用 HTTPDNS 和 APM,但是对于网络策略就不一定需要);理论上组件之间是完全隔离的,但是有一些特殊场景,比如网络策略需要跟 HTTPDNS 组件做一些通讯,我们内部封装了一些可插拔的服务,HTTPDNS 组件将能力暴露到插拔服务中,网络策略借助插拔服务对 HTTPDNS 组件进行调用;App 通过调用桥接(类似于 JSBridge)直接与组件进行通讯,这样后续组件接入和扩展起来成本就很低了。
组件不直接和网络内核通讯,我们在网络内核基础上封装了基础能力,包括一些必要的请求拦截,请求转发及网络监控能力。
我们希望通过这次网络库跨平台化实践,沉淀出一个可复用的网络框架:这部分网络框架可以在网易内部复用,不包含业务逻辑,仅仅是一个框架,里面会定期更新 Cronet 内核,包括一些安全补丁(我们也接过 Cronet 官方版本,运行一段时间后发现,线上有一些 Crash,这个需要我们通过打补丁的方式修复);我们在 Cronet 的基础上提供一些基础的组件管理和基础能力封装,旨在降低 C++ 级别定制成本。
另一方面,基于可复用网络框架,我们扩展了一部分能力集:所有业务都放在能力集里面,变成三方库,以组件的形式进行复用;接入方可以根据自身需求,灵活组合;能力集可以提供通用的能力(比如 APM 监控、HTTPDNS 服务),也可以提供个性化的能力(比如免流服务),旨在满足接入方多变的业务场景和定制述求。
最后,简单总结一下网络库设计,我们直接基于开源的 Cronet 方案,构建了云音乐自己的统一网络库方案,并采用了“可复用网络框架+可扩展能力集”的模式进行业务下沉。
Cronet 升级
我们在选这个方案时候经过了大量的网上调研,我们发现大部分公司在方案选型时,选择了直接使用 Cronet,而不是基于 Cronet 进行二次开发定制。他们都提到了一个共同点,如果对 Cronet 进行了定制,后续内核升级把控起来难度有点大或者成本会很高,这是他们不选这个方案的最主要理由。
那么面对升级我们该怎么办呢?首先讲一下为什么要升级,升级原因很简单,一个是要修复问题(比如出现一个安全漏洞,希望通过升级或者修补丁的方式修复这个问题);另外一个是获得特性(比如现在的 QUIC 在快速演进中,国内主流用的都是 gQUIC,但是 iQUIC 标准正在慢慢的统一,Google 也在逐渐向 iQUIC 靠拢,我们希望通过升级直接支持 iQUIC);最后,Google 对 Cronet 也在持续进行优化,我们希望通过升级,直接享受到对应的优化成果。
那升级具体有哪些痛点呢?简单点讲,可以把升级类比成一次代码提交:如果你提交足够及时,修改一部分就马上提交,你几乎不会遇到冲突,很顺利;有些同学习惯不好,写了两三天的代码,才一次性提交,这时候就比较容易遇到代码冲突,可能需要花点时间才能解决;如果继续放慢代码提交的节奏,像我们对 Cronet 的二次开发,可能半年或一年甚至更久,也不会与官方的最新 Cronet 进行合并,等到需要进行 Cronet 升级时,你会发现新版 Cronet 框架可能都变了,这个时候合并代码就非常痛苦了。
以前,我们做浏览器,每次进行内核升级,就有几千上万个文件冲突,光合并代码这部分要花半个月的时间。另外代码合并的时候,你其实不知道怎么合并它,变化太多了,合并很麻烦。最后一个,即便侥幸编译链接通过了,你会发现很多功能衰退。
基于此,我们想了一些对应的解决思路,比如对于代码冲突来说,我们基于源码二次定制时尽量减少修改,尽量做好隔离让修改的地方能够很轻易的辨识出来。这样合并者可以很清楚的知道这个地方要合,这个地方不要合。对功能衰退最有效的方法就是做单测,Chromium 项目本身单测覆盖率很广,你只要在新增的代码上做一些单测覆盖就够了。
下面重点讲下,如何减少侵入并做好隔离,这两点其实是很简单的技巧,没有什么难度,大家可以很轻易的参考并实践。
减少侵入我们是这样考虑的,主要是三个词:一个是提接口,第二个是基于接口搭框架,最后基于框架扩组件。
对 Cronet 的侵入,我们更多以接口的方式进行,而不是直接魔改。提接口包括新增一些代理、观察者或者拦截器(运用了部分设计模式的思路),把所有的修改都汇聚到几个小的点上,整个实践下来,提接口这部分,我们改造了快一年了,接口部分只占修改里面的 5% 甚至更少;基于新增的接口,我们搭了一个网络框架,网络框架主要做一些能力封装,包括组件机制和通道封装,再包括一些插拔服务等等;最后,我们会把各种策略、监控、业务都往组件上堆,整个下来,对源码的侵入程度控制得还是可以的。
下图代码是我们对 Cronet 源码进行修改的一处示例。左边是源文件关于 Socket 复用的代码,右边做了一些改造,我们做了一个回调,由业务侧决定是否可以进行 Socket 复用。乍一眼看,无法察觉两者的差异,这给内核升级带来一个很严重的挑战。我们代码完全没加任何隔离措施,一合并完全不知道自己做了哪些修改,完全没法合并。
怎么办呢?有同学可能会想到加注释这一类的。以前,刚开始做浏览器的时候,我们团队也是用加注释这种方式,这种方式简单“高效”,但总感觉有一点不舒服,虽然进行了隔离,但是有时候定位一个问题,你觉得这个 bug 不应该是 Cronet 的,应该是我们修改出来的,这时候,你可能需要编译原始代码进行验证,但是加注释的方式导致你无法快速编译出一个源版本。
对于 C++ 来说其实很简单,就是加一个宏开关,通过宏开关你可以很清楚的看到哪些地方改动了,一目了然,并且可以快速进行源切换,这是以前我所在的团队长期实践下来的经验。
大家看到所有修改都把源代码放在前面,把我们的修改放在后面,为什么要这样做?有一个很大的好处是,上面的代码因为都不是你修改的,合并的过程中不太会有冲突,后续如果有问题,你可以通过合并工具快速的识别差异。
对于 C++ 以外的代码,我们也做了与宏开关类似的开关进行代码隔离,如下图所示。
最后我们还做了文件级别的隔离。红框部分是新增的文件目录,我们把修改全部放在 wow 下面,把所有修改都隔离在自己的文件里面。举一个例子,net 目录下有一个文件,我们对它进行了修改,我们会把新建的文件放在 wow 下的 net 目录中,并对新增的文件加上 wow 前缀。
Cronet 升级无法避免,也没有银弹,我们在这里采用低成本、可推行的“技巧”,让整个升级变得相对简单一点。
避坑指南
解决了升级的顾虑,你是不是已经跃跃欲试了?别急,你还需要最后一步,先让 Cronet 代码跑起来。
下图是 Cronet 官方的文档,对源代码的 check out、build and run 给出了详尽的描述,你只需要对着文档一步步操作就行了。由于 Cronet 未单独开源,Cronet 与 Chromium 的代码是混在一起的,共用同一个代码仓库和构建系统,所以代码拉取和编译环境准备与 Chromium 一致,对于 Cronet 的编译,Google 给出了单独的文档描述,并且为 Android 和 iOS 提供了单独的编译脚本,简单点说就是一条命令行的事情。
年初我们对 Cronet 进行了调研,那时 Chromium 的内核版本是 M88,整个拉代码编译调试都很顺利,当然我们只测试了 Windows 端。我们接入 Cronet 的初衷只是为跨平台化,但随着调研的深入,我们看到了各大互联网公司关于 QUIC 方面的实践,他们给出了不同版本的性能提升数据,由于 Cronet 天然支持 QUIC,我们就想,等完成网络库跨平台化改造后,我们也开启 QUIC,看下效果,如果真有提升,那就更好了。
但当我们对 QUIC 进行一番调研后,我们发现国内云厂商主要支持的 QUIC 版本是 gQUIC 43,并且 Chromium M88 版本已经不默认支持 gQUIC43,也就是说我们不得不回退到某个默认支持 gQUIC 43 的低版本 Chromium,为了更好的做数据对比并降低 QUIC 接入的风险,我们选取了与传媒一致的版本 M72。
由于 M72 与 M88 隔了近2年的时间,当我们将 Chromium 代码切回 M72 时,我们发现无论是 Windows、Ubuntu、Mac 下,都编译不过,有各种奇怪的问题。捣腾了很久,后来才猛然意识的,可能是文档的问题,毕竟2年过去了,怎么找到2年前 M72 对应的官方文档呢?
Chromium 的官方文档和代码是统一管理的,你只需在 src 目录下找到对应的文档即可,或者线上直接根据 tags 找到对应的文档即可,有了匹配的文档,各种编译链接问题就少多了。
下图是根据官方文档和我们的使用习惯整理的 Cronet 在各端下的开发工具、编译环境和构建系统。唯一需要强调的是,在编译 iOS 版本时,需要将 XCode 中的 Command line tools 指向低版本、Win10 下记得安装对应的 SDK。另外,整个 SDK 的构建,使用了 GN+Ninja 的方式,可能刚接触的同学会有一点不适应,其实熟悉几天就可以上手了。
另外一个是调试。做开发的都知道,学一个东西很简单,就是把代码拉下来编译,然后 Debug。出乎我们意料的是,在 Android 下原生不支持调试 Cronet SDK,官方推荐一个是 VLOG,第二个是 NetLog。因为我们三端都有对应的开发同学,比如我习惯 Windows,直接基于 Cronet 建一个 Demo 进行调试,大部分场景都能覆盖。如果覆盖不了就用 VLOG 调试一下,这个问题是一个痛点,暂时还没有找到很好的解决方法。
最后是 App 接入。使用 Cronet 发起请求,Google 提供两种实现,一种是异步的 UrlRequest,另一种是符合移动端协议标准的,在 UrlRequest 基础上封装了流操作的 HttpURLConnection 和 NSURLProtocol 实现。
由于我们对网络请求进行了一些接口扩展,左侧的方案只要修改 URLRequest,右侧的方案既需要修改 UrlRequest,又需要修改上层的协议封装,出于减少对 Cronet 侵入的考虑,我们更倾向于所有的端都使用同一套网络接口,即 UrlRequest,事实上在主站 API 请求接入的过程中,我们的确是这样操作的。但是当接入 CDN 请求时,面对流操作,UrlRequest 使用起来并不太方便,业务侧需要更多的适配和修改才能接入,最后出于让 App 更低成本接入方面的考虑,我们还是整体切换到了使用 HttpURLConnection 和 NSURLProtocol 的方式。
这里面有一些试错成本,分享给大家,大家可以少走点弯路。避坑指南主要是基于 Cronet 进行二次开发相关,如何让代码跑起来只是一个小坑,跳过了以后就是康庄大道。
落地实践
我们一开始对 Cronet 不太了解,先接入了官方的 Cronet SDK,进行了试验;再基于 Cronet 做一些网络框架的搭建,然后将业务组件逐个下沉到网络库(用 C++ 重写);下沉完以后在 Android 端进行了落地(放量中);然后逐步把 QUIC 开启,代码里面只要一行代码开启就行了;最后,iOS 端也在接入过程中。
线上数据方面,Cronet 在未开启 QUIC 的情况下,响应时间较 OK 提升 16%~20%,开启 QUIC 后,响应时间进一步提升到 37%~41%。这个 40% 的性能提升非常可观,你可能优化一年也赶不上这个效果。我们为了把上层业务跨平台化,最终选择将底层网络库也一起跨平台化,因为方案选型的原因,我们直接享受了 Cronet 带来的网络性能的优化成果。
后续规划
整个跨平台网络库,已经在网易云音乐的主 App 侧部分落地了,接下来会把 Windows 和 Mac 覆盖掉;网络库,在云音乐主 App 完全落地后,我们会在云音乐产品矩阵中逐个落地;未来,我们会在网易内部进行推广。
Cronet 接入了,后面的调优之路才刚刚开始,包括预连接、参数调优、“竞速”优化、连接复用率、连接迁移、QUIC 集群独立部署等。
今年 QUIC 的标准化版本已经出来了。未来,等时机成熟,我们会通过升级 Cronet 内核版本直接支持 iQUIC。
我们实践下来网络库是最值得和最应该进行跨平台化改造的;推荐 Cronet 作为跨平台网络库的首选,稍微有一点点门槛,但是这个门槛不高;我们线上数据 Cronet 在 HTTP/2 的基础上已经有不错的性能优势,大概 20% 左右,开启 QUIC 后优势进一步放大;对于接入 Cronet,一开始我们也是观望的,你可以跟百度、微博、网易传媒一样,先尝试一下用官方的 Cronet 进行接入,再决定是否基于Cronet进行二次开发。