Serverless工程实践:从入门到进阶
上QQ阅读APP看书,第一时间看更新

1.2 Serverless规范

当对Serverless架构的基本组成及其基本工作原理/流程有了初步了解之后,为了更加深入地理解什么是Serverless,尤其是什么是FaaS,或者说什么是函数相关的问题,还需要对Serverless规范有一定的了解。CNCF对Serverless做了一定的规范和定义,例如其描述和定义了FaaS解决方案的基本模型、函数的生命周期以及触发器类型、种类等相关规范。

本节部分内容引用自《Serverless Handbook》《CNCF Serverless Whitepaper v1.0》《Serverless Workflow Specification》《CloudEvents - Version 1.0.1》等规范文档。

1.2.1 FaaS解决方案模型

FaaS解决方案由Event Sources、FaaS Controller、Function Instance以及平台服务等元素组成,如图1-6所示。

034-01

图1-6 FaaS解决方案组成

  • Event Sources:将Event触发或流式传输到一个或多个函数实例中。
  • Function Instance:可以根据需要扩展单个函数/微服务。
  • FaaS Controller:部署、控制和监视函数实例及其来源。
  • 平台服务:FaaS解决方案使用云厂商提供的其他云服务,例如云数据库、身份校验等。

1.2.2 函数的规范与定义

1. 函数代码

函数代码、依赖项和二进制文件可以驻留在外部存储库中,或由用户直接提供。如果代码在外部存储库中,则用户需要指定路径和凭据。

Serverless框架允许用户监听代码存储库中的更改,并在每次提交时自动构建函数镜像/二进制文件。

函数可能依赖于外部库或二进制文件,这些需要由用户提供(包括描述其构建过程的方式,例如,使用Dockerfile、Zip)。

2. 函数定义

Serverless函数定义可能包含以下规范和元数据,该函数定义是特定于版本的:

  • 唯一ID;
  • 名称;
  • 说明;
  • Labels(或tags);
  • 版本ID(或版本别名);
  • 版本创建时间;
  • 上次修改时间(函数定义);
  • 函数处理程序;
  • 运行时语言;
  • 代码+依赖关系或代码路径和凭据;
  • 环境变量;
  • 执行角色;
  • 资源(所需的CPU、内存);
  • 执行超时;
  • 日志记录失败(私信列队);
  • 网络策略/VPC;
  • 数据绑定。

3. 元数据详细信息

函数框架可能包括以下函数元数据。

  • 版本:每个函数版本应具有唯一的标识符,此外,可以使用一个或多个别名(例如latest、production、beta)来标记版本。API网关和事件源会通过版本、别名等将流量/事件路由到特定的函数版本。
  • 环境变量:用户可以指定在运行时提供给函数的环境变量。环境变量也可以从平台变量等派生(例如Kubernetes EnvVar定义)。环境变量使开发人员能够控制函数行为和参数,而无须修改代码或重建函数,从而获得更好的开发人员体验和函数重用。
  • 执行角色:函数应在特定的用户或角色身份下运行,以授予和审核其对平台资源的访问权限。
  • 资源:定义所需或最大的硬件资源,例如函数使用的内存等。
  • 超时:指定函数调用在平台终止之前可以运行的最长时间。
  • 故障日志(死信队列):队列或流的路径,它将存储具有适当详细信息的失败函数执行列表。
  • 网络策略:分配给函数的网络域和策略(函数与外部服务/资源进行通信)。
  • 执行语义:指定应如何执行函数(例如,每个事件至少执行一次,最多执行一次,恰好一次)。

4. 数据绑定

某些Serverless框架允许用户指定函数使用的输入/输出数据资源,这使开发更简单、性能更好(在执行期间保留数据连接、可以预取数据等)以及安全性更高(数据资源凭证是上下文的一部分,而不是代码)。

数据绑定可以采用文件、对象、记录、消息等形式,函数说明包括一组数据绑定定义,每个定义都指定数据资源、其凭证和使用参数。数据绑定可以引用事件数据,例如,DB键是从事件username字段派生的。

5. 函数输入

函数输入包括事件数据和元数据,还包括上下文对象。

事件详细信息应传递给函数处理程序,不同的事件可能具有不同的元数据,因此需要函数能够确定事件的类型并解析公共和特定于事件的元数据。

需要将事件类与实现分离,例如,不管流存储是Kafka还是Kinesis,处理消息流的函数都可以运行,在这两种情况下,它将接收消息正文和事件元数据,消息可能在不同框架之间路由。

事件可以是单个记录(例如,在请求/响应模型中),也可以是多个记录或微批处理(例如,在流模式中)。

FaaS解决方案使用的常见事件数据和元数据的示例如下:

  • 事件类型/种类;
  • 版本;
  • 事件;
  • 事件源;
  • 来源身份;
  • 内容类型;
  • 邮件正文;
  • 时间戳。

事件/记录特定元数据的示例如下。

  • HTTP:Path、Method、Header、查询参数;
  • 消息队列:Topic、Header;
  • 记录流:表、键、操作、修改时间、旧字段、新字段。

