Serverless应用优化与注意事项

通过冷启动优化、对无状态性的认识、Serverless架构下的资源评估、开发者工具的加持等方面的介绍对Serverless架构下的应用优化与注意事项进行总结。

函数基础与资源编排

1 函数并不是“函数”

众所周知,Serverless架构通常被认为是FaaS与BaaS的结合。所谓的FaaS是Function as a Service的简称,这里的Function是指传统业务想要部署到Serverless架构,要将传统的业务应用变成若干函数,还是并非传统编程领域所定义的函数,而是有着更深一层的含义?

传统编程领域的函数被定义为:一段可以直接被另一段程序或代码引用的程序或代码,也叫作子程序、方法(OOP中)。一个较大的程序一般分为若干个程序块,每一个程序块用来实现一个特定的功能。所有的高级语言中都有子程序这个概念,其用于实现模块功能。在C语言中,子程序由一个主函数和若干个函数构成。由主函数调用其他函数,其他函数也可以互相调用。同一个函数可以被一个或多个函数调用任意次。在程序设计中,我们常将一些常用的功能模块编写成函数,放在函数库中供公共选用。我们要善于利用函数,以减少重复编写程序段的工作量。函数分为全局函数、全局静态函数。在类中,我们还可以定义构造函数、析构函数、复制构造函数、成员函数、友元函数、运算符重载函数、内联函数等。

Serverless架构下,以阿里云与腾讯云为例,它们对自身的FaaS产品定义如下。

·阿里云:函数计算(Function Compute)是一个事件驱动的全托管Serverless计算服务,无须管理服务器等基础设施,准备好计算资源,并以弹性、可靠的方式运行代码。

·腾讯云:云函数(Serverless Cloud Function,SCF)是腾讯云为企业和开发者们提供的无服务器执行环境,支持用户在没有购买和无管理服务器的情况下运行代码。用户只需使用平台支持的语言编写核心代码并设置代码运行条件,即可在腾讯云基础设施上弹性、安全地运行代码。SCF是实时文件处理和数据处理等场景下理想的计算平台。

由此可见,云厂商定义自身的FaaS产品时,并没有单独解释“函数”这个词。在Serverless架构下,FaaS中的Function一词的定义变得模糊:既有传统编程领域中“函数”的影子,也有更为抽象的“功能”的含义。

Serverless架构中的FaaS平台所提供的运行时,在很多情况下是和编程语言相关的,例如Python 3的运行时、Node.js12的运行时等。此时,针对这类运行时有一个专有名词,叫作“函数入口”。所谓的函数入口,指的是Serverless架构接收到事件时,对函数触发的入口,类似于C语言中的main()函数等。以阿里云函数计算中的Python运行时为例,函数入口格式为:文件名.方法名,具体举例可以是index.handler,此时index.py就需要包括如下函数:

handler(args1, args2):    pass

该函数被事件触发时默认调用这个handler函数,并将对应的事件数据结构和上下文等作为参数传入。所以,在这个层面来看,函数计算中的函数在一定程度上拥有传统编程领域中“函数”的影子。

但是随着时间的推移,用户需求不断复杂化。在实际生产过程中,FaaS平台中Function的概念得以升级。

·从传统的某个语言运行时为例,其代码中不仅有函数入口,还可能存在初始化方法、结束方法,那么这里的“函数”到底是指哪个方法。

·随着自定义运行时、自定义镜像等概念逐渐被提出,所谓的函数入口逐渐变成启动指令,例如阿里云的custom runtime的函数入口实际上是Bootstrap中的启动命令,那么此时函数就不再是传统编程领域所说的“函数”了,更多情况下对应的是一个功能甚至一个项目,是FaaS平台中的一种资源粒度表示形式。而这种粒度在一定情况下也是复杂多样的。

·它可以是一个单纯的函数,即非常简单的一个方法。

·它也可以是一个相对完整的功能,即几个方法的结合,例如登录功能。

·它还可以是几个功能的结合,形成一个简单的模块,例如登录和注册模块。

·它甚至可以是一个框架,例如在一个函数中部署整个框架,如Express.js、Django等。

·它甚至是一个完整的服务,例如某个博客系统(Zblog、Wordpress)等部署到一个函数中对外提供服务。

正是因为对FaaS平台中Function概念把握不清,很多业务在部署到Serverless架构之前,都会面临很严重的挑战:一个服务对应一个函数还是一个功能对应一个函数?如果FaaS中的函数按照传统编程中的函数来看,无疑一个功能对应一个函数可能更合适,但如果按照一种资源粒度来看,那么即使一个服务全部放在一个函数中统一对外暴露也未尝不可。其实,针对这个问题,我们无须过分纠结,一般情况下只需要遵循以下两个原则。

