最近关于 Serverless 的讨论越来越多。看似与前端关系不大的 Serverless,其实早已和前端有了颇深渊源,并且将掀起新的前端技术变革。本文主要就根据个人理解和总结,从前端开发模式的演进、基于 Serverless 的前端开发案例以及 Serverless 开发最佳实践等方面,与大家探讨 Serverless 中的前端开发模式。本人也有幸在 QCon2019 分享了这一主题。
前端开发模式的演进 🔗
首先回顾一下前端开发模式的演进,我觉得主要有四个阶段。
- 基于模板渲染的动态页面
- 基于 AJAX 的前后端分离
- 基于 Node.js 的前端工程化
- 基于 Node.js 的全栈开发
基于模板渲染的动态页面 🔗
在早起的互联网时代,我们的网页很简单,就是一些静态或动态的页面,主要目的是用来做信息的展示和传播。这个时候开发一个网页也很容易,主要就是通过 JSP、PHP 等技术写一些动态模板,然后通过 Web Server 将模板解析成一个个 HTML 文件,浏览器只负责渲染这些 HTML 文件。这个阶段还没有前后端的分工,通常是后端工程师顺便写了前端页面。
基于 AJAX 的前后端分离 🔗
2005 年 AJAX 技术的正式提出,翻开了 Web 开发的新篇章。基于 AJAX,我们可以把 Web 分为前端和后端,前端负责界面和交互,后端负责业务逻辑的处理。前后端通过接口进行数据交互。我们也不再需要在各个后端语言里面写着难以维护的 HTML。网页的复杂度也由后端的 Web Server 转向了浏览器端的 JavaScript。也正因如此,开始有了前端工程师这个职位。
基于 Node.js 的前端工程化 🔗
2009年 Node.js 的出现,对于前端工程师来说,也是一个历史性的时刻。随着 Node.js 一同出现的还有 CommonJS 规范和 npm 包管理机制。随后也出现了 Grunt、Gulp、Webpack 等一系列基于 Node.js 的前端开发构建工具。
在 2013 年前后,前端三大框架 React.js/Angular/Vue.js 相继发布第一个版本。我们可以从以往基于一个个页面的开发,变为基于一个个组件进行开发。开发完成后使用 webpack 等工具进行打包构建,并通过基于 Node.js 实现的命令行工具将构建结果发布上线。前端开发开始变得规范化、标准化、工程化。
基于 Node.js 的全栈开发 🔗
Node.js 对前端的重要意义还有,以往只能运行在浏览器中的 JavaScript 也可以运行在服务器上,前端工程师可以用自己最熟悉的语言来写服务端的代码。于是前端工程师开始使用 Node.js 做全栈开发,开始由前端工程师向全栈工程师的方向转变。这是前端主动突破自己的边界。
另一方面,前端在发展,后端也在发展。也差不多在 Node.js 诞生那个时代,后端普遍开始由巨石应用模式由微服务架构转变。这也就导致以往的前后端分工出现了分歧。随着微服务架构的兴起,后端的接口渐渐变得原子性,微服务的接口也不再直接面向页面,前端的调用变得复杂了。于是 BFF(Backend For Frontend)架构应运而生,在微服务和前端中间,加了一个 BFF 层,由 BFF 对接口进行聚合、裁剪后,再输出给前端。而 BFF 这层不是后端本质工作,且距离前端最近和前端关系最大,所以前端工程师自然而然选择了 Node.js 来实现。这也是当前 Node.js 在服务端较为广泛的应用。
下一代前端开发模式 🔗
可以看到,每一次前端开发模式的变化,都因某个变革性的技术而起。先是 AJAX,而后是 Node.js。那么下一个变革性的技术是什么?不言而喻,就是 Serverless。
Serverless 服务中的前端解决方案 🔗
Serverless 简介 🔗
根据 CNCF 的定义,Serverless 是指构建和运行不需要服务器管理的应用程序的概念。(serverless-overview)
Serverless computing refers to the concept of building and running applications that do not require server management. — CNCF
其实 Serverless 早已和前端产生了联系,只是我们可能没有感知。比如 CDN,我们把静态资源发布到 CDN 之后,就不需要关心 CDN 有多少个节点、节点如何分布,也不需要关心它如何做负载均衡、如何实现网络加速,所以 CDN 对前端来说是 Serverless。再比如对象存储,和 CDN 一样,我们只需要将文件上传到对象存储,就可以直接使用了,不需要关心它如何存取文件、如何进行权限控制,所以对象存储对前端工程师来说是 Serverless。甚至一些第三方的 API 服务,也是 Serverless,因为我们使用的时候,不需要去关心服务器。
当然,有了体感还不够,我们还是需要一个更精确的定义。从技术角度来说,Serverless 就是 FaaS 和 BaaS 的结合。
Serverless = FaaS + BaaS。
简单来讲,FaaS(Function as a Service) 就是一些运行函数的平台,比如阿里云的函数计算、AWS 的 Lambda 等。
BaaS(Backend as a Service)则是一些后端云服务,比如云数据库、对象存储、消息队列等。利用 BaaS,可以极大简化我们的应用开发难度。
Serverless 则可以理解为运行在 FaaS 中的,使用了 BaaS 的函数。
Serverless 的主要特点有:
- 事件驱动
- 函数在 FaaS 平台中,需要通过一系列的事件来驱动函数执行。
- 无状态
- 因为每次函数执行,可能使用的都是不同的容器,无法进行内存或数据共享。如果要共享数据,则只能通过第三方服务,比如 Redis 等。
- 无运维
- 使用 Serverless 我们不需要关心服务器,不需要关心运维。这也是 Serverless 思想的核心。
- 低成本
- 使用 Serverless 成本很低,因为我们只需要为每次函数的运行付费。函数不运行,则不花钱,也不会浪费服务器资源
Serverless 服务中的前端解决方案架构图 🔗
上图是当前主要的一些 Serverless 服务,以及对应的前端解决方案。
从下往上,分别是基础设施和开发工具。
基础设施主要是一些云计算厂商提供,包括云计算平台和各种 BaaS 服务,以及运行函数的 FaaS 平台。
前端主要是 Serverless 的使用者,所以对前端来说,最重要的开发工具这一层,我们需要依赖开发工具进行 Serverless 开发、调试和部署。
框架(Framework) 🔗
如今还没有一个统一的 Serverless 标准,不同云计算平台提供的 Serverless 服务很可能是不一样的,这就导致我们的代码,无法平滑迁移。Serverless 框架一个主要功能是简化 Serverless 开发、部署流程,另一主要功能则是屏蔽不同 Serverless 服务中的差异,让我们的函数能够在不改动或者只改动很小一部分的情况下,在其他 Serverless 服务中也能运行。
常见的 Serverless 框架有 Serverless Framework、ZEIT Now、Apex 等。不过这些基本都是国外公司做的,国内还没有这样的平台。
Web IDE 🔗
和 Serverless 紧密相关的 Web IDE 主要也是各个云计算平台的 Web IDE。利用 Web IDE,我们可以很方便地在云端开发、调试函数,并且可以直接部署到对应的 FaaS 平台。这样的好处是避免了在本地安装各种开发工具、配置各种环境。常见的 Web IDE 有 AWS 的 Cloud9、阿里云的函数计算 Web IDE、腾讯云的 Cloud Studio。从体验上来说,AWS Cloud9 最好。
命令行工具 🔗
当然,目前最主要的开发方式还是在本地进行开发。所以在本地开发 Serverless 的命令行工具也必不可少。
命令行工具主要有两类,一类是云计算平台提供的,如 AWS 的 aws
、 Azure 的 az
、阿里云的 fun
;还有一类是 Serverless 框架提供的,如 serverless
、now
。
大部分工具如 serverless
、fun
等,都是用 Node.js 实现的。
下面是几个命令行工具的例子。
创建 🔗
# serverless
$ serverless create --template aws-nodejs --path myService
# fun
$ fun init -n qcondemo helloworld-nodejs8
部署 🔗
# serverless
$ serverless deploy
# fun
$ fun deploy
调试 🔗
# serverless
$ serverless invoke [local] --function functionName
# fun
$ fun local invoke functionName
应用场景 🔗
在开发工具上面一层,则是 Serverless 的一些垂直应用场景。除了使用传统的服务端开发,目前使用 Serverless 技术的还有小程序开发,未来可能还会设计物联网领域(IoT)。
不同 Serverless 服务的对比 🔗
上图从支持语言、触发器、价格等多个方面对不同 Serverless 服务进行了对比,可以发现有差异,也有共性。
比如几乎所有 Serverless 服务都支持 Node.js/Python/Java 等语言。
从支持的触发器来看,几乎所有服务也都支持 HTTP、对象存储、定时任务、消息队列等触发器。当然,这些触发器也与平台自己的后端服务相关,比如阿里云的对象存储触发器,是基于阿里云的 OSS 产品的存取等事件触发的;而 AWS 的对象存储触发器,则是基于 AWS 的 S3 的事件触发的,两个平台并不通用。这也是当前 Serverless 面临的一个问题,就是标准不统一。
从计费的角度来看,各个平台的费用基本一致。在前面也提到,Serverless 的计费是按调用次数计费。对于各个 Serverless,每个月都有 100 万次的免费调用次数,之后差不多 ¥1.3/百万次;以及 400,000 GB-s 的免费执行时间,之后 ¥0.0001108/GB-s。所以在应用体量较小的时候,使用 Serverless 是非常划算的。
基于 Serverless 的前端开发模式 🔗
在本章节,主要以几个案例来说明基于 Serverless 的前端开发模式,以及它和以往的前端开发有什么不一样。
在开始具体的案例之前,先看一下传统开发流程。
在传统开发流程中,我们需要前端工程师写页面,后端工程师写接口。后端写完接口之后,把接口部署了,再进行前后端联调。联调完毕后再测试、上线。上线之后,还需要运维工程师对系统进行维护。整个过程涉及多个不同角色,链路较长,沟通协调也是一个问题。
而基于 Serverless,后端变得非常简单了,以往的后端应用被拆分为一个个函数,只需要写完函数并部署到 Serverless 服务即可,后续也不用关心任何服务器的运维操作。后端开发的门槛大幅度降低了。因此,只需要一个前端工程师就可以完成所有的开发工作。
当然,前端工程师基于 Serverless 去写后端,最好也需要具备一定的后端知识。涉及复杂的后端系统或者 Serverless 不适用的场景,还是需要后端开发,后端变得更靠后了。
基于 Serverless 的 BFF 🔗
一方面,对不同的设备需要使用不同的 API,另一方面,由于微服务导致前端接口调用的复杂,所以前端工程师开始使用 BFF 的方式,对接口进行聚合裁剪,以得到适用于前端的接口。
下面是一个通用的 BFF 架构。
BFF @ SoundCloud
最底层的就是各种后端微服务,最上层就是各种前端应用。在微服务和应用之前,就是通常由前端工程师开发的 BFF。
这样的架构解决了接口协调的问题,但也带来了一些新的问题。
比如针对每个设备开发一个 BFF 应用,也会面临一些重复开发的问题。而且以往前端只需要开发页面,关注于浏览器端的渲染即可,现在却需要维护各种 BFF 应用。以往前端也不需要关心并发,现在并发压力却集中到了 BFF 上。总的来说运维成本非常高,通常前端并不擅长运维。
Serverless 则可以帮我们很好的解决这些问题。基于 Serverless,我们可以使用一个个函数来实各个接口的聚合裁剪。前端向 BFF 发起的请求,就相当于是 FaaS 的一个 HTTP 触发器,触发一个函数的执行,这个函数中来实现针对该请求的业务逻辑,比如调用多个微服务获取数据,然后再将处理结果返回给前端。这样运维的压力,就由以往的 BFF Server 转向了 FaaS 服务,前端再也不用关心服务器了。
上图则是基于 Serverless 的 BFF 架构。为了更好的管理各种 API,我们还可以添加网关层,通过网关来管理所有 API(比如阿里云的网关),比如对 API 进行分组、分环境。基于 API 网关,前端就不直接通过 HTTP 触发器来执行函数,而是将请求发送至网关,再由网关去触发具体的函数来执行。
基于 Serverless 的服务端渲染 🔗
基于当下最流行的三大前端框架(React.js/Anguler/Vue.js),现在的渲染方式大部分都是客户端渲染。页面初始化的时候,只加载一个简单 HTML 以及对应的 JS 文件,再由 JS 来渲染出一个个页面。这种方式最主要的问题就是白屏时间和 SEO。
为了解决这个问题,前端又开始尝试服务端渲染。本质思想其实和最早的模板渲染是一样的。都是前端发起一个请求,后端 Server 解析出一个 HTML 文档,然后再返回给浏览器。只不过以往是 JSP、PHP 等服务端语言的模板,现在是基于 React、Vue 等实现的同构应用,这也是如今的服务端渲染方案的优势。
但服务端渲染又为前端带来了一些额外的问题:运维成本,前端需要维护用于渲染的服务器。
Serverless 最大的优点就是可以帮我们减少运维,那 Serverless 能不能用于服务端渲染呢?当然也是可以的。
传统的服务端渲染,每个请求的 path 都对应着服务端的每个路由,由该路由实现对应 path 的 HTML 文档渲染。用于渲染的服务端程序,就是这些集成了这些路由的应用。
使用 Serverless 来做服务端渲染,就是将以往的每个路由,都拆分为一个个函数,再在 FaaS 上部署对应的函数。这样用户请求的 path,对应的就是每个单独的函数。通过这种方式,就将运维操作转移到了 FaaS 平台,前端做服务端渲染,就不用再关心服务端程序的运维部署了。
ZEIT 的 Next.js 就对基于 Serverless 的服务端渲染做了很好的实现。下面就是一个简单的例子。
代码结构如下:
.
├── next.config.js
├── now.json
├── package.json
└── pages
├── about.js
└── index.js
// next.config.js
module.exports = {
target: 'serverless'
}
其中 pages/about.js
和 pages/index.js
就是两个页面,在 next.config.js
配置了使用 Zeit 提供的 Serverless 服务。
然后使用 now
这个命令,就可以将代码以 Serverless 的方式部署。部署过程中,pages/about.js
和 pages/index.js
就分别转换为两个函数,负责渲染对应的页面。
基于 Serverless 的小程序开发 🔗
目前国内使用 Serverless 较多的场景可能就是小程开发了。具体的实现就是小程序云开发,支付宝小程序和微信小程序都提供了云开发功能。
在传统的小程序开发中,我们需要前端工程师进行小程序端的开发;后端工程师进行服务端的开发。小程序的后端开发和其他的后端应用开发,本质是是一样的,需要关心应用的负载均衡、备份冗灾、监控报警等一些列部署运维操作。如果开发团队人很少,可能还需要前端工程师去实现服务端。
但基于云开发,就只需要让开发者关注于业务的实现,由一个前端工程师就能够完成整个应用的前后端开发。因为云开发将后端封装为了 BaaS 服务,并提供了对应的 SDK 给开发者,开发者可以像调用函数一样使用各种后端服务。应用的运维也转移到了提供云开发的服务商。
下面分别是使用支付宝云开发(Basement)的一些例子,函数就是定义在 FaaS 服务中的函数。
操作数据库
// `basement` 是一个全局变量
// 操作数据库
basement.db.collection('users')
.insertOne({
name: 'node',
age: 18,
})
.then(() => {
resolve({ success: true });
})
.catch(err => {
reject({ success: false });
});
上传图片
// 上传图片
basement.file
.uploadFile(options)
.then((image) => {
this.setData({
iconUrl: image.fileUrl,
});
})
.catch(console.error);
调用函数
// 调用函数
basement.function
.invoke('getUserInfo')
.then((res) => {
this.setData({
user: res.result
});
})
.catch(console.error}
通用 Serverless 架构 🔗
基于上述几个 Serverless 开发的例子,就可以总结出一个通用的 Serverless 架构。
其中最底层就是实现复杂业务的后端微服务(Backend)。然后 FaaS 层通过一系列函数实现业务逻辑,并为前端直接提供服务。对于前端开发者来说,前端可以通过编写函数的方式来实现服务端的逻辑。对于后端开发者来说,后端变得更靠后了。如果业务比较较淡,FaaS 层能够实现,甚至也不需要微服务这一层了。
同时不管是在后端、FaaS 还是前端,我们都可以去调用云计算平台提供的 BaaS 服务,大大降低开发难度、减少开发成本。小程序云开发,就是直接在前端调用 BaaS 服务的例子。
Serverless 开发最佳实践 🔗
基于 Serverless 开发模式和传统开发模式最大的不同,就是传统开发中,我们是基于应用的开发。开发完成后,我们需要对应用进行单元测试和集成测试。而基于 Serverless,开发的是一个个函数,那么我们应该如何对 Serverless 函数进行测试?Serverless 函数的测试和普通的单元测试又有什么区别?
还有一个很重要的点是,基于 Serverless 开发的应用性能如何?应该怎么去提高 Serverless 应用的性能?
本章主要就介绍一下,基于 Serverless 的函数的测试和函数的性能两个方面的最佳实践。
函数的测试 🔗
虽然使用 Serverless 我们可以简单地进行业务的开发,但它的特性也给我们的测试带来了一些挑战。主要有以下几个方面。
Serverless 函数是分布式的,我们不知道也无需知道函数是部署或运行在哪台机器上,所以我们需要对每个函数进行单元测试。Serverless 应用是由一组函数组成的,函数内部可能依赖了一些别的后端服务(BaaS),所以我们也需要对 Serverless 应用进行集成测试。
运行函数的 FaaS 和 BaaS 在本地也难以模拟。除此之外,不同平台提供的 FaaS 环境可能不一致,不平台提供的 BaaS 服务的 SDK 或接口也可能不一致,这不仅给我们的测试带来了一些问题,也增加了应用迁移成本。
函数的执行是由事件驱动的,驱动函数执行的事件,在本地也难以模拟。
那么如何解决这些问题呢?
根据 Mike Cohn 提出的测试金字塔,单元测试的成本最低,效率最高;UI 测试(集成)测试的成本最高,效率最低,所以我们要尽可能多的进行单元测试,从而减少集成测试。这对 Serverless 的函数测试同样适用。
图片来源: https://martinfowler.com/bliki/TestPyramid.html
为了能更简单对函数进行单元测试,我们需要做的就是将业务逻辑和函数依赖的 FaaS(如函数计算) 和 BaaS (如云数据库)分离。当 FaaS 和 BaaS 分离出去之后,我们就可以像编写传统的单元测试一样,对函数的业务逻辑进行测试。然后再编写集成测试,验证函数和其他服务的集成是否正常工作。
一个糟糕的例子 🔗
下面是一个使用 Node.js 实现的函数的例子。该函数做的事情就是,首先将用户信息存储到数据库中,然后给用户发送邮件。
const db = require('db').connect();
const mailer = require('mailer');
module.exports.saveUser = (event, context, callback) => {
const user = {
email: event.email,
created_at: Date.now()
}
db.saveUser(user, function (err) {
if (err) {
callback(err);
} else {
mailer.sendWelcomeEmail(event.email);
callback();
}
});
};
这个例子主要存在两个问题:
- 业务逻辑和 FaaS 耦合在一起。主要就是业务逻辑都在
saveUser
这个函数里,而saveUser
参数的event
和conent
对象,是 FaaS 平台提供的。 - 业务逻辑和 BaaS 耦合在一起。具体来说,就是函数内使用了
db
和mailer
这两个后端服务,测试函数必须依赖于db
和mailer
。
编写可测试的函数 🔗
基于将业务逻辑和函数依赖的 FaaS 和 BaaS 分离的原则,对上面的代码进行重构。
class Users {
constructor(db, mailer) {
this.db = db;
this.mailer = mailer;
}
save(email, callback) {
const user = {
email: email,
created_at: Date.now()
}
this.db.saveUser(user, function (err) {
if (err) {
callback(err);
} else {
this.mailer.sendWelcomeEmail(email);
callback();
}
});
}
}
module.exports = Users;
const db = require('db').connect();
const mailer = require('mailer');
const Users = require('users');
let users = new Users(db, mailer);
module.exports.saveUser = (event, context, callback) => {
users.save(event.email, callback);
};
在重构后的代码中,我们将业务逻辑全都放在了 Users
这个类里面,Users
不依赖任何外部服务。测试的时候,我们也可以不传入真实的 db
或 mailer
,而是传入模拟的服务。
下面是一个模拟 mailer
的例子。
// 模拟 mailer
const mailer = {
sendWelcomeEmail: (email) => {
console.log(`Send email to ${email} success!`);
},
};
这样只要对 Users
进行充分的单元测试,就能确保业务代码如期运行。
然后再传入真实的 db
和 mailer
,进行简单的集成测试,就能知道整个函数是否能够正常工作。
重构后的代码还有一个好处是方便函数的迁移。当我们想要把函数从一个平台迁移到另一个平台的时候,只需要根据不同平台提供的参数,修改一下 Users
的调用方式就可以了,而不用再去修改业务逻辑。
小结 🔗
综上所述,对函数进行测试,就需要牢记金字塔原则,并遵循以下原则:
- 将业务逻辑和函数依赖的 FaaS 和 BaaS 分离
- 对业务逻辑进行充分的单元测试
- 将函数进行集成测试验证代码是否正常工作
函数的性能 🔗
使用 Serverless 进行开发,还有一个大家都关心的问题就是函数的性能怎么样。
对于传统的应用,我们的程序启动起来之后,就常驻在内存中;而 Serverless 函数则不是这样。
当驱动函数执行的事件到来的时候,首先需要下载代码,然后启动一个容器,在容器里面再启动一个运行环境,最后才是执行代码。前几步统称为冷启动(Cold Start)。传统的应用没有冷启动的过程。
下面是函数生命周期的示意图:
图片来源: https://www.youtube.com/watch?v=oQFORsso2go&feature=youtu.be&t=8m5s
冷启动时间的长短,就是函数性能的关键因素。优化函数的性能,也就需要从函数生命周期的各个阶段去优化。
不同编程语言对冷启动时间的影响 🔗
在此之前,已经有很多人测试过不同编程语言对冷启动时间的影响,比如:
- Compare coldstart time with different languages, memory and code sizes -by Yan Cui
- Cold start / Warm start with AWS Lambda - by Erwan Alliaume
- Serverless: Cold Start War - by Mikhail Shilkov
图片来源: Cold start / Warm start with AWS Lambda
从这些测试中能够得到一些统一的结论:
- 增加函数的内存可以减少冷启动时间
- C#、Java 等编程语言的能启动时间大约是 Node.js、Python 的 100 倍
基于上述结论,如果想要 Java 的冷启动时间达到 Node.js 那么小,可以为 Java 分配更大的内存。但更大的内存意味着更多的成本。
函数冷启动的时机 🔗
刚开始接触 Serverless 的开发者可能有一个误区,就是每次函数执行,都需要冷启动。其实并不是这样。
当第一次请求(驱动函数执行的事件)来临,成功启动运行环境并执行函数之后,运行环境会保留一段时间,以便用于下一次函数执行。这样就能减少冷启动的次数,从而缩短函数运行时间。当请求达到一个运行环境的限制时,FaaS 平台会自动扩展下一个运行环境。
以 AWS Lambda 为例,在执行函数之后,Lambda 会保持执行上下文一段时间,预期用于另一次 Lambda 函数调用。其效果是,服务在 Lambda 函数完成后冻结执行上下文,如果再次调用 Lambda 函数时 AWS Lambda 选择重用上下文,则解冻上下文供重用。
下面以两个小测试来说明上述内容。
我使用阿里云的函数计算实现了一个 Serverless 函数,并通过 HTTP 事件来驱动。然后使用不同并发数向函数发起 100 个请求。
首先是一个并发的情况:
可以看到第一个请求时间为 302ms,其他请求时间基本都在 50ms 左右。基本就能确定,第一个请求对应的函数是冷启动,剩余 99 个请求,都是热启动,直接重复利用了第一个请求的运行环境。
接下来是并发数为 10 的情况:
可以发现,前 10 个请求,耗时基本在 200ms-300ms,其余请求耗时在 50ms 左右。于是可以得出结论,前 10 个并发请求都是冷启动,同时启动了 10 个运行环境;后面 90 个请求都是热启动。
这也就印证了之前的结论,函数不是每次都冷启动,而是会在一定时间内复用之前的运行环境。
执行上下文重用 🔗
上面的结论对我们提高函数性能有什么帮助呢?当然是有的。既然运行环境能够保留,那就意味着我们能对运行环境中的执行上下文进行重复利用。
来看一个例子:
const mysql = require('mysql');
module.exports.saveUser = (event, context, callback) => {
// 初始化数据库连接
const connection = mysql.createConnection({ /* ... */ });
connection.connect();
connection.query('...');
};
上面例子实现的功能就是在 saveUser
函数中初始化一个数据库连接。这样的问题就是,每次函数执行的时候,都会重新初始化数据库连接,而连接数据库又是一个比较耗时的操作。显然这样对函数的性能是没有好处的。
既然在短时间内,函数的执行上下文可以重复利用,那么我们就可以将数据库连接放在函数之外:
const mysql = require('mysql');
// 初始化数据库连接
const connection = mysql.createConnection({ /* ... */ });
connection.connect();
module.exports.saveUser = (event, context, callback) => {
connection.query('...');
};
这样就只有第一次运行环境启动的时候,才会初始化数据库连接。后续请求来临、执行函数的时候,就可以直接利用执行上下文中的 connection
,从而提后续高函数的性能。
大部分情况下,通过牺牲一个请求的性能,换取大部分请求的性能,是完全可以够接受的。
给函数预热 🔗
既然函数的运行环境会保留一段时间,那么我们也可以通过主动调用函数的方式,隔一段时间就冷启动一个运行环境,这样就能使得其他正常的请求都是热启动,从而避免冷启动时间对函数性能的影响。
这是目前比较有效的方式,但也需要有一些注意的地方:
- 不要过于频繁调用函数,至少频率要大于 5 分钟
- 直接调用函数,而不是通过网关等间接调用
- 创建专门处理这种预热调用的函数,而不是正常业务函数
这种方案只是目前行之有效且比较黑科技的方案,可以使用,但如果你的业务允许“牺牲第一个请求的性能换取大部分性能”,那也完全不必使用该方案,
小结 🔗
总体而言,优化函数的性能就是优化冷启动时间。上述方案都是开发者方面的优化,当然还一方面主要是 FaaS 平台的性能优化。
总结一下上述方案,主要是以下几点:
- 选用 Node.js / Python 等冷启动时间短的编程语言
- 为函数分配合适的运行内存
- 执行上下文重用
- 为函数预热
总结 🔗
作为前端工程师,我们一直在探讨前端的边界是什么。现在的前端开发早已不是以往的前端开发,前端不仅可以做网页,还可以做小程序,做 APP,做桌面程序,甚至做服务端。而前端之所以在不断拓展自己的边界、不断探索更多的领域,则是希望自己能够产生更大的价值。最好是用我们熟悉的工具、熟悉的方式来创造价值。
而 Serverless 架构的诞生,则可以最大程度帮助前端工程师实现自己的理想。使用 Serverless,我们不需要再过多关注服务端的运维,不需要关心我们不熟悉的领域,我们只需要专注于业务的开发、专注于产品的实现。我们需要关心的事情变少了,但我们能做的事情更多了。
Serverless 也必将对前端的开发模式产生巨大的变革,前端工程师的职能也将再度回归到应用工程师的职能。
如果要用一句话来总结 Serverless,那就是 Less is More。