一些实现将JSON作为事件信息的数据格式并传递给函数。对于部分性能要求严格的函数(例如,流处理)或低能耗设备(IoT),这可能会增加大量的序列化/反序列化开销。在这些情况下,可使用本地语言结构或其他序列化机制。

6. 函数上下文

调用函数时,框架提供对跨多个函数调用的平台资源或常规属性的访问,而不是将所有静态数据放入事件中或强制该函数在每次调用时初始化平台服务。

上下文(Context)可以是一组输入属性、环境变量或全局变量,或是这三者的结合。

上下文示例如下:

  • 函数名称、版本、ARN;
  • 内存限制;
  • 请求ID;
  • 地域;
  • 环境变量;
  • 安全密钥/令牌;
  • 运行时/绑定路径;
  • 日志;
  • 数据绑定。

有的实现初始化日志对象(例如,AWS中的全局变量或Azure中的部分上下文),用户可以使用平台集成的工具查看日志来跟踪函数执行。除了传统的日志记录,未来的实现可能会将计数器/监控和跟踪活动抽象为平台上下文的一部分,以进一步提高函数的可用性。

数据绑定是函数上下文的一部分,平台根据用户配置启动与外部数据资源的连接,并且这些连接可以在多个函数调用之间重用。

7. 函数输出

当函数退出时,它可能:

  • 将值返回给调用方(例如,在HTTP请求/响应示例中);
  • 将结果传递到工作流程中的下一个执行阶段;
  • 将输出写入日志。

应该有确定的方式通过返回的错误值或退出代码来知道函数是成功还是失败。

函数输出可以是结构化的(例如HTTP响应对象),也可以是非结构化的(例如某些输出字符串)。

1.2.3 函数生命周期

1. 函数部署流水线

如图1-7所示,函数的生命周期从编写代码并提供规范元数据开始,一个Builder实体将获取代码和规范,然后编译并将其转换为工件,接下来将工件部署在具有控制器实体的集群上,该控制器实体负责基于事件流量或实例上的负载来扩展函数实例的数量。

038-01

图1-7 函数部署流水线示意图

2. 函数操作

Serverless框架使用以下动作控制函数的生命周期。

  • 创建:创建新函数,包括其规范和代码。
  • 发布:创建可以在集群上部署的函数的新版本。
  • 更新别名/标签(版本):更新版本别名。
  • 执行/调用:不通过事件源调用特定版本。
  • 事件源关联:将函数的特定版本与事件源连接。
  • 获取:返回函数元数据和规范。
  • 更新:修改函数的最新版本。
  • 删除:删除函数,可以删除特定版本或所有版本的函数。
  • 列表:显示函数及其元数据的列表。
  • 获取统计信息:返回有关函数运行时使用情况的统计信息。
  • 获取日志:返回函数生成的日志。

上述操作在实际过程中的流程示意图如图1-8所示。

038-02

图1-8 函数创建/更新流程示意图

1)在创建函数时,提供其元数据作为函数创建的一部分,将对其进行编译使其具有可发布的特性,接下来可以启动、禁用函数。函数部署需要能够支持以下用例。

①事件流:在此用例中,队列中可能始终存在事件,但是可能需要通过请求暂停/恢复进行处理;

②热启动:在任何时候具有最少实例数的函数,使得所接收的“第一”事件具有热启动,因为该函数已经部署并准备好为事件服务(而不是冷启动),其中函数获得通过“传入”事件在第一次调用时部署。

2)用户可以发布一个函数,这将创建一个新版本(最新版本的副本),发布的版本可能会被标记或有别名。

3)用户可能希望直接执行/调用函数(绕过事件源或API网关)以进行调试和开发过程。用户可以指定调用参数,例如所需版本、同步/异步操作、详细日志级别等。

4)用户可能想要获得函数统计(例如调用次数、平均运行时间、平均延迟、失败、重试等)。

5)用户可能想要检索日志数据。这可以通过严重性级别、时间范围、内容来过滤。Log数据是每个函数级别的,它包括诸如函数创建和删除、警告或调试消息之类的事件,以及可选的函数的Stdout或Stderr。优选每次调用具有一个日志条目或者将日志条目与特定调用相关联的方式(以允许更简单地跟踪函数执行流)。

3. 函数版本控制和别名

一个函数可能具有多个版本,使用户能够运行不同级别的代码,例如beta/production、A/B测试等。使用版本控制时,默认情况下函数版本为latest,latest版本可以进行更新和修改,可能会在每次更改时触发新的构建过程。

如果用户想要冻结一个版本可以使用发布操作,该操作将创建一个具有潜在标签或别名(例如beta、production)的新版本以配置事件源,事件或API调用可以被路由到特定的函数版本。非最新的函数版本是不可变的(它们的代码以及所有或某些函数规范),并且一旦发布就不能更改。函数不能“未发布”,而应将其删除。另外,当前的大多数实现都不允许函数branch/fork(更新旧版本代码),因为这会使实现和用法变得复杂,但是将来可能需要这样做。