1)资源相似原则:在某个业务中,对外暴露的接口资源消耗是否类似。例如,某个后端服务对外暴露10个接口,其中9个接口只需要128MB的内存,超时限制为3秒;而另一个接口需要2048MB的内存与超时限制为60秒,此时我们可以认为前9个接口资源消耗相似,可以放在“一个函数中实现”,最后一个单独放到一个函数中实现。

2)功能相似原则:在某个业务中,功能概念、定义相差非常大时,不建议将这些功能放在一个函数中。例如某个聊天系统中有一个聊天功能(Websocket),还有一个注册和登录功能,如果将两者放在一个函数中实现,这在一定程度上会增加项目的复杂度,也不益于后期管理,此时我们可以考虑拆分成聊天函数和注册/登录函数。

对于传统业务部署到Serverless架构时所面临的业务拆分问题,在一定程度上也是一个哲学问题,我们需要根据具体业务情况具体分析。

·业务拆得太细时,问题如下。

〇函数太多,不易于管理。

〇业务出现问题,不便于排查具体情况。

〇产生很多模块,配置重复使用。

〇在一定情况下,冷启动变得比较频繁。

·业务耦合得太严重时,问题如下。

〇现阶段下,容易产生比较大的资费问题。

〇在高并发的情况下,容易出现流量限制问题。

〇更新业务代码可能有较大的风险。

〇不便于调试等。

2 对无状态性的认识

UC Berkeleyd发表的“Cloud Programming Simplified: A Berkeley View on Serverless Com-puting”针对ServerFul和Serverless进行了比较细腻的总结:“Serverless架构弱化了存储和计算之间的联系”。服务的存储和计算被分开部署和收费,存储不再是服务的一部分,而是演变成独立的云服务,这使得计算资源变得无状态化,更容易调度和扩缩容,同时也降低了数据丢失的风险。CNCF的Serverless Whitepaper v1.0对Serverless架构适用场景的总结为:短暂、无状态的应用,对冷启动时间不敏感的业务。

众所周知,在传统架构下,我们很少接触到“无状态”这个概念,而在Serverless架构下,“无状态”成了其特性之一,甚至对其应用场景等有一定影响。那么,Serverless架构下的无状态指的是什么?

其实,所谓的无状态就是没有状态的意思,也就是说Serverless架构下,我们无法在实例中保存某些永久状态,因为所有的实例都会被销毁。一旦实例被销毁,那么之前存储在实例状态中的数据、文件等将会消失。但是,这并不意味着函数的前后两次触发互不影响。就目前来看,各个云厂商的FaaS产品均存在着实例复用的情况,也就是说即使长久来看实例会被释放,但是并不能确保每次都会被释放,因为在某些条件允许的情况下,实例可以被复用进而降低冷启动等带来的影响,此时函数的无状态性就不纯粹了。

·无论从长久来看,实例会被释放,还是从并发角度来看,多个实例处理某个请求,函数计算的实例都不适合长期保存某个状态,因为该状态可能因为并发处理而不一致,也可能因为释放而丢失。

·由于实例存在复用的情况,即使确信函数计算实例不能长久存储状态,也不能忽略实例复用时前一请求残留状态对本次处理的影响。例如某函数实例在某次请求时创建临时文件A,在请求结束后出现了实例复用的情况,此时新的请求在该实例中再次创建临时文件A,可能存在临时文件A已经存在,且无法覆盖的情况,进而导致之后所有实例复用的请求无法顺利得到预期的响应。

当然,并不是实例复用导致Serverless架构的无状态性变得不纯粹,进而影响部分触发返回不可控,在一定程度上合理复用实例,以及对部分临时状态合理利用,会提升Serverless应用性能,尤其是在对冷启动比较敏感的业务场景下。

综上所述,Serverless架构的无状态性在一定程度上指的是Serverless架构的函数实例不适合持久化存储文件、数据等内容。但是,开发者不能忽略实例复用时“有状态”对业务的影响,合理复用实例有助于提升业务的性能。

3 资源评估的重要性

很多资料在对比Serverless架构和传统云主机架构下的应用开发时,往往会有类似的描述:“传统架构下的业务上线时,我们需要评估资源使用,根据资源评估结果购买云主机,并且需要根据业务发展不断对主机等资源进行升级维护,而Serverless架构,则不需要这样复杂的流程,只需要将函数部署到线上,一切后端服务交给运营商来处理,哪怕是瞬时高并发,也有云厂商为您自动扩缩。”

通过这段描述,很多人心中会有一个明确的心智产生:Serverless架构下并不需要资源评估,一切弹性能力都交给云厂商,又因为是按量付费模型,所以Serverless应用理论上成本更低。但是在目前Serverless架构发展阶段,资源评估是否是必要的;如果Serverless架构下也需要资源评估,那么应该通过什么方法进行评估;评估哪些资源,这些问题还需要考虑。

