资讯专栏INFORMATION COLUMN

基于 Node.js 的轻量级云函数功能实现

noONE / 673人阅读

摘要:各个云函数相互独立,简单且目的单一,执行环境相互隔离。而云函数,正是架构得以实现的途径。获取所有依赖的函数依赖循环既然可以相互依赖,那必然会可能出现这种循环的依赖情况,所以需要在开发者提交云函数的时候,检测依赖循环。

导语

在万物皆可云的时代,你的应用甚至不需要服务器。云函数功能在各大云服务中均有提供,那么,如何用“无所不能”的 node.js 实现呢?

一、什么是云函数?

云函数是诞生于云服务的一个新名词,顾名思义,云函数就是在云端(即服务端)执行的函数。各个云函数相互独立,简单且目的单一,执行环境相互隔离。使用云函数时,开发者只需要关注业务代码本身,其它的诸如环境变量、计算资源等,均由云服务提供。

二、为什么需要云函数?

程序员说不想买服务器,于是便有了云服务;
程序员又说连 server 都不想写了,于是便有了云函数。

Serverless 架构

通常我们的应用,都会有一个后台程序,它负责处理各种请求和业务逻辑,一般都需要跟网络、数据库等 I/O 打交道。而所谓的无服务器架构,就是把除了业务代码外的所有事情,都交给执行环境处理,开发者不需要知道 server 怎么跑起来,数据库的 api 怎么调用——一切交给外部,在“温室”里写代码即可。

FaaS

而云函数,正是 serverless 架构得以实现的途径。我们的应用,将是一个个独立的函数组成,每一个函数里,是一个小粒度的业务逻辑单元。没有服务器,没有 server 程序,“函数即服务”(Functions as a Service)。

三、如何实现?

由于本实现是应用在一个 CLI 工具里面的,函数声明在开发者的项目文件里,因而大致过程如下:

1、函数声明与存储 声明

我们的目标是让云函数的声明和一般的 js 函数没什么两样:

module.exports = async function (ctx) {
    return "hahha"
  }
};

由于云函数的执行通常伴随着接口的调用,所以应该要能支持声明 http 方法:

module.exports = {
  method: "POST",
  handler: async function (ctx) {
    return "hahha"
  }
};
存储

由于有 method 等配置,因此编译的时候,需要把上述声明文件 require 进来,此时,handler 字段是一个 Function 类型的对象。可以调用其 toString 方法,得到字符串类型的函数体:

const f = require("./func.js");
const method = f.method;
const body = f.handler.toString();
// async function (ctx) {
//  return "hahha"
// }

有了字符串的函数体,存储就很简单了,直接存在数据库 string 类型的字段里即可。

2、函数执行 url

如果用于前端调用,每个云函数需要有一个对应的 url,以上述声明文件的文件名为云函数的唯一名称的话,可以简单将 url 设计为:

/f/:funcname
构造独立作用域(重点)

在 js 世界里,执行一个字符串类型的函数体,有以下这么一些途径:

eval 函数

new Function

vm 模块

那么要选哪一种呢?让我们回顾云函数的特点:各自独立,互不影响,运行在云端
关键是将每个云函数放在一个独立的作用域执行,并且没有访问执行环境的权限,因此,最优选择是 nodejs 的 vm 模块。关于该模块的使用,可参考官方文档。
至此,云函数的执行可以分为三步:

从数据库获取函数体

构造 context

// ctx 为 koa 的上下文对象 
const sandbox = {
    ctx: {
      params: ctx.params,
      query: ctx.query,
      body: ctx.request.body,
      userid: ctx.userid,
    },
    promise: null,
    console: console
  }
  vm.createContext(sandbox);

执行函数得到结果

const code = `func = ${funcBody}; promise = func(ctx);`;
vm.runInContext(code, sandbox);
const data = await sandbox.promise;

NPM社区的 vm2 模块针对 vm 模块的一些安全缺陷做了改进,也可用此模块,思路大抵相同。

3、引用

虽然说原则上云函数应当互相独立,各不相欠,但是为了提高灵活性,我们还是决定支持函数间的相互引用,即可以在某云函数中调用另外一个云函数。

声明

很简单,加个函数名称的数组字段就好:

module.exports = {
  method: "POST",
  use: ["func1", "func2"],
  handler: async function (ctx) {
    return "hahha"
  }
};
注入

也很简单,根据依赖链把函数都找出来,全部挂载在 ctx 下就好,深度优先或者广度优先都可以。

if (func.use) {
    const funcs = {};
    const fnames = func.use;
    for (let i = 0; i < fnames.length; i++) {
        const fname = fnames[i];
        await getUsedFuncs(ctx, fname, funcs);
    }

    const funcCode = `{
        ${Object.keys(funcs).map(fname => `${fname}:${funcs[fname]}`).join("
")}
    }`;

    code = `ctx.methods=${funcCode};${code}`;
} else {
    code = `ctx.methods={};${code}`;
}