当同一函数有多个版本时,用户必须指定要操作的函数版本以及如何在不同版本之间划分事件流量。例如,用户可以决定路由90%的事件流量到稳定版本,10%的流量到Beta版(又称canary update)。可以通过指定确切版本或通过指定版本别名来实现,版本别名通常将引用特定的函数版本。

用户创建或更新函数时,它可能会根据变更的性质来驱动新的构建和部署。

4. 事件源到函数关联

由于事件源触发事件而调用函数。函数和事件源之间存在一个n:m映射。每个事件源都可以用于调用多个函数,而一个函数可以由多个事件源触发。事件源可以映射到函数的特定版本或函数的别名,后者提供了一种用于更改函数并部署新版本的方法,而无需更改事件关联。事件源还可以定义为使用同一函数的不同版本,并定义应为每个函数分配多少流量。

创建函数后或稍后的某个时间,需要关联事件源,该事件源应触发作为该事件的函数调用。这需要一系列动作和方法,例如:

  • 创建事件源关联;
  • 更新事件源关联;
  • 列出事件源关联。

5. 事件源

不同类型的事件源如下所示。

  • 事件和消息传递服务,例如RabbitMQ、MQTT、SES、SNS、Google Pub/Sub。
  • 存储服务,例如S3、DynamoDB、Kinesis、Cognito、Google Cloud Storage,Azure Blob、iguazio V3IO(对象/流/数据库)。
  • 端点服务,例如物联网、HTTP网关、移动设备、Alexa、Google Cloud Endpoint。
  • 配置存储库,例如Git、CodeCommit。
  • 使用特定于语言的SDK的用户应用程序。
  • SchEnable定期调用函数。

尽管每个事件提供的数据在不同事件源之间可能会有所不同,但事件结构应该具有通用性,能够封装有关事件源的特定信息(详细信息见事件数据和元数据)。

6. 函数要求

根据当前的技术水平,函数和Serverless运行时应满足的一组通用要求如下。

  • 函数必须与不同事件类的基础实现分离。
  • 可以从多个事件源调用函数。
  • 无须为每个调用方法使用不同的函数。
  • 事件源可以调用多个函数。
  • 函数可能需要一种与基础平台服务进行持久绑定的机制,可能是跨函数调用。函数的寿命可能很短,但是如果需要在每次调用时都进行引导,那么引导可能会很昂贵,例如在日志记录、连接、安装外部数据源的情况下。
  • 同一个应用程序中每个函数可以使用不同的语言编写。
  • 函数运行时应尽可能减少事件序列化和反序列化的开销(例如,使用本地语言结构或有效的编码方案)。

7. 工作流相关要求

工作流相关要求如下:

  • 函数可以作为工作流的一部分被调用,一个函数的结果可以作为另一个函数的触发;
  • 可以由事件或“and/or事件组合”触发函数;
  • 一个事件可能触发按顺序或并行执行的多个函数;
  • “and/or事件组合”可能触发顺序运行、并行运行或分支运行的m个函数;
  • 在工作流的中间,可能会收到不同的事件或函数结果,这将触发分支切换到不同的函数;
  • 函数的部分或全部结果需要作为输入传递给另一个函数;
  • 函数可能需要一种与基础平台服务进行持久绑定的机制,这可能是跨函数调用或函数寿命很短。

8. 函数调用类型

函数调用类型如图1-9所示,可以根据不同的用例从不同的事件源调用函数,例如:

  • 同步请求(Req/Rep),例如HTTP请求、gRPC调用。
  • 客户发出请求并等待立即响应。
  • 异步消息队列请求(发布/订阅),例如RabbitMQ、AWS SNS、MQTT、电子邮件、对象(S3)更改、计划事件(如CRON作业)。
  • 消息发布到交换机并分发给订阅者;
  • 没有严格的消息排序,以单次处理为粒度。
  • 消息/记录流:例如Kafka、AWS Kinesis、AWS DynamoDB Streams。
  • 通常,每个分片使用单个工作程序(分片消费者)将流分片为多个分区/分片;
  • 可以从消息、数据库更新(日志)或文件(例如CSV、JSON、Parquet)生成流;
  • 事件可以推送到函数运行时或由函数运行时拉动。
  • 批量作业,例如ETL作业、分布式机器学习、HPC模拟。
  • 作业被调度或提交到队列,并在运行时中使用并行的多个函数实例进行处理,每个函数实例处理工作集的一个或多个部分(任务);
  • 当所有并行工作程序完成所有计算任务时,作业完成。
041-01

图1-9 函数调用类型

1.2.4 其他规范

众所周知,Serverless应用是由事件驱动的,当应用观察的事件源中有情况发生时,就会触发相应的函数。函数执行后会到达某个状态,就像状态机一样,随着一系列的事件发生,会触发函数顺序,并会并行运行,这里就会涉及事件数据结构、传递与工作流的规范。

1. CloudEvent

