70%以上业务由H5开发,手机QQ Hybrid 的架构如何优化演进?

MichaelaMcL 8年前
   <p>随着前端开发的兴起,QQ也逐渐演变为Web与原生终端混合的开发模式。得到Web动态运营能力的同时,QQ也在交互响应速度、后台服务压力、海量用户集的带宽冲击等方面,受到了更多的挑战。在快速的Web运营节奏下,必须保证嵌入QQ的第三方业务也始终处于一个高质量的服务状态。针对这些问题,QQ团队除了采用动态CDN、后台渲染等全栈手段优化体验,也构建了围绕速度、成功率、页面异常等维度的监控体系来保障服务质量。</p>    <h2><strong>写在前面</strong></h2>    <p>首先自我介绍,我叫涂强。我于2005年加入腾讯,那个时候还不流行移动端、hybrid等开发。我当时主要开发PC版本的QQ,后来负责PC版QQ UI引擎的时候做过一些尝试,即在PC客户端上集成浏览器内核,那个时候做了一些H5和native混合开发的框架性工作。之后我加入了腾讯QQ会员团队,负责QQ会员在移动终端上的技术,同时也有很艰巨的任务:维护手机QQ中的所有H5 hybrid开发的框架,即WebView组件的技术工作。</p>    <p>言归正传,现在主流的hybrid还是H5 + native,H5开发对现在移动终端的重要性不必多提,但H5在native中很明显的问题大家都看得到,比如打开应用的时候要等很久的页面loading,loading时用户看到转菊花的界面很可能就流失掉,这也是产品经理不想看到的状况。还有一点是每次打开H5都涉及到网络交互、文件下载,这些操作会消耗用户的流量,如果流量消耗大用户也会不高兴。</p>    <p>今天给大家分享的内容主要是介绍QQ会员团队如何在页面打开时间以及用户流量方面所做的优化,分别对应sonic和reshape的两个自主技术框架。</p>    <h2><strong>传统页面的动静分离</strong></h2>    <p>所有的技术选型与框架都是要结合业务形态来选择,大家对QQ会员的业务形态可能有简单的了解。手机QQ中可以说有大概70%以上的业务由H5开发,像会员的主要商城:游戏分发中心、会员特权中心和我现在负责的个性化业务的商城等等。这些商城特点很明显,它们不是UGC生成的页面,是产品经理在后台配置的内容,比如在页面上可以看到的表情和主题等等。</p>    <p>这些页面相对传统,在最初的时候,一个传统的H5页面为了提升速度和体验会做一些动静分离的优化,比如页面顶部的banner以及下面我们称作为item的物品区域,这些区域的数据可以由产品经理自由编辑随时更换,我们会通过页面loading之后发起CGI请求,从dataServer获取数据,然后再拼接起来。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/f095b9bd5260e22019f0564d3bb5d78b.png"></p>    <p>这里的流程大概如下,用户从click开始,到launch WebView,WebView去加载CDN上的HTML文件,页面loading起来后才会去获取JSON,为了加速这个过程可能会用到localStroage做缓存,这整个过程是非常传统的静态页面加载过程,相对比较简单。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/8b2812c6c2dbe3605c0bc2e109591146.png"></p>    <p>但上述方案有一些问题,比如我们在launch WebView的时候网络处于空等状态,这会浪费时间。我们团队内部统计了Android机器launch WebView大概需要1秒以内(因为手机QQ是多进程的架构,WebView生存在另一个进程内部,launch一次WebView除了进程loading还有浏览器内核的加载)。</p>    <p>其次,发布在CDN上的静态页面内部不包含item数据,所以用户第一眼看到从CDN下载的页面,里面的banner区域和item区域处于一片空白,这对用户体验也是很大的伤害。</p>    <p>还有一个问题,页面loading起来要refresh当前的DOM,即拉取JSON之后拼接DOM结构再refresh,我们发现在一些QQ用户所使用的低端Android机器里,这个执行也会非常消耗时间。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/2b349bdb10d1db273c0693960c2704bd.jpg"></p>    <h2><strong>静态直出+离线预推</strong></h2>    <p>面对这些问题我们大胆采取了一些技术手段,我们称之为静态直出+离线预推的模式。首先我们把WebView的加载和网络请求做了并行,我们所有的网络请求并不是从WebView内核发起request,而是loading WebView的过程中,我们通过native的渠道建立自己的HTTP链接,然后从CDN和我们称作offlineServer的地方获取页面,这个offlineServer也就是大家听说过的离线包缓存策略。</p>    <p>我们在native会有offlineCache,发起HTTP请求的时候首先检查offlineCache里有没有当前HTML缓存,这个缓存和WebView的缓存是隔离的,不会受到WebView的缓存策略影响,完全由我们自控。</p>    <p>如果offlineCache没有缓存才会去offlineServer去同步文件,同时也会去从CDN去下载更新。我们在CDN上存储的HTML已经把banner和item等所有的数据打在静态页面里,这个时候WebView只要拿到HTML就不需要再做refresh和执行任何JS,整个页面可以直接展示出来,用户也可以进行交互。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/97ee1117f62eeee98554d7c47bc48e08.png"></p>    <p>这个方案首先会节省WebView launch的时间,这段时间可以直接网络传输,另外如果本地有offlineCache甚至也不需要网络传输请求,相当于完全加载一个本地页面。但很多时候我们为了保险起见,还是加了页面loading,然后做refresh的操作防止数据的不一致性。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/754a449f31ec6427cfdb50c0c073808b.jpg"></p>    <p>这套机制上线后效果不错,但真的去实施这种H5加载模式会遇到一些坑,例如产品经理配置的banner图片和item数据可能会存在多份数据版本不一致的情况。</p>    <p>产品经理肯定是在dataServer上配置最新数据信息,但CDN上的页面内置的数据有可能仍处于上一版本,更差的情况是离线包服务器和offlineServer生成的HTML又是另外一个版本。当用户本地的缓存和server同步不及时即常见的缓存刷新问题,很有可能存储的数据又是另外一份。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/a5046c88877d7453ad4b4005886da7dc.jpg"></p>    <p>所以这套系统刚开始灰度试用的时候,产品经理很快就找我们吐槽:打开页面时看到的是一份数据,过了一秒页面刷新后看到的内容又不一样,而且每次进入页面都会发生这种情况。</p>    <h2><strong>如何统一数据</strong></h2>    <p>如何快速把四个版本的数据全部统一?我们针对静态直出这种模式做了小型的自动构建系统,产品经理在管理端配置数据要同步dataServer时,我们会立刻启动我们内部称为vnues的构建系统。</p>    <p>这套系统是基于Node.js搭建的,会把开发所编写的代码文件和UI素材图片等等数据实时生成最新版本的HTML,然后发布到CDN以及同步到offlineServer上,这可以解决CDN的文件与最新数据不一致的问题。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/af0ecfd9b69075fa83034b2555e87cb4.jpg"></p>    <p>但离线包缓存是放在用户手机上的,我们如何最快速地把用户手机上的离线缓存也更新起来?大家可能会这么做:QQ客户端每次登录上来把offlineServer最新文件下载回来就好了,但这个方案会遇到巨大的流量挑战。</p>    <p>QQ现在每天的活跃用户好几亿,登录峰值差不多十几万每秒,即使一个100KB离线包的更新,发布一次动辄就需要几百GB的带宽,无论从成本还是技术层面都不是我们能接受的事情。</p>    <p>我们offlineServer内部分为流控和offline计算两部分。当一个页面的所有资源需要进行离线包计算打包的时候,offline计算这部分除了把所有的资源打包,内部也会存储之前所有的历史版本,同时根据历史版本和最新版本生成所有的diff,即每个离线包的差样部分。</p>    <p>这个方案也是根据我们业务形态而定的,因为每次产品经理更新的页面数据并不会太多,基本是几KB到10+KB的范围,所以我们没有必要每次离线包的更新都让用户去下载全量的包。</p>    <p>当QQ用户登录后,每次都会询问offline流控server看有没有最新的包可以下载,如果当前流控server统计的带宽在可接受的成本(目前暂定为10GB到20GB的空间),当CDN的带宽撑得住的时候就会把最新的diff下发给客户端,这样就做到离线包一有更新时客户端能以最小的流量代价得到刷新。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/8b4eb9283e774573a79c5481d50b01f2.jpg"></p>    <h2><strong>数据以及效果</strong></h2>    <p>通过这套系统,我们只花了十几GB的带宽,就把整个BG所有H5业务的离线包覆盖率维持在大概80%到90%。从这个工作中我们也发现一个非常反常规的事情,即大家以为离线包预推会非常消耗带宽,但其实只是偶尔预推才消耗大量带宽;如果长年累月不停地推送,实际上对带宽的消耗非常小,因为时时刻刻都保持在差量下发的状态。</p>    <p>做了这个工作后我们采集了现网数据,静态直出和传统页面这两种模式对比非常明显。下图页面耗时部分,由于静态直出页面不需要任何JS执行,只需要WebView渲染,所以页面耗时静态直出相比传统页面降低了大概500毫秒到1秒左右。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/7062d09fc6caaaed6efc5a708ae4c6ca.jpg"></p>    <p>这里有趣的现象是离线包的性价比问题,可以看到传统页面使用离线包可以在网络耗时部分节省700多毫秒,但静态直出这种模式使用离线包只能节省300毫秒左右,这是因为使用静态直出在网络过程中所依赖的外部CSS和JS都已经直出到HTML内部了,不需要额外的网络请求,所以其本身网络耗时有所减少,这时使用离线包的收益开始逐渐下降。</p>    <p>这里可能有疑问,为什么静态直出在离线包的情况下网络耗时还需要800多毫秒,本地有缓存不应该是零耗时吗?</p>    <p>我们统计的网络耗时是从WebView load URL开始到页面首行这段时间,实际上包括一部分页面加载,WebView内核的启动,网络组件和渲染组件的加载,所以耗时比较高。</p>    <p>这里肯定也有优化空间,但当我们的客户端团队正要优化网络耗时这部分的时候,我们的业务形态变了。之前是产品经理配置什么页面就显示什么,所有用户看到的内容都是一样的,现在产品经理说每个用户进入到商城首页看到的内容要完全不一样。</p>    <p>以下图为例,像左边首页的内容是随机推荐的,右边实际上是通过机器学习根据用户过去表情发送的行为习惯和我们后台的物品做计算匹配,根据用户的喜好行为而推荐的内容。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/9fbb8f76862fbd4663b80be31b5d0bf3.jpg"></p>    <p>每个用户进来看到的内容都是不一样,那么静态直出这种模式行不通了,因为我们不可能把所有用户的页面都在后台生成然后发到CDN上,然而这种模式也有很简单的办法来解决。</p>    <h2><strong>动态直出</strong></h2>    <p>我们并不在CDN上存储HTML,而是后台Node.js服务器上动态拼接出整个HTML文件,数据来源是从dataServer去拉取。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/c0b87eae6dd540619f12671e6355b704.jpg"></p>    <p>这种模式解决了产品的需求,但引入了新的问题。WebView获取html要请求Node.js,Node.js要进行后台页面拼装,中间的网络耗时和后台运算耗时比我们想象中要大。在这个过程中,整个页面是无法渲染的,用户进入我们商城首页则看到一片空白,产品经理同样无法接受,用户也不买单。</p>    <p>另外这种模式下几乎无法利用WebView本身的缓存,因为后台直出同样在CSS/JS已经全部都在后端执行,WebView很难将一个纯粹的静态HTML全部缓存下来。为了解决上述问题,我们引入了动态缓存的机制。</p>    <h2><strong>动态缓存</strong></h2>    <p>同样我们不让WebView直接访问我们的Node.js服务器,我们在这中间加上之前提到地类似offlineCache的中间层sonicBridge,这个中间层首先会从Node.js服务器下载完整的HTML给WebView,同时会把下载回来的内容在本地完整地做缓存。</p>    <p>我们之前是全网所有用户缓存同一份HTML,现在修正为全网用户缓存的内容都是从真实server里拉回来的页面。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/93aafedf62a1b8ddd6fe4ba9c2b58ded.jpg"></p>    <p>当用户第二次进入页面时,sonicBridge会优先把本地缓存的页面提交给WebView,用户进入页面不需要等待网络请求就可以看到内容,这对用户侧在速度上的体验提升比较大,但它又引入了另外一个问题。</p>    <p>实际上用户每次打开WebView看到的内容都不一样,Node.js每次返回的数据都是最新的,因此拉回来的数据我们必须让WebView进行reload,这给用户的体验是:明明已经打开了本地缓存好的HTML并且看到内容,但一操作页面却整个reload了。在一些低端机型上WebView reload非常耗时,用户能很明显感觉到整个WebView H5页面白屏一下,然后才刷新出新的内容。</p>    <p>结合前面提到的静态直出局部refresh部分DOM的经验,我们可以减少网络传输量和减少提交页面的数据量。我们首先做的事情是减少网络传输量,避免refresh的时间太靠后。</p>    <h2><strong>减少传输数据</strong></h2>    <p>我们改变了Node.js组HTML的协议,当sonicBridge在第二次请求数据的时候,Node.js服务器并不会返回整个HTML给sonicBridge,而是返回给我们称为data数据的部分。</p>    <p>拿到data数据之后,我们和H5页面做了约定,由native侧调用页面的固定刷新函数,并传递数据给页面。页面会去局部刷新自己的DOM节点,这样即使页面需要刷新也不会reload整个页面。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/5915e30e0832abd12054798f59c20691.jpg"></p>    <p>具体从数据内容的流程上看,首次sonicBridge加载页面返回的仍是完整的HTML,同时会返回我们称为template-tag的id,这个template-tag会标记这个页面中静态不变的那部分hash值,这措施是为了控制缓存。在返回的HTML中我们会也有一些标记,比如sonicdiff-banner,这个banner决定了它的刷新id是什么。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/db1d619493af5c6337f49c900f209f81.png"></p>    <p>当第二次加载返回的数据就没有前面看到的整个HTML,只会返回大概37KB的数据,这个data实际上就是一个JSON,但它定义了前面对应例如sonicdiff-banner的DOM结构。为了节省H5中执行的代码,我们直接在JSON中把DOM节点代码拼好,这样页面只需要做id的匹配和刷新。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/c6b60d2b9803c063e4267b97428a4b52.jpg"></p>    <p>这里37KB的传输数据很难避免,我们观察到不同业务的刷新数据量还不一样。能否能再减少一些提交给页面去刷新的数据量呢,毕竟产品经理每次修改的数据也不会很多。</p>    <h2><strong>减少提交页面数据</strong></h2>    <p>我们在sonicCache这一层除了前面提到的我们会缓存完整的HTML、template,还会把数据提取出来做dataCache。</p>    <p>template是在首次访问的时候,根据sonicdiff里的id信息,把所有可变的数据剔除掉剩下的页面框架。用户二次打开时只要根据返回的数据,在客户端本地和template做merge拼接就可以得到完整的HTML。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/874dd80d9d976cb4dbac64d8f80c66c4.jpg"></p>    <p>我们把每次的dataCache缓存下来后,对数据也做了差量,比如这一次请求返回的是37KB的数据,上次cache的也是37KB的数据,我们会判断内部真正变化的到底有多少,然后只把差量的部分交给HTML刷新,这样在大部分场景下我们的页面只需要处理大概9KB的数据就能刷新整个页面。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/09ea57ae74a24095b75bfc023ce5156e.jpg"></p>    <p>有了cache后用户在本地打开的速度非常快,差量数据的传输也使得用户刷新等待的时间减少了,最后加上这种数据提交时的diff使页面刷新范围也得到大幅的减少。</p>    <p>整个sonic模式流程如下,看上去比较复杂,但基本原理就是通过Bridge桥接把请求回的HTML分模版和数据进行缓存。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/94fb82a9b3910d6c97d98aa7702f89ee.jpg"></p>    <p>这里可能有疑问,前面静态直出花力气做的offlineServer和离线预推策略,在这里还有没有用?事实上动态页面和之前提到的静态页面离线缓存机制我们仍在使用,因为我们业务页面还有大量公用JS,比如QQ提供的JS API封装,还有一些共用的CSS也是通过离线包策略做的预推,这也是大家每次登录的时候都会进行的下载。</p>    <h2><strong>数据以及效果</strong></h2>    <p>完成这种模式之后数据效果相对明显,首次加载和普通的HTTP加载性能是差不多的,但用户第二次打开页面时,通常只需要1秒的时间就能看到页面,这1秒钟还包括客户端launch进程和WebView的开销,同时我们加载速度不再受用户网络环境的影响,不管是2G还是4G加载速度都接近一样。</p>    <p>而且还带来一个好处,如果用户的网络比较差,比如经常抖动连接不上,因为我们本地有缓存,因此就算用户当前处于断网状态我们的页面也能打开。</p>    <p>这里没有提到模板更新的场景,模板更新是指我们抽取出来的template在我们server是有可能动态变化的,这个时候的加载流程和我们前面提到的就不太一样了,当template有变化时,还是按照原来走HTML reload页面的流程,这里的耗时相对偏高,但我们统计发现大部分用户还是落在数据刷新也就是二次打开的状态。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/8d253cf15889e46b13920510827bf4bb.jpg"></p>    <h2><strong>是否使用持久连接</strong></h2>    <p>在做H5的提速优化时,大家很容易想到我们是否要利用持久连接去避免访问服务器的connect、DNS、握手之类的耗时,像QQ这种客户端它跟后台server是有持久化的连接的。假如用这个连接来向后台服务器请求HTML文件并交给WebView,会不会比临时建立一个connect请求更快呢?我们只需要搭一个反向代理服务就可以从QQ消息后台访问我们Node.js服务器,这个流程是可以打通,但是我们评估认为这种模式未必适合所有的场景。</p>    <p>确实是有一些App利用持久连接这种通道去加载页面,但在手机QQ比较难行得通,因为手机QQ客户端与sever的持久连接通道是一个非常传统的CS架构,它发送的是socket package,每次需要发送一个请求包,收到应答之后才会继续下一个请求。</p>    <p>这种应答机制决定了它每次都需要一个等待的过程,而且socket package的约束造成了每次传输的数据包的大小受到限制,像我们30+KB的数据很有可能要拆成五六个数据包,虽然是利用了持久连接节省了connect耗时,但和server多次来回通讯反而把整个耗时加大了。</p>    <p>另外,从Node.js服务器返回的数据是HTTP流式的,WebView不需要等待整个HTML加载完成后才能进行渲染和显示,只要拿到传输中的first byte就可以开始进行document的解析以及DOM的构造。</p>    <p>如果要使用持久连接,我们很有可能要经过客户端的加密解密以及组包等步骤,并且要等到整个HTML下载完成之后才能进行显示,我们认为这个时间反而拖慢了性能。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/0298302fc6230d523f3ff379f4f0eab7.jpg"></p>    <h2><strong>QQHybrid架构</strong></h2>    <p>经过上述的介绍后,大家对QQHybrid可能有了大概直观的印象:1. 我们在WebScope的前端开发同学做了一部分工作;2. 我们的native层终端开发同学做了bridge桥接,3.我们后台的同学做了很多的自动集成和offlineServer推送等工作。该部分架构如下:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/73aa7f72e8136385276dddacc3a12415.jpg"></p>    <p>接下来我将介绍架构图右边关于页面流量的部分。我们统计过各业务中关于流量的分布,如下图,我们可以明显看到大部分的流量都消耗在图片资源上,但我们做这个分析时也曾经有怀疑,是不是业务特性决定了我们图片消耗是最多的?手机QQ其他H5业务是不是也这样?</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/1e70b6a1afafb0fe8b106a68be8423f2.png"></p>    <h2><strong>春节红包的流量分析</strong></h2>    <p>恰好我们有了一次机会,在2016年春节时手机QQ做了一次几乎所有业务都有参与的活动——春节红包,大家可能还有印象在2016年春节晚上时不停戳屏幕领红包的操作。这种全民狂欢的背后带来了巨大的流量压力,每秒钟大概给用户发出30万左右的礼包,引导用户的web流量大概每秒钟会有十几万次H5页面的打开,当时评估的流量峰值超过1TB。</p>    <p>我们分析了里面的图片流量,确实占据了接近一半的水平,有一部分我们已经走离线包预推的方式提前下发到用户的手机中,但在活动期间现网的图片流量仍超过了200GB。</p>    <p>流量并不是简单花钱向运营商买就能解决的问题,春节的活动我们几乎遇到了单域名下流量接近200GB的情况,当时CDN的架构已经快扛不住了。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/1113400f3ddfd5d0a68362266cf4ed30.jpg"></p>    <p>我们都认为这里大有可为,如果图片流量能节省的话,带宽成本能够降低。用户侧所消耗的网络流量和手机电量等等体验都能更好,所以我们团队check了有关图片格式方面的新东西。</p>    <h2><strong>SharpP的应用</strong></h2>    <p>大家都比较熟悉WebP,而且Android对其支持也比较好,而QQ团队内部自己研发了叫SharpP的图片格式,在文件大小上能比WebP节省10%左右的体积。下面是抽取我们CDN服务器上已有图片进行的数据比对。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/d2815f5245e02f55a9eea84726ad1f9f.jpg"></p>    <p>图片体积是占优的,但解码速度方面呢?我们分别使用了高、中、低端的机型分析,很不幸SharpP确实会比WebP甚至比JPG要慢一点,但幸运的是我们业务的图片尺寸还不算太大,页面中多花几十毫秒也能接受,相比节省等待网络的时间我们觉得这是更有利的。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/9d2c9c5cfaf173033b4bfebf3aa71d4a.jpg"></p>    <p>于是我们准备在手机QQ H5业务中推广SharpP格式,但推广新图片格式会带来很大的应用成本。首先大部分的图片链接都是写死在代码里面,而且页面并不知道移动终端有没有能力去解码SharpP的格式。</p>    <p>难道H5页面要针对不同的手机QQ版本去准备不同的HTML?或者图片资源发布到CDN上时生成两个不同格式的链接,然后在H5内部根据终端版本选择不同的链接?这个开发成本当然是不可接受的。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/762d45f015a7d7ecc5eb14ebe81f1fc9.jpg"></p>    <p>除了图片格式的问题,我们发现用户的不同机型会存在流量浪费的情况。我们的UI设计通常都是针对iPhone6的屏幕尺寸做的,默认是750px的图片素材。小屏幕的手机,比如640px和480px,同样是下载750px的图片,然后在渲染的时候进行缩小。</p>    <p>这样实际浪费了非常大的带宽,所以我们思考CDN是否能根据用户手机屏幕尺寸来下发不同格式的图片。</p>    <h2><strong>reshape架构</strong></h2>    <p>这种屏幕自适应的策略也面临近似私有格式的成本,因为CDN也不知道手机的情况,最后我们提出了reshape的架构,从图片下载完整的环节来看,大概可以分成4个层级:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/8e3a20bdd5175ccdc14fbff4b0891b29.jpg"></p>    <ul>     <li>最底层我们称作CDN源站,这里我们部署了图片格式转换工具,业务方不需要care JPG制作出来后再生成sharpP还是WebP,只需要把图片发布在CDN源站上就能自动转成对应的格式和屏幕分辨率;</li>     <li>往上是用户手机接入的CDN节点,部署全国各地用于加速和缓存文件的server。</li>     <li>我们和浏览器团队做了合作,把sharpP的解码格式放在浏览器内核中,这样最上层的业务不需要关心当前的浏览器是否支持WebP还是sharpP。</li>    </ul>    <p>在打开页面的时候,WebView会自动带上终端的屏幕尺寸以及支持哪些图片格式给CDN节点,CDN节点再从源站获取最新的图片,源站这个时候有可能已经离线或实时生成好对应的图片了。</p>    <p>拆开来看WebView这一层,除了集成sharpP的解码库之外,其他事情相对简单,例如:</p>    <ul>     <li>在请求header里面额外添加了字段,比如User-Agent里添加了“Pixel/750”,如果是480px的机器这个值就变成480;</li>     <li>在Accept里增加了sharpP的协议头:image/sharpP</li>    </ul>    <p style="text-align:center"><img src="https://simg.open-open.com/show/645f3d8bb6853c9f683d9bd6b17d01b6.png"></p>    <p>在源站里会存储3*3数量的图片,每一张业务图片提交给源站发布的时候,都会生成9张图片。CDN节点会根据WebView的请求,在回源的时候向CDN源站请求对应类型的图片,但对于业务和WebView来看请求的还是同一个链接,这样手机QQ所有的H5页面都不需要任何一行前端代码的修改,就能享受图片格式所带来的尺寸自适应和流量节省。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/9c54139515bdef8325d900ee0cbebd8f.jpg"></p>    <p>以下是更形象的过程,在Accept增加字段,然后返回对应的图片:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/961a3d64ba11b76c2b5a46275f1f0a7d.jpg"></p>    <p>这个技术并不复杂,我个人认为不存在太深的技术门槛,更多的是从客户端、Web到CDN后台这整个链条的打通。但过程中我们也踩了一些坑:我们在灰度的时候发现很多iOS用户来投诉说页面展示时图片不能展示。</p>    <p>这个让我们非常惊讶,因为当时还没有把这项技术部署到iOS上,只是Android在应用。我们检查了CDN的代码也没有问题,那为什么会把sharpP的图片下发到iOS用户呢?</p>    <p>后来分析发现,中国不同地区运营商之间,会做类似CDN Cache的缓存服务。当Android用户第一次请求sharpP图片的时候,运营商的server从我们的CDN拿到了sharpP格式链接。当缓存生效期间内,同一个地区其他iOS用户上来请求时,运营商发现URL一样,直接就把sharpP格式的图片返回给iOS用户。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/b262a894cd9b077c0a631fb0f4dc2e9b.jpg"></p>    <p>这个问题是我们整体架构上没有去全盘review而踩中的坑,HTTP有个标准的约定可以解决这种缓存问题。在CDN分发内容的时候,通过Vary字段指定缓存的时候要去参考Accept和User-Agent里的字段,我们把这个Vary加上之后问题基本解决了。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/87e595dae44cd1b052479ed0b2db1aa1.png"></p>    <p>这个案例给了我们额外的启发。我们现网中,Pixel字段有三种取值:480px、640px、750px。我们内部讨论过是否可以把屏幕尺寸直接写在User-Aagent里,这样以后Android出了一些新的屏幕分辨率我们也能在后台做更好的自适应,为每种机型去生成不同格式的图片。</p>    <p>假如真的这么做的话,会给运营商和我们自己的CDN缓存带来巨大的回源开销。每个分辨率的图片都要缓存一份,例如498px,中间运营商没有这机型的缓存,就会到我们的服务去回源,这样N个屏幕尺寸会给我们CDN带来N倍的回源压力。</p>    <h2><strong>数据以及效果</strong></h2>    <p>言归正传,最后数据效果也是比较明显的,下图是我们在Android灰度的效果数据。我们H5业务的图片流量从40+GB下降了20+GB。对于腾讯来说20+GB的带宽不是特别大的成本,但是在春节活动场景下,可以增加近一倍的业务空间。额外带来的好处,是用户看到页面图片所等待的时间相对缩减了,用户侧的流量也节省了一半。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/6088f6db2f6c097196a5d633c5af262f.jpg"></p>    <h2><strong>H5快速运营时的稳定性</strong></h2>    <p>我们解决了页面加载速度和流量消耗的问题时,也开始考虑H5在快速运营下的稳定性问题。相信前端开发都有遇到过某个页面代码一改,其他功能就不正常了的情况。采用hybrid开发很有可能native是要给JS页面提供很多API的,客户端小小的改动可能导致JS API受到影响,从而导致全网的H5页面功能不正常。</p>    <p>除了功能稳定性之外还有一个很大的问题,我们每天都在发布前端页面,那页面的优化性能如何不被劣化?我们好不容易花了时间把页面加载的性能降低到1秒,会不会有一些前端的修改比如引入更多的外链JS/CSS依赖导致整个页面性能劣化?我们做了一些工具去解决这些问题。</p>    <h2><strong>Quick Test Automation</strong></h2>    <p>这是我们内部称作快速自动化的工具。我们会把前端所有的测试用例集写成自动化测试,然后每天都会把全网所有页面所有的测试用例集跑一遍,检查功能是否正常。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/d7a81b0137706a89d7b77f5ba0ad79f0.jpg"></p>    <h2><strong>Web Performance Test</strong></h2>    <p>我们会对Web的性能做Web Performance Test的监控,这里我们首要观察的是页面每次打开所消耗的流量,因此会用工具去分析页面中所有加载的图片是不是有些可以转成sharpP但仍然使用了JPG的。有了这套监控,能促使我们团队之外的H5开发者去优化他们的页面。</p>    <p>前端经常提到优化时要减少请求数量等等,这些可以认为是军规的规则,我们都会在测试中进行监管。前面没有详细提到客户端优化的一些方法,但是我们对于WebView在客户端启动的耗时也做了一些监控。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/e276c6d4caf22b5d855f49e4bb9b520a.jpg"></p>    <h2><strong>前端部署流程</strong></h2>    <p>我们还有更严格的前端发布流程,所有在测试环境中写好的并且测试通过的代码,如果要发布到正式环境必须要通过QTA和WPT的验证,如果自动化测试成功率低于95%则不允许发布。</p>    <p>发布到正式环境之后,我们在外网还有综合评分监控的系统,其首要监控的指标是关于速度的,我们把页面打开的速度拆解为客户端耗时、网络耗时和页面耗时并对它们分别监控。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/3593dae68ab875d4ffaf7978b03649e7.jpg"></p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/7de5d7affe9e0b3a32421117ac1c4cfc.png"></p>    <p>我们每天都会输出如下的监控报表来观察每天速度变化,这里我们并不单纯关心全网的性能怎么样,我们更关心慢速用户的体验,例如大于5秒的用户最近占比会有多少。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/78cd7a6a1f4a75b61912a3638a873035.png"></p>    <p>除了这些,H5经常遇到的某些JS报错而导致页面不正常,加载过慢导致用户看到白屏的时间过长等等问题,我们对这些都有系统的监控。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/e6b45c487ad2a8f2c32773b0df8f4e2e.png"></p>    <h2><strong>集成运营体系</strong></h2>    <p>除了之前提到的内容之外我们还做了Debug平台,许多调试能力已经提前部署在所有手机QQ终端。我们可以通过远程命令去检查用户的DNS解析情况,命中了哪台server,用户是否受到运营商劫持等等。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/df4bb299d2ce94fb85909d41a2a83aae.jpg"></p>    <h2><strong>写在最后</strong></h2>    <p>整个QQHybrid的架构基本介绍完了,我们除了性能上的优化,也对CDN上的架构做了调整,也做了运营监控工具。我认为正是有了运营监控系统,才能让我们整个H5和hybird团队能放心大胆地修改页面和发布新功能,同时保证稳定和可靠。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/1a49769026652430a2d946b21bb94ccb.jpg"></p>    <p>整个过程中也让我们感到hybrid架构并不像以前大家理解的,只是客户端和前端配合的工作就OK了,在整个架构体系中后台技术也发挥了很大的作用。而CDN改造我们也请教了运维团队的支持,QTA和WPT中也有测试开发团队的参与。可以说整个体系的建立,是所有岗位并肩作战的结果。</p>    <p> </p>    <p>来自:http://www.iteye.com/news/32259</p>    <p> </p>