# 爱奇艺国际站WEB端网页优化实践
# 背景
爱奇艺国际站 (opens new window)提供了优质的视频给海外各国用户,自上线以来,现已支持几十个国际站点,并且在东南亚多个国家保证了海量用户高速观看体验。
国际站业务的特点是用户在境外访问,后端服务器也是部署在国外。这样就面临着比较复杂的客观条件:每个国家的网络及安全政策都不太一样,各国用户的网络建设水平不一。国内互联网公司出海案例不多,爱奇艺国际站的建设也都是在摸索中前进。
为给海外用户提供更好的使用体验,爱奇艺后端团队在这段时间做了不少性能优化的工作,我们也希望将这些探索经验留存下来,与同行沟通交流。
在这篇文章中,我们将针对其中的亮点内容详细解析,包括但不限于:
- WEB性能全链路优化
- 特有的AB方案,横向数据对比,逐层递进
- redis自有API实现的多实例本地缓存同步、缓存预热
- 业务上实现热剧秒级更新
- 自研缓存框架,方便接入
# 业务背景
地区语言
爱奇艺国际站业务有其特殊性,除中国大陆,世界上有二百多个国家,运营的时候,有些不同国家会统一运营,比如马来西亚和新加坡;有的国家独立运营,比如泰国。这种独立于国家之上的业务概念,爱奇艺称之为模式(也可叫做站点)。业务运营时,会按照节目版权地区,分模式独立运营。这并不同于国内,所有人看到的非个性化推荐内容都是一样的。
还有个特殊性是多语言,不同国家语言不同,用户的语言多变,爱奇艺需要维护几十种语种的内容数据。
并且在国际站,用户属性和模式强绑定,用户模式和语言会写在cookie里,轻易不能改变
服务器端渲染
既然做国际站业务,那必不可少做Google SEO
,搜索引擎的结果是爱奇艺很大的流量入口,而SEO也是一个庞大的工程,这里不多描述,但是这个会给爱奇艺前端技术选型带来要求,所以前端页面内容是服务端渲染的。与传统 SPA (单页应用程序 (Single-Page Application)) 相比,服务器端渲染 (SSR) 的优势主要在于:
- 更好的 SEO,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面。
- 更快的内容到达时间 (time-to-content),特别是对于缓慢的网络情况或运行缓慢的设备。无需等待所有的 JavaScript 都完成下载并执行,才显示服务器渲染的标记,所以你的用户将会更快速地看到完整渲染的页面。通常可以产生更好的用户体验,并且对于那些「内容到达时间(time-to-content) 与转化率直接相关」的应用程序而言,服务器端渲染 (SSR) 至关重要。
# 技术调研
都说缓存和异步是高并发两大杀器。而一般做技术性能优化,技术方案无外乎如下几种:
性能优化是个系统性工程,涉及到后端、前端、系统网络及各种基础设施,每一块都需要做各自的性能优化。比如前端就包含减少Http请求,使用浏览器缓存,启用压缩,CDN加速等等,后端优化就更多了。本文会挑选爱奇艺国际站后端团队做的优化工作及取得的阶段性成果进行更详细的介绍。
注:当分析系统性能问题时,可以通过以下指标来衡量:
- Web端:FP(全称“First Paint”,翻译为“首次绘制”),FCP(全称“First Contentful Paint”,翻译为“首次内容绘制”)等。首屏时间是指从用户打开网页开始到浏览器第一屏渲染完成的时间,是最直接的用户感知体验指标,也是性能领域公认的最重要的核心指标。
这个爱奇艺直接使用Google提供的firebase工具就可以拿到直接的结果,它是通过客户端投递进行实时分析的。
- 后端:响应时间(RT)、吞吐量(TPS)、并发数等。 后端系统响应时间是指系统对请求做出响应的时间(应用延迟时间),对于面向用户的Web服务,响应时间能很好度量应用性能,会受到数据库查询、RPC调用、网络IO、逻辑计算复杂度、JVM垃圾回收等多方面因素影响。对于高并发的应用和系统,吞吐量是个非常重要的指标,它与request对CPU、内存资源的消耗,调用的外部接口及IO等紧密关联。这些数据能从公司后端的监控系统能拿到数据
# 网页缓存服务
国际站WEB端首版本上线后,简要架构如下:
爱奇艺国际站有Google SEO的要求,所以节目相关的数据都会在服务端渲染。可以看到客户端浏览器直接和前端SSR服务器交互(中间有CDN服务商等),前端渲染node服务器会有短暂的本地缓存。版本上线后,表现效果不理想。在业务背景的时候介绍过,提供给用户是分站点(国家)、语言的节目内容,这些存放在cookie里,不方便在CDN服务做强缓存。所以,做了一次架构优化
可以看到,增加了一层网页缓存服务,该服务为后端Java服务,职责是把前端node渲染的页面细粒度进行缓存,并使用redis集中式缓存。上线后,缓存命中率得到极大提高。
# 浏览器缓存优化
增加了网页缓存服务后,会缓存5min的前端渲染页面,5min后缓存自动失效。这个时候会触发请求到SSR服务,返回并写入缓存。绝大多数情况下,页面并没有更新,而用户可能在刷新页面,这种数据不会发生变化,适合使用浏览器协商缓存
# 压缩优化
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 依然能够提供非常高的压缩率。
可以看到,是nginx服务支持了gzip压缩。
并且后端网页服务的redis存储的是压缩后的内容,并且使用自定义序列化器,即读取写入不做处理,减少cpu消耗,redis的value就是压缩后的字节数组。
nginx支持brotli 原始nginx并不直接支持brotli压缩,需要进行重新安装编译
# 服务端缓存优化
经过浏览器缓存优化和内容压缩优化后,整体网页性能得到不少提升。把优化目标放到服务端缓存模块,这也是此次分享的重点内容
# 本地缓存+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的存储空间,但是服务可用性得到大大提高
# 参考
← 链接 爱奇艺海外App的网络优化实践 →