// 获取所有依赖的函数
const getUsedFuncs = async (ctx, funcName, methods) => {
    const func = getFunc(funcName);
    methods[funcName] = func.body;
    if (func.use) {
        const uses = func.use.split(",");
        for (let i = 0; i < uses.length; i++) {
            await getUsedFuncs(ctx,uses[i], methods);
        }
    }
}
依赖循环

既然可以相互依赖,那必然会可能出现 a→b→c→a 这种循环的依赖情况,所以需要在开发者提交云函数的时候,检测依赖循环。
检测的思路也很简单,在遍历依赖链的过程中,每一个多带带的链条都记录下来,如果发现当前遍历到的函数在链条里出现过,则发生循环。

const funcMap = {};
flist.forEach((f) => {
    funcMap[f.name] = f;
});

const chain = [];
flist.forEach((f) => {
    getUseChain(f, chain);
});

function getUseChain(f, chain) {
    if (chain.includes(f.name)) {
        throw new Error(`函数发生循环依赖:${[...chain, f.name].join("→")}`);
    } else {
        f.use.forEach((fname) => {
            getUseChain(funcMap[fname], [...chain, f.name]);
        });
    }
}
4、性能

上述方案中,每次云函数执行的时候,都需要进行一下几步:

获取函数体

编译代码

构造作用域和独立环境

执行

步骤3,因为每次执行的参数都不一样,也会有不同请求并发执行同一个函数的情况,所以作用域 ctx 无法复用;步骤4是必须的,那么可优化点就剩下了1和2。

代码缓存

vm 模块提供了代码编译和执行分开处理的接口,因此每次获取到函数体字符串之后,先编译成 Script 对象:

// ...get code
const script = new vm.Script(code);

执行的时候可以直接传入编译好的 Script 对象:

// ...get sandbox
vm.createContext(sandbox);
script.runInContext(sandbox);
const data = await sandbox.promise;
函数体缓存

简单的缓存,不需要很复杂的更新机制,定一个时间阈值,超过后拉取新的函数体并编译得到 Script 对象,然后缓存起来即可:

const cacheFuncs = {};
// ...get script
cacheFuncs[funcName] = {
    updateTime: Date.now(),
    script,
};

// cache time: 60 sec
const cacheFunc = cacheFuncs[cacheKey];

if (cacheFunc && (Date.now() - cacheFunc.updateTime) <= 60000) {
    const sandbox = { /*...*/ }
    vm.createContext(sandbox);
    cacheFunc.script.runInContext(sandbox);
    const data = await saandbox.promise;
    return data;
} else {
    // renew cache
}
四、参考资料 相关文章

什么是Serverless(无服务器)架构?

业界的 serverless

腾讯云 - 无服务云函数
阿里云 - 函数计算
AWS - Lambda
Azure - Azure Functions

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/105385.html

相关文章

  • 第三方库

    摘要:微信支付,支付宝支付,银联支付三大支付总结支付宝植入总结支付宝的植基于和百度地图的组件库基于百度地图封装的组件库,使用这个库最好需要先了解和百度地图。 Commento - 多说 & Disqus 开源替代品 Commento - 多说 & Disqus 开源替代品 anime.js 简单入门教程 强大轻量的动画库 anime.js 入门教程 来自B站的开源的MagicaSakura源...

    seanHai 评论0 收藏0
  • 第三方库

    摘要:微信支付,支付宝支付,银联支付三大支付总结支付宝植入总结支付宝的植基于和百度地图的组件库基于百度地图封装的组件库,使用这个库最好需要先了解和百度地图。 Commento - 多说 & Disqus 开源替代品 Commento - 多说 & Disqus 开源替代品 anime.js 简单入门教程 强大轻量的动画库 anime.js 入门教程 来自B站的开源的MagicaSakura源...

    gityuan 评论0 收藏0
  • 什么是Node.js

    Node.js从2009年诞生至今,已经发展了两年有余,其成长的速度有目共睹。从在github的访问量超过Rails,到去年底Node.jsS创始人Ryan Dalh加盟Joyent获得企业资助,再到今年发布Windows移植版本,Node.js的前景获得了技术社区的肯定。InfoQ一直在关注Node.js的发展,在今年的两次Qcon大会(北京站和杭州站)都有专门的讲座。为了更好地促进Node.j...

    CrazyCodes 评论0 收藏0
  • 2015 年度小结(技术方面)

    摘要:因为路由层面受业务影响很大,经常修改一些功能的行为,所以后来大部分测试都是针对层面的单元测试。在我了解的过程中,我发现中文网络上对的讨论非常分散,于是我创建了中文社区,到年末已经有个注册用户和个帖子了。 https://jysperm.me/2016/02/programming-of-2015/ 从 2014 年末开始开发的一个互联网金融项目终于在今年三月份上线了,这是一个 Node...

    宋华 评论0 收藏0

发表评论

0条评论

noONE

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<