当 SSR 遇上 FaaS

1,356 阅读3分钟

前言

为什么 SSR ?

我所在的部门是阿里巴巴国际站,国际站是一个全球 B 类跨境贸易平台,平台服务的买卖家是来自于全球一百多个国家和地区,这些国家和地区的发展和贸易环境差异较大例如:一些亚非拉地区的网络环境和终端设备较差,网站性能在这些地区堪忧。同时由于 B 类贸易较 C 类更为严肃,很大部分地区(欧美)更习惯使用 WEB,网站很大部分流量来自于 SEO,针对于这种情况,我们需要支持服务端渲染,来提高首屏性能,保证用户访问网站的体验和提升 SEO 流量。(下图是客户端渲染 CSR  和 服务端渲染 SSR 的首屏渲染差异)

前端 SSR 历程

传统的异构服务端渲染

互联网发展之初没有前端这一角色,早期的页面基本都是由后端一把梭.随着 ajax 的诞生,而替代传统 web 交互模式的 ajax 技术使得动态更新网页变成可能,由于页面交互的复杂度提升,基于专业性的要求导致前后端的分工,前端开始承接视图的开发工作。但页面容器还是由传统的 PHP 和 java 等去承载:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>demo</title>
<link rel="stylesheet" type="text/css" href="xxx">
</head>
<body>
  <?php $searchQuery = $_GET['q']; ?>
  <h1>You searched for: <?php echo $searchQuery; ?></h1>
  <p>this is a demo</p>
</body>
</html>

类似还有 ASP、JSP 等脚本语言,将页面的动态内容嵌入到页面中。

BFF 架构前端接管 UI 层

随着 13 年 react 的出现并火热,其基于 vdom 的设计使UI逻辑和渲染分离,能够将一套代码多端渲染,至此拉开了前端框架的 SSR 之路,同时这一阶段随着前端工程化的不断发展,为了提升前端开发效率,传统的异构渲染服务已经成为开发效率的瓶颈。同时随着Sam Newman 提出了 Backends For Frontends ,根据服务自治的原则,前端为了开发的灵活性,开始承接整个 UI 层(接口和页面 SSR)

BFF SSR 困境

虽然 BFF 在业界和阿里有较多的实践和沉淀但还是局限在一些特定领域。如阿里很多前台场景已有多年的 java 服务运行稳定,改造成本较高且风险大,落地不太现实,最终还是在一些内部效能平台,卖家平台等场景有较多落地。同时加一层 BFF 还有以下一些挑战:

渲染服务

为什么提供渲染服务

回头来看我们需要 SSR 的场景,大多都是流量较大,直面大量用户(买家)的场景,而这些业务场景很多只是部分页面需要有 SSR 的能力,同时前端没有能力全部使用 BFF 来承接流量。在这个过程中,我们也探索和实践其他方案:java 的js 引擎 nashorn 来提供 SSR 渲染,最后发现整体性能远低于 node ;同机部署 node 来支持 SSR ,同机部署和 java 进程的资源竞争和运维复杂度都带来了挑战。
基于以上原因,我们在思考,能不能把渲染能力作为一个服务,需要渲染模板(组件)直接调用返回 html 片段即可,不需要对业务应用侵入性这么强。

渲染服务实现

确定了问题后,渲染服务就需要提供一种轻量级的接入方式来满足不通场景的渲染诉求。那我们就需要满足以下诉求:

  1. 低成本开发&部署:我们期望同构的代码不需要进行较大的改造,最好是零改造就可以直接支持渲染能力。
  2. 多模板引擎支持:由于不通业务方对渲染有自己的自定义诉求,除了单纯的 react 渲染以外,支持移动端的 rax 渲染,支持搭建场景的基于特定 schema 的渲染诉求。
    针对以上诉求,我们提供了一个 RPC 服务,只要调用方提供相应的渲染基础信息和渲染数据,我们即可返回渲染结果,配置信息如下:
{
  app: "silkworm-render-demo",        // 应用名
  loader: "webpack",                  // 加载方式
  module: "xxx/pages/demo/index.js", // 资源入口
  deps: [],                          // 依赖资源
  version: "0.0.1",
  engine: "react",                   // 渲染引擎
  props: {                           // 渲染数据
    count: 100,
  },
}

当业务方调用我们的渲染服务后,我们根据资源描述信息从 CDN 或者静态资源服务器上拉取资源到服务器本地,然后根据不同的加载器执行文件的加载和解析,之后根据用户选择的渲染引擎,执行不同的渲染逻辑。整个架构图如下:

在保障性能和稳定性上:我们针对资源做了本地文件和内存级别的缓存策略,同时针对不通页面渲染做了基于 vm 沙箱的渲染隔离,提供单独的上下文兼容 window 等客户端书写问题,同时做了超时处理

渲染服务效果&问题

