人人都是Serverless架构师之传统内容管理系统改造实战三[性能优化]

简介: 内容管理系统是很常见的一种web应用场景,可以用到个人独立站,企业官网展示等场景,具有很高的实用价值,一个标准的内容管理系统主要由三个部分组成 主站展示部分、后台管理系统、API接口服务,本系列文章会以一个已有内容管理系统的Serverless架构重构展开,介绍改造的基本思路,改造细节,以及性能优化业务可观测设计等。涉及大家关心的Serverless生产遇到的一些问题,比如数据库、日志、动静态分离、调试、维护、灰度方案等。最真实的展现Serverless架构的实施落地细节。

本次改造的项目题材取自GiiBee CMS
Serverless架构改造的仓库地址

项目改造后预览地址 SSR方案地址

1. 应用性能瓶颈分析

用户获取网站内容并且在本地预览受到网络性能,资源量,接口速度,页面渲染性能等诸多因素的影响,本篇主要讨论方向是如何通过改进架构以及利用云服务的基础设施能力来提升访问性能。接下来我们需要对这个架构的各个组成部分展开分析。

1.1 入口网关

本次使用的是API网关的共享实例,他的好处是只会收取调用次数+网络流量的费用,成本非常低,坏处就是无法保证后端延时及性能,下图是跟专享版本的对比(截取部分)

专享版本有各种优势但是价格偏高,对中小型用户不推荐,此外网关本身不具备CDN能力,所以做不到就近访问的能力,对于网络请求有极致要求的用户也不能够满足要求。

1.2 后端函数服务

这里包含两个部分,一个是SSR渲染的服务,一个是后端API服务,他们都部署在函数计算上,我们都知道Serverless的一大特征是会有冷启动的现象,一旦触发冷启动会耗时较长,严重影响用户访问体验,尽管产品侧已经在冷启动方面做了一些优化,但是业务侧的冷启动耗时依然不能不忽视。

关于冷启动优化的建议可以参考这里。除了建议,本次也提供了直观的配置优化方案,接下来会给大家做详细介绍。

1.3 前端静态资源

静态资源如果是大的文件,网络请求会花费比较长的时间,可以根据需要对相应的图片进行压缩处理,此外可以考虑静态资源跟后端服务的分离,目前前端的静态资源是放到nas,通过api服务响应给用户,后续可以将请求直接转给OSS的静态服务,也就是说后端函数服务不再影响前端静态资源。

2. 优化方案

2.1 网关优化

2.1.1 边缘应用程序介绍

在[开篇]中介绍了以CDN边缘程序作为网关入口的架构方案。其最大的好处就是充分利用了内容分发网络的优势,根据用户访问地域找到最近的服务节点快速给予响应。CDN也是在基础架构层面提升访问性能的有效工具

阿里云的边缘程序EdgeRoutine(ER)可以让开发者在阿里云全球边缘节点上编写Js业务代码,比如处理请求路径并进行后端服务的调用返回,可以用来替代API网关作为应用服务的分流入口。其原理如下

利用ER我们还可以做很多事情,比如缓存请求进一步加速用户获取网站内容的速度、根据请求标识做A/B测试验证产品功能的好坏、改写返回内容满足特异性需求以及身份验证等等。我们本次主要实现路由分发的编写,以及添加缓存能力。

2.1.2 边缘应用程序开通使用

参考官网

2.1.3 边缘应用程序网关代码示例

const staticUrl = 'http://xxxx.oss-cn-hangzhou.aliyuncs.com';  //静态资源的oss地址,包含管理后台地址 (CSR)以及 portal (SSG)
const apiUrl = 'http://modern-app-new.modern-app-new.xxxxxx.cn-hangzhou.fc.devsapp.net'; // api接口地址 
const portalUrl = 'http://modern-app-portal.xxxxxxx.cn-hangzhou.fc.devsapp.net'; // 官网主页地址 SSR
const init = {
    headers: {
        "content-type": "text/html;charset=UTF-8",
    },
}

const API_ROUTER_REG = /^\/(prod-api|api)/g;
const UPLOAD_ROUTER_REG = /^\/(uploads)\/?/g;
const ADMIN_OR_PORTAL_ROUTER_REG = /^\/(admin|portal)\/?/g;

const API_PATH = '/api';
async function gatherResponse(response) {
    const headers = response.headers
    const contentType = headers.get("content-type") || ""
    if (contentType.includes("application/json")) {
        return JSON.stringify(await response.json())
    } else if (contentType.includes("application/text")) {
        return response.text()
    } else if (contentType.includes("text/html;charset=UTF-8")) {
        return response.text()
    } else {
        return response.blob()
    }
}

