前面,我们在应用中创建了一个云函数,这个云函数的名字起得也很随意就用的它默认的node-app
。当然了我们也在云函数中对Express进行了整合,配合了云数据库去实现增删改查,但是在这过程中并强调整个业务逻辑的安全性、健壮性,只是一个简单的demo。这种方式不是serverless的最佳范式,因为在我们的代码当中,将整个后端的业务能力都写进了一个云函数里,所有的增删改查都放在了node-app
这个云函数中,这样做的好处是方便管理,但是一个应用下只有一个云函数,它的能力实际上是有效的,因为在我们的云平台上,单个云函数的并发是有限制的,并行的函数实例个数是由云厂商决定的,而超过限制之后,事件队列就需要等待其他函数实例执行完毕后,再生成新的函数实例。不是说serverless是弹性伸缩的吗?不是说会根据业务处理的需求会自动调配资源吗?那为什么现在使用的云函数有并发限制呢?这就涉及到了Faas的运行机制。
在Faas平台中,函数默认是不运行的,也不会分配任何的资源,甚至Faas中都不会保存我们的函数代码,只有当Faas接受到触发器的事件后,才会启动并运行函数。前面我们就是用http的触发器来执行函数代码的。整个函数的运行实际上是分为四个阶段的:
首先是代码下载,Faas平台本身是不会存储代码的,而是将函数放在对象存储中,需要执行函数的时候再从对象存储中将函数代码下载并且解压,因此Faas平台一般会对代码大小有限制,通常不会超过50M。
再接下来就是启动容器了,函数代码下载完成后Faas会根据函数的配置启动对应的容器,Faas会使用容器进行资源隔离。
紧接着就是初始化运行环境,分析代码的依赖,执行用户初始化的逻辑。
最后一步,才是代码的运行。
当函数第一次运行的时候会完整的经过这四个阶段,前三个阶段称作“冷启动”阶段,最后一步则是“热启动”。整个冷启动耗时可以达到上百毫秒的级别。
函数运行完毕后,运行环境会保留一段时间,这个时间在几分钟到十几分钟不等,具体和云厂商如何设置相关,如果这段时间内函数需要再次运行,那么Faas平台就会使用上一次的运行环境。这就是所谓的“执行上下文重用”,也叫做“实例复用”。函数的这个启动过程也叫“热启动”。“热启动” 的耗时就完全是启动函数的耗时了。当一段时间内没有请求时,函数运行环境就会被释放,直到下一次事件到来,再重新从冷启动开始初始化。
考虑下面这个云函数:
在第一次调用该云函数的时候,函数返回值为 1,这是符合预期的。
但如果连续调用这个云函数,其返回值有可能是从 2 递增,也有可能变成 1,这便是实例复用的结果:
- 当热启动时,执行函数的 Node.js 进程被复用,进程的上下文也得到了保留,所以变量 i 自增。
- 当冷启动时,Node.js 进程是全新的, 代码会从头完整的执行一遍,此时返回 1。
下面是一个函数的请求示意图,其中 “请求1” “请求3” 是冷启动,“请求2” 是热启动。
函数执行完毕后销毁运行环境,虽然对首次函数执行的性能有损耗,但极大提高了资源利用效率,只有需要执行代码的时候才初始化环境、消耗硬件资源。并且如果你的应用请求量比较大,则大部分时候函数的执行可能都是热启动。
从函数运行的生命周期中你可以发现,如果函数每分钟都执行,则函数几乎都是热启动的,也就是会重复使用之前的执行上下文。执行上下文就包括函数的容器环境、入口函数之外的代码。
云平台会根据当前负载情况,自动控制云函数实例的数量,并且均衡地分发请求。连续的多次请求有可能由同一实例处理,也可能不是。这就是我们在上面的代码中看到的,i 的值非常放肆,根本就找不到规律。所以,我们在编写云函数时,应注意保证云函数是无状态的、幂等的,即当次云函数的执行不依赖上一次云函数执行过程中在运行环境中残留的信息。
再次回到我们的Todo 案例中,因为我们将全部的业务逻辑放到了一个云函数中,因此,处理的并发量会受到极大的限制。当并发量达到一定的程度时,无法创建更多的函数实例,也就无法分配更多的服务器资源。更好的方式是对我们的业务逻辑进行拆分,一个云函数就对应一个独立的业务逻辑处理。这在小程序的云开发中就有体现,默认给我们的小程序云开发模板中,就是一个小程序应用对应对个云函数的处理方式。