前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >如何搭建一个高可用的服务端渲染工程

如何搭建一个高可用的服务端渲染工程

作者头像
有赞coder
发布2020-08-25 15:38:37
7660
发布2020-08-25 15:38:37
举报

可能大家在看到这个标题的时候,会觉得,只不过又是一篇烂大街的SSR从零入门的教程而已。别急,往下看,相信你或多或少会有一些不一样的收获呢。

在落地一种技术的时候,我们首先要想一想:

  1. 是否一定需要引入这种技术呢?他能解决什么问题,或者能带来什么收益?
  2. 为什么要采用这种技术选型而不是其他的?
  3. 引入了这种技术后,会带来什么问题吗(比如额外的开发成本等)?

上面三个问题思考清楚之后,才能真正地去落地。上面三个问题思考清楚之后,才能真正地去落地。而有赞教育接入服务端渲染,正是为了优化H5页面的首屏内容到达时间,带来更好的用户体验(顺便利于SEO)。

说了这么多,以下开始正文。

一、后端模版引擎时代

在较早时期,前后端的配合模式为:后端负责服务层、业务逻辑层和模版渲染层(表现层);前端只是实现页面的交互逻辑以及发送AJAX。比较典型的例子就是JSP或FreeMarker模板引擎负责渲染出html字符串模版,字符串模版里的js静态资源才是真正前端负责的东西。

而这种形式,就是天然的服务端渲染模式:用户请求页面 -> 请求发送到应用服务器 -> 后端根据用户和请求信息获取底层服务 -> 根据服务返回的数据进行组装,同时JSP或FreeMarker模版引擎根据组装的数据渲染为html字符串 -> 应用服务器讲html字符串返回给浏览器 -> 浏览器解析html字符串渲染UI及加载静态资源 -> js静态资源加载完毕界面可交互。

那么既然后端模版引擎时代带来的效果就是我们想要的,那为啥还有以后让前端发展服务端渲染呢?因为很明显,这种模式从开发角度来讲还有挺多的问题,比如:

  1. 后端需要写表现层的逻辑,但其实后端更应该注重服务层(和部分业务逻辑层)。当然,其实也可以让前端写JSP或FreeMarker,但从体验上来说,肯定不如写JS来的爽;
  2. 本地开发的时候,需要启动后端环境,比如Tomcat,影响开发效率,对前端也不友好;
  3. 所赋予前端的能力太少,使得前端需要的一些功能只能由后端提供,比如路由控制;
  4. 前后端耦合。

二、SPA时代

后来,诞生了SPA(Single Page Application),解决了上面说的部分问题:

  1. 后端不需要关心表现层的逻辑,只需要注重服务层和业务逻辑层就可以了,暴露出相应的接口供前端调用。这种模式也同时实现了前后端解耦。
  2. 本地开发的时候,前端只需要启动一个本地服务,如:dev-server就可以开始开发了。
  3. 赋予了前端更多的能力,比如前端的路由控制和鉴权,比如通过SPA + 路由懒加载的模式可以带来更好的用户体验。

但同时,也带来了一些问题:

  1. 页面的DOM完全由js来渲染,使得大部分搜索引擎无法爬取渲染后真实的DOM,不利于SEO。
  2. 页面的首屏内容到达时间强依赖于js静态资源的加载(因为DOM的渲染由js来执行),使得在网络越差的情况下,白屏时间大幅上升。

三、服务端渲染

正因为SPA带来的一些问题(尤其是首屏白屏的问题),接入服务端渲染显得尤为必要。// 终于讲到服务端渲染这个重点了。

而正是Node的发展和基于Virtual DOM的前端框架的出现,使得用js实现服务端渲染成为可能。因此在SPA的优势基础上,我们顺便解决了因为SPA引入的问题:

  1. 服务端渲染的首屏直出,使得输出到浏览器的就是完备的html字符串模板,浏览器可以直接解析该字符串模版,因此首屏的内容不再依赖js的渲染。
  2. 正是因为服务端渲染输出到浏览器的是完备的html字符串,使得搜索引擎能抓取到真实的内容,利于SEO。 同时,通过基于Node和前端MVVM框架结合的服务端渲染,有着比后端模版引擎的服务端渲染更明显的优势:可以优雅降级为客户端渲染(这个后续会讲,先占个坑)。

3.1 实现

既然服务端渲染能带来这么多好处,那具体怎么实现呢?