以国内某云厂商为例,在创建函数时,有两个配置需要明确。

·内存配置:从64MB到1536MB;

·超时时间配置:从1s到900s。

之所以会关注这两个配置,是因为函数的按量付费模型会涉及这两个资源。以该厂商的付费模型为例,账单由以下3部分组成。每部分由以下计算方式计算得出,结果以元为单位,并保留小数点后两位:

资源使用费用=调用次数费用+外网流量费用

其中,调用次数费用和外网流量费用相对来说是比较容易理解的。对于资源使用量而言,其复杂程度略高:

资源使用量=函数配置内存×运行时长

其中,函数配置内存单位由MB转换为GB,计费时长由毫秒(ms)转换为秒(s),因此,资源使用量的计算单位为GBs(GB-秒)。例如,配置为256MB的函数,单次运行了1760ms,计费时长为1760ms,则单次运行的资源使用量为(256/1024)×(1760/1000)=0.44GBs。针对函数的每次运行,平台均会计算资源使用量,并按小时汇总求和,作为该小时内的资源使用量。

也就是说,开发者进行函数创建时,所配置的内存会直接影响资源使用量,进而影响整体费用支出;所配置的超时时间,在一些极端条件下对资源使用量有影响,进而影响整体费用支出。

下面用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))

当函数内存配置为1536MB时,程序输出结果如下所示。

函数内存配置为1536MB时程序输出结果示意图 

此时,资源使用量为(1536/1024)×(3200/1000)=4.8GBs。

同样的方法,函数内存配置为250MB,程序输出结果如下所示。

函数内存配置为256MB时程序输出结果示意图 

此时的资源使用量为(256/1024)×(3400/1000)=0.85GBs。相对比上一次,程序执行时间增加了0.2s,但是资源使用量降低了将近6倍。按照该产品的资源使用量单价0.00011108元/GBs进行计算,并假设每天有1000此次请求,按照30天进行计算,那么仅计算资源使用量费用,而不计算调用次数和外网流量费用时,前后两种配置对应的费用为:

函数内存配置为1536MB:4.8×1000×0.00011108×30≈15元

函数内存配置为256MB:0.85×1000×0.00011108×30≈2.83元

可以清楚地看到,仅仅是一个功能的接口,在仅是函数内存配置不同的前提下,内存配置为1536MB的函数1个月的费用支出为15元,而后者不到3元。这也充分说明Serverless架构下需要进行资源评估,如果评估不合理,可能会多几倍的成本支出,只不过这里所说的资源评估相对于传统架构下的资源评估来说,所需要评估的资源维度更少,影响因素更简单,更容易获得资源评估的结果。

在应用上线函数之前,对资源进行基本评估时,我们可以参考如下做法。

·简单运行几次,评估一下基础资源使用量,然后设置一个稍微偏高的值。

·函数运行一段时间,得到一定的样本值,再进行数据可视化和基本的数据分析,得到一个相对稳定权威的数据。

4 工作流的加持

Serverless工作流(Serverless Workflow)是一个用来协调多个分布式任务执行的全托管云服务。

如下所示,在Serverless工作流中,用户可以用顺序、分支、并行等方式来编排分布式任务,Serverless工作流会按照设定好的步骤可靠地协调任务,跟踪每个任务的状态转换,并在必要时执行用户定义的重试逻辑,以确保任务顺利完成。Serverless工作流通过日志和审计来监视工作流的执行,方便轻松诊断和调试应用。Serverless工作流简化了开发和运行业务流程中所需要的任务协调、状态管理以及错误处理等工作,让用户聚焦于业务逻辑开发。

Serverless工作流具有以下能力和特性。

1)服务编排能力:Serverless工作流可以帮助用户将流程逻辑与任务执行分开,节省编写代码的时间。

2)协调分布式组件:Serverless工作流能够协调在不同基础架构、不同网络中以不同语言编写的应用。不管应用是从私有云(专有云)平滑过渡到混合云(或公共云),还是从单体架构演进到微服务架构,Serverless工作流都能发挥协调作用。

3)内置错误处理:Serverless工作流提供内置错误重试和捕获功能,方便用户自动重试失败或超时的任务,对不同类型错误做出不同响应,并定义回退逻辑。

4)可视化监控:Serverless工作流提供可视化界面来定义工作流和查看执行状态输入和输出等,方便用户快速识别故障位置,并快速排除故障。

5)支持长时间运行流程:Serverless工作流可以跟踪整个流程,持续长时间执行确保流程执行完。

6)流程状态管理:Serverless工作流会管理流程执行中的所有状态,包括跟踪它所处的执行步骤,以及存储在步骤之间的数据。用户无须自己管理流程状态,也不必将复杂的状态管理构建到任务中。

Serverless工作流示例  