Serverless应用是由事件驱动的,事件产生者倾向于以不同的方式描述事件,这就导致Serverless应用在同一云厂商的不同类型的事件中是不同的,或者同一种事件在不同云厂商中的表现是不同的。缺少通用的事件描述方式意味着开发人员需要不断重新学习如何使用事件。这也限制了库、工具和基础架构帮助跨环境(例如SDK、事件路由器或跟踪系统)传递事件数据的潜力,让事件数据实现的可移植性和生产率受影响。

CloudEvent是以通用格式描述事件数据的规范,以提供跨服务、平台和系统的互操作性。事件格式指定如何使用某些编码格式序列化CloudEvent。支持这些编码兼容CloudEvent的实现必须遵守相应事件格式中指定的编码规则。所有实现都必须支持JSON格式。

图1-10所示是对CloudEvent中部分术语的关系描述。

042-01

图1-10 CloudEvent基础流程与名字关系简图

  • Occurrence(发生):是在软件系统运行期间捕获的事实陈述。这可能是由于系统发出的信号或系统正在观察的信号因状态变化、计时器完成或任何其他值得注意的活动而发生的。例如,由于电池电量不足或虚拟机将要执行重启计划,设备可能会进入警报状态。
  • Event(事件):表示Occurrence及其上下文的数据记录。Event从Event生产者(Source)路由到感兴趣的Event使用者。Event路由可以基于Event中包含的信息,但是Event不会标识特定的路由目的地。Event包含两种类型的信息:表示Occurrence的Event Data和提供有关Occurrence上下文信息的Context元数据。一次发生可能会导致多个事件。
  • Producer(生产者):是创建描述CloudEvent的数据结构的特定实例、过程或设备。
  • Source(源):是发生事件的上下文。在分布式系统中,Source可能包含多个Producer。如果Source不知道CloudEvent,则外部Producer将代表Source创建CloudEvent。
  • Consumer(消费者):接收事件并对其采取行动。Consumer使用上下文和数据执行某些逻辑,这可能导致新Event的发生。
  • Intermediary(中介):接收包含Event的消息,目的是将Event转发到下一个接收者,该接收者可能是另一个Intermediary或Consumer。Intermediary的典型任务是根据Context中的信息将Event路由到接收者。
  • Context(上下文):元数据封装在Context Attribute(上下文属性)中。工具和应用程序代码可以使用此信息来标识Event与系统各方面或其他Event的关系。
  • Data(数据):有关事件的特定于域的信息(即有效负载)。这可能包括有关Occurrence的信息,有关已更改数据的详细信息或更多信息。
  • Message(消息):Event是通过消息从源传递到目的地。
  • Protocol(协议):可以通过各种行业标准协议(例如HTTP、AMQP、MQTT、SMTP)、开源协议(如Kafka、NATS)或特定于平台/供应商的协议(AWS Kinesis、Azure Event Grid)传递Message。

(1)Context Attribute(上下文属性)

每个符合此规范的CloudEvent必须包含指定为REQUIRED的上下文属性,并且可以包括一个或多个OPTIONAL上下文属性。

这些属性虽然描述了事件,但设计为可以独立于事件数据进行序列化。这样就可以在目的地对它们进行检查,而不必对事件数据进行反序列化。

(2)属性命名规范

CloudEvent规范定义了到各种协议和编码的映射,随附的CloudEvent SDK针对各种运行时和语言。其中一些将元数据元素视为区分大小写,而其他元素则不区分大小写,并且单个CloudEvent可能会通过涉及协议、编码和运行时混合的多个跃点进行路由。因此,本规范限制了所有属性的可用字符集,以防止区分大小写问题或与通用语言中标识符的允许字符集相冲突。CloudEvent属性名称必须由ASCII字符集的小写字母(a~z)或数字(0~9)组成,并且必须以小写字母开头。属性名称应具有描述性和简洁性,长度不得超过20个字符。

(3)类型系统

以下抽象数据类型可用于属性,每个类型都可以以不同的方式表示,包括“通过不同的事件格式”和“在传输元数据字段中”。该规范为所有实现必须支持的每种类型定义了规范的字符串编码。

1)Required属性。以下属性必须出现在所有CloudEvent中。

  • id:String
  • source:URI-reference
  • specversion:String
  • type:String

2)OPTIONAL属性。以下属性是可选的,可以出现在CloudEvent中。

  • datacontenttype:String
  • dataschema:URI
  • subject:String
  • time:Timestamp

3)扩展Context Attribute。CloudEvent Producer可以在Event中包含其他Context Attribute,这些属性可以在与Event处理相关的辅助操作中使用。

该规范对扩展属性的语义没有任何限制,但必须使用类型系统中定义的类型。扩展的每个定义都应完全定义属性的所有方面,例如属性的名称、语义含义和可能的值,甚至表明它对其值没有任何限制。新的扩展名定义应该使用具有足够描述性的名称,以减少与其他扩展名发生名称冲突的几率。特别是扩展作者应该检查扩展文档中的已知扩展集,不仅是可能的名称冲突,还是可能感兴趣的扩展。

每个定义如何序列化CloudEvent的规范都将定义扩展属性的显示方式。

扩展属性必须使用与所有CloudEvent上下文属性相同的常规模式进行序列化。例如,在二进制HTTP中,这意味着它们必须显示为带有ce-前缀的HTTP标头。属性的规范可以定义一个二级序列化,其中数据在消息中的其他位置重复。