从官网给出的原理图,我们可以清晰地看出:

  • Source为我们的源代码区,即工程代码;
  • Universal Appliation Code和我们平时的客户端渲染的代码组织形式完全一致,只是需要注意这些代码在Node端执行过程触发的生命周期钩子不要涉及DOM和BOM对象即可;
  • 比客户端渲染多出来的app.js、Server entry 、Client entry的主要作用为:app.js分别给Server entry 、Client entry暴露出createApp()方法,使得每个请求进来会生成新的app实例。而Server entry和Client entry分别会被webpack打包成vue-ssr-server-bundle.json和vue-ssr-client-manifest.json(这两个json文件才是有用的,app.js、Server entry 、Client entry可以抽离,开发者不感知);
  • Node端会根据webpack打包好的vue-ssr-server-bundle.json,通过调用createBundleRenderer生成renderer实例,再通过调用renderer.renderToString生成完备的html字符串;
  • Node端将render好的html字符串返回给Browser,同时Node端根据vue-ssr-client-manifest.json生成的js会和html字符串hydrate,完成客户端激活html,使得页面可交互。

3.2 优化

按照Vue SSR官方文档建立起一个服务端渲染的工程后,是否就可以直接上线了呢?别急,我们先看看是否有什么可以优化的地方。

3.2.1 路由和代码分割

一个大的SPA,主文件js往往很大,通过代码分割可以将主文件js拆分为一个个单独的路由组件js文件,可以很大程度上减小首屏的资源加载体积,其他路由组件可以预加载。

代码语言:javascript
复制
// router.js
const Index = () => import(/* webpackChunkName: "index" */ './pages/Index.vue');
const Detail = () => import(/* webpackChunkName: "detail" */ './pages/Detail.vue');
const routes = [
    {
        path: '/',
        component: Index
    },
    {
       path: '/detail',
       component: Detail
    }
];
const router = new Router({
    mode: 'history',
    routes
});

3.2.2 部分模块(不需要SSR的模块)客户端渲染

因为服务端渲染是CPU密集型操作,非首屏的模块或者不重要的模块(比如底部的推荐列表)完全可以采用客户端渲染,只有首屏的核心模块采用服务端渲染。这样做的好处是明显的:1. 较大地节省CPU资源;2. 减小了服务端渲染直出的html字符串长度,能够更快地响应给浏览器,减小白屏时间。

代码语言:javascript
复制
// Index.vue
asyncData({ store }) {
    return this.methods.dispatch(store); // 核心模块数据预取,服务端渲染
}
mounted() {
    this.initOtherModules(); // 非核心模块,客户端渲染,在mounted生命周期钩子里触发
}

3.2 3 页面缓存/组件缓存

页面缓存一般适用于状态无关的静态页面,命中缓存直接返回页面;组件缓存一般适用于纯静态组件,也可以一定程度上提升性能。

代码语言:javascript
复制
// page-level caching
const microCache = LRU({
    max: 100,
    maxAge: 1000 // 重要提示:条目在 1 秒后过期。
})
server.get('*', (req, res) => {
    const hit = microCache.get(req.url)
    if (hit) { // 命中缓存,直接返回页面
      return res.end(hit)
   }
   // 服务端渲染逻辑
   ...
})
代码语言:javascript
复制
// component-level caching
// server.js
const LRU = require('lru-cache')
const renderer = createRenderer({
    cache: LRU({
        max: 10000,
        maxAge: ...
    })
});

// component.js
export default {
    name: 'item', // 必填选项
    props: ['item'],
    serverCacheKey: props => props.item.id,
    render (h) {
        return h('div', this.item.id)
    }
};

3.2.4 页面静态化

如果工程中大部分页面都是状态相关的,所以技术选型采用了服务端渲染,但有部分页面是状态无关的,这个时候用服务端渲染就有点浪费资源了。像这些状态无关的页面,完全可以通过Nginx Proxy Cache缓存到Nginx服务器,可以避免这些流量打到应用服务器集群,同时也能减少响应的时间。

3.3 降级

进行优化之后,是否就可以上线了呢?这时我们想一想,万一服务端渲染出错了怎么办?万一服务器压力飙升了怎么办(因为服务端渲染是CPU密集型操作,很耗CPU资源)?为了保证系统的高可用,我们需要设计一些降级方案来避免这些。具体可以采用的降级方案有:

  • 单个流量降级 -- 偶发的服务端渲染失败降级为客户端渲染
  • Disconf / Apollo配置降级 -- 分布式配置平台修改配置主动降级,比如可预见性的大流量情况下(双十一),可提前通过配置平台将整个应用集群都降级为客户端渲染
  • CPU阈值降级 -- 物理机 / Docker实例CPU资源占用达到阈值触发降级,避免负载均衡服务器在某些情况下给某台应用服务器导入过多流量,使得单台应用服务器的CPU负载过高
  • 旁路系统降级 -- 旁路系统跑定时任务监控应用集群状态,集群资源占用达到设定阈值将整个集群降级(或触发集群的自动扩容)
  • 渲染服务集群降级 -- 若渲染服务和接口服务是独立的服务,当渲染服务集群宕机,html的获取逻辑回溯到Nginx获取,此时触发客户端渲染,通过ajax调用接口服务获取数据