警惕冷启动

Serverless架构下,开发者提交代码之后,通常代码只会被持久化存储并不会放在执行环境,所以当函数第一次被触发时会有一个比较漫长的环境准备过程(即冷启动过程)。这个过程包括把网络环境全部打通、将所需的文件和代码等资源准备好。一般情况下,冷启动会带来直接的性能损耗,例如某个接口正常情况下的响应时间是数毫秒,在冷启动情况下响应时间可能是数百毫秒甚至数秒,那么对于一些对延时敏感的业务来说,冷启动可能是致命的;对于一些C端产品来说,冷启动在一定程度上会让客户体验大打折扣。所以近些年来,针对Serverless架构的冷启动问题的研究非常多。本节将通过云厂商侧对冷启动优化以及开发者侧降低冷启动影响两个部分,综合对冷启动问题以及相关优化进行总结和探讨。

云厂商侧的冷启动优化方案

通过对“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”等也均对冷启动现象等进行描述和深入探讨,并且提出了一些业务侧应对函数冷启动问题的方案和策略。

通常情况下,冷启动的解决方案包括实例复用、实例预热以及资源池化,如下所示。

函数冷启动问题的常见解决方案 

1.实例复用方案

从资源复用层面来说,对实例的复用相对来说是比较重要的。一个实例并不是在触发完成之后就结束生命周期,而是会继续保留一段时间,在这段时间内如果函数再次被触发,那么可以优先被分配完成相应的请求。在这种情况下,我们可以认为函数的所有资源是准备妥当的,只需要再执行对应的方法即可,所以实例复用是大多数云厂商会采取的一个降低冷启动影响的措施。在实例复用方案中,实例静默状态下要被保留多久是一个成本话题,也是厂商不断探索的话题。如果实例保留时间过短,函数会出现较为严重的冷启动问题,影响用户体验;如果实例长期不被释放则很难被合理利用,平台整体成本会大幅提高。

值得注意的是,实例复用的时候,前一请求残留某些状态可能会影响本次请求。所以,即便Serverless架构是无状态的,我们也要考虑实例复用时,状态对业务逻辑的影响。

2.实例预热方案

从预热层面来说,我们可以通过某些手段判断函数在下一时间段可能需要多少实例,并且进行实例资源的提前准备,以解决函数冷启动问题。实例预热方案是大部分云厂商重视并不断深入探索的方向。常见的实例预热方案如下所示。

函数预热的常见方案 

被动预热通常指的是系统自动预热函数的行为。这一部分主要包括规则预热、算法预热和混合预热。所谓的规则预热是指设定一个实例数量范围(例如每个函数同一时间点最低需要0个实例,最多需要300个实例),然后通过一个或几个比例关系进行函数下一时间段的实例数量扩缩,例如设定比例为1.3,当前实例数量为110,实际活跃实例数量为100,那么实际活跃数量×所设定的比例的结果为130个实例,与当前实际存在时110个实例相比需要额外扩容20个实例,那么系统就会自动将实例数量从110提升到130。这种做法在实例数量较多和较少的情况下会出现扩缩数量过大或过小的问题(所以有部分云厂商引入不同实例范围内采用不同比例的方法来解决这个问题),在流量波动较频繁且波峰和波谷相差较大的时候会引起预热滞后问题。算法预热实际上是根据函数之间的关系、函数的历史特征以及深度学习等算法,进行下一时间段实例的扩缩操作。但是在实际生产过程中,环境是复杂的,对流量进行一个较为精确的预测是非常困难的,这也是算法预测方案迟迟没有落地的一个重要原因。还有一种方案是混合预热,即将规则预热与算法预热进行权重划分,共同预测下一时间段的实例数量,并提前决定扩缩行为及扩缩数量等。

主动预热通常指的是用户主动进行预热的行为。由于被动预热在复杂环境下的不准确性,很多云厂商提供了用户手动预留实例的能力。目前来说,主动预热主要分为简单配置和指标配置两种。所谓的简单配置,就是设定预留实例数量,或者设定某个时间范围内的预留实例数量,所预留的实例将一直保持存活状态,不会被释放;所谓的指标配置,即在简单配置基础上增加一些指标,例如当前预留的空闲容器数量小于某个值时进行某个规律的扩容,反之进行某个规律的缩容等。通常情况下,用户主动预留模式比较适用于有计划的活动,例如某平台在“双十一”期间要进行促销活动,那么可以设定“双十一”期间的预留资源以保证高并发下系统良好的稳定性和响应速度。通常情况下,主动预留可能会产生额外的费用。

3.资源池化方案