在定义了二级序列化的情况下,扩展规范还必须说明如果两个序列化位置的数据不同,CloudEvent的接收者将要做什么。另外,发送者需要为intermediary和接收者不知道其扩展的情况做好准备,因此专用序列化版本很可能不会作为CloudEvent扩展属性进行处理。

许多传输支持发送者包括附加元数据的功能,例如作为HTTP标头。虽然未强制要求CloudEvent接收器处理和传递它们,但建议通过某种机制来进行处理,以使其清楚地知道它们不是CloudEvents的元数据。

这是一个说明需要其他属性的示例。在许多物联网和企业用例中,Event可以在无服务器应用程序中使用,该应用程序跨多种Event类型执行操作。为了支持这种用例,Event Producer需要向Context Attribute添加其他标识属性,Event使用者可以使用这些属性将这个事件与其他事件相关联。如果此类身份属性恰好是事件Data的一部分,则Event生成器还将身份属性添加到Context Attribute,Event使用者可以轻松访问此信息,而无须解码和检查Event Data。此类身份属性还可用于帮助中间网关确定如何路由Event。

4)Event Data(事件数据)。按照术语Data(数据)的定义,CloudEvent可以包含有关事件的特定于域的信息。如果存在,此信息将封装在数据中。

(4)Size Limit(大小限制)

在许多情况下,CloudEvent将通过一个或多个通用Intermediary进行转发,每个inter-mediary都可能会对转发Event的大小施加限制。CloudEvent也可能会路由到受存储或内存限制的Consumer(如嵌入式设备),因此会遇到大型单一事件。

Event的“Size(大小)”是其线路大小,根据所选的Event格式和所选的协议绑定,传输frame-metadata(帧元数据)、event metadata(事件元数据)和event data(事件数据)。

如果应用程序配置要求Event跨不同的传输进行路由或Event进行重新编码,则应该考虑应用程序使用的效率最低的传输和编码应符合以下大小限制:

  • Intermediary必须转发大小为64 KB或更小的事件。
  • Consumer应该接受至少64 KB的事件。

实际上,这些规则将允许Producer安全地发布最大为64KB的Event。此处的安全是指通常合理的做法是,期望所有Intermediary都接受并转发该事件。无论是出于本地考虑,还是要接受或拒绝该大小的事件,它都在任何特定的Consumer控制之下。

通常,CloudEvent发布者应该通过避免将大型数据项嵌入Event有效负载中来保持事件紧凑,而是将Event有效负载链接到此类数据项。从访问控制的角度来看,此方法还允许Event的更广泛分布,因为通过解析链接访问Event相关的详细信息可实现差异化的访问控制和选择性公开,而不是将敏感的详细信息直接嵌入事件中。

(5)隐私与安全

互操作性是此规范的主要推动力,要使这种行为成为可能,就需要明确提供一些信息,从而可能导致信息泄漏。

需要考虑以下事项,以防止意外泄漏,尤其是在利用第三方平台和通信网络时:

  • Context Attribute:敏感信息不应携带和表示在上下文属性中。CloudEvent Producer、Consumer和Intermediary可以内省并记录Context Attribute。
  • 数据:特定于域的Event数据应该被加密以限制对可信方的可见性。用于这种加密的机制是Producer和Consumer之间的协议,因此不在本规范的范围之内。
  • 传输绑定:应当采用传输级安全性来确保CloudEvent的可信和安全交换。

2. Workflow

Workflow是供应商中立的规范,用于定义用户指定或描述其Serverless应用程序流的格式或原语。

许多Serverless应用程序不是由单个事件触发的简单函数,而是由系列函数执行的多个步骤组成,而函数在不同步骤中由不同事件触发。如果某个步骤涉及多个函数,则该步骤中的函数可能会根据不同的事件触发器依次执行、并行执行或在分支中执行。为了使Serverless平台正确执行Serverless应用程序的函数工作流程,应用程序开发人员需要提供工作流程规范。

为了给业界提供一种标准方法,CNCF Serverless工作组成立了Workflow子组,供用户指定其Serverless应用程序工作流,以促进Serverless应用程序在不同供应商平台之间的可移植性。

为此CNCF Serverless工作组Workflow小组制定了一个完整的协议,使给定的事件时间轴和工作流始终产生相同的作用。

(1)功能范围

函数工作流用于将函数编排为协调的微服务应用程序。函数工作流中的每个函数可能由来自各种来源的事件驱动。函数工作流将函数和触发事件分组到一个连贯的单元中,并描述函数的执行和以规定方式传递的信息。具体来说,函数工作流程允许用户:

  • 定义Serverless应用程序中涉及的步骤/状态和工作流程。
  • 定义每个步骤中涉及的函数。
  • 定义哪个事件或事件组合触发一个或多个函数。
  • 定义在触发多个函数时如何安排这些函数依次执行还是并行执行。
  • 指定如何在函数或状态之间过滤信息和传递事件。
  • 定义在哪种错误状态下需要重试。
  • 如果函数是由两个或多个事件触发的,则定义应使用什么标签/键将那些事件与相同的函数工作流实例相关联。