3.4 上线前准备

3.4.1 压测

压测可以分为多个阶段:本地开发阶段、QA性能测试阶段、线上阶段。

  • 本地开发阶段:当本地的服务端渲染开发完成之后,首先需要用loadtest之类的压测工具压下性能如何,同时可以根据压测出来的数据做一些优化,如果有内存泄漏之类的bug也可以在这个阶段就能被发现。
  • QA性能测试阶段:当通过本地开发阶段的压测之后,我们的代码已经是经过性能优化且没有内存泄漏之类严重bug的。部署到QA性能测试环境之后,通过压真实QA环境,和原来的客户端渲染做对比,看QPS会下降多少(因为服务端渲染耗更多的CPU资源,所以QPS对比客户端渲染肯定会有下降)。
  • 线上阶段:QA性能测试阶段压测过后,若性能指标达到原来的预期,部署到线上环境,同时可以开启一定量的压测,确保服务的可用性。

3.4.2 日志

作为生产环境的应用,肯定不能“裸奔”,必须接入日志平台,将一些报错信息收集起来,以便之后问题的排查。

3.4.3 灰度

如果上线服务端渲染的工程是提供核心服务的应用,应该采用灰度发布的方式,避免全量上线。一般灰度方案可以采用:百分比灰度、白名单灰度、自定义标签灰度。具体采用哪种灰度方式看场景自由选择,每隔一段时间观察灰度集群没有问题,所以渐渐增大灰度比例/覆盖范围,直到全量发布。

3.5 落地

在有赞电商的服务端渲染的落地场景中,我们抽离了单独的依赖包,提供各个能力。

3.6 效果

从最终的上线效果来看,相同功能的页面,服务端渲染的首屏内容时间比客户端渲染提升了300%+。

3.7 Q & A

Q1:为什么服务端渲染就比客户端渲染快呢?

A:首先我们明确一点,服务端渲染比客户端渲染快的是首屏的内容到达时间(而非首屏可交互时间)。至于为什么会更快,我们可以从两者的DOM渲染过程来对比:

客户端渲染:浏览器发送请求 -> CDN / 应用服务器返回空html文件 -> 浏览器接收到空html文件,加载的css和js资源 -> 浏览器发送css和js资源请求 -> CDN / 应用服务器返回css和js文件 -> 浏览器解析css和js -> js中发送ajax请求到Node应用服务器 -> Node服务器调用底层服务后返回结果 -> 前端拿到结果setData触发vue组件渲染 -> 组件渲染完成

服务端渲染:浏览器发送请求 -> Node应用服务器匹配路由 -> 数据预取:Node服务器调用底层服务拿到asyncData存入store -> Node端根据store生成html字符串返回给浏览器 -> 浏览器接收到html字符串将其激活:

我们可以很明显地看出,客户端渲染的组件渲染强依赖js静态资源的加载以及ajax接口的返回时间,而通常一个page.js可能会达到几十KB甚至更多,很大程度上制约了DOM生成的时间。而服务端渲染从用户发出一次页面url请求之后,应用服务器返回的html字符串就是完备的计算好的,可以交给浏览器直接渲染,使得DOM的渲染不再受静态资源和ajax的限制。

Q2:服务端渲染有哪些限制?

A:比较常见的限制比如:

  1. 因为渲染过程是在Node端,所以没有DOM和BOM对象,因此不要在常见的Vue的beforeCreate和created生命周期钩子里做涉及DOM和BOM的操作
  2. 对第三方库的要求比较高,如果想直接在Node渲染过程中调用第三方库,那这个库必须支持服务端渲染

Q3:如果我的需求只是生成文案类的静态页面,需要用到服务端渲染吗?

A:像这些和用户状态无关的静态页面,完全可以采用预渲染的方式(具体见Vue SSR官方指南),服务端渲染适用的更多场景会是状态相关的(比如用户信息相关),需要经过CPU计算才能输出完备的html字符串,因此服务端渲染是一个CPU密集型的操作。而静态页面完全不需要涉及任何复杂计算,通过预渲染更快且更节省CPU资源。

-The End-

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-09-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 有赞coder 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、后端模版引擎时代
  • 二、SPA时代
    • 3.1 实现
      • 3.2 优化
        • 3.3 降级
          • 3.4 上线前准备
            • 3.5 落地
              • 3.6 效果
                • 3.7 Q & A
                相关产品与服务
                内容分发网络 CDN
                内容分发网络(Content Delivery Network,CDN)通过将站点内容发布至遍布全球的海量加速节点,使其用户可就近获取所需内容,避免因网络拥堵、跨运营商、跨地域、跨境等因素带来的网络不稳定、访问延迟高等问题,有效提升下载速度、降低响应时间,提供流畅的用户体验。
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档