Serverless 架构下,虽然我们更多精力是关注我们的业务代码,但是实际上对于一些配置和成本也是需要进行关注的,并且在必要的时候,还需要根据配置与成本进行对我们的 Serverless 应用进行配置优化和代码优化。
资源评估依旧重要
Serverless 架构虽然是按量付费的,但是并不代表他就一定比传统的服务器租用费用低,如果我们对自己的项目评估不准确,对一些指标设置不合理,Serverless 架构所产生的费用可能是巨大的。
一般情况下,FaaS 平台的收费是和三个指标具有直接关系的:
所配置的内存规格;
程序所消耗的时间;
以及产生的流量费用。
通常情况下程序所消耗的时间可能会与内存规格、程序本身所处理的业务逻辑有关。流量费用与程序本身和客户端交互的数据包大小有关,所以在这三个常见的指标,可能因为配置不规范导致计费出现比较大偏差的就是内存规格。以阿里云函数计算为例,我们假设有一个 Hello World 的程序,每天都会被执行 10000 次,可以统计不同规格的实例所产生的费用(不包括网络费用):
阿里云
通过上表可以看到,当程序在 128MB 规格的内存中可以正常执行,如果我们错误地将内存规格设置成了 3072MB,可能每月产生的费用将会暴涨 25 倍!所以我们在上线 Serverless 应用之前,要对资源进行评估,以便得到更合理的配置来进一步降低我们的成本。
合理的代码包规格
各个云厂商的 FaaS 平台中都对代码包大小有着限制,抛掉云厂商对代码包的限制,单纯地说代码包的规格可能会产生的影响,通过函数的冷启动流程可以看到:
image
在函数启动的过程中,有一个过程是加载代码的过程,那么当我们所上传的代码包过大,或者说文件过多导致解压速度过慢,就会直接导致加载代码这个过程变长,进一步直接导致冷启动时间变久。
可以设想一下,当我们有两个压缩包,一个是只有 100KB 的代码压缩包,另一个是 200MB 的代码压缩包,两者同时在千兆的内网带宽下理想化(即不考虑磁盘的存储速度等)下载,即使最大速度可以达到 125MB/S,那么前者的下载速度只有不到 0.01s,后者需要 1.6s。除了下载时间之外,还有文件的解压时间,那么两者的冷启动时间可能就相差 2s。
一般情况下,一个传统的 Web 接口,如果要 2s 以上的响应时间,实际上对很多业务来说是不能接受的,所以在我们打包代码时就要尽可能的降低压缩包大小。以 Node.js 项目为例,打包代码包时,可以采用 Webpack 等方法,来压缩依赖包大小,进一步降低整体代码包的规格,提升函数的冷启动效率。
合理利用实例的复用
在各个云厂商的 FaaS 平台中,为了更好的解决冷启动的问题,为了更合理的利用资源,是存在“实例”复用情况的。所谓的实例复用,就是当一个实例完成一个请求后并不会释放,而是进入“静默”的状态。在一定时间范围内,如果有新的请求被分配过来,则会直接调用对应的方法,而不需要再初始化各类资源等,这在很大程度上减少了函数冷启动的情况出现。为了验证,我们可以创建两个函数:
函数1:
def handler(event, context):
print("Test")
return 'hello world'
函数2:
print("Test")
def handler(event, context):
return 'hello world'
我们在控制台多次点击“测试”按钮,对这两个函数进行测试,判断其是否在日志中输出了“Test”,我们可以统计结果:
image
根据上面的情况,我们可以看到,其实实例复用的情况是存在的。因为“函数 2”并不是每次都会执行入口函数之外的一些语句。根据“函数 1”和“函数 2”,我们也可以进一步思考,如果 print("Test") 语句是一个初始化数据库连接,或者是加载一个深度学习的模型,是不是“函数 1”的写法就是每次请求都会执行,而“函数 2”的写法是可以存在复用已有对象的情况?
所以在实际的项目中,有一些初始化操作,是可以按照“函数 2”来进行实现的,例如:
- 机器学习场景下,在初始化的时候加载模型,避免每次函数被触发都会加载模型带来的效率问题,提高实例复用场景下的响应效率;
- 数据库等链接操作,可以在初始化的时候进行链接对象的建立,避免每次请求都创建链接对象;
其他一些需要首次加载时下载文件,加载文件的场景,在初始化的时候进行这部分需求的实现,可以在实例复用的时候效率更高;
善于利用函数特性
各个云厂商的FaaS平台都有一些“平台特性”,所谓的平台特性,是指这些功能可能并不是《CNCF WG-Serverless Whitepaper v 1.0》中规定的能力,或者描述的能力,仅仅是作为云平台根据自身业务发展和诉求,从用户角度出发挖掘出来,并且实现的功能,可能只在某个云平台或者某几个云平台所拥有的功能。这类功能一般情况下如果利用得当会让我们的业务性能等有质的提升。
1、Pre-freeze & Pre-stop
以阿里云函数计算为例,在平台发展过程中,用户痛点(尤其是传统应用平滑迁移至 Serverless 架构)如下:
- 异步背景指标数据延迟或丢失:如果在请求期间没有发送成功,则可能被延迟至下一次请求,或者数据点被丢弃。
- 同步发送指标增加延迟:如果在每个请求结束后都调用类似Flush接口,不仅增加了每个请求的延迟,对于后端服务也产生了不必要的压力。
- 函数优雅下线:实例关闭时应用有清理连接,关闭进程,上报状态等需求。在函数计算中实例下线时机开发者无法掌握,也缺少 Webhook 通知函数实例下线事件。
根据这些痛点发布了运行时扩展(runtime extensions)功能。该功能在现有的 HTTP 服务编程模型上扩展,在已有的 HTTP 服务器的模型中增加了 PreFreeze 和 PreStop webhooks。扩展开发者实现 HTTP handler,监听函数实例生命周期事件,如下图所示:
image
- PreFreeze:在每次函数计算服务决定冷冻当前函数实例前,函数计算服务会调用 HTTP GET /pre-freeze 路径,扩展开发者负责实现相应逻辑以确保完成实例冷冻前的必要操作,例如等待指标发送成功等。函数调用 InvokeFunction 的时间不包 PreFreeze Hook 的执行时间。
image
- PreStop:在每次函数计算决定停止当前函数实例前,函数计算服务会调用 HTTP GET /pre-stop 路径,扩展开发者负责实现相应逻辑以确保完成实例释放前的必要操作,如关闭数据库链接,以及上报、更新状态等。
image
2、单实例多并发
众所周知,各个厂商的函数计算通常是请求级别的隔离,即当客户端同时发起 3 个请求到函数计算,理论上会产生三个实例来进行应对,这个时候可能会涉及到冷启动问题,可能会涉及到请求之间状态关联问题等,但是部分云厂商提供了单实例多并发的能力(例如阿里云函数计算),该能力允许用户为函数设置一个实例并发度(InstanceConcurrency),即单个函数实例可以同时处理多少个请求。
如图下图,假设同时有 3 个请求需要处理,当实例并发度设置为 1 时,函数计算需要创建3个实例来处理这 3 个请求,每个实例分别处理 1 个请求;当实例并发度设置为 10 时(即 1 个实例可以同时处理 10 个请求),函数计算只需要创建 1 个实例就能处理这 3 个请求。
单实例多并发效果简图
单实例多并发的优势如下:
- 减少执行时长,节省费用。例如,偏I/O的函数可以在一个实例内并发处理,减少实例数从而减少总的执行时长。
- 请求之间可以共享状态。多个请求可以在一个实例内共用数据库连接池,从而减少和数据库之间的连接数。
- 降低冷启动概率。由于多个请求可以在一个实例内处理,创建新实例的次数会变少,冷启动概率降低。
- 减少占用 VPC IP 在相同负载下,单实例多并发可以降低总的实例数,从而减少 VPC IP 的占用。
单实例多并发的应用场景是比较广泛的,例如函数中有较多时间在等待下游服务的响应的场景就比较适合使用该种功能,但是单实例多并发也并不适合全部应用场景,例如当函数中有共享状态且不能并发访问的场景,单个请求的执行要消耗大量 CPU 及内存资源的场景,就不适合使用单实例多并发这个功能。
更多内容关注 Serverless 微信公众号(ID:serverlessdevs),汇集 Serverless 技术最全内容,定期举办 Serverless 活动、直播,用户最佳实践。