维护几十种语言和站点,爱奇艺国际站WEB端网页优化实践
1.前言
爱奇艺国际站()提供了优质的视频给海外各国用户,自上线以来,现已支持几十个国际站点,并且在东南亚多个国家保证了海量用户高速观看体验。
国际站业务的特点是用户在境外访问,后端服务器也是部署在国外。这样就面临着比较复杂的客观条件:每个国家的网络及安全政策都不太一样,各国用户的网络建设水平不一。国内互联网公司出海案例不多,爱奇艺国际站的建设也都是在摸索中前进。
为给海外用户提供更好的使用体验,爱奇艺后端团队在这段时间做了不少性能优化的工作,我们也希望将这些探索经验留存下来,与同行沟通交流。
在这篇文章中,我们将针对其中的亮点内容详细解析,包括但不限于:
WEB性能全链路优化; 特有的AB方案,横向数据对比,逐层递进; redis自有API实现的多实例本地缓存同步、缓存预热; 业务上实现热剧秒级更新; 自研缓存框架,方便接入。
2.技术调研
都说缓存和异步是高并发两大杀器。而一般做技术性能优化,技术方案无外乎如下几种:
并且性能优化是个系统性工程,涉及到后端、前端、系统网络及各种基础设施,每一块都需要做各自的性能优化。比如前端就包含减少Http请求,使用浏览器缓存,启用压缩,CDN加速等等,后端优化就更多了。本文会挑选爱奇艺国际站后端团队做的优化工作及取得的阶段性成果进行更详细的介绍。
注:当分析系统性能问题时,可以通过以下指标来衡量:
Web端:FP(全称“First Paint”,翻译为“首次绘制”),FCP(全称“First Contentful Paint”,翻译为“首次内容绘制”)等。首屏时间是指从用户打开网页开始到浏览器第一屏渲染完成的时间,是最直接的用户感知体验指标,也是性能领域公认的最重要的核心指标。
这个爱奇艺直接使用Google提供的firebase工具就可以拿到直接的结果,它是通过客户端投递进行实时分析的。
后端:响应时间(RT)、吞吐量(TPS)、并发数等。
后端系统响应时间是指系统对请求做出响应的时间(应用延迟时间),对于面向用户的Web服务,响应时间能很好度量应用性能,会受到数据库查询、RPC调用、网络IO、逻辑计算复杂度、JVM垃圾回收等多方面因素影响。对于高并发的应用和系统,吞吐量是个非常重要的指标,它与request对CPU、内存资源的消耗,调用的外部接口及IO等紧密关联。这些数据能从公司后端的监控系统能拿到数据。
3.业务背景
在介绍优化过程之前,需要简要介绍下爱奇艺国际站的特有业务特点,以及这些业务特点带来的难点和挑战。
3-1 模式语言
爱奇艺国际站业务有其特殊性,除中国大陆,世界上有二百多个国家,运营的时候,有些不同国家会统一运营,比如马来西亚和新加坡;有的国家独立运营,比如泰国。这种独立于国家之上的业务概念,爱奇艺称之为模式(也可叫做站点)。业务运营时,会按照节目版权地区,分模式独立运营。这并不同于国内,所有人看到的非个性化推荐内容都是一样的。
还有个特殊性是多语言,不同国家语言不同,用户的语言多变,爱奇艺需要维护几十种语种的内容数据。
并且在国际站,用户属性和模式强绑定,用户模式和语言会写在cookie里,轻易不能改变。
3-2 服务端渲染
既然做国际站业务,那必不可少做google SEO,搜索引擎的结果是爱奇艺很大的流量入口,而SEO也是一个庞大的工程,这里不多描述,但是这个会给爱奇艺前端技术选型带来要求,所以前端页面内容是服务端渲染的。与传统 SPA (单页应用程序 (Single-Page Application)) 相比,服务器端渲染 (SSR) 的优势主要在于:
更好的 SEO,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面。
更快的内容到达时间 (time-to-content),特别是对于缓慢的网络情况或运行缓慢的设备。无需等待所有的 JavaScript 都完成下载并执行,才显示服务器渲染的标记,所以你的用户将会更快速地看到完整渲染的页面。通常可以产生更好的用户体验,并且对于那些「内容到达时间(time-to-content) 与转化率直接相关」的应用程序而言,服务器端渲染 (SSR) 至关重要。
4.优化步骤
总体来说,CDN和服务端页面渲染这块有其他团队也在一直做技术改进,国际站后端团队的核心工作点在前端缓存优化和后端服务优化上。主要包括以下内容:
浏览器缓存优化 压缩优化 服务端缓存优化
4-1 网页缓存服务
WEB端一个页面通常会渲染几十个节目,如果每次都去请求后端API,响应速度肯定会变慢很多,所以必须要添加缓存。但是缓存有利有弊,并且如何做好缓存其实并不是个容易的课题。
鲁迅先生曾经说过,一切脱离业务的技术空谈都是耍流氓。所以在结合业务做好缓存这件事上,道阻且长。
国际站WEB端首版本上线后,简要架构如下:
爱奇艺国际站有Google SEO的要求,所以节目相关的数据都会在服务端渲染。可以看到客户端浏览器直接和前端SSR服务器交互(中间有CDN服务商等),前端渲染node服务器会有短暂的本地缓存。
版本上线后,表现效果不理想。在业务背景的时候介绍过,提供给用户是分站点(国家)、语言的节目内容,这些存放在cookie里,不方便在CDN服务做强缓存。所以,做了一次架构优化,优化后如下:
可以看到,增加了一层网页缓存服务,该服务为后端Java服务,职责是把前端node渲染的页面细粒度进行缓存,并使用redis集中式缓存。上线后,缓存命中率得到极大提高。
4-2 AB方案
后端网页缓存上线后,想继续对服务进行优化。但是后端优化分步骤进行,如何最快查看准确的优化效果?一般比较会有两种纬度:横向和纵向。纵向即时间验证结果,可以使用Google Cloud Platform为应用开发者们(特别是全栈开发)推出的应用后台服务。借助Firebase,应用开发者们可以快速搭建应用后台,集中注意力在开发client上,并且有实时可观测的数据库,有时间纬度的网页性能数据,根据优化操作的上线时间点,就可以看到时间纬度的性能变化。但是上面也提到,网页性能影响因素过多,CDN及前端团队也都在做优化,时间纬度并不能准确看到优化成果。
那就是要使用横向比较,怎么做呢?
答案还是firebase,在firebase上新增项目B,网页缓存服务会把优化的流量更新为项目B投递,这样横向比较项目A和B的性能,就能直接准确表现出优化效果。具体如下图:
PlanB为灰度优化方案,判断方案B的方式有很多种,但是需要确保该用户两次访问时,都会命中同一个方案,以免无法命中缓存。爱奇艺国际站目前采用按照IP进行灰度,确保用户在IP不变更情况下,灰度策略不调整时他的灰度方案是不变的。第二节有提到缓存key里的B的作用,就是这里的PlanB。
详细的比较方式和流程见下图,后续的所有优化策略,都是通过这个流程来判断是否有效:
浏览器请求到后端服务,服务器获取端IP 根据配置中心配置的灰度比例,计算当前请求是plan A or plan B 如果是灰度方案B,则走优化逻辑 并且SSR会根据灰度方案返回不同的firebase配置 firebase进行分开数据投递,控制台拿到两种对比的性能数据 分析数据,比较后得到优化结果
可以看到,这样的流程下来,实现了横向对比,能较准确地拿到性能对比结果,便于持续优化。
4-3 浏览器缓存优化
增加了网页缓存服务后,会缓存5min的前端渲染页面,5min后缓存自动失效。这个时候会触发请求到SSR服务,返回并写入缓存。
绝大多数情况下,页面并没有更新,而用户可能在刷新页面,这种数据不会发生变化,适合使用浏览器协商缓存:
协商缓存:浏览器与服务器合作之下的缓存策略协商缓存依赖于服务端与浏览器之间的通信。协商缓存机制下,浏览器需要向服务器去询问缓存的相关信息,进而判断是重新发起请求、下载完整的响应,还是从本地获取缓存的资源。
如果服务端提示缓存资源未改动(Not Modified),资源会被重定向到浏览器缓存,这种情况下网络请求对应的状态码是 304(not modified)。具体流程如下:
使用Etag的方式实现浏览器协商缓存,上线后,304的请求占比升至4%,firebase灰度方案B性能提高5%左右,网页性能提高。
4-4 压缩优化
Google 认为互联网用户的时间是宝贵的,他们的时间不应该消耗在漫长的网页加载中,因此在 2015 年 9 月 Google 推出了无损压缩算法 Brotli。Brotli 通过变种的 LZ77 算法、Huffman 编码以及二阶文本建模等方式进行数据压缩,与其他压缩算法相比,它有着更高的压塑压缩效率。启用 Brotli 压缩算法,对比 Gzip 压缩 CDN 流量再减少 20%。
根据 Google 发布的研究报告,Brotli 压缩算法具有多个特点,最典型的是以下 3 个:
针对常见的 Web 资源内容,Brotli 的性能相比 Gzip 提高了 17-25%;
当 Brotli 压缩级别为 1 时,压缩率比 Gzip 压缩等级为 9(最高)时还要高;
在处理不同 HTML 文档时,Brotli 依然能够提供非常高的压缩率。
并且从日志中看到,爱奇艺的用户浏览器大多支持br压缩。之前,后台服务是支持gzip压缩的,具体如下:
可以看到,是nginx服务支持了gzip压缩。
并且后端网页服务的redis存储的是压缩后的内容,并且使用自定义序列化器,即读取写入不做处理,减少cpu消耗,redis的value就是压缩后的字节数组。
nginx支持brotli
原始nginx并不直接支持brotli压缩,需要进行重新安装编译:
网页缓存项目支持br压缩
http协议中,客户端是否支持压缩及支持何种压缩,是根据头Accept-Encoding来决定的,一般支持br的Accept-Encoding内容是“gzip,br”。
nginx服务支持br压缩后,网页缓存服务需要对两种压缩内容进行缓存。逻辑如下:
从上图可以看到,当服务端需要支持Br压缩和gzip压缩,并且需要支持灰度方案时,他的业务复杂度变成指数增长。
上图的业务都存在上文图中的“(后端)网页缓存服务”。以及后面也会重点对这个服务进行优化。
该功能灰度一周后,firebase上方案B和方案A的数据对比发现,br压缩会使页面大小下降30%,FCP性能上升6%左右。
4-5 服务端缓存优化
经过浏览器缓存优化和内容压缩优化后,整体网页性能得到不少提升。把优化目标放到服务端缓存模块,这也是此次分享的重点内容。
本地缓存+redis二级缓存
对于缓存模块,首先增加了本地缓存。本地缓存使用了更加前沿优秀的本地缓存框架caffeine,它使用了W-TinyLFU算法,是一个更高性能、高命中率的本地缓存框架。这样就形成了如下架构:
可以看到就是很常见的二级缓存,本地和redis缓存失效时间都是5分钟。本地缓存的空间大小和key数量有限,命中淘汰策略后的缓存key,会请求redis获取数据。
增加本地缓存后,请求redis的网络IO变少,优化了后端性能
本地缓存+redis二级主动刷新缓存
上面方案运行一段时间后,数据发现,5min的本地缓存和redis命中率并不高,结果如下:
看起来缓存命中率还有较大的优化空间。那缓存失效是因为缓存时间太短,能否延长缓存失效时间呢?有两种方案:
增加缓存失效时间 增加后台主动刷新,主动延长缓存失效时间
方案1不可取,因为业务上5分钟失效已经是最大限度了。那方案2如何做呢?最开始尝试针对所有缓存,创建延迟任务,主动刷新缓存。上线后发现下游压力非常大,cpu几乎打满。
分析后发现,还是因为key太多,同样的页面,可能会离散出几十个key,主动刷新的qps超过了本身请求的好多倍。这种影响后台本身性能的缓存业务肯定不可取,但是在不影响下游的情况下,如何提高缓存命中率呢?
然后把请求进行统计后发现,大多数请求集中在频道页和热剧上,统计结果大致如下:
上图蓝色和绿色区域为首页访问和热剧访问,可以看到,这两种请求占了50%以上的流量,可以称之为热点请求。
然后针对这种数据结果,分析后做了以下架构优化:
可以看到,增加了refresh-task模块。会针对业务热点内容,进行主动刷新,并严格监控并控制QPS。保证页面缓存长期有效。详细流程如下:
缓存服务接收到页面请求,获取缓存 如果没有命中,则从SSR获取数据 判断是否是热点页面 如果是热点页面,发送延时消息到rockmq job服务消费延时消息,根据key获取请求头和请求体,刷新缓存内容
上线后看到,热点页面的缓存命中率基本达到100%。firebase上的性能数据FCP也提高了20%。
本地缓存(更新)+redis二级实时更新缓存
大家知道爱奇艺是做视频内容网站,保持最新的优质内容才会有更多的用户,而技术团队就是要做好技术支撑保证更好的用户体验。
而从上面的缓存策略上看,还有一个重大问题没有解决,就是节目更新会有最大5分钟的时差。果然,收到不少前台运营反馈,WEB端节目更新延迟情况比较严重。设身处地地想想,内容团队紧锣密鼓地准备字幕等数据就赶在21:00准时上线1集内容,结果后台上线后,WEB端过5min才更新这一集,肯定无法接受。
所以,从业务上分析,虽然是纯展示服务,也就是CRUD里基本只有R(Read),并不像交易系统那样有很多的写操作,但是爱奇艺展示的内容,有5%左右的内容是强更新的,即需要及时更新,这就需要做到实时更新。
但是如果仅仅是监听消息,更新缓存,当有多台实例的时候,一次调用只会选择一台实例进行更新本地缓存,其他实例的本地缓存还是没有被更新,这就需要用到广播。一般会想到用消息队列去实现,比如activeMq等等,但是会引入其他第三方中间价,给业务带来复杂度,给运维带来负担。
调研后发现,Redis通过 PUBLISH、SUBSCRIBE等命令实现了订阅与发布模式,这个功能提供两种信息机制,分别是订阅/发布到频道和订阅/发布到模式。SUBSCRIBE命令可以让客户端订阅任意数量的频道,每当有新信息发送到被订阅的频道时,信息就会被发送给所有订阅指定频道的客户端。可以看到,用redis的发布/订阅功能,能实现本地缓存的更新同步。
由此变更了缓存架构,变更后的架构如下:
可以看到,相比之前增加了本地缓存同步更新的功能逻辑,具体实现方式就是用redis的pub/sub。流程如下
服务收到更新消息 更新redis缓存 发送pub消息 各本地实例订阅且收到消息,从redis更新或者清除本地缓存
可以看到,这种方案可以保证分布式多实例场景下,各实例的本地缓存都能被更新,保证端上拿到的是最新的数据。
上线后,能保证节目更新在可接受时间范围内,避免了之前因引入缓存导致的5分钟延迟。
Tips:Redis 5.0后引入了Stream的数据结构,能够使发布/订阅的数据持久化,有兴趣的读者可以使用新特性替换。
本地缓存(更新)+redis二级实时更新缓存+缓存预热
众所周知,后端服务的发布启动是日常操作,而本地缓存随服务关闭而消失。那么在启动后的一个时间段里,就会存在本地缓存没有的空窗期。而在这个时间里,往往就是缓存击穿的重灾区间。爱奇艺国际站类似于创业项目,迭代需求很多,发布频繁,精彩会在发布启动时出现慢请求,这里是否有优化空间呢?
能否在服务启动后,健康检查完成之前,把其他实例的本地缓存同步到此实例,从而避免这个缓存空窗期呢?基于这个想法,对缓存功能做了如下更新。
具体流程如下:
新实例启动时发布初始化消息 其他实例收到订阅消息后,获取本地可配置数量,通过caffeine的热key算法,获取缓存keys,发送更新消息 新实例收到订阅消息后,从redis或者从远程服务新增本地缓存。 这样能使new client变"warm"(即预热)
这样的预热操作在健康检查之前,就可以保证在流量进来之前,服务已经预热完成。
预热功能新增后,服务的启动后1分钟内的本地缓存命中率大大提升,之前冷启动导致的慢请求基本不复存在。
本地缓存(更新)+redis二级实时更新缓存+缓存预热+兜底缓存
在迭代过程中,会发现在业务增长期,前后端迭代需求很多,运营这边也一直在操作后台。偶尔会出现WEB端页面不可用的情况出现,这个时候,并没有可靠的降级方案。
经过对现有方案的评估和复盘,发现让redis缓存数据失效时间变长,当作备份数据。当SSR不可用或者报错时,缓存击穿后拿不到数据,可以用redis的兜底数据返回,虽然兜底数据的时效行不强,但是能把页面渲染出来,不会出现最差的渲染失败的情况。经过设计,架构调整如下:
可以看到,并没有对主体的二级缓存方案做变更,只是让redis的数据时效时间变长,正常读缓存时,还是会拿5min的新鲜数据。当SSR服务降级时,会取24小时时效的兜底数据返回,只是增加了redis的存储空间,但是服务可用性得到大大提高。
4-6 二级缓存工具
从上面看到,针对服务端二级缓存做了很多操作,而且有业务经验的同学会发现,这些实际上是可以复用的,很多业务上都能有这些功能,比如二级缓存、缓存同步、缓存预热、缓存主动刷新等等。
由此,基于开源框架进行二次开发,结合了caffeine和redis的自有API,研发了二级缓存工具。
更多功能还在持续开发中。
如果业务方需要二级缓存中的这些功能,无需大量另外开发,引入工具包,只需进行少量配置,就能支持业务中的各种缓存需求。
5.优化成果
经过不懈努力,咱们国际站WEB端的性能得到大大提升,可以看看数据:
这只是其中一项FCP数据,还有后端服务的缓存命中率和服务指标,都有显著的变化。Amazon 十年前做的一项研究表明,网页加载时间减少 100 毫秒,收入就会增加 1%。放在现在这个要求恐怕更高,所以优化的成果还是很显著的。
但是我们并没有停下脚步,也还在尝试后端服务进行GC优化、服务响应式改造等,这也是性能优化的另一大课题,期待后续的优化成果。
作者:
Peter Lee 爱奇艺海外事业部后端开发
Isaac Gao 爱奇艺海外事业部后端开发经理
也许你还想看