写点什么

维护几十种语言和站点,爱奇艺国际站 WEB 端网页优化实践

  • 2021-03-30
  • 本文字数:6807 字

    阅读完需:约 22 分钟

维护几十种语言和站点,爱奇艺国际站WEB端网页优化实践

1.前言

爱奇艺国际站(www.iq.com)提供了优质的视频给海外各国用户,自上线以来,现已支持几十个国际站点,并且在东南亚多个国家保证了海量用户高速观看体验。


国际站业务的特点是用户在境外访问,后端服务器也是部署在国外。这样就面临着比较复杂的客观条件:每个国家的网络及安全政策都不太一样,各国用户的网络建设水平不一。国内互联网公司出海案例不多,爱奇艺国际站的建设也都是在摸索中前进。


为给海外用户提供更好的使用体验,爱奇艺后端团队在这段时间做了不少性能优化的工作,我们也希望将这些探索经验留存下来,与同行沟通交流。在这篇文章中,我们将针对其中的亮点内容详细解析,包括但不限于:

  • 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 和服务端页面渲染这块有其他团队也在一直做技术改进,国际站后端团队的核心工作点在前端缓存优化和后端服务优化上。主要包括以下内容:

  1. 浏览器缓存优化

  2. 压缩优化

  3. 服务端缓存优化

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。


详细的比较方式和流程见下图,后续的所有优化策略,都是通过这个流程来判断是否有效:



  1. 浏览器请求到后端服务,服务器获取端 IP

  2. 根据配置中心配置的灰度比例,计算当前请求是 plan A or plan B

  3. 如果是灰度方案 B,则走优化逻辑

  4. 并且 SSR 会根据灰度方案返回不同的 firebase 配置

  5. firebase 进行分开数据投递,控制台拿到两种对比的性能数据

  6. 分析数据,比较后得到优化结果


可以看到,这样的流程下来,实现了横向对比,能较准确地拿到性能对比结果,便于持续优化。

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. 增加缓存失效时间

  2. 增加后台主动刷新,主动延长缓存失效时间


方案 1 不可取,因为业务上 5 分钟失效已经是最大限度了。那方案 2 如何做呢?最开始尝试针对所有缓存,创建延迟任务,主动刷新缓存。上线后发现下游压力非常大,cpu 几乎打满。


分析后发现,还是因为 key 太多,同样的页面,可能会离散出几十个 key,主动刷新的 qps 超过了本身请求的好多倍。这种影响后台本身性能的缓存业务肯定不可取,但是在不影响下游的情况下,如何提高缓存命中率呢?


然后把请求进行统计后发现,大多数请求集中在频道页和热剧上,统计结果大致如下:



上图蓝色和绿色区域为首页访问和热剧访问,可以看到,这两种请求占了 50%以上的流量,可以称之为热点请求。


然后针对这种数据结果,分析后做了以下架构优化:



可以看到,增加了 refresh-task 模块。会针对业务热点内容,进行主动刷新,并严格监控并控制 QPS。保证页面缓存长期有效。详细流程如下:


  1. 缓存服务接收到页面请求,获取缓存

  2. 如果没有命中,则从 SSR 获取数据

  3. 判断是否是热点页面

  4. 如果是热点页面,发送延时消息到 rockmq

  5. 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。流程如下


  1. 服务收到更新消息

  2. 更新 redis 缓存

  3. 发送 pub 消息

  4. 各本地实例订阅且收到消息,从 redis 更新或者清除本地缓存


可以看到,这种方案可以保证分布式多实例场景下,各实例的本地缓存都能被更新,保证端上拿到的是最新的数据。


上线后,能保证节目更新在可接受时间范围内,避免了之前因引入缓存导致的 5 分钟延迟。


Tips:Redis 5.0 后引入了 Stream 的数据结构,能够使发布/订阅的数据持久化,有兴趣的读者可以使用新特性替换。

本地缓存(更新)+redis 二级实时更新缓存+缓存预热

