Serverless 布道师在讲述 Serverless 架构和云主机区别的时候,常会有这样的描述:
传统业务开发想要上线,需要先评估资源使用,并根据资源评估结果购买云主机,之后还要根据业务发展不断对主机等资源进行升级维护。而 Serverless 架构不需要这样复杂的流程,将函数部署到线上后,一切后端服务交给运营商来处理,哪怕是瞬时高并发,也有云厂商来自动扩缩。
但在实际生产生活中,Serverless 真的可以做到无需对资源评估吗?还是说在 Serverless 架构下,资源评估的内容或对象发生了变化,或者进行了简化?
探索 Serverless 下的资源评估
以国内某云厂商为例,在其云函数中,我们创建一个云函数之后,设置页面会出现可设置的选项:
这两个设置范围分别是从 64M-1536M 和 1-900S,这样的配置其实就涉及到资源评估了。
首先是超时时间,一个项目、函数或 Action 都有执行时间,如果超过某个时间没执行完就可以评估其为发生了“意外”,可以被“干掉“了,这个就是超时时间。例如一个获取用户信息的请求,在 10S 内没有返回,证明其不能满足业务需求,那么,我们就可以把超时设置为 10S。当一个运行速度很慢的业务,至少要 50S 才能执行完,那么这个值设置的时候就要大于 50,否则程序可能因为超时被强行停止。
然后是内存,内存是一个有趣的东西,可能衍生两个关联点。
关联点 1: 程序本身需要一定的内存,这个内存要大于程序本身的内存,以 Python 语言为例:
# -*- coding: utf8 -*-
import jieba
def main_handler(event, context):
jieba.load_userdict("./jieba/dict.txt")
seg_list = jieba.cut("我来到北京清华大学", cut_all=True)
print("Full Mode: " + "/ ".join(seg_list)) # 全模式
seg_list = jieba.cut("我来到北京清华大学", cut_all=False)
print("Default Mode: " + "/ ".join(seg_list)) # 精确模式
seg_list = jieba.cut("他来到了网易杭研大厦") # 默认是精确模式
print(", ".join(seg_list))
seg_list = jieba.cut_for_search("小明硕士毕业于中国科学院计算所,后在日本京都大学深造") # 搜索引擎模式
print(", ".join(seg_list))
复制代码
对程序代码的说明:为了让结果更加直观,差距更加大,所以这里每次都重新导入了自带 dict,这个操作本身就是相对浪费时间和内存的。在实际使用中 jieba 自带缓存,并且无需手动导入本身的 dict
当导入一个自定义的 dict 到 jieba 中,如果此时函数内存设置的默认 128M 内存限制+3S 超时限制就会这样:
此时可以看到,由于在导入自定义 dict 时内存消耗过大, 默认的 128 不足以满足需求,所以需要将其修改成最大:
这时又再次提醒超时,还需要修改超时时间为适当的数值(此处设定为 10S):
总而言之,在关注程序本身的前提下,要将内存设置在一个合理范围内,这个范围是>=程序本身需要的内存数值。
关联点 2: 计费相关,在云函数的文档中,我们可以看到:
云函数 SCF 按照实际使用付费,采用后付费小时结,以 元 为单位进行结算。
SCF 账单由以下三部分组成,每部分根据自身统计结果和计算方式进行费用计算,结果以 元 为单位,并保留小数点后两位。
资源使用费用
调用次数费用
外网出流量费用
调用次数和出网流量都是与程序或者使用量相关的,无需格外关注,但在资源使用费用有一些注意点:
资源使用量 = 函数配置内存 × 运行时长
用户资源使用量,由函数配置内存,乘以函数运行时的计费时长得出。其中配置内存转换为 GB
单位,计费时长由毫秒(ms)转换为秒(s)单位,因此,资源使用量的计算单位为 GBs (GB-秒)。
例如,配置为 256MB 的函数,单次运行了 1760 ms,计费时长为 1760
ms,则单次运行的资源使用量为(256/1024)×(1760/1000) = 0.44 GBs。
针对函数的每次运行,均会计算资源使用量,并按小时汇总求和,作为该小时的资源使用量。
这里有一个非常重要的公式,那就是函数配置内存*运行时长。函数配置内存就是我们为程序选择的内存大小,运行时长就是运行程序之后得到的结果:
以这个程序为例,使用的是 1536MB,则使用量为(1536/1024) * (3200/1000) = 4.8GBs
当然,如果此时是 250MB,那程序也可以运行:
此时的资源使用量为(256/1024) * (3400/1000) = 0.85GBs
对比上一次,程序执行时间增加了 0.2S,但是资源使用量降低了将近 6 倍!
产品单价是:
虽然 GBs 的单价很低,但是业务量上来之后,这个数字也是要值得注意的。刚才的只是一个单次请求,如果每天有 1000 单次请求,那费用就会出现差距(仅计算资源使用量费用,而不计算调用次数/外网流量):
1536MB: 4.810000.00011108 = 0.5 元
256MB:0.8510000.00011108 = 0.09442 元
如果不是 1000 次调用,而是 10 万次调用,则就是 50 元和 9 元的区别,随着流量越大,差距越大。
当然,多数情况函数执行时间不会这么久,以下面函数为例:
计费时间均是 100ms,每日调用量在 6000 次左右:
如果按照 64M 内存来计算,单资源费用只要 76 元一年,而如果内存都设置为 1536,则一年要 1824 元!这个费用相当于:
所以,超时时间需要对代码和业务场景进行评估来进行设置,可能关系到程序运行的稳定和功能的完整性;内存则不仅仅在程序使用层面有着不同的需求,在费用成本等方面也占有极大的比重,所以内存设置需要对程序进行一个评估。那么问题来了,内存设置多大比较划算?同样是之前的代码,在本地进行简单的脚本编写:
from tencentcloud.common import credential
from tencentcloud.common.profile.client_profile import ClientProfile
from tencentcloud.common.profile.http_profile import HttpProfile
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
from tencentcloud.scf.v20180416 import scf_client, models
import json
import numpy
import matplotlib.pyplot as plt
try:
cred = credential.Credential("", "")
httpProfile = HttpProfile()
httpProfile.endpoint = "scf.tencentcloudapi.com"
clientProfile = ClientProfile()
clientProfile.httpProfile = httpProfile
client = scf_client.ScfClient(cred, "ap-shanghai", clientProfile)
req = models.InvokeRequest()
params = '{"FunctionName":"hello_world_2"}'
req.from_json_string(params)
billTimeList = []
timeList = []
for i in range(1, 50):
print("times: ", i)
resp = json.loads(client.Invoke(req).to_json_string())
billTimeList.append(resp['Result']['BillDuration'])
timeList.append(resp['Result']['Duration'])
print("计费最大时间", int(max(billTimeList)))
print("计费最小时间", int(min(billTimeList)))
print("计费平均时间", int(numpy.mean(billTimeList)))
print("运行最大时间", int(max(timeList)))
print("运行最小时间", int(min(timeList)))
print("运行平均时间", int(numpy.mean(timeList)))
plt.figure()
plt.subplot(4, 1, 1)
x_data = range(0, len(billTimeList))
plt.plot(x_data, billTimeList)
plt.subplot(4, 1, 2)
plt.hist(billTimeList, bins=20)
plt.subplot(4, 1, 3)
x_data = range(0, len(timeList))
plt.plot(x_data, timeList)
plt.subplot(4, 1, 4)
plt.hist(timeList, bins=20)
plt.show()
except TencentCloudSDKException as err:
print(err)
复制代码
运行之后会输出一个简单的图像:
从上到下分别是不同次数计费时间图、计费时间分布图以及不同次数运行时间图和运行时间分布图。256M 起步,1536M 终止,步长 128M,每个内存大小串行靠用 50 次,统计表:
注:为了让统计结果更加清晰,差异性比较明显,在程序代码中进行了部分无用操作用来增加程序执行时间。正常使用 jieba 的速度基本都是毫秒级的:
通过表统计可以看到在满足程序内存消耗的前提下,内存大小对程序执行时间的影响并不是很大,反而是对计费影响很大。
当然上面是两个重要指标,除此之外还有一个参数需要用户来评估:函数并发量,在项目上线之后,需要对项目可能产生的并发量进行评估,当评估的并发量超过默认的并发量,要及时联系售后同学或者提交工单进行最大并发量数值的提升。
除了上面的简单评估,有兴趣的同学也可以进行多进程/多线程与函数执行时间/内存关系的评估,但是考虑到很多时候云函数的业务都不涉及到多进程/多线程,所以这里不仅行单独测试。
应该如何设置函数的内存与超时时间
上一部分主要说了 Serverless 架构与资源评估:性能与成本探索。探索之后,就不得不引出一个新的问题:在使用 Serverless 架构时如何来设置运行内存和超时时间呢?
评估方法有很多,我的做法是先将函数上线,选择一个稍大的内存执行一次。
得到上图结果,再函数设置为 128M 或者 256M,超时时间设置成 3S,接着运行一段时间,例如接口每天触发次数大约为 4000+次:
将函数日志写成脚本,重新做统计:
import json, time, numpy, base64
import matplotlib.pyplot as plt
from matplotlib import font_manager
from tencentcloud.common import credential
from tencentcloud.common.profile.client_profile import ClientProfile
from tencentcloud.common.profile.http_profile import HttpProfile
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
from tencentcloud.scf.v20180416 import scf_client, models
secretId = ""
secretKey = ""
region = "ap-guangzhou"
namespace = "default"
functionName = "course"
font = font_manager.FontProperties(fname="./01.ttf")
try:
cred = credential.Credential(secretId, secretKey)
httpProfile = HttpProfile()
httpProfile.endpoint = "scf.tencentcloudapi.com"
clientProfile = ClientProfile()
clientProfile.httpProfile = httpProfile
client = scf_client.ScfClient(cred, region, clientProfile)
req = models.GetFunctionLogsRequest()
strTimeNow = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(time.time())))
strTimeLast = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(time.time()) - 86400))
params = {
"FunctionName": functionName,
"Limit": 500,
"StartTime": strTimeLast,
"EndTime": strTimeNow,
"Namespace": namespace
}
req.from_json_string(json.dumps(params))
resp = client.GetFunctionLogs(req)
durationList = []
memUsageList = []
for eveItem in json.loads(resp.to_json_string())["Data"]:
durationList.append(eveItem['Duration'])
memUsageList.append(eveItem['MemUsage'] / 1024 / 1024)
durationDict = {
"min": min(durationList), # 运行最小时间
"max": max(durationList), # 运行最大时间
"mean": numpy.mean(durationList) # 运行平均时间
}
memUsageDict = {
"min": min(memUsageList), # 内存最小使用
"max": max(memUsageList), # 内存最大使用
"mean": numpy.mean(memUsageList) # 内存平均使用
}
plt.figure(figsize=(10, 15))
plt.subplot(4, 1, 1)
plt.title('运行次数与运行时间图', fontproperties=font)
x_data = range(0, len(durationList))
plt.plot(x_data, durationList)
plt.subplot(4, 1, 2)
plt.title('运行时间直方分布图', fontproperties=font)
plt.hist(durationList, bins=20)
plt.subplot(4, 1, 3)
plt.title('运行次数与内存使用图', fontproperties=font)
x_data = range(0, len(memUsageList))
plt.plot(x_data, memUsageList)
plt.subplot(4, 1, 4)
plt.title('内存使用直方分布图', fontproperties=font)
plt.hist(memUsageList, bins=20)
# with open("/tmp/result.png", "rb") as f:
# base64_data = base64.b64encode(f.read())
print("-" * 10 + "运行时间相关数据" + "-" * 10)
print("运行最小时间:\t", durationDict["min"], "ms")
print("运行最大时间:\t", durationDict["max"], "ms")
print("运行平均时间:\t", durationDict["mean"], "ms")
print("\n")
print("-" * 10 + "内存使用相关数据" + "-" * 10)
print("内存最小使用:\t", memUsageDict["min"], "MB")
print("内存最大使用:\t", memUsageDict["max"], "MB")
print("内存平均使用:\t", memUsageDict["mean"], "MB")
print("\n")
plt.show(dpi=200)
except TencentCloudSDKException as err:
print(err)
复制代码
运行结果:
----------运行时间相关数据----------
运行最小时间: 1 ms
运行最大时间: 291 ms
运行平均时间: 63.45 ms
----------内存使用相关数据----------
内存最小使用: 21.20703125 MB
内存最大使用: 29.66015625 MB
内存平均使用: 24.8478125 MB
复制代码
通过上图可以看出,近 500 次,每次函数的时间消耗和内存使用。时间消耗基本在 1S 以下,所以此处超时时间设置成 1S 是合理的,而内存使用基本是 64M 以下,所以此时内存设置成 64M 就可以。当然,通过这个图可以看出云函数在执行时可能会有一定的波动,所以无论是内存使用还是超时时间,都可能会出现一定的波动,可以根据自身的业务需求来做一些舍弃,将资源使用量压到最低,节约成本。
总结
综上所述,Serverless 架构也是需要资源评估的,而且资源评估同样是和成本直接挂钩的,只不过这个资源评估的对象逐渐发生了变化,相对之前的评估维度、难度而言,都是大幅度缩小或者降低的。
上线函数之前,进行资源评估的做法基本是分为两步走:
评论