1.4.1 平台
本节将介绍典型的商用函数计算服务Lambda和Azure Function,以及两个开源的系统OpenWhisk和Knative。
1.4.1.1 支持1∶1触发的AWS Lambda
AWS于2014年11月推出Lambda函数计算服务,可响应事件运行代码,并自动管理计算资源,从而轻松构建快速响应事件的应用程序。当时的AWS Lambda应用场景是图片上传、应用内事件、网站单击或连接设备的输出事件等,其目标是在这些事件发生后的1毫秒内开始自动启动代码运行并处理事件。此外,根据自定义请求自动触发的后端服务也是其应用场景。Lambda可以实现自动弹性扩容,降低了基础设施运维管理成本,也减轻了流量动态变化时的应用扩容和缩容负担。除了架构创新,Lambda还创新了计费模式,将以往按照资源使用时长(以小时为单位)的计费模式,调整为以100ms(2020年AWS re:Invent大会宣布以1ms为计费单位)为计费单位,可以大幅降低一些后台任务或请求量较低的后端服务的资源开销。
Lambda是第一个商用的函数计算服务,接下来我们将以Lambda的架构为例,介绍事件在Lambda中处理的流程及其组件的基本功能,然后介绍其编程模型及当前的一些新特性和趋势。
图1-13是AWS Lambda的逻辑架构[2],分为控制面和数据面。控制面分为开发者工具和控制面接口,数据面主要用于支撑对函数的同步、异步调用,以及处理对函数的请求。
图1-13 AWS Lambda的逻辑架构(详见AWS Reinvent演讲)
第一部分控制面包含如下内容。
• 开发者工具:Lambda Console是给开发人员使用的Web 控制台,用来编辑、管理函数。SAM CLI是Lambda提供的命令行工具,同时抽象了函数部署的模型,方便开发人员使用命令行管理函数、自动化部署等。
• 控制面接口:提供给控制台/SAM CLI调用,是进行函数生命周期管理、代码包管理的接口。
第二部分数据面包含如下内容。
• Pollers/State Manager/Leasing Service:Pollers处理拉模型下的Poll触发器,从SQS、Kinesis、Dynamodb服务中异步获取事件,然后将事件抛给Frontend Invoke处理。State Manager和Leasing Service配合Pollers完成上述过程。
• FrontEnd Invoke:处理对函数的同步请求和异步请求,请求Worker Manager获取空闲的函数实例,并将对函数的请求转发给该函数实例。
• Counting Service:监控租户在整个Region上的并发度,如果请求超过并发上限,可按照用户的请求提高并发上限。
• Worker Manager:管理函数实例的状态空闲或繁忙,并接收FrontEnd Invoke的请求,返回可用的实例信息。
• Worker:准备执行租户函数代码的安全环境。
• Placement Service:池化调度服务,将Worker分配给Worker Manager,并通过调度策略通知Worker启动安全沙箱,下载函数代码并执行。
Lambda采用1∶1的触发模型,只要没有空闲的函数实例,Lambda就会启动新的函数实例来处理新的请求,下面是其典型的请求处理流程,如图1-14所示。
① FrontEnd Invoke收到函数的请求,进行鉴权检查,并判断其是否超过该租户的并发上限。
② FrontEnd Invoke没有找到可用的函数实例,向Worker Manager申请新的函数实例。
③ Worker Manager没有找到合适的Worker,向Placement Service申请新的Worker。
④ Placement Service分配新的Worker,并通知Worker Manager。
⑤ Worker Manager通知Worker拉起沙箱,下载函数代码、初始化Runtime,完成函数实例准备,并通知FrontEnd Invoke,提供到函数实例的路由信息。
⑥ FrontEnd Invoke将请求转给函数实例,函数实例完成消息处理后,通知Work Manager当前实例已空闲,可以接收下一次请求。
图1-14 Lambda中请求处理的典型流程
如果函数实例未被系统回收,那么对函数的请求可以直接处理,不需要经历②~⑤的流程。
从运行模型的角度看,Lambda采用轻量级虚拟机技术来保证逻辑多租时的安全性,并提供面向多语言的Runtime来执行不同的函数,如图1-15所示。
图1-15 Lambda的运行模型
Lambda的节点基于裸金属服务器而非虚拟机,沙箱基于容器技术。对于物理多租的场景,比如容器服务出现逃逸攻击等安全问题,其风险仅限于该租户。而函数计算是逻辑多租的,某个租户的函数出现安全问题会影响所有租户。因此,Lambda自主研发了轻量级虚拟机(MicroVM)技术FireCracker,基于KVM(基于内核的虚拟机)并使用Rust语言开发实现,可以有效保证租户函数的安全。FireCracker相比基于QEMU的虚拟机更为轻量,其基础资源占用内存小于5MB,部署密度较高。不过,FireCracker的冷启动时间还是相对比较高的,约为125ms,这是影响函数冷启动时间的主要因素。Lambda的数据面在裸金属服务器上运行,而不在EC2的虚拟机上运行,也缘于此。嵌套虚拟化的资源开销过高,影响执行效率及成本。FireCracker提供安全、低开销的隔离虚拟机,在虚拟机中运行容器沙箱,并针对不同语言(如Java、JavaScript等)提供函数的运行时环境。在Lambda最上层的为函数代码,Runtime将事件转给代码处理,完成后再由Runtime返回。
Lambda提供了同步、异步编程模型,也提供了不同的错误自动处理机制。以Lambda文档中JavaScript代码为例,Lambda提供了invoke(params = {}, callback)接口完成对函数的同步调用和异步调用。该接口默认为同步调用,第二个参数为回调函数,处理成功或失败的响应。当请求失败时,Lambda最多可能会重试两次,彻底失败后抛出异常,下面是同步调用的代码样例。
异步调用使用相同接口,只是在请求的参数中,需要将InvocationType修改为Event。Lambda会把异步的请求先发送到消息队列中,如果函数限于并发能力无法及时处理事件,则可能出现事件丢失甚至事件重复发送的情况,所以函数处理的业务最好保证幂等。Lambda提供了死信队列功能,用于保存出错的异步请求或没有处理的事件。用户配置好死信队列后,异步请求出错或未处理的事件会被发送到死信队列(基于SQS服务),用户可以根据自己的业务逻辑来继续处理这些异步请求失败的事件。下面是Lambda进行异步调用的代码样例。
DataDog是一个专注于云基础设施监控和安全的公司,2020年DataDog在向AWS提供的Serverless服务调研报告(详见DataDog官网)中指出,约有50%的AWS用户使用了Lambda,而在使用容器的用户中,有80%使用了Lambda。从DataDog的报告中可以发现,上云的客户可能呈现从虚拟机到容器再到函数的使用趋势。AWS在2020年re:Invent上发布的一些新特性会加速这一趋势,具体分析如下。
• 计费单位改为1ms:过去Lambda的计费模型是以100ms为计费单位的,更换为1ms的计费单位后,用户使用函数的成本可能会大幅降低。如果函数的实际执行时间为28ms,按照以前的计费方式,对于1GB的内存配置,6000万次请求的资源开销为100$,而按照以1ms为粒度的计费模式只需要28$,开销降低了72%。
• 内存以1MB为步长计费:过去的Lambda函数的内存规格以128MB起步,以64MB为步长增加。如果函数只需要140MB的内存,可申请的规格也只能是192MB,有52MB的内存被浪费了。以1MB为计费步长,可以降低函数的使用成本。
• 最大支持6VCPU和10GB内存配置,并支持AVX2指令集:提升函数最大资源规格,可以将函数计算的应用范围拓展到机器学习和推理、视频处理、高性能计算、科学模拟及财经建模等丰富场景。AVX2指令集是Intel服务器CPU支持的指令集,可以在每个CPU周期进行更多整数和浮点数运算,对于图片处理等场景可以提升30%的性能。Lambda不会对此收取额外费用,用户只需要重新编译依赖的类库以增加对AVX2的支持。
• 自定义容器:Lambda以前只支持自定义Runtime,只能在层功能的基础上实现。层(Layer)类似于容器的分层文件系统,用于在函数间共享代码。自定义容器的自由度更大,开发者可以将业务代码打包成容器,并集成Lambda Runtime API,将其发布到AWS的容器镜像服务后,就可以在函数创建时将其指定为镜像。自定义容器极大地方便了开发者在本地进行调测,也方便容器化的应用近似无缝地向Lambda迁移。
从Lambda的最新特性不难看出其正在通过以下三种方式加速开发者向Lambda迁移。
• 降低成本:计费形式的改变及对指令集的支持(提升性能会减少运行时间)都进一步降低了Lambda的使用成本。这是吸引开发者从虚拟机、容器向函数服务迁移的最大动力。
• 扩大应用场景:新的资源规格将Lambda的触手伸向了功能和性能要求更高的领域。例如,近年来学术界出现了基于函数进行高性能计算、机器学习的研究案例。虽然函数的资源(CPU、内存、I/O和网络带宽等)是受限的,但是其扩展性强。例如,2017年伯克利大学的里斯实验室推出了基于Lambda的PyWren框架[3],并发2800个函数实例,其峰值算力达到了40TFLOPS,峰值I/O达到了80GB/s(读)、60GB/s(写)。这意味着不用去申请和管理高性能计算集群,通过函数计算平台也可以获得高性能的算力。
• 降低容器应用迁移成本:自定义容器可以极大地降低容器应用迁移到Lambda的成本,开发者不用修改业务代码,只需增加一段支持Lambda Runtime API的脚本。开发者也可以不使用Lambda的编程模型,继续使用以往的开发框架及工具集,使本地调测更容易。在开发效率不降低的同时,发布和维护会大幅简化。
从AWS Lambda推出的新特性不难看出,AWS在不断地扩大Serverless的应用场景,同时不停降低其成本,让基于容器、虚拟机的服务更容易地迁移到函数平台上。这不禁会让人遐想,Serverless的未来可能真会如伯克利所预言的那样,成为云时代默认的计算范式。
1.4.1.2 支持数据绑定的Azure Function
Azure的函数计算平台和Lambda的实现机制不同,在使用方式上也有差异,但整体也采用事件驱动的架构,并提供了多种事件源的接入,如图1-16所示。
图1-16 Azure Function逻辑结构
Azure和Lambda的具体差异如下。
• 触发模型:Lambda是1∶1的触发模型,即每个函数实例只处理一个事件,如果有新的事件,系统启动新的函数实例来处理。而Azure Function则是1∶N的模型,一个函数实例处理多个请求。
• 管理粒度:Lambda管理的粒度是函数,而Azure管理的粒度是App,每个App下可以有多个函数,App对Function进行统一管理。同理,Azure的扩容粒度也是App。
• BaaS访问:Lambda并没有抽象BaaS(对象存储服务、数据库、缓存)的接口,函数访问不同的BaaS服务需要开发者自己接入不同的SDK。Azure Function通过Data Binding抽象了不同的BaaS服务,开发人员只要使用配置文件就可以操作数据,简化了BaaS服务使用的方式,如图1-17所示。Data Binding将函数对数据的操作抽象为in/out,开发人员只需要通过function.json配置数据in/out的信息,比如要读取的数据源是哪种BaaS服务(是消息队列还是NoSQL数据库)、对应的表是什么,进而在函数中直接操作配置Binding的对象即可,无须再调用BaaS服务SDK、创建连接的客户端等。同理,函数只需要返回对象,剩余的操作都由Azure Function运行时代为处理。
• 可移植性:Azure Function可以直接基于Kubernetes运行,结合KEDA(详见KEDA官网),可以让Azure的函数代码运行在任何Kubernetes的环境中,其可移植性比Lambda更好。
图1-17 Azure Function的编程模型
下面是Azure Function文档中Data Bindings使用示例的配置文件function.json的内容。
Bindings的配置文件有两部分,第一部分是输入即消息队列触发器的消息,包含接收事件源的消息队列Topic为myqueue-items,映射到代码中的对象名称为order;第二部分是函数输出的信息,函数会将处理完的返回值写到Azure的Table Storage中,所以direction是out。配置信息表示函数的返回值将会写到outTable中。
配置文件中的Connection是包含连接字符串的为应用程序设置的名称,用来表示对消息队列和Table Storage的连接。
Data Bindings示例对应的Javascript函数代码如下。
order是消息队列myqueue-items的一个事件,它是一个JSON对象。代码将分区的键改为Order,然后随机生成了一个ID并将其作为RowKey的值,再将order返回。
在这个过程中,代码没有创建读写BaaS服务的客户端,没有管理BaaS服务连接信息,编码量大大降低。同时,开发人员不需要去了解不同BaaS服务的SDK,Bindings的配置会隐藏这些接口,同时Azure Function的Runtime会完成剩余的实际数据操作。
1.4.1.3 支持服务型和事件型应用的Knative
Knative是基于Kubernetes生态的开源Serverless项目,其目的是提供一个容器平台,既可以支持开发者运行Serverless容器,帮助开发者解决服务型应用的负载均衡、路由及弹性扩容和缩容(Scale to 0),又可以支持事件驱动型应用的Serverless化运行。简单来说它就是服务网格和Lambda的结合体,只是它没有编程模型的约束。2019年,Google发布基于Knative的新服务Cloud Run,这也是开源FaaS平台中少数实现商用的项目。
Knative基于Kubernetes,主要由两大部分组成:Serving(应用容器运行,基于Istio能力实现负载均衡、流量管理及自动扩容)和Eventing(支持事件驱动的应用,对接云服务商或其他事件源),如图1-18所示。本节主要介绍Eventing的相关内容。
图1-18 Knative的组成和基本功能
Knative Eventing旨在满足微服务开发的通用需求,提供可组合的方式绑定事件源和事件消费者,其设计目标如下。
• 提供松耦合服务,可独立开发和部署。松耦合服务可跨平台使用(如Kubernetes、VM、SaaS、FaaS)。
• 事件的生产者(事件源)和消费者相互独立。事件的生产者可以于事件的消费者监听之前产生事件;同样,事件的消费者可以于事件产生之前监听事件。
• 支持第三方的服务对接。
• 确保跨服务/云平台的互操作性,遵循CNCF的Cloud Event规范,详见1.4.3节。
Eventing主要由事件源(EventSource)、事件处理(Flow)及事件消费者(Event Consumer)三部分构成,如图1-19所示,Eventing支持的事件源较多,包括典型的消息队列(Kafka、RabbitMQ)、数据库(CouchDB)、WebHooks(Gitlab、Github)、WebSocket,以及第三方事件源(如AWS云侧服务、Slack等)。
图1-19 Eventing的组成
如图1-20所示,Eventing支持三种事件处理方式。
• 事件直接处理:通过事件源直接转发到单一事件消费者。
• 事件转发和持久化:通过事件通道及事件订阅转发事件,以保证事件不丢失并可进行缓冲处理。订阅事件可以将事件发给多个消费者处理(扇出)。
• 事件过滤:由Broker接收事件源发送的事件,通过事先定义好的一个或多个Trigger将事件发送给消费者,并且可以按照事件的属性进行过滤。
图1-20 Eventing的三种事件处理方式
事件消费者是用来最终接收事件的,Eventing定义了两个通用的接口作为事件消费者。
• Addressable:提供可用于事件接收和发送的HTTP请求地址,并通过status.address.hostname字段定义。
• Callable:接收并转换事件,可以按照处理来自外部事件源事件的方式,对这些返回的事件做进一步处理,以用于事件转发的场景。
Eventing中的组件都以自定义资源的方式部署,其扩展性较好,如果需要支持新的事件源、Broker及Trigger,只需要按照接口实现部署即可。对于熟悉Kubernetes生态的开发者,Knative Eventing是一个不错的选择。
1.4.1.4 支持多平台部署的OpenWhisk
OpenWhisk是IBM发起的开源Serverless平台,现已被捐献给Apache基金会,在2019年7月份晋升为Apache基金会顶级项目。
与IBM云平台同源的OpenWhisk,有很多优秀的特性,比如支持多平台部署(Docker、Kubernetes、Openshift、Mesos等),开发者通过docker-compose就可以将整个平台在自己的PC上运行起来。OpenWhisk支持多语言,支持同步、异步编程方式,提供Composer来实现函数的编排(基于函数的编程模型),降低了整体的学习成本。在扩展性方面,OpenWhisk支持按照请求的弹性伸缩。
OpenWhisk是事件驱动的编程模型,包含Action、Trigger、Rule、Feed等概念。OpenWhisk的编程模型如图1-21所示。
• Event Source(事件源):生成事件的服务,事件通常反映数据的变化或本身携带的数据,如消息队列中的消息、数据库中数据的变化、网站或Web应用交互、对API的调用等。
• Feed(事件流):属于某个触发器的事件流。OpenWhisk支持以钩子、轮询和长连接的方式处理事件流。
• Trigger(触发器):接收来自事件源的一类事件的管道,每个事件只能发给一个触发器。
• Rule(规则):关联触发器和函数的规则。与其他Serverless平台不同,OpenWhisk可以通过规则让一个触发器绑定不同的函数,以原生支持Fan-out(扇出)。
• Action(函数):Action是在OpenWhisk上执行的无状态、短生命周期的函数。
图1-21 OpenWhisk的编程模型(详见OpenWhisk官方文档)
图1-22是OpenWhisk架构图,OpenWhisk的组件及详细作用如下。
图1-22 OpenWhisk架构图
• Nginx:Nginx是进入OpenWhisk的第一个组件,在整个系统中起着网关的作用。作为系统的反向代理,Nginx将消息转发给Controller,同时完成SSL卸载。
• Controller:Controller作为OpenWhisk的控制组件,负责函数的调用、请求负载均衡,以及函数和触发器的管理API(函数和触发器的增、删、改、查等)。Controller会通过CouchDB对请求进行鉴权,鉴权通过后再触发对函数的请求。不同于其他Serverless平台,OpenWhisk的Controller使用Scala语言开发。Controller包含Load Balancer和Activator两个组件。Load Balancer用于选择合适的Invoker来执行函数,通过健康检查感知OpenWhisk系统中可用的Invoker,进而通过哈希算法选择合适的Invoker处理请求。这样做的好处是可以将相同的函数调度到同一个Invoker上,最大程度重用缓存、容器等资源,避免容器的创建及初始化等操作开销。Activator用于处理触发器的事件,可根据规则调用触发器绑定的函数。
• Kafka:和其他Serverless平台只对异步请求使用消息队列的方式不同,OpenWhisk对于同步请求和异步请求都使用Kafka消息队列,借助Kafka的持久化能力,当系统崩溃时消息不会丢失,并且消息队列可以作为高负载下的缓冲队列,降低系统的内存占用。
• Invoker:Invoker是OpenWhisk的核心模块,用于从Kafka中读取需要触发的函数代码和参数,并根据参数启动执行函数的容器,然后返回结果。考虑到执行函数的隔离性和安全性,它使用Docker来启动运行时,执行具体的函数。
• CouchDB:CouchDB是OpenWhisk的状态存储数据库,用来存储鉴权认证信息、函数、触发器、规则等定义及函数的运行响应信息。
OpenWhisk的事件处理流程如图1-23所示,当消息通过Nginx进入Controller后,Controller根据哈希算法选择对应的Invoker来执行函数,同时为该请求生成全局唯一的ActivationID(在CouchDB中创建数据项),并将请求的事件、ActivationID等信息写入Invoker注册的消息队列中。Invoker先获取要触发的函数信息及参数,再从CouchDB获取函数代码及其元数据,然后启动对应语言运行时的容器执行函数。首次启动的容器会调用/init接口进行初始化,然后调用/run接口执行Action。根据请求的类型不同,Invoker会将结果返回到不同的地方。如果是异步请求,则Invoker会将执行结果存入CouchDB,客户端可以根据ActivationID从CouchDB中查询到结果。如果是同步请求,则Invoker会将执行结果直接写到完成的消息队列中,Controller会注册到该队列中并获得相关消息。根据消息中的ActivationID,Controller可以将正确的响应内容返回给客户端。
图1-23 OpenWhisk的事件处理流程
OpenWhisk设计架构简单、易理解,支持多平台操作,且原生支持编排(参见1.4.4)也降低了开发者的学习成本。此外,OpenWhisk的开发者生态也较为完备,支持多语言,文档完善,提供自动化工具CLI,这些方面优于其他开源Serverless平台。