1

前言

Serverless是近些年来流行起来的架构理念,进入市场化应该是2014年亚马逊发布FaaS Lambda
一个热词的产生,必然会有一些商家抢注商标的现象,所以,我们目前搜索Serverless,搜索结果第一页会看到名为Serverless的产品。
我们日常说的Serverless,一般是指架构理念,或基于架构理念产生的产品全类,而非指某个具体的产品。


Serverless是对运维体系的极端抽象,这里有一个名次“抽象”、两个定语“运维体系的”、“极端”。

  • Serverless是一个抽象,就说明Serverless不是指具体的某个产品。
  • "运维体系的",说明了Serverless的职能边界,是对运维体系流程的优化,当然,也对开发流程产生了一些副作用,但,主要的职能在运维方向。
  • "极端抽象",是表明基于Serverless理念输出的产品,将运维体系的复杂度内化,只预留简单的接口供外部调用。

带来的结果就是,可以让零运维经验的人,几分钟就部署一个Web应用上线,稍后我会在==示例==中演示一下。

运维发展史

ops-history
先看一下整个发展流程,经历了手动运维、自动运维、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可以分为Serverless,其中less不是指无服务器端,或者少服务器端,而是指无感知,也对应了“Serverless是对运维体系的极端抽象”这句话。

发展现状

目前大多数互联网公司都还在DevOps时代。
部分一线大厂有自己的Serverless解决方案并对外开放。如阿里云的函数计算、腾讯云的云函数。
目前Serverless架构实现并没有统一的规范,实现和提供服务的厂商强关联,如果在不同厂商之间迁移,会有很大的工作量和困难。

函数计算

以阿里云平台的函数计算来介绍一下FaaS函数即服务。
panel
我们先熟悉一下平台设计。

  • 可以通过支付宝扫码授权登陆。
  • 直接用“产品”菜单下的搜索功能搜索“函数计算”。
  • 点击“控制台”直接进入。
  • 顶部

    • 可以切换代码部署的地域
    • 如果在“服务/函数”下找不到自己已有的代码,检查一下地域是否选择正确。
  • “概览”页面

    • 可以直观的看到使用量、监控的概览,还有一些快捷入口。
    • 免费执行次数和免费资源使用量,在测试阶段可以有效的防止用超过,也很难用超过。
    • 监控的可视化图形
    • 新建函数的快捷入口
  • “服务及函数”

    • 可以创建新服务、新函数,查看已有服务和函数。
    • 点击服务列表中的某项,可以在右侧查看、编辑包含的函数列表、服务相关的配置信息。
    • 点击函数列表中的某项,可以进入函数详情,查看、配置函数的信息。
  • 自定义域名

    • 通过自定义域名访问FC函数,需要配合HTTP触发器使用
    • ==HTTP触发器==后续讲函数类型的时候会提到。

FaaS

下面我们具体看一下函数计算。
首先,我们创建一个服务、一个函数。
创建好一个服务以后,默认打开“服务配置”Tab,从该Tab页,我们可以查看服务当前的配置并进行修改。


切换到“函数列表”Tab页,点击新增函数按钮,这时会发现,函数有两类:

  • 事件函数
  • HTTP函数

这里HTTP函数,就是上边所说,有HTTP触发器的函数,可以通过网络请求触发FC函数的执行;


因为上边我们提到了HTTP触发器,那就先创建一个HTTP函数。
创建成功后,默认进入函数的“触发器”Tab页,可以看到“事件类型”是http,请求方法是GETPOST,不需要授权访问。
为了更清晰的看到触发器的配置项,我们重新创建一个触发器。
然后,切换到“代码执行”Tab页,我们可以看到示例代码。

HTTP函数示例代码:

  1. 结构:exports.handler = (req, resp, context) => {}

    • 函数调用时,执行定义的handler逻辑,参数是req、resp、context;
    • 这些参数后续==调试阶段==我们可以看一下
  2. 打印标准版的输出hello world
  3. 组装请求数据字段
  4. body数据提取并输出组装的数据

我们执行一下看看会发生什么?

  1. 打印返回的结果
  2. 打印函数执行日志
  3. 打印RequestID

    • 这是唯一存在的ID,每次执行都会改变。
    • 可以通过该ID查询日志。

在“执行”按钮处,可以配置一些参数,改变一下配置看看输出的结果。

  • POST请求
  • 路径
  • Params改变URL上的过滤参数
  • Body改变POST的请求输出,GET请求下不会出现该Tab页

而且,在修改的过程中,会发现上方的URL会发生变化。
我们可以通过Postman去请求该地址,调用FC函数,可以通过“日志查询”查看调用结果。

最后,我们看一下exports导出的函数,默认函数名为handler,这个名字能修改么?
答案是肯定的。

  • 切换到“概览”Tab页,“修改配置”,修改“函数入口”
  • 切换回“代码执行”,执行看一下结果,报错
  • exports.[fnName]修改成配置项,“保存”,再执行,成功。

看完了HTTP函数,我们返回去看一下事件函数。
返回到服务列表页面。
“新增函数” ——> “事件函数” ——> “配置部署”
配置页面:

  • 运行环境
  • 弹性实例

    • 弹性实例有免费额度
    • 性能实例没有免费额度
    • 性能实例扩容速度慢,弹性伸缩能力不及弹性实例:对比文档
  • 函数入口

    • 和“HTTP函数”一样,可以修改约定的导出函数名