还有一种解决冷启动问题的方案是资源池化,但是通常情况下该方案带来的效果可能不是热启动,可能是温启动。所谓的温启动,就是实例所需要的相关资源已经被提前准备,但是没有完全准备好。所谓的池化,就是在实例从0到1过程中的任何一步进行一些资源预留。例如,在底层资源准备层面,可以提前准备一些底层资源;在运行时准备层面,也可以准备一些运行时资源。池化的好处是可以将实例的冷启动链路尽可能缩短,例如运行时层面的池化,可以避免底层资源准备时产生的时间消耗,让启动速度更快,同时可以更加灵活地应对更多情况,包括但不限于将池化的实例分配给不同的函数,函数被触发时可以优先使用池化资源,达到更快的启动速度。当然,池化也是一门学问,包括池化的资源规格、运行时的种类、池化的数量、资源的分配和调度等。

通常情况下,在冷启动过程中,比较耗时的环节包括网络资源打通时间(很多函数平台是在函数容器里绑定弹性网卡以便访问开发者的其他资源,这个网络响应需要达到秒级)、实例底层资源的准备时间,以及运行时等的准备时间。

开发者侧降低冷启动影响的方案

1.代码包优化

各个云厂商的FaaS平台中都有对代码包大小的限制。抛掉云厂商对代码包的限制,我们先单纯看一下代码包大小对函数冷启动产生的影响。这里从函数冷启动流程进行分析,如下所示。

函数冷启动流程 

在函数启动过程中,有一个环节是加载代码,那么当所上传的代码包过大或者文件过多导致解压速度过慢,加载代码时间就会变长,进一步使冷启动时间变长。

设想一下,当有两个压缩包,一个是只有100KB的代码压缩包,另一个是200MB的代码压缩包,两者同时在千兆内网下理想化(即不考虑磁盘的存储速度等)下载,即使最大速度可以达到125MB/s,那么前者的下载速度只有不到0.01s,后者需要1.6s。除了下载时间之外,还有文件的解压时间,那么两者的冷启动时间可能就相差2s。一般情况下,如果一个传统的Web接口响应时间在2s以上,很多业务是不能接受的,所以我们在打包代码时就要尽可能地缩小压缩包。以Node.js项目为例,打包代码包时,可以采用Webpack等方法来压缩依赖包,进一步缩小整体代码包的规格,提高函数的冷启动效率。

2.合理进行实例复用

各个云厂商为了更好地解决冷启动问题,合理地利用资源,会使用存实例复用方案。为了验证,可以创建如下两个函数。

函数1:

# -*- coding: utf-8 -*-

def handler(event, context):    

  print("Test")    

  return 'hello world'

函数2:

# -*- coding: utf-8 -*-

print("Test")

def handler(event, context):    

  return 'hello world'

在控制台多次单击“测试”按钮,对这两个函数进行测试,判断其是否在日志中输出了Test,并进行结果统计,具体如下所示。

函数测试实验结果

可以看到,实例复用的情况是存在的,因为函数2并不是每次都会执行入口函数之外的一些语句。根据函数1和函数2,也可以进一步思考,如果print("Test")功能是初始化数据库连接,或者是加载一个深度学习的模型,是不是函数1的写法会使得每次请求都要执行Print()语句,而函数2的写法可以复用已有对象? 

所以在实际项目中,一些初始化操作是可以按照函数2来实现的,例如机器学习场景下,在初始化的时候加载模型,避免每次函数被触发都会加载模型带来的效率问题,提高实例复用场景下的响应效率;数据库等连接操作可以在初始化的时候进行连接对象的建立,避免每次请求都创建连接对象。

3.单实例多并发

众所周知,各云厂商的函数计算通常是请求级别的隔离,即当客户端同时发起3个请求到函数计算,理论上会产生3个实例进行应对,这时候可能会引发冷启动问题、请求之间状态关联问题等。但是,部分云厂商提供了单实例多并发功能(例如阿里云函数计算)。该功能允许用户为函数设置一个实例并发度(Instance Concurrency),即单个函数实例可以同时处理多少个请求。

如下所示,假设同时有3个请求需要处理,当实例并发度设置为1时,函数计算需要创建3个实例来处理这3个请求,每个实例分别处理1个请求;当实例并发度设置为10时(即1个实例可以同时处理10个请求),函数计算只需要创建1个实例就能处理这3个请求。

单实例多并发的优势是减少执行时长,节省费用,例如,偏I/O的函数可以在1个实例内并发处理,减少实例数从而减少总的执行时长;请求之间可以共享状态,因为多个请求可以在1个实例内共用数据库连接池,以减少和数据库之间的连接数;降低冷启动概率,由于多个请求可以在一个实例内处理,创建实例的次数会变少,进而降低冷启动概率;减少占用VPC IP,即在相同负载下,总的实例数减少,VPC IP的占用也相应会减少。

