前言
Serverless
是近些年来流行起来的架构理念,进入市场化应该是2014年亚马逊发布FaaS Lambda
。
一个热词的产生,必然会有一些商家抢注商标的现象,所以,我们目前搜索Serverless
,搜索结果第一页会看到名为Serverless
的产品。
我们日常说的Serverless
,一般是指架构理念,或基于架构理念产生的产品全类,而非指某个具体的产品。
Serverless
是对运维体系的极端抽象,这里有一个名次“抽象”、两个定语“运维体系的”、“极端”。
-
Serverless
是一个抽象,就说明Serverless
不是指具体的某个产品。 - "运维体系的",说明了
Serverless
的职能边界,是对运维体系流程的优化,当然,也对开发流程产生了一些副作用,但,主要的职能在运维方向。 - "极端抽象",是表明基于
Serverless
理念输出的产品,将运维体系的复杂度内化,只预留简单的接口供外部调用。
带来的结果就是,可以让零运维经验的人,几分钟就部署一个Web应用上线,稍后我会在==示例==中演示一下。
运维发展史
先看一下整个发展流程,经历了手动运维、自动运维、DevOps开发运维、智能运维几个阶段。
按社会的精细化分工来说:
- 手动运维阶段,开发者交付代码,运维团队需要进行服务器协调、运行环境部署、上线、版本控制、日志监控、扩缩容设计、容错容灾高可用设计...等等工作。
- 如果线上产品出现问题,需要开发者和运维团队共同查找问题。
- 自动运维阶段,通过编排脚本命令将一些简单的工作进行打包处理,一定程度上减少重复性的手工操作。
- 随着微服务、容器技术的发展,来到了
DevOps
阶段,DevOps
=Development
+Operations
,开发者开始承担一部分的运维职责,甚至有些公司出现了跨职能团队:运维、开发团队融合,打破手动运维阶段开发、运维两座孤岛的现象。这个阶段,就Docker
工具而言,开发者交付镜像,运维团队不必在操心代码的运行环境问题。 - 智能运维阶段,
Serverless
只是其中的一个发展节点。- 各大服务厂商将基础设施云化,对外提供接口,实现基础设施即代码,让开发者可以通过应用程序代码访问、配置基础设施(BaaS:抽象粒度大多在机器级别)。
- 计算机算力的提升,如函数计算[阿里云]、云函数[腾讯云]将计算服务的抽象粒度提高到了函数级别,实现实时的弹性伸缩容机制,按毫秒级计量、按需计费(FaaS)。
产生背景
- 从整个发展史可以看出,技术的发展起到了重要的推动作用。
- 历史运维体系的痛点:企业中长尾应用的运营成本问题。
- 什么是中长尾应用?就是每天大部分时间没有流量或者有很少流量的应用
- 为了保证这些应用的正常运行,至少要安排一台服务器跑这些应用。
- 而
Serverless
借助计算机算力,可以实现实时的弹性扩缩容机制。
- 减少研发人员的关注点,研发人员无需管理、维护底层的基础设施,无需规划预估容器所需要的计算资源,降低整合和决策的代价,只需要专注应用程序代码的编写,提高研发效能。
下个定义
狭义Serverless
= FaaS
架构
狭义的Severless
是指基于函数计算将Serverless
体系产品整合在一起,构建成一个Serverless
应用。
狭义Serverless
= FaaS
架构 = Trigger
+ FaaS
+ BaaS
= FaaS
+ Baas
广义Serverless
是指具备Serverless
特性的云服务。
Serverless
可以分为Server
和less
,其中less
不是指无服务器端,或者少服务器端,而是指无感知,也对应了“Serverless
是对运维体系的极端抽象”这句话。
发展现状
目前大多数互联网公司都还在DevOps
时代。
部分一线大厂有自己的Serverless
解决方案并对外开放。如阿里云的函数计算、腾讯云的云函数。
目前Serverless
架构实现并没有统一的规范,实现和提供服务的厂商强关联,如果在不同厂商之间迁移,会有很大的工作量和困难。
函数计算
以阿里云平台的函数计算来介绍一下FaaS
函数即服务。
我们先熟悉一下平台设计。
- 可以通过支付宝扫码授权登陆。
- 直接用“产品”菜单下的搜索功能搜索“函数计算”。
- 点击“控制台”直接进入。
- 顶部
- 可以切换代码部署的地域
- 如果在“服务/函数”下找不到自己已有的代码,检查一下地域是否选择正确。
- “概览”页面
- 可以直观的看到使用量、监控的概览,还有一些快捷入口。
- 免费执行次数和免费资源使用量,在测试阶段可以有效的防止用超过,也很难用超过。
- 监控的可视化图形
- 新建函数的快捷入口
- “服务及函数”
- 可以创建新服务、新函数,查看已有服务和函数。
- 点击服务列表中的某项,可以在右侧查看、编辑包含的函数列表、服务相关的配置信息。
- 点击函数列表中的某项,可以进入函数详情,查看、配置函数的信息。
- 自定义域名
- 通过自定义域名访问FC函数,需要配合HTTP触发器使用
- ==HTTP触发器==后续讲函数类型的时候会提到。
FaaS
下面我们具体看一下函数计算。
首先,我们创建一个服务、一个函数。
创建好一个服务以后,默认打开“服务配置”Tab,从该Tab页,我们可以查看服务当前的配置并进行修改。
切换到“函数列表”Tab页,点击新增函数按钮,这时会发现,函数有两类:
- 事件函数
- HTTP函数
这里HTTP函数,就是上边所说,有HTTP触发器的函数,可以通过网络请求触发FC函数的执行;
因为上边我们提到了HTTP触发器,那就先创建一个HTTP函数。
创建成功后,默认进入函数的“触发器”Tab页,可以看到“事件类型”是http
,请求方法是GET
、POST
,不需要授权访问。
为了更清晰的看到触发器的配置项,我们重新创建一个触发器。
然后,切换到“代码执行”Tab页,我们可以看到示例代码。
HTTP函数示例代码:
- 结构:exports.handler = (req, resp, context) => {}
- 函数调用时,执行定义的handler逻辑,参数是req、resp、context;
- 这些参数后续==调试阶段==我们可以看一下
- 打印标准版的输出
hello world
- 组装请求数据字段
- 将
body
数据提取并输出组装的数据
我们执行一下看看会发生什么?
- 打印返回的结果
- 打印函数执行日志
- 打印
RequestID
- 这是唯一存在的ID,每次执行都会改变。
- 可以通过该ID查询日志。
在“执行”按钮处,可以配置一些参数,改变一下配置看看输出的结果。
-
POST
请求 - 路径
-
Params
改变URL上的过滤参数 -
Body
改变POST
的请求输出,GET
请求下不会出现该Tab页
而且,在修改的过程中,会发现上方的URL会发生变化。
我们可以通过Postman
去请求该地址,调用FC函数,可以通过“日志查询”查看调用结果。
最后,我们看一下exports
导出的函数,默认函数名为handler
,这个名字能修改么?
答案是肯定的。
- 切换到“概览”Tab页,“修改配置”,修改“函数入口”
- 切换回“代码执行”,执行看一下结果,报错
- 将
exports.[fnName]
修改成配置项,“保存”,再执行,成功。
看完了HTTP函数,我们返回去看一下事件函数。
返回到服务列表页面。
“新增函数” ——> “事件函数” ——> “配置部署”
配置页面:
- 运行环境
- 弹性实例
- 弹性实例有免费额度
- 性能实例没有免费额度
- 性能实例扩容速度慢,弹性伸缩能力不及弹性实例:对比文档
- 函数入口
- 和“HTTP函数”一样,可以修改约定的导出函数名
点击“完成”创建函数。
“HTTP函数”跳转到“触发器”Tab,而“事件函数”直接跳转到“代码执行”Tab。
切换到“触发器”,我们可以看到,没有任何数据。
我们看一下“事件函数”的实例代码:
- 结构:exports.handler = (event, context, callback) => {}
- 函数调用时,执行定义的handler逻辑,参数是event, context, callback;
- 这些参数我们依旧在后续==调试阶段==看一下
- 依旧打印标准版的输出
hello world
- 通过callback返回数据
- callback(err, data)
- 第一个参数是错误信息
- 第二个参数是数据,只有在第一个参数为
null
时,才返回数据
代码的“执行”按钮在上边,尝试修改代码,也能看到是自动保存。
执行一下程序看看会发生什么?
- 打印返回的结果
- 打印函数执行日志
- 打印
RequestID
- 这是唯一存在的ID,每次执行都会改变。
- 可以通过该ID查询日志。
我们从两个示例函数中,都可以看到注释的exports.initializer
函数。
这个函数是做什么的呢?
通过函数名,可以知道,这是实例的初始化函数,保证同一实例成功且仅成功执行一次。
值得注意的是:这个函数没有返回值。
将“事件函数”中的注释去掉,“保存并执行”,看看有什么不同。
发现执行结果和原来没什么不同,初始化函数中的console.log('initializing')
并没有打印出来。
要怎么做呢?
要初始化函数执行,需要特殊的配置。
切换到“概览”Tab,“修改配置” ——> “是否配置函数初始化入口”,定义为刚刚解注的函数名,“确认”后跳转至“代码执行”。
“执行”代码,查看执行结果:报错——> 无效的函数名。
重新“修改配置”,初始化入口定义为index.initialzer
即可。
FC Initialize Start RequestId: e8acfe4c-9670-4255-86f1-2659291031c1
load code for handler:index.initializer
2020-12-24T09:13:36.846Z e8acfe4c-9670-4255-86f1-2659291031c1 [verbose] initializing
FC Initialize End RequestId: e8acfe4c-9670-4255-86f1-2659291031c1
会看到函数执行日志中,多出来几条日志。
连续多次点击“执行”,也仅仅在第一次执行的时候,会多这几条日志,表明“初始化函数”仅仅执行一次。
修改“初始化函数”中的callback(null, 123)
发现执行日志中并没有输出,表明“初始化函数”没有输出。
有没有疑惑:
var ret = '';
function handlerRet() {
console.log('-------');
ret = 'return success';
}
handlerRet();
exports.handler = (event, context, callback) => {
console.log(ret);
callback(null, 'hello world');
}
上边这个代码的执行结果是怎样的?
和“初始化函数”有什么不同?
- 执行时机不同
- “初始化函数”在函数实例初始化之前执行;
- 上述看似“全局”的代码是在实例化之后执行的;
- 执行次数
- 上述代码和“初始化函数”一样,都仅执行一次;
我们可以看到,上述三种类型的函数(HTTP函数、事件函数、初始化函数)与普通定义的函数最大的区别在于,FC的函数预置了Context
参数,这是和Runtime
运行平台/上下文相关的参数。
我们可以通过URL请求去调用“HTTP函数”,那如何去调用“事件函数”呢?
- 创建触发器
- 我们切换到“触发器”面板,“创建触发器”,以一个最简单的“定时触发器”为例。
- 最小1分钟时间间隔
- 默认“启动触发器”
- 通过“日志查询”面板,“每分钟自动刷新”,可以查看执行日志(会有延迟)。
- 修改触发器的“触发消息”:JSON数据,修改“代码执行”,在入口函数中打印
event
:console.log(JSON.parse(event))
查看输出结果。- 可以看到,我们可以通过“触发消息”传递参数。
- 关闭“触发器”的状态
- 我们切换到“触发器”面板,“创建触发器”,以一个最简单的“定时触发器”为例。
- SDK调用
- 本地编写代码程序
'use strict'; var FCClient = require('@alicloud/fc2'); var client = new FCClient( '<account id>', { accessKeyID: '<access key>', accessKeySecret: '<access key secret>', region: 'cn-beijing', timeout: 10000 // milliseconds, default is 10s } ); async function test () { try { var ret = await client.invokeFunction('case-1.LATEST', 'case-event', 'event') console.log('invoke function: %j', ret); } catch (err) { console.error(err); } } test().then();
-
node invoke/index.js
- 可以看到本地终端有日志打印出来,正是代码中的
console.log('invoke function: %j', ret);
执行的结果
- 可以看到本地终端有日志打印出来,正是代码中的
- 控制台切换到“日志查询”,查看执行日志,确定FC的函数被触发。
- 本地编写代码程序
上述编写的函数除了“HTTP函数”并没有引入外部依赖,如何引入第三方依赖呢?
其实,“HTTP函数”引入的依赖是阿里云平台的Node.js
环境内置好的第三方包,如果我们需要使用没有内置的依赖包,需要在本地开发环境去安装、编写代码逻辑。
所以,我们接下来说一下本地开发环境的配置:
- 安装
Docker
; //编译代码、安装依赖以及在本地运行调试等操作都是在Docker
镜像中进行; -
Visual Studio Code
中查找aliyun serverless
插件并安装;- 安装过程中需要输入
account id
、access key
、access key secret
。 - 可以通过阿里云官网账号一栏找到这些信息。
- 我们会看到
Visual Studio Code
右侧面板多出了两个FC的Logo选项。
- 可以通过界面查看到远程控制台创建的服务及函数。
- 将远程服务及函数下载到本地
- 从
A
区域,我们可以看到下载到本地对应的服务、函数及触发器列表,点击列表中的某项,会跳转到template.yml
文件对应的配置 -
B
区域,是对列表项的操作- 服务:添加函数操作
- 函数:查看源码、调试、执行操作
- 触发器:无
- 安装过程中需要输入
接下来,我们通过代码调试先看一下编写代码时,遗留的函数参数结构的问题,然后再说依赖问题:
查看case-event
函数的源码,在行号上添加断点,点击“调试”操作
即可查看对应的参数结构。
引入第三方NPM包
- 通过
Visual Studio Code
“资源管理器”查看一下case-event
函数所在的路径 - “终端”切换到函数对应目录
cd case-1/case-event
-
npm init -y
初始化环境 -
npm i -S xss
做示例 - 修改代码
'use strict';
var xss = require('xss');
/*
To enable the initializer feature (https://help.aliyun.com/document_detail/156876.html)
please implement the initializer function as below:
*/
exports.initializer = (context, callback) => {
console.log('initializing');
callback(null, '123');
};
exports.handler = (event, context, callback) => {
console.log('hello world');
var html = xss('<script>alert</script>')
callback(null, html);
}
- 执行函数,查看输出结果:依赖正常执行。
FC Initialize Start RequestId: e2c60d38-bed8-4a92-a48f-56b7c7949d9a
load code for handler:index.initializer
2020-12-25T03:21:47.571Z e2c60d38-bed8-4a92-a48f-56b7c7949d9a [verbose] initializing
FC Initialize End RequestId: e2c60d38-bed8-4a92-a48f-56b7c7949d9a
123FC Invoke Start RequestId: e2c60d38-bed8-4a92-a48f-56b7c7949d9a
load code for handler:index.handler
2020-12-25T03:21:47.651Z e2c60d38-bed8-4a92-a48f-56b7c7949d9a [verbose] hello world
FC Invoke End RequestId: e2c60d38-bed8-4a92-a48f-56b7c7949d9a
<script>alert</script>
- 然后,我们将服务整体上传或在函数上右键单独上传,替换控制台的代码
- 会将依赖
node_modules
一起上传 -
FC函数所需要的依赖必须一同打包上传,否则,会报资源查找不到。
- 会将依赖
介绍完阿里云平台的函数计算,结合Serverless
的定义思考一下,Serverless
=FaaS
架构,Serverless
具有实时弹性扩缩容的优势,函数计算怎么实现这个优势的呢?
这和FC函数的进程模型有关:
- 服务托管细粒化到了语言单位,即函数调用
- 事件驱动的计算模型
- 用完即毁型设计:函数实例准备好后,执行完函数就直接结束。
- 无状态,不存储任何状态
- 正因为没有任何状态,因此在并发量高的时候,我们可以对无状态节点横向扩容,而没有流量时我们可以缩容到 0
刚刚说到FaaS
或FC的函数是无状态的,那我们需要状态共享的时候,应该怎么做?
借助于BaaS
: 后端即服务。
“自定义域名”中,我们可以将编写的函数与备案好的域名绑定在一起,这样,可以通过自定义的域名访问我们的“HTTP函数”。
应用示例
Nuxt.js
应用的迁移
- 迁移应用需要使用
Funcraft
命令行工具npm i -g @alicloud/fun
全局安装; -
fun --version
查看版本信息验证是否安装成功; - 这里我下载了一个已有的项目,进入项目目录下,确保
node
版本在12.*
以上,npm i
安装开发依赖; -
npm run dev
保证我们的项目本地正常运行; -
npm run build
编译项目; -
npm run start
保证编译后的项目能够正常启动; -
fun deploy -y
部署项目至函数计算;current folder is not a fun project. Generating /Users/****/Desktop/case/migc-open-act-master/bootstrap... Generating template.yml... Generate Fun project successfully!
- 自动生成
template.yml
文件ROSTemplateFormatVersion: '2015-09-01' Transform: 'Aliyun::Serverless-2018-04-03' Resources: migc-open-act-master: # service name Type: 'Aliyun::Serverless::Service' Properties: Description: This is FC service migc-open-act-master: # function name Type: 'Aliyun::Serverless::Function' Properties: Handler: index.handler Runtime: custom CodeUri: oss://fun-gen-cn-beijing-***/9c517abf18826f644880440a12eebef7 MemorySize: 1024 InstanceConcurrency: 5 Timeout: 120 Events: httpTrigger: Type: HTTP Properties: AuthType: ANONYMOUS Methods: ['GET', 'POST', 'PUT'] Domain: Type: Aliyun::Serverless::CustomDomain Properties: DomainName: Auto Protocol: HTTP RouteConfig: Routes: "/*": ServiceName: migc-open-act-master FunctionName: migc-open-act-master
- 自动生成
bootstrap
文件#!/usr/bin/env bash export PORT=9000 npx nuxt start --hostname 0.0.0.0 --port $PORT
- 自动生成一个可访问的临时域名
Detect 'DomainName:Auto' of custom domain 'Domain' Request a new temporary domain ... The assigned temporary domain is http://38880398-******.test.functioncompute.com,expired at 2021-01-04 15:13:18, limited by 1000 per day. Waiting for custom domain Domain to be deployed...
- 自动生成
这两个文件是做什么的呢?
带着疑问,我们看Custom Runtime
Custom Runtime
刚刚我们迁移了Nuxt.js
应用,如果想迁移其它应用呢?
迁移应用之前,必须要了解一个前提:要在平台支持的开发环境基础上迁移项目。
-
Custom Runtime
就是在平台的基础上,自定义运行环境。 -
Custom Runtime
的本质是HTTP Server
。
那如何创建Custom Runtime
?
- 搭建一个监听
9000
固定端口的HTTP Server
// 部署静态页面为例 var Koa = require('koa'); var path = require('path'); var htmlRender = require('koa-html-render'); var app = new Koa(); var port = 9000; app.use(htmlRender()); app.use(async (ctx) => { await ctx.html(path.resolve(__dirname, ctx.path)); }) app.listen(process.env.PORT || port, () => { console.log(`----koa is running on ${process.env.PORT || port}=====`) })
- 将启动Server的命令保存在一个名为
bootstrap
的文件// 创建bootstrap文件 #!/usr/bin/env bash export PORT=9000 node app.js
-
fun deploy -y
将项目部署到函数计算上 - 可以通过临时链接访问该静态项目
由此,我们可以看到bootstrap
文件是HTTP Server
的启动文件。
template.yml
对应我们服务列表、函数列表的配置项。
Koa
应用的迁移
上述例子,是静态页面的迁移,也可以看作是Koa
应用的迁移。
连接MongoDB
示例
这里,开通了阿里云MongoDB
的服务,代码示例链接数据库,将testColl
文档数据导出。
这个示例需要注意依赖版本require('mongodb')
,mongodb
的版本需要是2.2.*
。
var uuid = require('node-uuid');
var sprintf = require("sprintf-js").sprintf;
var mongoClient = require('mongodb').MongoClient;
var host = "dds-****-pub.mongodb.rds.aliyuncs.com";
var port = 3717;
var username = "user***";
var password = "***";
var demoDb = "sls";
var demoColl = "testColl";
// 官方建议使用的方案
var url = sprintf("mongodb://%s:%d/%s", host, port, demoDb);
console.info("url:", url);
var conn;
exports.initializer = async function (context, callback) {
// 获取mongoClient
await mongoClient.connect(url, function(err, db) {
if(err) {
console.error("connect err:", err);
return 1;
}
// 授权. 这里的username基于admin数据库授权
var adminDb = db.admin();
adminDb.authenticate(username, password, function(err, result) {
if(err) {
console.error("authenticate err:", err);
return 1;
}
conn = db;
// 取得Collecton句柄
conn.db(demoDb)
callback(null, '')
});
});
}
exports.handler = function (event, context, callback) {
var collection = conn.collection(demoColl);
collection.find({}).toArray(function(err, docs) {
console.log("Found the following records");
console.log(docs)
callback(null, docs);
});
}
总结
应用场景
- 长尾应用
- 大规模批处理任务
- 弹性伸缩
- 基于事件驱动架构的应用
- 事件驱动
- 运维自动化
- 触发器
局限
- 用户对底层计算资源没有可控性
- 由于目前技术的成熟度,
Serverless
领域尚没有形成行业标准,意味着用户将一个平台上的Serverless
应用移植到另一个平台时付出的成本较高
前端学习Serverless
的出发点
- 打破潜意识技术边界
- 调优行业内的开发岗位分层结构
- Serverless补足了前端工程师的现有能力,前端与Serverless结合,是对前端的诉求从页面开发向开发交付整个应用转变
- 享受云服务红利
- 零运维
- Node.js + Serverless,向全栈进发
- 云开发者的切入点
- 熟悉云开发模式与思想
- 文件:
boostrap
、template.yml
分别是什么 - 调试:查看请求参数的结构
- 示例:几分钟部署上线
- 概念:HTTP触发器