基于Node的FaaS简易实现

基于Node的FaaS简易实现

Tags
NodeJS
开发
Published
Published June 23, 2022
Property

概述

 
什么是FaaS?通常定义为函数即服务,本质是无服务(ServerLess)架构中的核心功能,即无需频繁构建及启动服务等基础设施,在初始化配置时由云服务提供容器,开发者只关注业务函数本身。
为什么要使用FaaS服务,现有的应用部署满足不了业务场景吗?
首先通过一张图例可以看出各服务之间的差异性,在普通应用程序中,开发者需要关注运维、服务器等一系列的服务配置,比如一个简单的接口服务缺搭建一整套环境,既浪费时间也浪费资源。在FaaS的理念中,开发者关注点只需要聚焦在函数上,这样开发的效率会大大提高,同时也可以降低应用资源成本。
notion image
(图片来源:https://juejin.cn/post/6989896019127304229)
本文以一个简单示例来展示一个FaaS应用实现,可以通过示例代码搭建个人具服务,比如机器人消息推送、获取测试数据等简单的函数服务,提供服务给内部的工作小组使用。

实现

项目根目录fns 存放所有的函数文件,然后通过对这个目录的读取、移动等操作模拟函数的执行。
函数执行过程主要是通过 node 提供的 vm 模块在沙箱中执行代码,同时在沙箱中注入 contextrequire等参数方便自定义函数调用。
const sandbox: Record<string, any> = { setInterval, setTimeout, require: require, console: console, context: { req: this.request }, } const data = vm.runInContext(fnContent, sandbox, { timeout: TIMEOUT_MILLISECOND, })
为了防止函数执行时间长占用资源,代码层面提供一个简易的超时服务,当函数执行时间大于默认时间时,中断执行。
const data = await new Promise(async (resove, reject) => { try { timeoutId = setTimeout(() => { reject(`服务超时,最大执行时间 ${TIMEOUT_MILLISECOND}ms`) }, TIMEOUT_MILLISECOND) vm.createContext(sandbox) const data = vm.runInContext(...) if (timeoutId) { clearTimeout(timeoutId) timeoutId = null } resove(data) } catch (err) { console.log(err) reject(err) } })
然后新建自定义函数,文件夹为函数名 如:load_news 内部结构:
config.json #部署配置文件 load_news # 函数目录,文件夹为名称 ├── handler.js #执行函数 └── package.json
函数内容以bootstrap 为主函数入口
const axios = require('axios') const bootstrap = async () => { const res = await axios.get('https://photo.home.163.com/api/designer/pc/home/index/word') return res.data }
然后部署后访问 http://localhost:3000/load_news 即可查看接口数据。
示例代码参考如下,服务基于NestJS 框架,服务层代码:
import { Injectable, Inject, HttpException, HttpStatus } from '@nestjs/common' import { REQUEST } from '@nestjs/core' import { Request } from 'express' import * as vm from 'vm' import * as fs from 'fs' import * as path from 'path' const TIMEOUT_MILLISECOND = 5 * 1000 @Injectable() export class AppService { private fns: string[] private fnsDir: string = path.join(__dirname, '../fns') constructor(@Inject(REQUEST) private request: Request) { this.readFns() } findFn(fnName: string): string { return this.fns.find(fn => fn === fnName) } getFns(): string[] { return this.fns } async readFns() { const fns = await fs.readdirSync(this.fnsDir) this.fns = fns } async executeFn(fn: string): Promise<any> { if (!this.findFn(fn)) { throw new HttpException('函数不存在', HttpStatus.NOT_FOUND) } let timeoutId = null const handler = fs.readFileSync(`${this.fnsDir}/${fn}/handler.js`, 'utf-8') const fnContent = `${handler};bootstrap(context)` const sandbox: Record<string, any> = { setInterval, setTimeout, require: require, console: console, context: { req: this.request }, } const data = await new Promise(async (resove, reject) => { try { timeoutId = setTimeout(() => { reject(`服务超时,最大执行时间 ${TIMEOUT_MILLISECOND}ms`) }, TIMEOUT_MILLISECOND) vm.createContext(sandbox) const data = vm.runInContext(fnContent, sandbox, { timeout: TIMEOUT_MILLISECOND, }) if (timeoutId) { clearTimeout(timeoutId) timeoutId = null } resove(data) } catch (err) { console.log(err) reject(err) } }) return data } }
控制层代码:
import { Controller, Get, Param } from '@nestjs/common' import { AppService } from './app.service' @Controller() export class AppController { constructor(private readonly appService: AppService) {} @Get('/show_all_fns') getFns() { return this.appService.getFns() } @Get('/:fn') executeFn(@Param('fn') fnName) { return this.appService.executeFn(fnName) } }

部署

实现自动化部署需要提供配置文件及命令行来操控这些函数,可以使用 Git WebhooksDocker等推送镜像的方式部署。
这里只是简单模拟了函数的部署,内部环境的情况下可以使用 SFTP 等工具上传服务器
#!/usr/bin/env node const program = require('commander') const pkg = require('./package.json') const { deploy, remove, show, view } = require('./cli') program.version(pkg.version, '-v, --version') program.command('deploy [fn]').description('部署函数').action(deploy) program.command('remove [fn]').description('删除函数').action(remove) program.command('list').description('函数列表').action(show) ...
部署命令函数示例:
exports.deploy = fn => { try { const fns = fn ? { [fn]: config.fns[fn] } : config.fns Object.keys(fns).map(key => { //模拟上传部署动作 const destPath = dest(key) fs.copy(path.resolve(key), destPath, { overwrite: true }, () => { //模拟构建 shell.cd(destPath) shell.exec(`npm install`) console.log(`${key} 部署成功`) }) }) } catch (error) { console.log('部署失败->', error.message) } }
以上只是提供一个大概的思路,实际应用还需要考虑很多的边界问题,可以做个小组基建的demo使用。

应用

实际 FaaS 服务要考虑的点要远远大于示例,比如以下一些基本的服务功能:
  • 函数相互引用
  • 第三方依赖
  • 应用密钥白名单
  • 代码容器隔离
  • 集群部署及更新
  • 第三方实例调用(如数据库)
  • 动态扩容
  • 安全性能问题等等
不过如果只是简单的使用不对外,只是做为工具集合等当然是越简单越好了,感兴趣的可以顺着示例的思路进一步扩展使用。
当然随着 FaaS 服务已经发展多年,现有各大云厂商也都有提供整套的 FaaS 服务。如果不想上云只是在内部使用,也可以选择在开源方案中比较出名的 OpenFaaS 等框架,依靠这些优秀的开源框架可以快速搭建自有 FaaS 平台加以提效。

OpenFaaS简介

OpenFaaS 开源的faas框架,使用Kubernetes、Docker等集成部署,架构如下:
notion image
安装示例:
# 拉取官方镜像 git clone https://github.com/openfaas/faas-netes # 创建namespace cd faas-netes && kubectl apply -f namespaces.yml # 设置账号密码 kubectl -n openfaas create secret generic basic-auth \ --from-literal=basic-auth-user=admin \ --from-literal=basic-auth-password=admin # 创建应用 kubectl apply -f ./yaml/ # 查看pod状态 kubectl get pods -n openfaas # 查看服务 kubectl get service -n openfaas
然后本地访问:http://127.0.0.1:31112/ 查看可视化页面
开发函数:
curl -sL https://cli.openfaas.com | sh # 安装 faas-cli faas-cl new fns -lang node14 # 创建函数 faas-cli build -f hellofaas.yml # 构建 faas-cli push -f hellofaas.yml # 上传docker faas-cli deploy -f hellofaas.yml # 部署函数 curl http://127.0.0.1:31112/function/hellofaas #访问函数
步骤也是相当简洁,这里就不做详细介绍了,感兴趣的可以去官网看看~