如图1-11所示是涉及事件和函数的函数工作流的示例。使用这样的函数工作流,用户可以轻松指定事件与函数之间的交互以及如何在工作流程中传递信息。

046-01

图1-11 事件和函数的函数工作流示例图

使用函数工作流,用户可以定义集合点(状态)以等待预定义的事件,然后再执行一个或多个函数并继续执行函数工作流。

(2)Workflow模型

可以将函数工作流(Function Workflow)视为状态的集合以及这些状态之间的转换和分支,并且每个状态可以具有关联的事件和/或功能。函数工作流可以从CLI命令调用,也可以在事件从事件源到达时动态触发。来自事件源的事件也可能与函数工作流中的特定状态相关联。函数工作流中的这些状态将等待一个或多个事件源中的一个或多个事件到达,然后再执行其关联的操作并进入下一个状态。其他工作流程功能包括:

  • 函数的结果可用于启动重试操作或确定下一个要执行的函数或要转换到的状态。
  • 函数工作流提供了一种在事件处理过程中对JSON事件有效负载进行过滤和转换的方法。
  • 函数工作流为应用程序开发人员提供了一种在事件中指定唯一字段的方法,该字段可用于将事件源中的事件关联到同一函数工作流实例。

可以很自然地将函数工作流建模为状态机。以下是函数工作流的定义/规范提供的状态列表。工作流的规范称为工作流程模板。工作流模板的实例称为工作流实例(Workflow Instance)。

  • Event State(事件状态):用于等待事件源中的事件,然后调用一个或多个函数以顺序或并行运行。
  • Operation State(操作状态):允许一个或多个函数按顺序或并行运行,而无须等待任何事件。
  • Switch State(切换状态):允许转换到其他多个状态(例如,不同的函数导致前一个状态触发分支/转换到不同的下一个状态)。
  • Delay State(延迟状态):使工作流执行延迟指定的持续时间或直到指定的时间/日期。
  • End State(结束状态):失败/成功终止工作流。
  • Parallel State(并行状态):允许多个状态并行执行。

函数工作流由工作流规范描述。

(3)工作流规范

函数工作流规范定义了函数工作流的行为和操作。函数工作流规范结构应允许用户定义事件到达触发的执行函数。它应具有足够的灵活性,以涵盖从单个函数的简单调用到涉及多个函数和多个事件的复杂应用程序和各种微服务应用程序。

从高层看,函数工作流规范包括两部分:触发器定义(trigger definition)和状态定义(state definition)。

{
    "trigger-defs" : [],
    "states": []
}

trigger-defs数组(仅在存在与工作流关联的事件时才需要)是与函数工作流关联的事件触发器的数组。如果应用程序工作流中涉及多个事件,则必须在该事件触发器中指定一个用于将事件与同一工作流实例的其他事件相关联的关联令牌(correlation-token)。状态数组(必需)是与函数工作流相关联的状态数组。下面是JSON格式的函数工作流示例,其中涉及事件状态和该事件状态的触发器:

{
    "trigger-defs":[
        {
            "name":"OBS-EVENT",
            "source":"CloudEvent source",
            "eventID":"CloudEvent eventID",
            "correlation-token":"A path string to an identification label field in the event message"
        },
        {
            "name":"TIMER-EVENT",
            "source":"CloudEvent source",
            "eventID":"CloudEvent eventID",
            "correlation-token":"A path string to an identification label field in the event message"
        }
    ],
    "states":[
        {
            "name":"STATE-OBS",
            "start":true,
            "type":"EVENT",
            "events":[
                {
                    "event-expression":"boolean expression 1 of triggering events",
                    "action-mode":"Sequential or Parallel",
                    "actions":[
                        {
                            "function":"function name 1"
                },
                {
                    "function":"function name 2"
                }
            ],
            "next-state":"STATE-END"
            },
            {
                "event-expression":"boolean expression 2 of triggering events",
                "action-mode":"Sequential or Parallel",
                "actions":[
                    {
                        "function":"function name 3"
                    },
                    {
                        "function":"function name 4"
                    }
                ],
                "next-state":"STATE-END"
            }
            ]
        },
        {
            "name":"SATATE-END",
            "type":"END"
        }
    ]
}

以下是带有操作状态的函数工作流的另一个示例:

{
    "states":[
        {
            "name":"STATE-ALARM-NOTIFY",
            "start":true,
            "type":"OPERATION",
            "action-mode":"Sequential or Parallel",
            "actions":[
                {
                    "function":"function name 1"
                },
                {
                    "function":"function name 2"
                }
            ],
            "next-state":"STATE-END"
        }
    ]
}

(4)触发器定义

trigger-defs数组由一个或多个事件触发器组成。以JSON格式定义的事件触发器示例如下:

{
    "trigger-defs":[
        {
            "name":"EVENT-NAME",
            "source":"CloudEvent source",
            "eventID":"CloudEvent eventID",
            "correlation-token":"A path string to an identification label field in the event message"
        }
    ]
}