单实例多并发的应用场景是比较广泛的,例如函数中有较多时间在等待下游服务响应的场景就比较适合使用该种功能,但其也有不适合的应用场景,例如函数中有共享状态且不能并发访问的场景,执行单个请求要消耗大量CPU及内存资源的场景。

单实例多并发功能简图 

4.预留实例

虽然预留实例模式在一定程度上违背了Serverless架构的精神,但是在目前的业务高速发展与冷启动带来的严重挑战背景下,预留模式逐渐被更多云厂商所采用,也被更多开发者、业务团队所接纳。预留实例模式在一定程度上可降低冷启动的发生次数,但并非杜绝冷启动,同时在使用预留实例模式时,配置的固定预留值会导致预留函数实例利用不充分,所以,云厂商们通常还会提供定时弹性伸缩和指标追踪弹性伸缩等多种模式进一步解决预留实例所带来的问题。

1)定时弹性伸缩:由于部分函数调用有明显的周期性规律或可预知的流量高峰,我们可以使用定时预留功能来提前预留函数实例。所谓的定时弹性伸缩,指的是开发者可以更加灵活地配置预留的函数实例,在将预留的函数实例量设定成指定时间需要的值,使函数实例量更好地贴合业务的并发量。在函数调用高峰到来前,通过第一个定时配置将预留函数实例扩容至较大的值,当流量减小后,通过第二个定时配置将预留函数实例缩容到较小的值。

2)指标追踪弹性伸缩:由于在实际生产下,函数并发规律并不容易预测,所以在一些复杂情况下我们就需要通过一些指标进行实例预留设定。例如阿里云函数计算所拥有的指标追踪弹性伸缩能力就是通过追踪监控指标实现对预留函数实例的动态伸缩。这种模式通常适用于以下场景:函数计算系统周期性采集预留的函数实例并发利用率指标,并使用该指标和配置的扩容触发值、缩容触发值来控制预留函数实例的伸缩,使预留的函数实例量贴合资源的真实使用量。

定时弹性伸缩效果示例图 

如下所示,函数计算系统根据指标情况每分钟对预留资源进行一次伸缩,当指标超过扩容阈值时,以积极的策略扩容函数实例量;当指标低于缩容阈值时,以保守的策略缩容函数实例量。如果在系统中设置了伸缩最大值和最小值,预留的函数实例量会在最大值与最小值之间进行伸缩,超出最大值时将停止扩容,低于最小值时将停止缩容。

指标追踪弹性伸缩效果示例图 

应用开发注意事项

Serverless架构下应用开发在一定程度上相对于传统架构下应用开发具有比较大的区别,例如分布式架构会让很多框架丧失一定的“便利性”,无状态特点又让很多“传统架构下看起来再正常不过的操作”变得有风险。

1 如何上传文件

在传统框架中,上传文件是非常简单和便捷的,例如Python的Flask框架:

f = request.files['file']

f.save('my_file_path') 

但是在Serverless架构下,不能直接上传文件,理由如下。

·一些云平台的API网关触发器将二进制文件转换成了字符串,不便直接获取和存储。

·API网关与FaaS平台之间传递的数据包有大小限制,很多平台将其限制在6MB以内。

·由于无状态特性,文件存储到当前实例后,会随着实例释放而丢失。

综上,传统框架中常用的上传方案是不太适合在Serverless架构中直接使用的。在Server-less架构上传文件的方法通常有两种。

1)编码为Base64后上传,持久化到对象存储或者NAS中。这种方法可能会受到API网关与FaaS平台之间传递的数据包大小限制,所以该方法通常适用于上传头像等小文件的业务场景。

2)通过对象存储等平台来上传。因为在客户端直接通过密钥等信息将文件传到对象存储是有一定风险的,所以通常情况是在客户端发起上传预请求,函数计算根据请求内容执行预签名操作,并将预签名地址返给客户端,客户端再使用指定的方法进行上传,上传完成之后,可以通过对象存储触发器等对上传结果进行更新等,如下所示。

Serverless架构下文件上传方案示例 

2 文件读写与持久化方法

应用在执行过程中可能会涉及文件的读写操作,或者一些文件的持久化操作。在传统的云主机模式下,系统通常可以直接读写文件,但是在Serverless架构下并不是这样的。

由于FaaS平台的无状态特性,并且文件用过之后会被销毁,所以文件不能直接持久化存储在实例中,但可以持久化存储在其他的服务中,例如对象存储、NAS等。

同时,在不配置NAS的情况下,FaaS平台中的/tmp目录具有可写权限,所以部分临时文件可以缓存在/tmp文件夹下。

3 慎用部分Web框架的特性

1.异步

