自建faas利器之安全运行环境

2,173 阅读4分钟

书接上文

上次谈到了如何搭建自己的faas?,被同行评论有些标题党,这篇文章将接着上文,来介绍一些干货。同时在写这篇文章的时候,核心功能vmbox已经开源,欢迎大家点赞fork。

faas是云厂商提出的一种函数即服务的程序部署模式,以函数为核心,实现以函数粒度的服务伸缩,这项技术非常复杂,有很多的技术资料,大家可以查看云厂商的文档。我们这里聊到的是比较简单的实现方式,以传统服务为基础,借用nodejs的沙盒能力,实现安全执行用户的函数,达到函数即服务的目的

起因

相信大家项目中都会遇到多个项目使用同一个函数,该函数具有规则复杂逻辑独立等特点的纯函数,前端往往是发布一个npm包,直接引入,粗暴点的直接复制到另一个项目中。假设校验身份证件号的真伪等函数,有时后端也需要用,由于语言的差异,直接复用不太可能,将这类函数抽象成为单独的函数的想法便应运而生,决定尝试最前沿的编程思想(前端的世界就是疯狂试探),自建faas工程就这样开始了。

目标

将js函数转化为API服务,即实现函数即服务

安全执行环境vmbox

实现上述目标的核心难题是js函数的安全执行环境,上一期已经谈到node的沙盒存在的问题,这里不再赘述。具体的解决方案,已经开源,大家可以点击查看vmbox。对源码感兴趣的同学可以对照上篇文章中的流程图查看代码,欢迎贡献。

回归正题,vmbox是经过真实项目检验的node沙盒库,具有六大特点

  • 死循环强制退出(一旦执行超过指定时间,可能存在死循环,kill子进程,释放资源)
  • 跨进程函数调用(使用IPC跨进程调用函数)
  • 函数互相调用(借助context实现函数间调用)
  • 内部任务队列(子进程忙碌状态,任务脚本进入队列等待)
  • 进程自治(杀死自启动)
  • 返回promise(编程体验上只需要作为一个异步任务即可)

vmbox的实例只有一个run方法,返回值是一个promise,接收三个参数

参数名 类型 是否选填 默认值 简介
code string 必填 - 运行的js代码
context object 选填 {} 函数运行上下文
stack boolean 选填 false 函数内调用其他函数,记录函数调用栈

如果代码运行出错,会使用Promise.reject(error)抛出异常,需要对异常进行捕获

基本用法

const VMBox = require('vmbox');
const vmBox = new VMBox({
  timeout: 100,
  asyncTimeout: 500
});

const context = {
  sum(a, b){
    return a + b;
  }
}

const fn = `sum(2, 3)`

vmBox.run(fn).then(console.log)
// 打印5

高级用法

借助函数运行上下文,可以做很多事情,下面实现了一个从函数内部调用其他函数的方法。

const VMBox = require('vmbox');
const vmBox = new VMBox({
  timeout: 100,
  asyncTimeout: 500
});

const fnGroup = {
  sum: `async function main({params, fn}){
    const {a, b} = params;
    return a + b
  }`,
  caller: `async function main({params, fn}){
    return await fn.call('sum', params);
  }`
};

async function run(code, context, stack = false) {
  const runCode = code + `;\n(async () => { return await main({params, fn}); })()`
  return vmBox.run(runCode, context, stack);
}

const fn = {
  call: (name, params) => {
    const code = fnGroup[name];
    if (code) {
      return run(code, { params, fn }, true);
    } else {
      return null;
    }
  }
}

const context = {
  fn,
  params: {
    a: 10,
    b: 20
  }
}

const code = fnGroup.caller;
try {
  const res = await run(code, context);
  console.log(res); // 打印30
} catch (error) {
  console.log(error);
}

关注点

  • vmbox能够解决vm2异步死循环的问题,但是进程启动的成本比较高,因此在使用过程中尽量避免进程的重启,提高运行效率。
  • context是函数的上下文,可以注入任何想提供给函数的功能,扩展函数的能力,比如:数据库访问能力、http能力等。
  • 关注代码的同学会注意到,为什么不采用多进程模型而采用单进程?这里有两个理由,一是node服务运行环境一般单核,单进程实用性更强,如果需要多进程,可以采用node的cluster模块封装实现。二是需要实现函数间相互调用,如高级用法,多进程调用中存在死循环。