跳到主要内容

深入理解云函数

云函数中,Node.js 的运行机制和本地 Node.js 运行的行为有些差异。

启动时间#

云函数存在两种启动:

  • 冷启动:需要平台分配计算资源、加载代码、启动 Node.js 进程,耗时较长
  • 热启动:函数实例、执行进程都被复用(即下文提到的「实例复用」),耗时很短

云函数如果在一定时间内(几十分钟)没有被调用,那么平台会收回分配的计算资源,直到函数被调用前,再分配计算资源,这种情况下,会发生冷启动。

如果对云函数发起连续的请求,已经冷启动完毕的实例会得到复用,可以在很短时间投入计算,此时即热启动。

提示

CloudBase 会根据您云函数长期的访问情况,自动调度、调配实例的数量,保证足够好的性能的同时,节省您的资源。

实例复用#

考虑下面这个云函数:

let i = 0;exports.main = async (event = {}) => {  i++;  console.log(i);  return i;};

在第一次调用该云函数的时候函数返回值为 1,这是符合预期的。

但如果连续调用这个云函数,其返回值有可能是从 2 递增,也有可能变成 1,这便是实例复用的结果:

  • 热启动时,执行函数的 Node.js 进程被复用,进程的上下文也得到了保留,所以变量 i 自增
  • 冷启动时,Node.js 进程是全新的, 代码会从头完整的执行一遍,此时返回 1

所以,开发者在编写云函数时,应注意保证云函数是无状态的、幂等的,即当次云函数的执行不依赖上一次云函数执行过程中在运行环境中残留的信息。

时区#

云函数中的时区默认是 UTC+0,在函数中获取本地时间会和北京时间有 8 个小时的差异。

定义环境变量 TZ 可以改变函数运行时的时区,例如设置 TZAsia/Shanghai 可以指定函数的时区为北京时间。

更多关于时区的信息,可以参考:Time Zone Database

提示

在服务端处理时间(包括云数据库和云函数)应尽量避免使用受到时区影响的本地时间,而是使用 Unix 时间戳这样的绝对值,这样可以避免服务端和客户端时区差异带来的众多问题。

Node.js 8 的异步行为#

考虑下面这段代码:

exports.main = async (event = {}, context) => {  setTimeout(() => {    console.log("rid: ", context.request_id);  }, 0);  return "ok";};

在本地调用时,函数返回 “ok”,并且随后可以看到 requestId 的打印。

但如果在 Node.js 8 的云函数中运行此代码,函数依然返回 “ok”,但是 setTimeout 中异步函数打印的内容却不会在这次调用日志里看到,例如:

screenshot0

如果继续调用第二次,您可能会在调用日志里看到上次 setTimeout 中打印

screenshot1

这一点是很多开发者感到困惑的地方。

对于异步函数,主流程(例如示例中的 await main(event, context))执行完成后,函数实例进程会被冻结,进程中的所有异步任务会暂停执行,直到这个进程被再次唤起。

另一方面,如果函数实例进程由于某些原因没被复用(例如更新了函数代码),这个异步流程中的代码就永远不会被执行

提示

开发者不应将关键代码放入 Node.js 8 云函数的异步流程中。 如果想让关键代码稳定运行,请将其放到函数主流程中,或者使用 Node.js 10 及以上版本的云函数

示例#

Node.js 8 云函数的异步特性可能会带来某些预期之外的行为,例如:

小程序 wx-server-sdk 提供了 getWXContext 方法来获取函数的一些上下文信息(appId,openId,unionId)来方便开发者,该方法本质是从环境变量中读取若干参数。

如果在异步流程中使用该方法获取 openIdunionId,可能会产生身份漂移的异常情况。

考虑这样的一段代码:

const cloud = require("wx-server-sdk");
exports.main = async (event) => {  console.log("openid a: ", cloud.getWXContext().OPENID);  setTimeout(() => {    const { OPENID } = cloud.getWXContext();    console.log("openid b: ", OPENID);  }, 0);};

假如两个不同的用户通过小程序访问该函数,用户 A 的 openid 为 1111,用户 B 的 openid 为 2222。

他们两次调用的日志将分别为:

screenshot2

screenshot3

可以看到,第一次调用打印出了用户 A 的 openid,这是符合预期的,但异步流程的逻辑当次调用并没有执行

第二次调用时,函数实例进程得到复用,第一次调用产生的异步逻辑继续运行,打印出了用户 B 的 openid,而这次调用其实是属于用户 A 调用函数的异步流程