async function handleRequest(request) {
    try {
        const requestHeaders = request.headers || init;
        const body = request.body;
        const method = request.method;
        const url = new URL(request.url)
        const { pathname, search } = url;
        const cacheResponse = await cache.get(request.url);
        if(cacheResponse) {
             return cacheResponse;
        }
        let response = {};
        if (pathname.match(API_ROUTER_REG)) {
            const matchedData = pathname.match(API_ROUTER_REG)[0];
            let finalurl = (apiUrl + pathname + search).replace(matchedData, API_PATH);
            response = await fetch(finalurl, { headers: requestHeaders, method,body });
        } else if (pathname.match(UPLOAD_ROUTER_REG)) {
            let finalurl = apiUrl + pathname + search;
            response = await fetch(finalurl, { headers: requestHeaders, method ,body})
        }  else if(pathname.match(ADMIN_OR_PORTAL_ROUTER_REG)){
            let finalurl = staticUrl + pathname + search;
            response = await fetch(finalurl, { headers: requestHeaders, method });
        } else {
            let finalurl = portalUrl + pathname + search;
            response = await fetch(finalurl, init);
        }
  
        const results = await gatherResponse(response)
        const finalResponse =  new Response(results, response.headers);
        try {
             await cache.put(request.url,finalResponse);
        } catch(e) {}
       
        return finalResponse;
    } catch (e) {
        return new Response(JSON.stringify({ message: e.message }), {
            headers: {
                "content-type": "application/json;charset=UTF-8"
            }
        })
    }

}

addEventListener("fetch", event => {
    return event.respondWith(handleRequest(event.request))
})


代码比较简单,对到来的请求做正则区分,然后分别代理给相应的后端服务,比如静态资源,后端函数服务等


2.2 函数冷启动优化

增加对服务的心跳触发,避免服务关闭。改造方案非常简单,只需要在 s.yaml中配置一个插件即可,url需要指定自己后端函数服务

api-server:
  component: fc
  actions:
  post-deploy: # 在deploy之后运行
    - plugin: keep-warm-fc
      args:
        url: http://modern-app-new.modern-app-new.xxxxxx.cn-hangzhou.fc.devsapp.net

该插件是部署后置的,当你的函数部署完之后执行,它会帮助你自动创建一个新的后端函数,用以定时向服务发起检测请求,从而避免函数的冷起动。注意该函数可能会引起一定的资费,如觉得不需要可以直接在函数计算控制台删除

2.2.1 静态渲染(SSG)

SSG说明

我们前面的实现方案是利用了SSR技术,并且以独立的函数服务部署到函数计算上,SSR本身是需要先从后端接口服务拉取数据做好服务端渲染然后再返回给客户端的,受接口服务的响应性能影响较大。 为了进一步的提高用户获取网站内容的效率,我们可以采用预渲染技术即SSG, 利用Nuxtjs的特性,提前拿到后台数据形成静态界面,这样我们可以不依赖于后端服务,可以直接将静态资源存储到OSS上,并且结合边缘程序以及内置的缓存能力,极大的提升站点的访问性能。最终实现下面的效果

SSG VS SSR

关于二者访问性能对比,这里有两点可以说明一下:

  1. 大家可以直接访问笔者制作好的项目地址 SSR方案SSG方案, 这二者都是在相对平等的条件下产出的,没有做专门的体验优化。大家可以直观感受一下效果
  2. PageSpeed Insights 测试结果对比:

SSR方案移动及桌面性能测试结果展示:

SSG方案移动及桌面性能测试结果展示

可以看到不管是PC还是H5,SSG方案都是优于SSR

3. 问题总结与未来拓展

关于Serverless架构整体系统性能优化本篇也只是揭开了冰山一角,而且已经展示出来的部分也还存在着一些不足,比如SSG方案固然是好,但也存在着缺点,比如我们如何通过自动化机制保障更新内容后会自动触发SSG并且更新站点缓存,针对大规模的网站SSG方案的执行效率也会影响业务迭代本身。

不过值得庆幸的是,这个方案在基础设施以上都是对前端可见的,意味着我们可以更好的掌控我们的web服务的访问性能,从边缘节点到静态资源再到后端的服务,他也为广大前端工程师提供了一个走向更加专业全栈的可能性。

接下来我还会继续对项目进行深入改造,会解决上面提到的持续集成中保障SSG的正常运行,以及继续扩展系统的稳定性保障,深入可观测体系跟大家一起探讨Serverless架构的可观测方案,敬请期待。

作者介绍
目录

相关产品

  • 函数计算