作者 | 刘宇

自从 Serverless 架构被提出,函数计算这个名词变得越发的火热,甚至在很多时候有人会认为 Serverless 就是函数计算。

作为 Serverless 架构中的一个重要组成部分,云函数确实值得,也应该备受关注,无论是吐槽他的调试能力,还是抱怨它的冷启动,亦或者对弹性伸缩表示怀疑,但是我们不得不承认,更多人正在越来越关注 Serverless,也越来越关注 Serverless 中的 FaaS 部分。

经常看到有人在吐槽函数计算的冷启动问题,时至今日,不知道各平台的冷启动是什么样子的。本文将会通过相对客观的数据来进行基本的验证。

1冷启动验证

首先说到冷启动,就要先说明什么是冷启动,开发者提交代码之后你不知道他调不调用,函数第一次调用会有一个函数冷启动,把网络的环境全部打通,这个函数才能提供服务。如果没有优化好冷启动优化这部分,可能对于一些比较关键的产品首次启动会产生超时,体验非常不好,以前开发者本地运行函数的时候,并不会关注本地函数执行多少毫秒和微妙,但是在云函数场景下就不一样了,云函数有一个部署的过程;无论是公有云的平台上还是开源方案上,冷启动都是值得不断探讨话题和优化的方向。

打开网易新闻 查看精彩图片

在《Serverless: Cold Start War》这篇文章中,作者对 AWS Lambda,Azure Function 以及 Google Cloud Function 等三个工业级的 Serverless 架构产品的冷启动测试。作者将函数启动划分成四个部分:

打开网易新闻 查看精彩图片

然后作者通过对多种语言的“Hello World”与是否有依赖等进行搭配,进行测试,测试结果:

打开网易新闻 查看精彩图片

通过《Understanding AWS Lambda Performance—How Much Do Cold Starts Really Matter?》与《Serverless: Cold Start War》这两个文章的分析和结果,我们可以看到冷启动问题确实存在,而且不同厂商,不同语言,不同测试方法得到的冷启动数据都是有所差异的。这也充分说明,各个厂商也在通过一些规则和策略努力降低冷启动率。除此之外文章《Understanding Serverless Cold Start》、《Everything you need to know about cold starts in AWS Lambda》、《Keeping Functions Warm》、《I'm afraid you're thinking about AWS Lambda cold starts all wrong》等也均对冷启动现象等进行描述和深入的探讨,并且提出了一些业务侧应对函数冷启动的解决方案和策略。

再说回来,时至今日,主流云厂商都已经开始开发探索 Serverless 架构,那么各个云厂商的冷启动已经"热"到了什么程度?

由于我是国内的开发者,所以我将测试分为两部分,一部分是国内云厂商(腾讯云、阿里云、华为云),另一部分是国外云厂商(AWS、谷歌)。

由于在实际生产过程中,冷启动的诞生往往是和 API 网关共同体现,也就是说,实际上让用户感知相对明显,或者比较常见感知到冷启动出现的情况,通常是函数与网关结合,做了一个接口 / 服务,访问该服务的时候,放大 / 体现了冷启动。

所以,我的做法很简单和暴力,通过函数与 API 网关触发器的结合,针对不同厂商来创建一个 API 服务,通过本地机器来对该服务进行访问,在客户端判断其耗时。当然,这种做法诚然不能精确的表现出冷启动的具体数据,因为网络因素也将会是影响其准确性的一个重要因素,但是至少可以大概的对比出,不同云厂商的冷启动优化情况,以及服务稳定情况。

国内的云厂商,在测试的时候更多使用的是国内区,国外云厂商则是国外区,这样就会出现一个额外的问题,国内外云厂商的数据不具有对比性,毕竟网络因素占了很大的一部分,所以本次对比将会是国内和国内对比,国外和国外对比。

客户端程序:

