生成式AI领域的最新成果都在这里!抢 QCon 展区门票 了解详情
写点什么

深入浅出动态化 SSR 服务(二):SSR 服务篇

  • 2020-04-02
  • 本文字数:4092 字

    阅读完需:约 13 分钟

深入浅出动态化SSR服务(二):SSR服务篇

现代 SSR 之殇

在第一篇的技术选型中我们有说到,对于 SSR 的技术选型实际上有两个思路,其分别是:


  1. HandleBars等以纯字符串渲染引擎为主的思路

  2. Vue/React等现代 UI 框架以 Virtual DOM 为主的思路


虽然我们最后贯彻 开发友好 为准则选择了Vue/React等现代 UI 框架,但实际上其弊端(性能和内存占用)相对于字符串渲染引擎来说仍然有非常大的差距。从Vue的初始化过程我们很容易分析出,由于整个过程需要首先生成大量的 Virtual DOM 对象,然后再从 Virtual DOM 对象生成模板字符串,因此相比字符串渲染引擎来说避免不了会有更多的内存占用和 CPU 消耗。


这个问题在动态化的页面可视化 SSR 服务来说会更严重,考虑到直接使用Vue CLI同构并不进行优化的场景,对于普通页面可视化 SSR 服务来说,其开发编译和执行过程如下:


  1. 我们存在一个服务的入口页面组件,其包含所有编写的组件,并通过Vue<component>进行动态初始化,同时暴露此页面组件对象给外部调用

  2. 使用Vue CLI进行打包,获得entry-server.js

  3. 启动 SSR 服务,监听端口,准备接收请求进行渲染操作

  4. 当请求到达时,我们请求需要渲染的页面数据信息,拿到当前页面所依赖的组件信息

  5. 传递对应组件信息给入口页面组件,并使用VuerenderToString方法获取得到对应的 HTML 字符串信息


从上面的过程我们可以看到,由于页面对应的组件是完全动态的,因此入口页面组件实际上需要注册所有已知的开发组件。Vue的初始化注册操作实际上是很耗时的,特别是在自身组件越来越多的情况下,而且对于某些简单的页面来说,这样的做法实际上会造成很多的性能浪费。考虑《开发工具篇》我们提到的一个场景:


D1 发布结果包含了 100 个组件,但是对于某一生成页面而言,只需要对 2 个组件进行重复渲染。


因此我们需要进行按需加载以及按需初始化注册,避免整体组件的加载及初始化注册,以此提高性能。

组件的按需加载

在《开发工具篇》中我们已经通过sis拿到了我们依赖关系表,同时也对每个组件的编译进行了拆分,因此要达到组件的按需加载需求实际上是比较容易的,如图所示:



其执行过程为:


  1. 客户端请求对应的页面数据

  2. 根据页面 ID 调用相关接口或数据库/缓存获取得到对应的页面与组件的数据信息(步骤 1-2)

  3. 根据组件的信息查找依赖关系表,获取得到组件代码的加载路径(步骤 3-4)

  4. 加载对应的加载路径,获取对应的组件代码并进行返回(步骤 5-6)

  5. 动态执行对应的组件代码,并对Vue实例进行注册,并调用renderToString方法获取对应的 HTML 信息

  6. 返回 HTML 信息给客服端


其用代码描述大致如下:



值得说明的是,由于sissis-ssr是以一个公共服务来进行地设计,各个团队对应的编译产物是存放在云端的对象存储之中。这样,其他团队使用此平台就不需要(也不应该)涉及到sis-ssr的部署及重启。因此,对于页面的组件代码获取而言是需要依靠downloader来进行本地/远程加载的。

组件代码的缓存

由于整个系统涉及到了组件代码的动态化加载,因此downloader的性能也会一定情况下影响单请求单页面的渲染信息返回速度。通过《开发工具篇》我们可以知道,使用合并优化后仍然会有一些公用依赖代码,但实际生产的运行过程中,公用代码的缓存使用率会比较高,因此这个时候我们可以在downloader内部增加缓存。另外,除了Node自身的内存缓存外,我们还可以增加一层文件缓存,尽可能的保证加载的性能(例如服务重启后的加载性能)。对于内存缓存而言,我们一般使用 LRU,以此来帮助我们主动清理 HIT 数量比较低的缓存内容,其代码如下:



当然,整个过程中你还可以使用Promise.all进行并行加载来进一步提高对应的组件代码的加载效率。

页面缓存

除了组件代码的缓存外,在正常的实际运行中,我们一般还需要增加页面的缓存,在这里我们同样使用LRU和文件缓存来达到这一目的,代码如下:


简单页面的压力测试

现在我们来对一个 Hello 页面进行简单的压力测试,其渲染的组件代码为一个简单的子组件的嵌套,如代码所示:



sis-ssr对比的实现为vue2-ssr-example, 两者依赖库/框架均为:


  • vue@2.6.11

  • vue-server-renderer@2.6.11

  • express@4.17.0


测试的服务器配置为:


  • 阿里云 - 计算型 - 4 核 8G


