本资源由 itjc8.com 收集整理

这一讲我将带你了解如何自定义函数运行时。

我们知道 Serverless 应用的函数代码是在 FaaS 中运行的,到目前为止,你也只能选择 FaaS 平台支持的编程语言开发应用,而 FaaS 平台支持的编程语言不但有限,还只支持极少数的特定版本,比如函数计算只支持 Node.js 12、Node.js 8、Java、Python 等 。这样一来,当你想用 FaaS 平台不支持的编程语言(比如 TypeScrip、Golang、Ruby )或各种编程语言的小版本(比如最新的 Node.js)时,该怎么办呢?这就需要使用自定义运行时了。

主流的 FaaS 平台都支持自定义运行时,实现原理也都大致相同。为了让你弄清楚自定义运行时的原理,并学会开发一个自定义运行时,这一讲我将分为原理和实现两部分。

  • 自定义运行时的原理: 首先我会带你了解一个通用的 FaaS 自定义运行时原理,这样你使用任何 FaaS 平台都可以触类旁通了。

  • 自定义运行时的实现: 我会带你分别实现一个 TypeScript 运行时,和 Golang 的运行时,从易到难让你彻底掌握自定义运行时的实现。

话不多说,我们进行今天的学习。

自定义运行时的原理

运行时(Runtime)是程序运行时所依赖的环境(环境包括任何库、框架或平台)。FaaS 中的运行时,就是你创建函数时指定的运行环境,比如函数计算的 Node.js 运行时,就包括 Node.js 运行环境以及一些内置的模块,如 ali-oss、tablestore,此外还有 Java 运行时、Python 运行时等。那自定义运行时就是你可以在 FaaS 自定义一个运行环境,如 TypeScript,然后你就可以使用 TypeScript 来编写代码并部署到 FaaS 平台上运行。

在了解自定义运行时的原理前,咱们先来回顾 FaaS 的运行原理(我在“04 | 运行原理:Serverless 应用是怎么运行的?”讲中提到过)。在 FaaS 中,运行时被预先定义,比如在创建函数时可以指定 runtime:nodejs12 ,接下来,用户通过触发器驱动函数执行后,FaaS 就会以 Node.js 12 作为运行时来创建函数实例,函数代码也就在 Node.js 12 这个运行环境中执行。

07|运行时:使用自定义运行时支持自定义编程语言 - 图1

FaaS 运行原理

那怎么才能让函数在自己定义的运行环境中执行呢? 这就涉及“06 | 依赖管理:Serverless 应用怎么安装依赖?”的内容了。在这一讲中我讲了:安装依赖的本质就是要把函数运行所需要的依赖都打包上传到 FaaS 中,这些依赖既可以是代码依赖包,还可以是系统依赖库。那你能不能把函数的运行时也打包上传到 FaaS 中,让 FaaS 利用你上传的运行时来执行你的代码呢?

当然可以,并且 FaaS 平台的自定义运行时也是这样实现的。

比如你可以用 TypeScript 编写代码,然后将代码和 TypeScript 运行时都上传到 FaaS 中,然后通过特定的配置,让 FaaS 通过自定义的 TypeScript 运行时来运行你的代码。比如通过 runtime:custom配置告诉 FaaS 你使用的时自定义运行时,然后用 bootstrap: ts-node index.ts配置来告诉 FaaS 函数启动时,使用 ts-node 来运行 index.ts。

这样一来,我们就解决了使用自定义运行时运行自定义编程语言的代码问题。但还存在一个问题:FaaS 平台在运行函数时会有很多参数(比如事件信息、函数上下文等),这些参数怎么传递给自定义运行时呢?这本质上是远程数据通信问题,最简单的就是 HTTP 协议来实现:在自定义运行时中实现一个 HTTP 服务,FaaS 平台通过 HTTP 请求把数据传递给自定义运行时。

讲到这儿,一个自定义运行时的原理图就可以总结出来了:

07|运行时:使用自定义运行时支持自定义编程语言 - 图2

自定义运行时原理

简单来讲,自定义运行时就是一个使用自定义编程语言实现的 HTTP 服务。然后你需要为你的 HTTP 服务指定一个启动命令,通用的做法是将启动命令保存在名为 bootstrap 的文件中。bootstrap 文件示例:

!/bin/bash```

./node_modules/ts-node/dist/bin.js server.ts

  1. FaaS 平台在创建函数实例时,会执行 bootstrap 文件启动 HTTP 服务,然后将所有请求及参数都转发到你的 HTTP 服务中,由 HTTP 服务处理所有请求。<br />
  2. 讲完自定义运行时的原理之后,我们接着来实现一个自定义运行时。
  3. <a name="d75306c0"></a>
  4. ### 自定义运行时的实现
  5. 我会从易到难,先讲解如何实现一个 TypeScript 运行时,然后再介绍如何实现 Golang 运行时,这是两个很典型的例子,并且你掌握了如何自定义 Golang 运行时之后,就可以轻松自定义其他编程语言的运行时了。为了方便你进行实践,我选择了用得比较多的函数计算进行演示,同时我也为你提供了[示例程序](https://github.com/nodejh/serverless-class/tree/master/07),你可以直接下载使用。
  6. <a name="e75e59e4"></a>
  7. #### 实现一个 TypeScript 运行时
  8. TypeScript JS 代码增加了类型系统,可以大大提升代码的可读性和可维护性。然而现在大多数 FaaS 平台都不直接支持 TypeScript,要想使用 TypeScript 编写 Serverless 应用,通常需要把代码编译为 JavaScript 再运行。显然这没有直接使用部署并执行 TypeScript 代码高效。我们如果想要直接运行 TypeScript 代码,可以通过 ts-node 来实现。所以你可以基于 ts-node 实现一个 TypeScript 运行时,这样就可以直接使用 TypeScript 编写 Serverless 应用了。
  9. 那怎么实现呢?
  10. 首先在本地创建一个 TypeScript 项目,然后安装必要的依赖,为了将依赖都上传到 FaaS,我们需要将 ts-node 等相关依赖(ts-nodetypescript @types/node)都安装在项目的 node_modules 中,如下所示:
  11. $ npm i -S ts-node$ npm i -S typescript$ npm i -D @types/node
  12. 前面我们已经学习了自定义运行时需要实现一个 HTTP 服务来接收 FaaS 平台的请求,所以接下来我们就使用 TypeScript 编写一个 HTTP 服务:
  13. importasfrom'http'/**
  14. * 你可以在这里实现具体的业务逻辑
  15. */function sayHello(name: string): string return`Hello, ${name}`// 创建一个 HTTP 服务constfunction (req: http.IncomingMessage, res: http.ServerResponse): void // 获取 RequestIdconst"x-fc-request-id"console`FC Invoke Start RequestId: ${requestId}`// 拼接请求参数let""'data'function (chunk) 'end'function ()

// 处理业务逻辑,比如这里是输出欢迎语 const body = sayHello(rawData); // 设置 HTTP 响应 res.writeHead(200); res.end(body); console.log(FC Invoke End RequestId: <span class="hljs-subst">${requestId}</span>);

  1. });<br />
  2. });<br />
  3. server.timeout = 0;<br />
  4. server.keepAliveTimeout = 0;<br />
  5. // 启动 HTTP 服务并监听 9000 端口<br />
  6. server.listen(9000, '0.0.0.0', function () {<br />
  7. console.log('FunctionCompute typescript runtime initialized.');<br />
  8. });<br />
  • http ;

{ ; }

server = http.createServer({

requestId = req.headers[]; .log();

rawData = ; req.on(, { rawData += chunk; }); req.on(, {

  1. 这段代码启动了一个 HTTP 服务,监听 0.0.0.0:9000 端口(这也是函数计算要求的)。然后我们可以先在本地测试,通过安装在项目中的 ts-node 命令来运行上述代码:
  2. # 启动 HTTP 服务$ ./node_modules/ts-node/dist/bin.js server.ts
  3. 然后在另一个终端中使用 curl 命令进行测试:
  4. $ curl 0.0.0.0:9000 -X POST -d "Serverless" -H "x-fc-request-id:abcde"

Hello, Serverless

  1. HTTP 服务测试正常后,我们的自定义运行时就完成了。你可以在接收 HTTP 请求后处理业务逻辑,然后将处理结果再以 HTTP 响应返回给 FaaS 平台。
  2. 当然了,在将自定义运行时部署到 FaaS 之前还需要创建一个名为 bootstrap 的文件,在文件中添加启动命令,这样 FaaS 才知道如何启动你的自定义运行时,如下所示:

!/bin/bash

./node_modules/ts-node/dist/bin.js server.ts

  1. 接下来我们还需要添加函数计算的 template.yaml 配置,定义函数信息:
  2. ROSTemplateFormatVersion:'2015-09-01'Transform:'Aliyun::Serverless-2018-04-03'Resources:custom-typescript-demo:Type:'Aliyun::Serverless::Service'Properties:Description:'helloworld'typescript-demo:Type:'Aliyun::Serverless::Function'Properties:Runtime:customMemorySize:512Handler:index.handlerCodeUri:'./'
  3. 你要注意一下,在这份 YAML 配置中,Runtime 的值必须为 customHandler 属性在这里没有实际意义但是必须填写。
  4. 接下来就可以使用 fun deploy 将自定义运行时部署到函数计算了。部署后可以使用 fun invoke 进行测试:
  5. $ fun deploy -y

… Waiting for service custom-typescript-demo to be deployed… Waiting for function typescript-demo to be deployed… Waiting for packaging function typescript-demo code… The function typescript-demo has been packaged. A total of 363 files were compressed and the final size was 10.4 MB function typescript-demo deploy success service custom-typescript-demo deploy success

  1. $ fun invoke -e "Serverless"

… FC Invoke Result: Hello Serverless

  1. 这时函数计算就是使用自定义的 TypeScript 运行环境直接运行我们的 TypeScript 代码。
  2. TypeScript 的运行时比较简单,因为 ts-node 可以直接安装在 node_modules 中,那么其他无法直接安装在依赖目录中的编程语言,比如 Golang 或最新版 Node.js 的自定义运行时应该怎么实现呢?
  3. <a name="ce7ae00a"></a>
  4. #### 实现一个 Golang 的运行时
  5. 如果要沿用 TypeScript 这种自定义启动命令的方案,就需要将 Golang 和代码打包,但 Golang 是直接安装在操作系统上的,依赖系统环境,好像无从下手。
  6. **但你思考一下,将运行环境和代码打包,这种思想是不是和容器技术很像?** 容器技术就是将应用和运行所依赖环境打包为镜像,这样应用可以轻松迁移、部署。那能不能把 Golang 运行环境构建为 Docker 镜像,然后让 FaaS 平台使用你的 Docker 镜像去执行代码呢?这样只要 FaaS 平台能支持自定义容器,就能实现任意编程语言的运行时了。答案是肯定的,很多 FaaS 平台(比如 Lambda 和函数计算)也都提供了自定义容器的能力。
  7. 使用容器自定义运行时,你需要先构建一个容器镜像,然后通过函数的配置告诉 FaaS 平台使用你的容器镜像。在函数执行时,FaaS 平台就会拉取容器镜像并启动容器执行代码。与前面 TypeScript 运行时一样,在自定义容器镜像中你也需要实现一个 HTTP 服务,接收 FaaS 平台的所有请求。
  8. ![](https://s0.lgstatic.com/i/image2/M01/05/14/CgpVE1_7uayAMv-5AATwsp__Ojw457.png#alt=Drawing%202.png)
  9. 自定义运行时使用流程
  10. 那我们就一起来实现一下吧!
  11. 还是以函数计算为例,首先你需要准备一个镜像仓库,用来存放你的镜像,函数计算目前只支持[容器镜像服务](https://cr.console.aliyun.com/cn-beijing/instances/repositories)中的镜像(大部分 FaaS 也都只支持自家的镜像仓库),所以你需要构建自定义运行时镜像然后上传到容器镜像服务中。你可以提前在容器服务中创建一个命名空间和镜像仓库,创建完毕后记住你的仓库地址,格式为registry.<地域>.aliyuncs.com/<命名空间>/<仓库名> ,例如 registry.cn-hangzhou.aliyuncs.com/serverless-image/nodejs15 。
  12. 接下来进入开发步骤。
  13. 首先我们使用 Golang 实现一个 HTTP 服务,代码如下:
  14. packageimport"fmt""net/http""runtime"func HelloHandler(w http.ResponseWriter, r *http.Request)"Hello Serverless! This is Golang runtime, version: %s"func main ()"/"":8080"nil

main (

) { fmt.Fprintf(w, , runtime.Version()) } { http.HandleFunc(, HelloHandler) http.ListenAndServe(, ) }

  1. 在这个 HTTP 服务中我们定义了 / 这个路由,逻辑就是返回当前 Golang 的版本。当基于容器实现自定义运行时,函数计算会将 HTTP 触发器的请求转发到 / 路由,将事件触发器的请求转发到 /invoke 路由。
  2. 然后我们也可以先在本地测试,为了简单,可以直接通过 go run main.go 的命令启动 HTTP 服务,然后使用 curl 命令测试:

启动 HTTP 服务

$ go run main.go

新开一个终端,通过 curl 命令测试

$ curl localhost:8080 Hello Serverless! This is Golang runtime, version: go1.13.5

  1. (这里返回的 golang 版本是 1.3.15,这是因为我的电脑上安装的是该版本)。
  2. 接下来我们需要编写一个 Dockerfile,用来构建包含 Golang 运行时及代码的镜像:

Dockerfile

FROM golang:1.15.6-alpine3.12 WORKDIR /go/src/app

将代码复制到工作目录

COPY . .

编译

RUN go build main.go

暴露 8080 端口

EXPOSE 8080

启动应用

ENTRYPOINT [ “./main” ]

  1. 在这个 Dockerfile 中,我们使用了 golang:1.15.6-alpine3.12 作为基础镜像,其中 alpine 是最小体积的 golang 运行环境。然后我们使用 go build 来编译代码,最后启动应用。
  2. 接下来就可以构建并上传镜像了,如果是第一次使用容器镜像服务,则需要先使用 docker login 登录。

指定镜像名称,例如 registry.cn-hangzhou.aliyuncs.com/serverless-image/golang:v0.1

$ export IMAGE_NAME=”你的镜像仓库:版本” $ docker build -t $IMAGE_NAME . $ docker push $IMAGE_NAME

  1. 镜像上传后,就可以创建一个 template.yaml 来定义函数配置了:
  2. ROSTemplateFormatVersion:'2015-09-01'Transform:'Aliyun::Serverless-2018-04-03'Resources:serverless:Type:'Aliyun::Serverless::Service'Properties:Policies:-AliyunContainerRegistryReadOnlyAccessInternetAccess:truegolang-runtime:Type:'Aliyun::Serverless::Function'Properties:Description:'Golang Runtime'Runtime:custom-containerTimeout:60CAPort:8080Handler:not-usedMemorySize:1024CodeUri:./CustomContainerConfig:Image:'registry.cn-hangzhou.aliyuncs.com/serverless-image/golang:v0.1'
  3. 这份 YAML 配置中,Runtime 值为custom-container ,表示该函数是自定义容器,然后通过 CustomContainerConfig 配置容器镜像。示例中我直接使用的 registry.cn-hangzhou.aliyuncs.com 这个 endpoint,但通常为了更快拉取镜像,一般会使用 VPC 地址,如registry-vpc.cn-beijing.aliyuncs.com/serverless-image/golang:v0.1
  4. 接下来就可以通过 fun deploy 进行部署,fun invoke 进行测试:
  5. .15.6

$ fun deploy $ fun invoke Hello Serverless! This is Golang runtime, version: go1 ```