import time, jsonimport urllib.requestimport matplotlib.pyplot as pltimport numpyfrom multiprocessing import Process, Manager# 测试地址# qcloudurl = "https://service-px5f98f4-1256773370.gz.apigw.tencentcs.com/release/scf_demo"# # huaweicloud# url = "https://2937587fe6ce4e6eb22d521d1d9b811c.apig.cn-east-2.huaweicloudapis.com/demo"# # aliyun# url = "https://50155512.cn-shanghai.fc.aliyuncs.com/2016-08-15/proxy/guide-hello_world/mydemo/"# # aws# url = "https://di6vbxf2lk.execute-api.us-east-1.amazonaws.com/default/mydemo"# # GoogleCloud# url = "https://us-central1-meta-imagery-277209.cloudfunctions.net/mydemo"# 此时times = 200# 串行处理serialColdStart = []serialHotStart = []for i in range(0,times):timeStart = time.time()responseAttr = urllib.request.urlopen(url)endTime = time.time()response = json.loads(responseAttr.read().decode("utf-8"))if response['isNew']:serialColdStart.append(endTime-timeStart)else:serialHotStart.append(endTime-timeStart)# 并行处理def worker(url, return_list):timeStart = time.time()responseAttr = urllib.request.urlopen(url)endTime = time.time()return_list.append({"duration": endTime-timeStart,"response": json.loads(responseAttr.read().decode("utf-8"))})manager = Manager()return_list = manager.list()jobs = []for i in range(times):p = Process(target=worker, args=(url ,return_list))jobs.append(p)p.start()for proc in jobs:proc.join()parallelColdStart = []parallelHotStart = []for eveData in return_list:if eveData['response']['isNew']:parallelColdStart.append(eveData['duration'])else:parallelHotStart.append(eveData['duration'])# 数据汇总print("-"*10, "串行测试", "-"*10)print("总触发次数:", len(serialColdStart) + len(serialHotStart))print("冷启动次数:", len(serialColdStart))print("热启动次数:", len(serialHotStart))print("最大耗时量:", max(serialColdStart + serialHotStart))print("最小耗时量:", min(serialColdStart + serialHotStart))print("平均耗时量:", numpy.mean(serialColdStart + serialHotStart))print("-"*10, "并行测试", "-"*10)print("总触发次数:", len(parallelColdStart) + len(parallelHotStart))print("冷启动次数:", len(parallelColdStart))print("热启动次数:", len(parallelHotStart))print("最大耗时量:", max(parallelColdStart + parallelHotStart))print("最小耗时量:", min(parallelColdStart + parallelHotStart))print("平均耗时量:", numpy.mean(parallelColdStart + parallelHotStart))plt.figure(figsize=(15,10))plt.subplot(4, 2, 1)plt.title('(Serial) Cold Start Time')plt.plot(range(0, len(serialColdStart)), serialColdStart)plt.subplot(4, 2, 3)plt.title('(Serial) Cold Start Time')plt.hist(serialColdStart, bins=20)plt.subplot(4, 2, 5)plt.title('(Serial) Hot Start Time')plt.plot(range(0, len(serialHotStart)), serialHotStart)plt.subplot(4, 2, 7)plt.title('(Serial) Hot Start Time')plt.hist(serialHotStart, bins=20)plt.subplot(4, 2, 2)plt.title('(Parallel) Hot Start Time')plt.plot(range(0, len(parallelColdStart)), parallelColdStart)plt.subplot(4, 2, 4)plt.title('(Parallel) Cold Start Time')plt.hist(parallelColdStart, bins=20)plt.subplot(4, 2, 6)plt.title('(Parallel) Hot Start Time')plt.plot(range(0, len(parallelHotStart)), parallelHotStart)plt.subplot(4, 2, 8)plt.title('(Parallel) Hot Start Time')plt.hist(parallelHotStart, bins=20)plt.show()import time, jsonimport urllib.requestimport matplotlib.pyplot as pltimport numpyfrom multiprocessing import Process, Manager# 测试地址# qcloudurl = "https://service-px5f98f4-1256773370.gz.apigw.tencentcs.com/release/scf_demo"# # huaweicloud# url = "https://2937587fe6ce4e6eb22d521d1d9b811c.apig.cn-east-2.huaweicloudapis.com/demo"# # aliyun# url = "https://50155512.cn-shanghai.fc.aliyuncs.com/2016-08-15/proxy/guide-hello_world/mydemo/"# # aws# url = "https://di6vbxf2lk.execute-api.us-east-1.amazonaws.com/default/mydemo"# # GoogleCloud# url = "https://us-central1-meta-imagery-277209.cloudfunctions.net/mydemo"# 此时times = 200# 串行处理serialColdStart = []serialHotStart = []for i in range(0,times):timeStart = time.time()responseAttr = urllib.request.urlopen(url)endTime = time.time()response = json.loads(responseAttr.read().decode("utf-8"))if response['isNew']:serialColdStart.append(endTime-timeStart)else:serialHotStart.append(endTime-timeStart)# 并行处理def worker(url, return_list):timeStart = time.time()responseAttr = urllib.request.urlopen(url)endTime = time.time()return_list.append({"duration": endTime-timeStart,"response": json.loads(responseAttr.read().decode("utf-8"))})manager = Manager()return_list = manager.list()jobs = []for i in range(times):p = Process(target=worker, args=(url ,return_list))jobs.append(p)p.start()for proc in jobs:proc.join()parallelColdStart = []parallelHotStart = []for eveData in return_list:if eveData['response']['isNew']:parallelColdStart.append(eveData['duration'])else:parallelHotStart.append(eveData['duration'])# 数据汇总print("-"*10, "串行测试", "-"*10)print("总触发次数:", len(serialColdStart) + len(serialHotStart))print("冷启动次数:", len(serialColdStart))print("热启动次数:", len(serialHotStart))print("最大耗时量:", max(serialColdStart + serialHotStart))print("最小耗时量:", min(serialColdStart + serialHotStart))print("平均耗时量:", numpy.mean(serialColdStart + serialHotStart))print("-"*10, "并行测试", "-"*10)print("总触发次数:", len(parallelColdStart) + len(parallelHotStart))print("冷启动次数:", len(parallelColdStart))print("热启动次数:", len(parallelHotStart))print("最大耗时量:", max(parallelColdStart + parallelHotStart))print("最小耗时量:", min(parallelColdStart + parallelHotStart))print("平均耗时量:", numpy.mean(parallelColdStart + parallelHotStart))plt.figure(figsize=(15,10))plt.subplot(4, 2, 1)plt.title('(Serial) Cold Start Time')plt.plot(range(0, len(serialColdStart)), serialColdStart)plt.subplot(4, 2, 3)plt.title('(Serial) Cold Start Time')plt.hist(serialColdStart, bins=20)plt.subplot(4, 2, 5)plt.title('(Serial) Hot Start Time')plt.plot(range(0, len(serialHotStart)), serialHotStart)plt.subplot(4, 2, 7)plt.title('(Serial) Hot Start Time')plt.hist(serialHotStart, bins=20)plt.subplot(4, 2, 2)plt.title('(Parallel) Hot Start Time')plt.plot(range(0, len(parallelColdStart)), parallelColdStart)plt.subplot(4, 2, 4)plt.title('(Parallel) Cold Start Time')plt.hist(parallelColdStart, bins=20)plt.subplot(4, 2, 6)plt.title('(Parallel) Hot Start Time')plt.plot(range(0, len(parallelHotStart)), parallelHotStart)plt.subplot(4, 2, 8)plt.title('(Parallel) Hot Start Time')plt.hist(parallelHotStart, bins=20)plt.show()