函数计算是请求级别的隔离,所以可以认为这个请求结束了,实例就有可能进入静默状态。而在函数计算中,API网关触发器通常是同步调用。(以阿里云函数计算为例,通常只在定时触发器、OSS事件触发器、MNS主题触发器和IoT触发器等情况下是异步触发,)这就意味着当API网关将结果返给客户端时,整个函数就会进入静默状态或者被销毁,而不会继续执行完异步方法,所以通常情况下像Tornado等框架就很难在Serverless架构下发挥其异步的作用。当然,如果使用者需要异步执行,可以参考云厂商提供的异步方法。以阿里云函数计算为例,阿里云函数计算为用户提供了异步调用功能。当异步调用触发函数后,函数计算会将触发事件放入内部队列,并返回请求ID,不返回具体的调用情况及函数执行状态。用户如果希望获得异步调用函数的执行结果,则可以通过配置异步调用目标来完成,如下所示。

函数异步调用功能原理简图 

2.定时任务

Serverless架构下,实例一旦完成当前请求,就会进入静默状态,甚至被销毁,这就导致一些自带定时任务的框架没有办法正常执行定时任务。因为函数计算通常是由事件触发,不会自主定时启动,例如Egg项目中设定了一个定时任务,但是在实际的函数计算中如果没有通过触发器触发该函数,那么该函数不会被触发,也不会从内部自动启动来执行定时任务。此时,我们可以使用各个云厂商为其FaaS平台提供的定时触发器,通过定时触发器触发指定方法来替代定时任务。

4 应用组成结构注意事项

Serverless架构下,静态资源更应该在对象存储与CDN的加持下对外提供服务,否则所有的资源都在函数中,并通过函数计算对外暴露,不仅会让函数的真实业务逻辑并发度降低,也会造成更多的成本支出。尤其是将一些已有的程序迁移到Serverless架构上,如Wordpress等,我们更要注意将静态资源与业务逻辑进行拆分,否则在高并发情况下,性能与成本都将会受到比较严峻的考验。

在众多云厂商中,函数计算收费标准依据的都是运行时间、配置的内存,以及产生的流量。如果一个函数的内存配置不合理,会导致成本成倍增加。想要保证内存配置合理,更要保证业务逻辑结构可靠。

以阿里云函数计算为例,当一个应用有两个对外接口,其中一个接口的内存占用在128MB以下,另一个接口的内存占用稳定在3000MB左右。这两个接口平均每天会被触发10000次,并且时间消耗均在100ms左右。如果两个接口写到一个函数中,那么这个函数内存配置可能在3072MB左右,当用户请求内存消耗较少的接口时,在冷启动的情况下难以有较好的性能表现;如果两个接口分别写到两个函数中,两个函数内存分别配置成128MB、3072MB,性能表现较好,如下所示。

函数资源合理拆分对比

可以看出,合理、适当地拆分业务在一定程度上能节约成本。上例成本节约近50%。 

5 如何实现WebSocket

WebSocket是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工(Full-duplex)通信,即允许服务器主动发送信息给客户端。而对于HTTP协议,服务端需推送的数据仅能通过轮询的方式来让客户端获得。

基于传统架构实现WebSocket协议是比较困难的,那么在Serverless架构下实现WebSocket协议呢?众所周知,部署在FaaS平台的函数通常情况下是由事件驱动的,且不支持WebSocket协议,因此Serverless架构下是否可以实现WebSocket协议是一个问题,如果可以实现,相对传统架构来说,难度是否会降低也是一个值得探索的话题。

其实,Serverless架构下是可以实现WebSocket协议的,而且基于Serverless架构实现WebSocket协议非常简单。

由于函数计算是无状态且触发式的,即在有事件到来时才会被触发,因此,为了实现WebSocket,函数计算与API网关相结合,通过API网关承接及保持与客户端的连接,即API网关与函数计算一起实现了服务端,如下所示。

Serverless架构下WebSocket实现原理简图 

客户端有消息发出时,先传递给API网关,再由API网关触发函数执行。服务端云函数向客户端发送消息时,先由云函数将消息POST到API网关的反向推送链接,再由API网关向客户端推送消息。

Serverless架构下在API网关处的业务流程简图如下所示。

整个流程如下。

1)客户端在启动的时候和API网关建立了WebSocket连接,并且将自己的设备ID告知API网关。

2)客户端在WebSocket通道发起注册信令。

3)API网关将注册信令转换成HTTP协议发送给用户后端服务,并且在注册信令上加上设备ID参数(增加在名称为x-ca-deviceid的header中)。

4)用户后端服务验证注册信令,如果验证通过,记住用户设备ID,返回200应答。

5)用户后端服务通过HTTP、HTTPS、WebSocket三种协议中的任意一种向API网关发送下行通知信令,请求中携带接收请求的设备ID。

6)API网关解析下行通知信令,找到指定设备ID的连接,将下行通知信令通过WebSocket连接发送给指定客户端。

7)客户端在不想收到用户后端服务通知的时候,通过WebSocket连接发送注销信令给API网关,请求中不携带设备ID。