点击“完成”创建函数。

“HTTP函数”跳转到“触发器”Tab,而“事件函数”直接跳转到“代码执行”Tab。
切换到“触发器”,我们可以看到,没有任何数据。


我们看一下“事件函数”的实例代码:

  1. 结构:exports.handler = (event, context, callback) => {}

    • 函数调用时,执行定义的handler逻辑,参数是event, context, callback;
    • 这些参数我们依旧在后续==调试阶段==看一下
  2. 依旧打印标准版的输出hello world
  3. 通过callback返回数据

    • callback(err, data)

      • 第一个参数是错误信息
      • 第二个参数是数据,只有在第一个参数为null时,才返回数据

代码的“执行”按钮在上边,尝试修改代码,也能看到是自动保存。

执行一下程序看看会发生什么?

  1. 打印返回的结果
  2. 打印函数执行日志
  3. 打印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数据,修改“代码执行”,在入口函数中打印eventconsole.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 idaccess keyaccess key secret
    • 可以通过阿里云官网账号一栏找到这些信息。
    • 我们会看到Visual Studio Code右侧面板多出了两个FC的Logo选项。

插件界面

  • 可以通过界面查看到远程控制台创建的服务及函数。
  • 将远程服务及函数下载到本地

下载服务
本地服务

  • A区域,我们可以看到下载到本地对应的服务、函数及触发器列表,点击列表中的某项,会跳转到template.yml文件对应的配置
  • B区域,是对列表项的操作

    • 服务:添加函数操作
    • 函数:查看源码、调试、执行操作
    • 触发器:无

接下来,我们通过代码调试先看一下编写代码时,遗留的函数参数结构的问题,然后再说依赖问题:
查看case-event函数的源码,在行号上添加断点,点击“调试”操作
debugger
即可查看对应的参数结构。

引入第三方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
&lt;script&gt;alert&lt;/script&gt;
  • 然后,我们将服务整体上传或在函数上右键单独上传,替换控制台的代码

    • 会将依赖node_modules一起上传
    • FC函数所需要的依赖必须一同打包上传,否则,会报资源查找不到。

代码上传


介绍完阿里云平台的函数计算,结合Serverless的定义思考一下,Serverless=FaaS架构,Serverless具有实时弹性扩缩容的优势,函数计算怎么实现这个优势的呢?

这和FC函数的进程模型有关:

  • 服务托管细粒化到了语言单位,即函数调用
  • 事件驱动的计算模型
  • 用完即毁型设计:函数实例准备好后,执行完函数就直接结束。

    • 无状态,不存储任何状态
    • 正因为没有任何状态,因此在并发量高的时候,我们可以对无状态节点横向扩容,而没有流量时我们可以缩容到 0

刚刚说到FaaS或FC的函数是无状态的,那我们需要状态共享的时候,应该怎么做?

借助于BaaS: 后端即服务。
BaaS包含后端服务、云厂商提供的云服务:云数据库、对象存储、消息队列等。

Serverless可以理解为运行在FaaS中的,调用BaaS的函数。


“自定义域名”中,我们可以将编写的函数与备案好的域名绑定在一起,这样,可以通过自定义的域名访问我们的“HTTP函数”。

应用示例

Nuxt.js应用的迁移

  1. 迁移应用需要使用Funcraft命令行工具npm i -g @alicloud/fun全局安装;
  2. fun --version查看版本信息验证是否安装成功;
  3. 这里我下载了一个已有的项目,进入项目目录下,确保node版本在12.*以上,npm i安装开发依赖;
  4. npm run dev保证我们的项目本地正常运行;
  5. npm run build编译项目;
  6. npm run start保证编译后的项目能够正常启动;

uri错误
因为我下载的这个项目配置的线上访问地址无法访问,所以,添加这步验证一下。

//定位migc-open-act-master/nuxt.config.js文件
switch (process.env.NODE_ENV) {
  case 'build': // 编译
    envBase = '/gcact/migc-open-act/'
    envHost = '0.0.0.0'
    envStaticUrl = '/gcact/migc-open-act'
    break
  case 'start': // 启动
    envBase = ''
    envHost = '0.0.0.0'
    envStaticUrl = '/gcact/migc-open-act'
    break
  case 'buildMice': // 编译
  
  
  
// 修改migc-open-act-master/nuxt.config.js文件
switch (process.env.NODE_ENV) {
  case 'build': // 编译
    envBase = './'
    envHost = '0.0.0.0'
    envStaticUrl = './'
    break
  case 'start': // 启动
    envBase = ''
    envHost = '0.0.0.0'
    envStaticUrl = './'
    break
  case 'buildMice': // 编译

重新执行5、6两步——现在成功访问;

  1. 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

  1. 搭建一个监听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}=====`)
    })
  2. 将启动Server的命令保存在一个名为bootstrap的文件

    // 创建bootstrap文件
    #!/usr/bin/env bash
    export PORT=9000
    node app.js
  3. fun deploy -y将项目部署到函数计算上
  4. 可以通过临时链接访问该静态项目

由此,我们可以看到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,向全栈进发
  • 云开发者的切入点

    • 熟悉云开发模式与思想

米花儿团儿
1.3k 声望75 粉丝