2国内云厂商

腾讯云

测试代码:

# -*- coding: utf8 -*-import jsonimport timeimport uuidrequestId = NonecontaineId = str(uuid.uuid1())createTime = time.time()def main_handler(event, context):time.sleep(1)tempId = str(uuid.uuid1())timeStart = time.time()global requestIdif not requestId:requestId = tempIdresponse = {"isNew": True,"oldRequestId": requestId,"newRequestId": tempId,"duration": time.time() - timeStart,"containeId": containeId,"createTime": createTime}response["isNew"] = True if requestId == tempId else Falsereturn response

输出数据:

---------- 串行测试 ----------总触发次数:200冷启动次数:8热启动次数:192最大耗时量:2.3507158756256104最小耗时量:1.0568928718566895平均耗时量:1.134293702840805---------- 并行测试 ----------总触发次数:186冷启动次数:170热启动次数:16最大耗时量:14.849930047988892最小耗时量:1.092796802520752平均耗时量:7.125524929774705

打开网易新闻 查看精彩图片

阿里云

测试代码:

# -*- coding: utf8 -*-import jsonimport timeimport uuidrequestId = NonecontaineId = str(uuid.uuid1())createTime = time.time()class AppClass:"""Produce the same output, but using a class"""def __init__(self, environ, start_response, response):self.environ = environself.start = start_responseself.response = responsedef __iter__(self):status = '200'response_headers = [('Content-type', 'text/html;charset=utf-8')]self.start(status, response_headers)yield self.response.encode("utf-8")def handler(environ, start_response):time.sleep(1)tempId = str(uuid.uuid1())timeStart = time.time()global requestIdif not requestId:requestId = tempIdresponse = {"isNew": True,"oldRequestId": requestId,"newRequestId": tempId,"duration": time.time() - timeStart,"containeId": containeId,"createTime": createTime}response["isNew"] = True if requestId == tempId else Falsereturn AppClass(environ, start_response, json.dumps(response))