8)API网关将注销信令转换成HTTP协议发送给用户后端服务,并且在注册信令上加上设备ID参数。

9)用户后端服务删除设备ID,返回200应答。


Serverless架构下在API网关处的业务流程简图 

Serverless架构下在API网关处具体实现WebSocket原理如下所示。

如果将上面的整个流程进一步压缩,可以得到核心的4个流程:

①开通分组绑定的域名的WebSocket通道;

②创建注册、下行通知、注销3个API,给这3个API授权并上线;

③用户后端服务实现注册,注销信令逻辑,通过SDK发送下行通知;

④下载SDK并嵌入客户端,建立WebSocket连接,发送注册请求,监听下行通知。

在这4个流程中,

第一个流程是准备工作,

第二个流程涉及API网关处实现WebSocket协议的配置流程,

第三个流程和第四个流程涉及Serverless架构下基于API网关实现WebSocket协议信息推动的核心功能。

第二个流程涉及注册、下行、注销3个API。其实际上对应实现WebSocket的3种管理信令。

Serverless架构下在API网关处实现WebSocket的原理 

1)注册信令:客户端发送给用户后端服务的信令,起到如下两个作用。

·将客户端的设备ID发送给用户后端服务,用户后端服务需要记住这个设备ID。用户不需要定义设备ID字段,设备ID字段由API网关的SDK自动生成。

·用户可以将此信令定义为携带用户名和密码的API,用户后端服务在收到注册信令之后,可以验证客户端的合法性。用户后端服务在返回注册信令应答的时候,若返回非200,API网关视注册失败。客户端要想收到用户后端服务发送的通知,需要先发送注册信令给API网关,收到用户后端服务返回的200应答后认为注册成功。

2)下行通知信令:用户后端服务在收到客户端发送的注册信令后,记住注册信令中的设备ID字段,然后向API网关发送接收方为该设备的信令。只要该设备在线,API网关就可以将该信令发送到接收方的客户端。

3)注销信令:客户端在不想收到用户后端服务的通知时发送注销信令给API网关,收到用户后端服务返回的200应答后认为注销成功,不再接收用户后端服务推送的下行消息。

综上所述,若想基于Serverless架构实现WebSocket,在一定程度上与API网关搭配是非常有必要的。客户端与API网关保持WebSocket连接,API网关与函数计算通过事件进行触发,既能保证Serverless架构的优势与交付的核心心智,又能保证WebSocket功能的快速实现。

6 善于利用平台特性

各个云厂商的FaaS都有一些平台特性。所谓的平台特性,就是这些平台所具有的功能可能并不是CNCF WG-Serverless Whitepaper v1.0中规定的,仅仅是云厂商根据自身业务发展和诉求,从用户角度出发开发出来的,并且这些功能可能只被某个云平台或者某几个云平台所拥有。这些功能一般情况下如果利用得当会让业务性能等有质的提升,例如阿里云函数计算平台提出的PreFreeze和PreStop。

以阿里云函数计算为例,首先挖掘平台发展过程的痛点(尤其是阻碍传统应用平滑迁移至Serverless架构的痛点)。

·异步背景指标数据延时或丢失:如果在请求期间没有发送成功,则可能被延时至下一次请求,或者数据点被丢弃。

·同步发送指标增加延迟:如果在每个请求结束后都调用类似Flush接口,不仅增加了每个请求的延时,对后端服务也产生了不必要的压力。

·函数优雅下线:实例关闭时应用有清理连接,关闭进程,上报状态等需求。而在函数计算中,实例下线时机开发者无法掌握,缺少Webhook通知函数实例下线事件。

根据这些痛点,阿里云发布了运行时扩展功能。该功能在现有的HTTP服务编程模型上扩展,在已有的HTTP服务器模型中增加了PreFreeze和PreStop Webhook。

扩展开发者负责实现HTTP handler,监听函数实例生命周期事件。

扩展编程模型工作简图如下所示。

扩展编程模型工作简图 

1)PreFreeze:在每次函数计算服务决定冷冻当前函数实例前,函数计算服务会调用HTTP GET /pre-freeze路径,扩展开发者负责实现相应逻辑以确保完成实例冷冻前的必要操作,例如等待指标发送成功等。

函数调用InvokeFunction的时间不包括PreFreeze hook的执行时间,如下所示。

PreFreeze时序图 

2)PreStop:在每次函数计算服务决定停止当前函数实例前,函数计算服务会调用HTTP GET /pre-stop路径,扩展开发者负责实现相应逻辑以确保完成实例释放前的必要操作,如关闭数据库链接,以及上报、更新状态等,如下所示。

PreStop时序图

posted @ 2023-01-27 10:59  muzinan110  阅读(111)  评论(0编辑  收藏  举报