众所周知,后端服务的发布启动是日常操作,而本地缓存随服务关闭而消失。那么在启动后的一个时间段里,就会存在本地缓存没有的空窗期。而在这个时间里,往往就是缓存击穿的重灾区间。爱奇艺国际站类似于创业项目,迭代需求很多,发布频繁,精彩会在发布启动时出现慢请求,这里是否有优化空间呢?


能否在服务启动后,健康检查完成之前,把其他实例的本地缓存同步到此实例,从而避免这个缓存空窗期呢?基于这个想法,对缓存功能做了如下更新。



具体流程如下:

  1. 新实例启动时发布初始化消息

  2. 其他实例收到订阅消息后,获取本地可配置数量,通过 caffeine 的热 key 算法,获取缓存 keys,发送更新消息

  3. 新实例收到订阅消息后,从 redis 或者从远程服务新增本地缓存。

  4. 这样能使 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  爱奇艺海外事业部后端开发经理


本文转载自:爱奇艺技术产品团队(ID:iQIYI-TP)

原文链接:维护几十种语言和站点,爱奇艺国际站WEB端网页优化实践

2021-03-30 08:002549

评论

发布
暂无评论
发现更多内容

k8s 插件管理工具之krew使用

雪雷

6月日更

分治(详解残缺棋盘 —— Java代码实现)

若尘

算法 分治 java代码 6月日更

【Flutter 专题】114 图解自定义 ACEProgressPainter 对比进度图

阿策小和尚

Flutter 小菜 0 基础学习 Flutter Android 小菜鸟 6月日更

情指勤一体化指挥调度平台搭建,情报研判分析系统搭建

ARTS- 日常打卡5

pjw

致恰达耶夫,致鸿蒙

脑极体

Hello Python! 第一天学 Pyhton 语言

在即

6月日更

有点难的 webpack 知识点:Dependency Graph 深度解析

范文杰

webpack 6月日更

你们公司的数据库出过问题么?

escray

学习 极客时间 朱赟的技术管理课 6月日更

关于第四次财富狂潮的思考,区块链如猛虎出笼?

CECBC

深圳首辆数字人民币主题观光巴士亮相

CECBC

面试系列-3 限流场景实践

李阿柯

php lua redis 面试 限流算法

Spring Boot FatJar类加载机制简要分析

luojiahu

Spring Boot 类加载 ClassLoader FatJar

“扯皮”终结者,区块链帮农民工计薪水

CECBC

BZZ算力挖矿系统开发功能丨BZZ算力挖矿源码设计

系统开发咨询1357O98O718

GrowingIO 前端团队对于 GraphQL 的实践总结

GrowingIO技术专栏

大前端 graphql

面试系列-2 redis列表场景分析实践

李阿柯

php 面试 redis cluster

Dubbo SPI

青年IT男

dubbo

Redis数据结构

邱学喆

数据库 redis 跳跃表

在Spring Bean实例过程中,如何使用反射和递归处理的Bean属性填充?

小傅哥

Java spring 小傅哥 反射调用 属性填充

【Vue2.x 源码学习】第二篇 - Vue的初始化流程

Brave

源码 vue2 6月日更

手把手教你在IDEA中配置Maven

打工人!

Java maven 6月日更

实现多级缓存架构设计方案

xcbeyond

缓存 缓存架构 6月日更

一文了解预训练语言模型!

博文视点Broadview

微博评论的高性能高可用计算架构设计

唐高为

React Hooks - 如何安全地使用state

蛋先生DX

大前端 React React Hooks JavaScrip 6月日更

详解Camtasia的注释功能

淋雨

视频剪辑 Camtasia 录屏

中断Hwi:提高鸿蒙轻内核系统实时性及执行效率的秘密武器

华为云开发者联盟

鸿蒙 硬件 中断 鸿蒙轻内核 中断信号

HarmonyOS 2正式发布 硬件生态品牌HarmonyOS Connect一同亮相

科技汇

react源码解析4.源码目录结构和调试

全栈潇晨

React Hooks react源码

直击Huawei Mate 40产线背后的华为云IoT智能制造

华为云开发者联盟

IoT 数字化转型 数字孪生 华为云IoT

维护几十种语言和站点,爱奇艺国际站WEB端网页优化实践_移动_爱奇艺技术产品团队_InfoQ精选文章