注意,在此压力测试中sis-ssr去除了页面缓存,仅保留了组件代码的缓存,页面的组件信息获取写死在代码之中,两者的渲染执行逻辑基本一致。以pm2 start index.js -i 4的方式启动并进行压测,其最终结果为:


结果分析

我们可以看到在总共 2000 个请求 300 个并发的情况下sis-ssr相比vue2-ssr-example的整体渲染性能会好很多,甚至高于 100%!可能有读者会有疑问:“按照分析来看,难道不是应该vue2-ssr-example性能会更高或者相差不大么?”,实际上确实应该如此,其拖慢vue2-ssr-example性能的最重要原因其实是Webpack编译时的逻辑。


我们知道对于Webpack等前端编译工具打包而言,其会按照对应的配置进行对应的代码ES5.1之类的语法编译及polyfill引入的优化,而对于sis来说,我们在打包 SSR 时并没有进行相关的优化,尽可能让对应的编译的代码结果足够干净,由于sis-ssr跑的代码大部分都是 Native 的语法和原生方法,相比vue2-ssr-example产出的代码来说会有不小的提升。


假设我们将这些部分进行类似sis的优化,那么实际上压力测试结果会比较相近。在 Hello 页面的压测之中彼此的性能差距在 3%左右。


从这个例子我们可以看出,对于性能优化而言是需要从细小处做起,多个细小处做到极致合起来就能得到意想不到的提升。

一般页面的压力测试

但在实际项目中,我们往往没有那么简单的页面,反而会有更复杂的组件嵌套以及调用关系。在这个压力测试中我们引入ElementUI中几个组件来进行比较,组件如代码所示:



注意,此次压力测试中我们仍然不修改vue2-ssr-example的相关编译配置,其最终测试结果为:


结果分析

我们可以看到sis-ssr相比较于vue2-ssr-example来说仍然有 20%的性能提升,这是符合我们的预期的,因为从ElementUI编译得到的代码后我们可以看到,实际上sis引入的代码中有相当多的代码已经被预编译成ES5.1并加入了对应的polyfill了,所有的执行热点基本上是由于ElementUI自身逻辑造成的,因此sis-ssr相对vue2-ssr-example的提升就不会像 Hello 页面那么明显了。


总之,基于sissis-ssr的 SSR 服务在实际生产的页面渲染场景相对于目前大部分其他实践方案来说是有比较可观的性能收益的。


这里的vue2-ssr-example的压测结果相比 Hello 页面 测试结果来说降低得并不是特别多,其原因在于:实际上我们引入的ElementUI组件还是比较简单的组件,相比 Hello 页面 而言增加的执行逻辑并没多太多,但sis-ssr因为以上讨论原因下降会比较明显。以上内容均可以通过NodeProfile工具分析得出。

超时、限流与降级

在实际生产中,服务高性能当然是值得高兴,但如果是没有稳定性的高性能,那么实际上就没有那么让人愉悦了。在sis-ssr中为了保证对应的单机服务的稳定性,我们分别采取了三个策略,其分别是:


  • 超时策略

  • 限流策略

  • 渲染降级策略


sis-ssr中实际上会出现不少的异步请求的情况,例如downloader的远程加载组件代码。对于这些服务端的异步请求,我们一般都会强制考虑请求的超时处理,防止请求长时间被挂起造成的问题,例如:



其次,为了保证我们的单机服务不被外部流量冲击,我们也需要加入限流的策略。其中比较常用的限流策略包括:固定窗口算法、滑动窗口算法、漏桶算法以及令牌桶算法等。在Node中有存在基于redisexpress-rate-limitNPM包帮助我们完成相关的逻辑。


最后sis-ssr为了应对各种请求/流量异常的情况还做了多级的降级策略:


  1. 服务端数据请求成功且Node端渲染成功,正常返回

  2. 服务端数据请求成功但Node端渲染失败,则抛弃首屏并将相关数据写入页面,由客户端进行渲染

  3. 流量异常/系统资源占用过高,完全由客户端请求数据并进行渲染


至此由上至下进行兜底,以此保证服务的可用性。除此之外,由于Node的单进程单主线程(在这里我们排除 I/O 异步等事件循环中的子线程)且页面渲染是纯 CPU 操作的特性,其在渲染大页面时经常会出现阻塞运行时主线程的情况。因此我们可以创建包含一定数量工作线程的线程池(使用Nodeworker_thread),然后将对应的页面渲染放置在Worker工作线程之中,当线程池中无空闲工作线程时,Master 线程进行主动的页面降级渲染以此来提高对应的性能。此功能已在sis-ssr中得到了实现并取得了极好的实际效果,其压力测试结果如图所示:



此次压测的各环境不变,同时测试对象主要是 Hello 页面。从结果中我们可以看到,搭配了主动降级+Worker的渲染方式相比后两者有非常大的提升,其原因也很简单,因为大部分请求由于Worker不空闲均被降级为牺牲首屏并将数据写入页面,由客户端渲染的方式了(在上面的压力测试下,大约有 10%左右的请求是真正的Node渲染,其余的都走降级页面了)。