(5)动作定义{#action-definition}

下面是JSON格式的定义。

{
    "actions":[
        {
            "function":"FUNCTION-NAME",
            "timeout":"TIMEOUT-VALUE",
            "retry":[
                {
                    "match":"RESULT-VALUE",
                    "retry-interval":"INTERVAL-VALUE",
                    "max-retry":"MAX-RETRY",
                    "next-state":"STATE-NAME"
                }
            ]
        }
    ]
}
  • function:指定调用的函数。
  • timeout:从请求发送给函数开始计时,等待函数指定完成的时间,单位为秒,必须为正整数。
  • retry:重试策略。
  • match:匹配的结果值。
  • retry-intervalmax-retry:当出现错误时使用。
  • next-state:当超过max-retry限制后到转移到下一个状态。

(6)状态定义

1)事件状态(Event State)。

{
    "states":[
        {
            "name":"STATE-NAME",
            "type":"EVENT",
            "start":true,
            "events":[
                {
                    "event-expression":"EVENTS-EXPRESSION",
                    "timeout":"TIMEOUT-VALUE",
                    "action-mode":"ACTION-MODE",
                    "actions":[
                    ],
                    "next-state":"STATE-NAME"
                }
            ]
        }
    ]
}

事件状态必须将type值指定为EVENT。

  • start:是否为起始状态。可选的字段,默认为false。
  • events:与该事件状态相关的事件数组。
  • event-expression:这是一个布尔表达式,由一个或多个事件操作数和布尔运算符组成。EVENTS-EXPRESSION可以是Event1 or Event2。到达并匹配EVENTS-EXPRESSION的第一个事件将导致执行此状态的所有操作,然后转换到下一个状态。
  • timeout:指定在EVENTS-EXPRESSION中等待事件的时间段。如果事件不在超时时间内发生,则工作流将转换为结束状态。
  • action-mode:指定函数是顺序执行还是并行执行,并且可以是SEQUENTIAL或PARALLEL。
  • next-state:指定在成功执行所有匹配事件的操作之后要转换到的下一个状态的名称。

2)操作状态(Operation State)。

{
    "states":[
        {
            "name":"STATE-NAME",
            "type":"OPERATION",
            "start":true,
            "action-mode":"ACTION-MODE",
            "actions":[
            ],
            "next-state":"STATE-NAME"
        }
    ]
}
  • action-mode:指定函数是顺序执行还是并行执行,并且可以是SEQUENTIAL或PARALLEL。
  • actions:由一系列动作构成的列表,指定接收到与事件表达式匹配的事件时要执行的函数的列表。
  • next-state:指定在成功执行所有匹配事件的操作之后要转换到的下一个状态的名称。

3)分支状态(Switch State)。

{
    "states":[
        {
            "name":"STATE-NAME",
            "type":"SWITCH",
            "start":true,
            "choices":[
                {
                    "path":"PAYLOAD-PATH",
                    "value":"VALUE",
                    "operator":"COMPARISON-OPERATOR",
                    "next-state":"STATE-NAME"
                },
                {
                    "Not":{
                        "path":"PAYLOAD-PATH",
                        "value":"VALUE",
                        "operator":"COMPARISON-OPERATOR"
                    },
                    "next-state":"STATE-NAME"
                },
                {
                    "And":[
                        {
                            "path":"PAYLOAD-PATH",
                            "value":"VALUE",
                            "operator":"COMPARISON-OPERATOR"
                        },
                        {
                            "path":"PAYLOAD-PATH",
                            "value":"VALUE",
                            "operator":"COMPARISON-OPERATOR"
                        }
                    ],
                    "next-state":"STATE-NAME"
                },
                {
                    "Or":[
                        {
                            "path":"PAYLOAD-PATH",
                            "value":"VALUE",
                            "operator":"COMPARISON-OPERATOR"
                        },
                        {
                            "path":"PAYLOAD-PATH",
                            "value":"VALUE",
                            "operator":"COMPARISON-OPERATOR"
                        }
                    ],
                    "next-state":"STATE-NAME"
                }
            ],
            "default":"STATE-NAME"
        }
    ]
}
  • choices:针对输入数据定义了一个有序的匹配规则集,以使数据进入此状态,并为每个匹配项转换为下一个状态。
  • path:JSON Path,用于选择要匹配的输入数据的值。
  • value:匹配值。
  • operator:指定如何将输入数据与值进行比较,例如“EQ”“LT”“LTEQ”“GT”“GTEQ”“StrEQ”“StrLT”“StrLTEQ”“StrGT”“StrGTEQ”。
  • next-state:指定在存在值匹配时要转换到的下一个状态的名称。
  • Not:必须是单个匹配规则,且不得包含next-state字段。
  • And和Or:必须是匹配规则的非空数组,它们本身不能包含next-state字段。
  • default:如果任何选择值都不匹配,则default字段将指定下一个状态的名称。
  • next-state:评估的顺序是从上到下,如果发生匹配,请转到next-state,并忽略其余条件。

