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

导语

在万物皆可云的时代,你的应用甚至不需要服务器。云函数功能在各大云服务中均有提供,那么,如何用「无所不能」的 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 世界里,执行一个字符串类型的函数体,有以下这么一些途径:

1.eval 函数

2. new Function

3.vm 模块

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

1. 从数据库获取函数 

2. 构造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);

3. 执行函数得到结果

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('\n')}	
    }`;	

	
    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、性能

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

1. 获取函数体

2. 编译代码

3. 构造作用域和独立环境

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(无服务器)架构?[2]

业界的 Serverless

  • 腾讯云 - 无服务云函数

  • 阿里云 - 函数计算

  • AWS - Lambda

  • Azure - Azure Functions

References

[1] 官方文档: https://nodejs.org/dist/latest-v12.x/docs/api/vm.html

[2] 什么是 Serverless(无服务器)架构?: https://jimmysong.io/posts/what-is-serverless/


想了解更多?

Hello Serverless 技术沙龙「深圳站」来了!

这场沙龙将围绕腾讯云 Serverless 2.0 的运行原理、应用场景,腾讯云云函数的架构设计、冷启动优化、本地开发调试,以及 Serverless 在乐凯撒新餐饮服务上的应用实践,从 0 到 1 介绍 Serverless 2.0,与开发者一同交流未来的无服务器形态。

活动时间:2019 年 8 月 17 日 13:00-17:30

活动地点:深圳市南山区深南大道 10000 号腾讯大厦 2F 多功能厅

640?wx_fmt=jpeg

点击文末 阅读原文 即可报名参会

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Node.js 是一种基于 Chrome V8 JavaScript 引擎的后端 JavaScript 运行时环境,它可以在服务器端运行 JavaScript 代码,用于构建高性能、可伸缩性的网络应用程序。它的出现革新了服务器端开发的方式,使得前端开发人员可以使用相同的编程语言 JavaScript 来开发服务器端应用程序。 Node.js 可以处理大量的并发连接,因为它采用了事件驱动、非阻塞 I/O 模型,使得应用程序可以在单个线程上处理大量的并发请求。同时,Node.js 的模块化体系结构使得开发人员可以轻松地构建复杂的应用程序,例如 Web 服务器、RESTful API、实时通信应用、微服务和分布式系统等。 由于 Node.js 的高效性、可伸缩性和灵活性,它在大型公司和初创企业中都得到了广泛应用,例如 Netflix、Uber、LinkedIn、PayPal 等。 ### 回答2: Node.js是一种基于Chrome V8引擎的开源、跨平台的JavaScript运行环境。它允许开发者使用JavaScript语言进行服务器端编程,处理大量并发请求,构建高性能的网络应用。 Node.js最初由Ryan Dahl于2009年创建,旨在解决传统服务器(如Apache)对于大量并发请求的性能问题。与传统服务器不同,Node.js采用了事件驱动、非阻塞I/O模型,使得在单个线程中能够处理大量的请求,提高了应用程序的吞吐量和性能。 Node.js的核心特点包括: 1. 事件驱动:通过事件机制和回调函数实现高性能的非阻塞I/O操作,避免了线程阻塞。 2. 高性能:基于Chrome V8引擎,具有快速的解析和执行JavaScript代码的能力。 3. 轻量和高效:Node.js具有较小的内存占用,可以轻松处理大量并发连接。 4. 跨平台:Node.js可以在各种操作系统上运行,例如Windows、Mac和Linux。 5. 丰富的模块库:Node.js拥有丰富的模块库,使得开发者可以方便地实现各种功能,如网络通信、文件操作等。 6. 社区活跃:Node.js有庞大的开发者社区,提供了大量的资源、工具和支持。 由于Node.js的优势,它已经被广泛应用于Web开发、服务器开发、实时通信等领域。例如,可以使用Node.js构建高性能的Web服务器、RESTful API、聊天应用、实时数据传输系统等。 总而言之,Node.js是一种快速、高性能、跨平台的JavaScript运行环境,为开发者提供了强大的工具和库,使得构建网络应用更加方便、高效。 ### 回答3: Node.js是一个开源的服务器端运行环境,可以解析和执行JavaScript代码。它基于Chrome V8引擎,构建了一个高效、轻量级的事件驱动的非阻塞I/O模型。与传统的服务器端技术相比,Node.js具有很多独特的优势。 首先,Node.js的事件驱动和非阻塞I/O模型使得它能够处理大量的并发连接,提供高性能和可扩展性。这意味着Node.js可以处理海量的请求,而不会造成阻塞或延迟。 其次,Node.js使用JavaScript作为编程语言,使得前端开发人员可以轻松地转向后端开发。这样可以实现前后端代码的统一,减少了开发人员的学习成本和代码的重复编写。 此外,Node.js的模块化设计也是其特点之一。它拥有丰富的模块库,可以轻松实现各种功能。同时,Node.js也支持第三方模块的开发和集成,加快了开发效率。 Node.js的应用场景非常广泛。它常用于构建实时的Web应用、聊天软件、网络代理等。Node.js还被广泛应用于物联网、大数据处理、人工智能等领域。 总之,Node.js是一个强大、高效、易用的服务器端运行环境,提供了一种新的开发方式,为开发人员带来了更多的便利和创造力。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值