在这种情况下,由于 Google 已经支持同步的前端渲染页面的收录,所以降级渲染请求并不会影响到 SEO。那么对于内容到达时间(time-to-content)呢?我们可以做一个粗糙的计算(实际请以数据日志为主),在我们 只关注于渲染时间且单机单进程 情况下,假设并发 10 个请求,Node 端每次渲染耗时 100ms,那么完全以 Node 端渲染来说,最后一个用户的内容到达时间为 1s。若考虑降级,降级页面的渲染时间为 50ms,则最后一个用户获得页面 HTML 的时间为 350ms,若静态资源加载加上同步的前端渲染页面耗时小于 650ms,则相对于完全 Node 端渲染的方案有提升。同时我们需要注意,随着并发数的增加,此提升会越来越高。若并发数提升为 20 时,同样的计算后我们可以得到完全 Node 渲染的方式最后一个用户的内容到达时间为 2s,而降级页面最后一个用户获得页面 HTML 的时间为 750ms,若静态资源加载加上同步的前端渲染页面耗时小于 1250ms 则具有对应的提升。

总结与期待

在本章我们较为详细的探讨了sis-ssr的一些内部逻辑,同时与vue2-ssr-example进行了单机的压力测试的比较并分析了对应的原因。同时我们也讲述了sis-ssr是如何保证生产环境的高可用性的。在下一篇中我们会从这些细节脱身,从更全局和整体的架构角度来看待整个动态化 SSR 服务。敬请期待《深入浅出动态化 SSR 服务(之三) - 架构篇》。


相关阅读


https://www.infoq.cn/article/SlgQEvW8VGt8EEiTeXEd


2020-04-02 10:002666

评论 1 条评论

发布
用户头像
对页面的缓存,如果再有登录态或者其他状态的情况下会不会有问题?
2021-12-07 10:08
回复
没有更多了
发现更多内容

Soul 源码阅读 05|Http 长轮询同步数据分析

哼干嘛

机器学习应用设计阶段的 10 个陷阱和 11 个最佳实践

浪潮云

机器学习

淘宝网前期技术架构演进分析

Andy

工程师思维是什么?能吃吗?

Justin

工程师思维 架构设计 28天写作

CSS实现数据统计

德育处主任

大前端 CSS小技巧 28天写作 纯CSS

你会读书吗?

xcbeyond

读书感悟 读书方式 28天写作

漫谈HTTP协议

架构精进之路

HTTP 七日更 28天写作

技术根儿扎得深,不怕“首都”狂风吹!

鲁米

操作系统

微服务容错时,这些技术你要立刻想到

华为云开发者联盟

微服务 线程 服务雪崩 断路器 服务降级

《论雨伞道德》- 不要和自己的良心捉迷藏

石云升

读书笔记 28天写作 雨伞道德

年底跳槽之 如何找工作方向?

一笑

职业规划 28天写作

我们都很忙

Ian哥

28天写作

音视频行业不可或缺的功能-云端录制

anyRTC开发者

音视频 WebRTC 在线教育 直播 RTC

Volcano 监控设计解读,一看就懂

华为云开发者联盟

Kubernetes 云原生 监控 Volcano 计算

数据中台:建立在数据网络效应之上的赛道

奇点云

大数据 数据中台 云原生 数据

前端知识总结输出文章目录大全

梁龙先森

JavaScript 大前端 编程语言 28天写作

开发质量提升系列:用户体验

罗小龙

最佳实践 方法论 28天写作

Vue3 中 v-if 和 v-show 指令实现的原理 | 源码解读

五柳

源码分析 大前端 Vue3

GNUCash 5: 报表

lidaobing

GNUCash 28天写作

IDEA 异常退出 解决方法

任广印

IDEA

代码 or 指令,浅析ARM架构下的函数的调用过程

华为云开发者联盟

函数 任务栈 arm架构

区块链作用之数字货币的影响

v16629866266

美国大选期间美股迎来大涨,舆情到底有何魔力?

星环科技

人工智能 大数据

甲方日常 91

句子

工作 随笔杂谈 日常

个人隐私之老话重谈

张老蔫

28天写作

前端模拟假数据(json-server光速入门篇)

德育处主任

json 大前端 Node 28天写作 json-server

面对key数量多和区间查询低效问题:Hash索引趴窝,LSM树申请出场

华为云开发者联盟

数据库 数据 存储 Hash索引 LSM树

HTML5中的拖放功能

我是哪吒

html html5 程序员 面试 大前端

AI、IoT、区块链、自主系统、下一代计算五大技术引领未来供应链发展

京东科技开发者

区块链 AI IoT 供应链

如何养成一个好习惯

熊斌

读书笔记 28天写作

架构师训练营知识点思维导图

晴空万里

架构师训练营第2期

深入浅出动态化SSR服务(二):SSR服务篇_数据库_赵洋_InfoQ精选文章