渲染服务上线后,我们支持了六七条业务线,囊括了大部分买家场景的渲染诉求,同时也支持了邮件推送的诉求(离线渲染邮件模板触达用户),整体运行平稳,主要效果还是在于以下几点:

  1. 接入成本低:前端应用(react)基本没有改造成本,只需要后端调用渲染服务回填页面即可。
  2. 稳定性
  • 非侵入:服务作为能力提供方,即使出现问题也不影响业务应用的运行。
  • 运维保障:提供了统一的监控、日志、错误处理能力,来保障渲染稳定性。

但渲染服务其实是一个中心化服务,随着应用的接入越来越多,中心化的风险越来越高,接入方的声音也越来越多:

  • 页面流量大了会不会撑不住?
  • 应用多了会不会互相影响?
  • 能不能定制加载策略、缓存优化?

    虽然渲染服务降低了接入和运维的成本,但是还有一些问题无法解决:
  • 弹性扩容
  • 容器隔离
  • 灵活定制

SSR 遇上 FaaS

serverless 简介

无服务器架构(Serverless architectures)是指一个应用大量依赖第三方服务(后端即服务,Backend as a Service,简称“BaaS”),或者把代码交由托管的、短生命周期的容器中执行(函数即服务,Function as a Service,简称“FaaS”)。serverless 潜在的几个优点:

  • (理论上)无限可用的计算资源
  • 用户再也不需要承担服务器运维的工作和责任
  • 服务的按需付费成为可能
  • 超大型数据中心的使用成本显著降低
  • 通过资源虚拟化管理,运维操作的难度大大降低
  • 得益于分时复用,物理硬件的利用率大大提高

以 web 应用为例,以下是一个 serverless web 的架构:

渲染服务迁移 FaaS

针对 serverless 的特点,结合之前提到的我们渲染服务在伸缩性上和渲染容器的隔离性上的不足,我们开始着手改造将渲染服务迁移至 FaaS 平台,在迁移前我们评估了渲染服务和 FaaS 在以下几点十分契合:

  • 基于事件触发是非长链接的服务处理
  • 渲染本身是纯粹的 CPU 计算,无状态服务
  • FaaS 的伸缩性很适合调用量波动较大的渲染服务
    基于以上的特点,我们将不通的引擎进行 FaaS 部署提供对外服务,整体架构如下:

在迁移之后,我们借助 FaaS 的扩缩容能力,可以有效的提高资源利用率,降低成本,同时降低了邮件离线渲染对在线渲染服务的影响。

FaaS SSR 一体化应用

那除了已有的页面想快速接入 SSR 能力,像已有的 BFF 应用,或者希望能借助  serverless 来承接整个 UI 层的前端,我们该如何借助 FaaS 来改造升级。
以 BFF 应用为例,我们前端承接了客户端的开发同时也负责服务端视图层接口的开发工作,我们可以将服务端的接口以 FaaS 形式进行部署,减少运维成本。而需要 SSR 的页面我们也发布到 FaaS 平台,以 page as function 的形式进行部署,开发和构建如下:


当页面和接口发布后我们通过网关层,来路由页面到不同的页面模板和 function 来提供 SSR 渲染和接口服务。

# 应用的网关配置
basePath: /demo
appName: demo

# 应用的路由
router:
	render:
    # 页面内容ssr
    path: /render/home-list   ## 页面内容 ssr
    method: get
    parameters:
    - name: page
      in: path
    # 接口的具体实现
    integration:
      type: function
      uri: render
  getPage:
    # 页面的 http 
    path: /home
    method: get
    integration: 
      type: template
      uri: 
        body:
          $ref: template/index.ejs

最终,我们的用户请求 SSR 渲染路径如下:

更多探索

除了现有的 FaaS 平台外,像阿里云提供的 EdgeRoutine(ER):支持在CDN边缘执行客户编写/编译的JavaScript(WebAssembly),也可以支持 SSR 在边缘节点进行渲染,能够更快的返回给用户页面内容。

由于本身边缘节点对运行耗时有更短的限制,同时受限的 js 运行时(service worker)对 node 开发不太友好,而且边缘节点渲染比服务端渲染优势没有那么明显,暂时不太建议在边缘节点上做 SSR。

但是我们可以通过将页面容器发布到边缘节点,当用户访问时上将页面非 SSR 部分流式返回给用户,同时向服务端发起 SSR 请求,最终返回给用户页面。这样好处是:让用户更早看到页面部分内容,同时资源的请求和 SSR 渲染是并行执行,能更快的提升首屏速度。

以上升级改造在进行中

总结

回到 SSR 初衷,本质上我们还是为了给用户更好的交互体验,如更好的性能。随着 serverless 相关技术的不断发展,更轻量级的业务开发变成可能,需要我们更深入业务场景,提供贴合深入的体验优化。共勉