可以看到,执行结果中 Golang 版本是 1.15.6,说明自定义运行时正常工作了。

总结

这一讲我首先为你讲解了自定义运行时的基本原理,然后以 TypeScript 运行时和 Golang 运行时为例,为你详细介绍了如何创建一个自定义运行时。关于这一讲的内容,我想要强调以下几点:

  • FaaS 平台提供了有限的编程语言及版本的支持,使用自定义运行时,你可以自定义编程语言进行开发;

  • 自定义运行时的原理是在函数中实现一个 HTTP 服务,FaaS 平台将触发器事件转发到你的 HTTP 服务;

  • 你可以通过将运行时上传到 FaaS,在 bootstrap 中定义启动命令来实现自定义运行时,比如 TypeScript;

  • 你也可以通过自定义容器镜像来实现任意编程语言的自定义运行时。

自定义运行时是 Serverless 应用开中非常重要的一个功能, 它可以让你突破 FaaS 平台运行环境的限制,可以让你使用 FaaS 平台不支持的编程语言进行开发。你应该也能发现,基于容器实现自定义运行时你可以很方便地安装依赖,因为依赖都打包到了镜像中。除此之外,基于自定义运行时,你还可以平滑地将原有系统或传统应用平滑迁移到 Serverless 架构。

07|运行时:使用自定义运行时支持自定义编程语言 - 图3

最后我留给你的作业就是:根据我所讲的内容,举一反三地实现一个最新版 Node.js 运行时。我们下一讲见。

本讲的代码地址:https://github.com/nodejh/serverless-class/tree/master/07