输出数据:

---------- 串行测试 ----------总触发次数:200冷启动次数:1热启动次数:199最大耗时量:1.8273499011993408最小耗时量:1.1592700481414795平均耗时量:1.221251163482666---------- 并行测试 ----------总触发次数:200冷启动次数:163热启动次数:37最大耗时量:3.184391975402832最小耗时量:1.1983528137207031平均耗时量:2.3849029302597047

打开网易新闻 查看精彩图片

华为云

测试代码:

# -*- coding: utf8 -*-import jsonimport timeimport uuidrequestId = NonecontaineId = str(uuid.uuid1())createTime = time.time()def handler(event, context):time.sleep(1)tempId = str(uuid.uuid1())timeStart = time.time()global requestIdif not requestId:requestId = tempIdresponse = {"isNew": True,"oldRequestId": requestId,"newRequestId": tempId,"duration": time.time() - timeStart,"containeId": containeId,"createTime": createTime}response["isNew"] = True if requestId == tempId else Falsereturn json.dumps({'statusCode': 200,'isBase64Encoded': False,'headers': {"Content-type": "text/html; charset=utf-8"},'body': json.dumps(response),})

输出数据:

---------- 串行测试 ----------总触发次数:200冷启动次数:1热启动次数:199最大耗时量:2.4535348415374756最小耗时量:1.202908992767334平均耗时量:1.4574852859973908---------- 并行测试 ----------总触发次数:200冷启动次数:72热启动次数:128最大耗时量:3.8169281482696533最小耗时量:1.232532024383545平均耗时量:2.3244904506206514

打开网易新闻 查看精彩图片

3国外云厂商

AWS

测试代码:

# -*- coding: utf8 -*-import jsonimport timeimport uuidrequestId = NonecontaineId = str(uuid.uuid1())createTime = time.time()def lambda_handler(event, context):time.sleep(1)tempId = str(uuid.uuid1())timeStart = time.time()global requestIdif not requestId:requestId = tempIdresponse = {"isNew": True,"oldRequestId": requestId,"newRequestId": tempId,"duration": time.time() - timeStart,"containeId": containeId,"createTime": createTime}response["isNew"] = True if requestId == tempId else Falsereturn {'statusCode': 200,'body': json.dumps(response)}

输出数据:

---------- 串行测试 ----------总触发次数:200冷启动次数:1热启动次数:199最大耗时量:6.628237009048462最小耗时量:1.917238712310791平均耗时量:2.1634005284309388---------- 并行测试 ----------总触发次数:200冷启动次数:176热启动次数:24最大耗时量:6.071150779724121最小耗时量:1.9705779552459717平均耗时量:2.370948977470398

打开网易新闻 查看精彩图片

Google Cloud

测试代码:

# -*- coding: utf8 -*-import jsonimport timeimport uuidrequestId = NonecontaineId = str(uuid.uuid1())createTime = time.time()def main_handler(event):time.sleep(1)tempId = str(uuid.uuid1())timeStart = time.time()global requestIdif not requestId:requestId = tempIdresponse = {"isNew": True,"oldRequestId": requestId,"newRequestId": tempId,"duration": time.time() - timeStart,"containeId": containeId,"createTime": createTime}response["isNew"] = True if requestId == tempId else Falsereturn json.dumps(response)

输出数据:

---------- 串行测试 ----------总触发次数:200冷启动次数:1热启动次数:199最大耗时量:4.707853078842163最小耗时量:1.226269006729126平均耗时量:1.3416448163986205---------- 并行测试 ----------总触发次数:200冷启动次数:198热启动次数:2最大耗时量:7.694962024688721最小耗时量:1.296091079711914平均耗时量:5.523866602182388

打开网易新闻 查看精彩图片

4测试结果

通过对上面的数据进行基本分析,可以作图:

打开网易新闻 查看精彩图片

通过这个表格可以看到,由于我是在国内测试的 aws 和 google cloud,而且还是国外区域,所以网络因素对其影响蛮大的。

5总结

函数冷启动问题确实是在项目中常见的一个现象,我做了一个微信公众号,后台绑定了两个函数,冷启动可怕到每次遇到冷启动,公众号的后台服务都会被微信判定为"故障,无法提供服务",在实际项目中,冷启动不能说是地球毁灭,但是也应该是一场大灾难。

我始终认为,一个项目对开发者再友好,提供再多的功能 / 能力都是建立起来的高楼,如果这个高楼的地基不稳定,那么这高楼注定坍塌。