4)延迟状态(Delay State)。

{
    "states":[
        {
            "name":"STATE-NAME",
            "type":"DELAY",
            "start":true,
            "time-delay":"TIME-VALUE",
            "next-state":"STATE-NAME"
        }
    ]
}
  • time-delay:指定时间延迟。TIME-VALUE是在此状态下延迟的时间(以秒为单位),必须是正整数。
  • next-state:指定要转换到的下一个状态的名称。STATE-NAME在函数工作流中必须是有效的State名称。

5)结束状态(End State)。

{
    "states": [
        {
            "name": "STATE-NAME",
            "type": "END",
            "status": "STATUS"
        }
    ]
}
  • status:该字段必须为SUCCESS或FAILURE,表示工作流结束。

6)并行状态(Parallel State)。

并行状态由多个并行执行的状态组成。并行状态具有多个同时执行的分支。每个分支都有一个状态列表,其中一个状态为开始状态。每个分支继续执行,直到达到该分支内没有下一个状态的状态为止。当所有分支都执行完成后,并行状态将转换为下一个状态。本质上,这是在并行状态内嵌套一组状态。

并行状态由状态类型PARALLEL定义,并包括一组并行分支,每个分支都有自己的独立状态。每个分支都接收并行状态的输入数据的副本。除END状态外,任何类型的状态都可以在分支中使用。

分支内状态的next-state转换只能是到该分支内的其他状态。另外,并行状态之外的状态不能转换到并行状态的分支内的状态。

并行状态会生成一个输出数组,其中每个元素都是分支的输出。输出数组的元素不必是同一类型。

{
    "states":[
        {
            "name":"STATE-NAME",
            "type":"PARALLEL",
            "start": true,
            "branches":[
                {
                    "name":"BRANCH-NAME1",
                    "states":[
                    ]
                },
                {
                    "name":"BRANCH-NAME2",
                    "states":[
                    ]
                }
            ],
            "next-state":"STATE-NAME"
        }
    ]
}
  • branch:同时执行的分支的列表。每个命名分支都有一个states列表。分支内每个状态的next-state字段必须是该分支内的有效状态名称,或者不存在以指示该状态终止该分支的执行。分支执行从分支内具有"start": true的状态开始。
  • next-state:指定在所有分支完成执行之后要转换到的下一个状态的名称。STATE-NAME在函数工作流中,必须是有效的状态名,但不能是已处于并行状态中的状态。

(7)信息传递

如图1-12显示了函数工作流的数据流,该函数工作流包括调用两个函数的事件状态。来自一个状态的输出数据作为输入数据传递到下一状态。过滤器用于过滤和转换进入和退出每个状态时的数据。从工作流中的Operation State调用时,来自先前状态的输入数据可能会传递到Serverless函数。

055-01

图1-12 函数工作流的数据流1

来自Serverless函数的响应中包含的数据将作为输出数据发送到下一个状态。如果状态(Operation State或Event State)包括一系列顺序操作,则将过滤来自一个Serverless函数的响应中包含的数据,然后在请求中将其发送给下一个函数。

在Event State下,在将请求从事件源接收到的CloudEvent元数据传递到Serverless函数之前,可以对其进行转换并将其与从先前状态接收到的数据进行组合。

同样,在从Serverless函数的响应中接收到的CloudEvent元数据可以转换并与从先前状态接收到的数据组合,然后再在发送到事件源的响应中进行传递。

在某些情况下,诸如API网关之类的事件源希望收到工作流的响应。在这种情况下,可以将从Serverless函数的响应中接收到的CloudEvent元数据进行转换,并与从先前状态接收到的数据进行组合,然后再将其在发送到事件源的响应中进行传递,如图1-13所示。

056-01

图1-13 函数工作流的数据流2

(8)过滤器机制

状态机维护一个隐式JSON数据,该数据可以从每个过滤器作为JSONPath表达式$进行访问。过滤器共有三种。

  • 事件过滤器(Event Filter)。
  • 当数据从事件传递到当前状态时调用。
  • 状态过滤器(State Filter)。
  • 当数据从先前状态传递到当前状态时调用;
  • 当数据从当前状态传递到下一个状态时调用。
  • 动作过滤器(Action是指定义Serverless函数的动作定义)。
  • 当数据从当前状态传递到第一个操作时调用;
  • 当数据从一个动作传递到另一个动作时调用;
  • 当数据从最后一个动作传递到当前状态时调用。

每个过滤器都有三种路径过滤器:

  • InputPath。
  • 选择事件、状态或操作的输入数据作为JSONPath默认值为$。
  • ResultPath。
  • 将Action输出的结果JSON节点指定为JSONPath;
  • 默认值为$。
  • OutputPath。
  • 将State或Action的输出数据指定为JSONPath;
  • 默认值为$。

(9)错误

状态机在运行时返回以下预定义的错误代码。通常,它在动作定义的retry字段中使用。

  • SYS.Timeout;
  • SYS.Fail;
  • SYS.MatchAny;
  • SYS.Permission;
  • SYS.InvalidParameter;
  • SYS.FilterError。