本文要点
微服务架构已经演化为云原生架构,其中由Kubernetes提供了许多基础设施关注点,并结合了服务网格和serverless 框架提供的额外抽象。
Kubernetes的杰出之处在于它通过Pod抽象为新的工作负载提供了可扩展性,同时,这也支持出现了新的云本地应用程序模式。
Kubernetes不仅支持无状态应用程序,还支持有状态工作负载、单例、批处理作业、cron作业,甚至通过CRDs和operator支持serverless 和自定义工作负载。
今天的serverless 平台有明显的限制,阻碍了具有很强的互操作性和可移植性约束的企业公司采用它们。
通过标准的探索和开放的打包、运行时和事件格式,serverless 生态系统正在不断发展。这些领域的进步正在模糊云原生工作负载和serverless 工作负载之间的差异,并且已经将serverless 的产品推向开放、可移植和可互操作的框架。
我在2019年红帽峰会上发表的“serverless 世界的集成模式”演讲的灵感来自于客户提出的问题,他们问,在当前由 Kubernetes 主导的企业云原生环境中,serverless 工作负载适用于何处。
本文总结了上述讨论,但未太多关注集成,而是更深入地研究了不同的 Kubernetes 工作负载和 serverless 产品的限制。如果您想参加我的另一个相关主题的网络研讨会,请查看本文末尾的详细信息。
分布式架构的发展
在预测 serverless 的发展方向和它适用于何处之前,我们首先需要分析一下我们是如何以及为什么走到这个位置的。
大型独体架构
当时,面向服务的架构(SOA)是最流行的架构,而企业服务总线(ESB)是其最常见的实现,我在大型集成项目中的职业生涯正是始于此时。
SOA 基于良好的原则,其中大多数原则仍然有效:契约优先开发、松散耦合、可组合和无状态的服务,这些服务也是自治的和可重用的。
ESB 框架提供了一组很好的能力,比如协议转换、技术连接器、路由和编排机制、错误处理和高可用性原语。
分布式架构的发展
从架构和组织的角度来看,SOA 和 ESB 的主要问题是集中化。SOA 的一个重要原则是服务和组件的重用,从而创建了分层的服务架构,该架构支持重用,但会导致紧密的架构服务耦合。在组织级,ESB 由一个团队拥有,这使得中间件成为技术和组织上可伸缩性的瓶颈,甚至,成为快速发展的瓶颈。复杂的行业规范由一小群人定义,发展缓慢。
微服务架构 1.0
虽然 SOA 为处理企业级复杂性提供了良好的基础,但是技术发展很快。开源模式带来了更快的创新,成为技术分发和标准化的新的机制。
与此同时,敏捷开发实践(如极限编程、Scrum 和看板)引领了迭代的软件开发,但是这种方法与现有的大型独体架构相冲突,现有架构无法处理快速部署的增量设计。因此,特性是在为期两周的迭代中开发的,但每年只部署到生产环境中一两次。
于是微服务架构应运而生,有望解决这些挑战。鉴于其指导原则,微服务架构使我们可以更快地变更。服务围绕业务领域建模,这有助于比可重用 SOA 服务更有效地控制服务边界内的变更。自治和独立部署的服务允许每个服务以自己的速度发展和扩展。
虽然这些新原则通过将每个应用程序转换为分布式系统,针对快速迭代和试验的时代对 SOA 进行了优化,但它们也带来了挑战。因此,微服务还必须具有高度自动化、高度可观察性、容错性以及分布式系统组件必须具备的任何其他特性。这是一个并非所有人一开始都意识得到,并且也不一定愿意付出的代价。
微服务架构作为快速、迭代开发和实验时代的技术解决方案应运而生,但是我们是在陈旧工具的基础上构建的第一代微服务,它们很难管理。在 ESB 时代,业务逻辑泄漏到平台中,导致了各种耦合,而在早期的微服务时代,我们发现情况反了过来:太多的基础设施问题泄漏到每个微服务中。早期的微服务也有自己的挑战;它们必须进行自己的服务发现、配置管理、网络弹性、日志分发等。能够创建数十或数百个新服务并不一定意味着组织已经准备好使用现有工具管理和部署它们到生产环境中。必须改进用于向运维团队发布、管理和处理服务的流程和支持工具。所有这些都把我们推到了Kubernetes时代。
原生云架构(也就是微服务 2.0)
为适应快速的变化,微服务架构对 SOA 进行了优化,但它是通过将代码复杂性转换为运维复杂性来实现这一点的。它的挑战带来了对容器的采用,并在一夜之间接管了整个行业。容器作为一种技术治愈了统一部署大量微服务的痛苦,并有效地使现代 DevOps 实践成为了可能。使用容器,我们可以打包应用程序并以开发和运维团队都能理解和使用的格式运行它们。很明显,即使是在早期,管理上百个容器也需要自动化,而 Kubernetes 从天而降,横扫所有竞争对手。Kubernetes 解决了技术挑战,DevOps 实践解决了微服务的文化方面。这种云原生工具是如此的基础,以至于导致了第二代微服务平台的出现。
这是一个新的转变的开始,一个基础设施职责从应用层转移到平台层的转变。云原生平台(最初有很多,但最终主要是 Kubernetes)提供了诸如资源隔离和管理、自动放置、声明式部署、健康探测和自修复、配置管理、服务发现、自动伸缩等能力。这些特性允许应用程序开发人员专注于业务逻辑,并使用开箱即用的平台特性统一解决基础设施问题。
当时,我将流行的微服务 1.0 工具(Spring Cloud 和 Netflix OSS)与微服务 2.0 工具(Kubernetes 作为事实上的标准)进行了比较,收到了读者的不同反应。如今,这是一个被更广泛理解和接受的转变,证实了 Kubernetes 作为微服务管理平台的完全主导地位,以及许多来自于上一世代的 Netflix OSS 库被弃用。
但这一切都已成为历史。让我们来探索一下 Kubernetes 接下来要做什么。
Kubernetes 的闪光点
Kubernetes 架构有许多迷人的元素:容器在其基础上提供公共打包、运行时和资源隔离模型;简单的控制回路机制,其监控组件的实际状态,并将其与所需状态进行协调;自定义资源定义。但是扩展 Kubernetes 以支持不同工作负载的真正推动者是 pod 这一概念。
pod 提供两组保证。部署保证确保 pod 的容器总是放置在同一个节点上。这会有一些有益之处,比如允许容器通过本地主机、进程间通信(IPC)或使用本地文件系统进行同步或异步通信。
pod 的生命周期保证确保 pod 中的容器实际上被管理为两组:初始化容器和应用程序容器。初始化容器先运行;它们一个接一个地运行,并且只有在前一个容器成功完成时才运行。初始化容器支持有顺序的流水线行为,单独的容器执行每个步骤。另一方面,应用程序容器并行运行,没有任何顺序保证。这组容器支持流行的边车模式,用于扩展和增强具有正交功能的现有容器的功能。
Pod 的部署和生命周期保证
可扩展的控制循环机制,加上通用的 pod 特性,使 Kubernetes 能够处理各种工作负载,包括 serverless 的工作负载。让我们检查一下这些不同的工作负载,并看看适用于每种工作负载的用例是什么。
云原生工作负载
要证明 Kubernetes 是一个能够支持不同工作负载和用例的通用平台,需要考察不同的工作负载类型及其需求。
有状态的服务
有一种工作负载让人一点儿也提不起兴致,但在企业环境中几乎总是出现它,让我们从此类工作负载开始吧:它就是有状态服务。通过将具有业务逻辑的有状态服务移动到外部数据存储中,可以将其转换为可伸缩的无状态服务。这样的设计将约束从业务服务转移到数据源上,数据源成为架构中的有状态组件。这些数据存储通常是现成的关系数据库、分布式缓存、键值存储、搜索索引等。在动态云基础设施上管理分布式有状态组件需要一些保证,比如:
持久存储——状态通常位于磁盘上,分布式有状态应用程序需要为每个实例提供专用的持久存储来保存其状态。
固定不变的网络ID——与存储需求类似,分布式有状态应用程序需要固定不变的网络标识。除了在存储空间中存储特定于应用程序的数据外,有状态应用程序还存储配置细节,如主机名和它们的对等点的连接细节。这意味着每个实例都应该通过一个不应该动态更改的可预测地址来访问,就像ReplicaSet中的pod IP地址一样。
固定不变的标识——正如我们从之前的需求中所看到的,集群的有状态应用程序严重依赖于掌控其长生命周期存储的每个实例和网络标识。这是因为在有状态的应用程序中,每个实例都是惟一的,并且知道自己的标识,而标识的主要组成部分是长生命周期的存储和网络坐标。在这个列表中,我们还可以添加实例的标识/名称(一些有状态的应用程序也需要惟一的持久性名称),即为Kubernetes中的pod名称。
序数性——除了一个独特的和长期存在的标识之外,集群的有状态应用程序的每个实例相对于其他相关应用程序都有一个固定的位置。这种顺序通常会影响实例伸缩的顺序,但它也可以用作一致哈希算法、数据分发和访问以及集群内行为(如锁、单例或主机)放置的基础。
Kubernetes 上的分布式有状态应用程序
这些正是 Kubernetes StatefulSet 所提供的保证。StatefulSet 为管理具有状态特征的 pod 提供了通用原语。除了带有 StatefulSet 的典型 ZooKeeper、Redis 和 Hazelcast 部署外,其他用例还包括消息代理,甚至事务管理器。
例如,Narayana 事务管理器使用 StatefulSet 来确保在使用分布式事务缩减服务的过程中不会遗漏任何 JTA 日志。在缩小集群消息代理的规模时,Apache Artemis 消息代理依赖于 StatefulSet 来消费光消息。StatefulSet 是一个强大的通用抽象,对于复杂的有状态用例非常有用。
全局单例
来自“四人帮”的单例模式是一个古老且易于理解的概念。在分布式的云原生世界中,与之相当的是一个单例组件(一个完整的服务或它的一部分)的概念,它是一个全局单例组件(在所有分布式服务中),但仍然是高度可用的。这种工作负载类型的用例通常来自于我们必须与之交互的其他系统的技术约束,例如,API、数据源和每次只允许一个客户机(单例)访问的文件系统。另一个用例是当消息排序必须由消费服务保持时,将其限制为单例。Kubernetes 有几个选项来支持这类用例。
最简单的选择是依赖 Kubernetes 来运行服务的单个实例。我们可以通过使用 replicas=1 的 ReplicaSet 或 StatefulSet 轻松实现这一点。这两种选择之间的区别在于,您是需要具有“最多一个”保证的强一致单例,还是需要具有“至少一个”保证的弱单例。ReplicaSet 支持可用性并优先保持单个实例(“至少一个”语义)。这有时会导致同时运行多个实例,例如,当一个节点与集群断开连接时,ReplicaSet 在另一个节点上启动另一个 pod,而没有确认第一个 pod 是否已停止。StatefulSet 倾向于一致性而不是可用性,并且提供“最多一个”语义。在节点与集群断开连接的情况下,它不会在健康节点上启动 pod。这种情况只有在运维人员确认断开连接的节点或 pod 确实关闭之后才会发生。这有时可能导致服务停机,但绝不会导致多个实例同时运行。
还可以在应用程序中实现自己管理的单例。虽然在前面的用例中,应用程序并不知道作为单例进行管理,但是自己管理的单例确保只激活一个组件,而不管启动了多少服务实例(pod)。这种单例方法需要特定于运行时的实现来获取锁并充当单例,但是它有一些优点。首先,没有意外错误配置的危险,并且副本的数量的增加仍然会保持在任何给定时间只有一个组件处于活动状态。其次,它允许服务的扩展,同时能够确保只针对服务的一部分(比如端点)进行单例行为。当由于特定操作或端点的外部技术限制,一个微服务仅部分而不是整体必须是单例时,这是非常有用的。这个用例的一个示例实现是 Apache Camel 的 Kubernetes 连接器的单例特性,它能够使用 Kubernetes ConfigMap 作为分布式密钥,并且只激活部署到 Kubernetes 中的多个 Camel 服务中的单个Camel消费者。
Kubernetes 上的单例工作负载
单例是另一种工作负载类型,它的数量很少,但是非常常见,很值得一提。单例和高可用性是两个相互冲突的需求,但是 Kubernetes 足够灵活,可以为两者提供可接受的折衷方案。
批处理作业
批处理作业的用例适合于管理处理独立原子工作单元的工作负载。在 Kubernetes 原语中,它被实现为作业抽象,在分布式环境中可靠地运行短期的 pod 以完成任务。
Kubernetes 上的批处理和重复工作负载
从生命周期的角度来看,批处理工作负载很少有类似于异步 serverless 工作负载的特性,因为它们集中于单个操作,并且它们生命周期短暂,最多持续到任务完成。但是,尽管基于作业的工作负载本质上是异步的,但它们不接受来自消费者的直接输入,也不直接启动以响应消费者的请求。他们通常知道从哪里检索输入数据,以及在哪里输出结果。如果作业具有时间维度,即它是已安排好的,那么它的执行将定期由时间事件触发。
无状态的工作负载(也就是 12-Factor-Apps)
无状态工作负载是 Kubernetes 上使用最广泛的工作负载类型。这是典型的基于12因素的应用程序,或者是在 Kubernetes 之上使用 ReplicaSet 管理的基于微服务的系统。通常,一个 ReplicaSet 将管理这样一个服务的多个实例,并使用不同的自动伸缩策略来横向和纵向伸缩这样的工作负载。
在 Kubernetes 上具有服务发现的无状态工作负载
对由 ReplicaSet 管理的服务的一个常见需求是服务发现和负载平衡。针对于此,Kubernetes 提供了多种开箱即用的选项。
Kubernetes 上的服务发现机制
这里的重点是,即使有各种服务发现机制可以动态地检测健康和不健康的 pod 实例,本质上各种服务类型也是相对静态的。Kubernetes服务原语不提供动态流量监控和转移能力。这正是服务网格的用武之地。
服务网格
实现基于微服务的系统时面临的挑战之一是构建不属于业务逻辑的产品特性,例如弹性通信、跟踪、监控等。该逻辑过去常常位于中央的 ESB 层,现在必须在微服务的智能客户端之间进行隔离和重复。服务网格技术旨在通过提供额外的增强网络能力来解决这个问题,例如:
流量路由——A/B测试、阶段性发布。
恢复能力——重试、断路器、连接限制、健康检查。
安全性——身份验证、授权、加密(mTLS)。
可观察性——度量、跟踪。
测试——故障注入、流量镜像。
平台独立性——多语言支持,允许运行时配置。
如果我们仔细研究这些特性,就会注意到集成框架提供的功能有明显的重叠。
服务网格和集成框架职责重叠
对于将所有这些职责移出服务是否是一种好方法,存在着不同的意见。虽然在此明显地将网络职责从应用层转移到了公共云原生平台,但并不是所有的网络职责都从应用程序中移出:
服务网格适合于基于连接的流量路由,服务内部的集成框架适合于基于内容的路由。
服务网格可以进行协议转换,集成框架可以进行内容转换。
服务网格可以进行“摸黑启动”,而集成框架可以进行“窃听”。
服务网格执行基于连接的加密,而集成框架可以执行内容加密。
某些需求可以在服务内部更好地处理,而有些需求则可以使用服务网格从外部处理。有些还必须同时在这两层处理:连接超时可以从服务网格层配置,但仍然必须从服务内部配置。对于其他行为,如通过重试和任何其他错误处理逻辑进行恢复,也是如此。服务网格、Kubernetes 和其他云服务是当今的工具,但应用程序端到端可靠性和正确性的最终责任在于服务实现及其开发和设计团队。这一点没有改变。
服务网格技术进一步强调了第一代和第二代微服务之间的主要区别:将某些运维职责转移到平台。Kubernetes 将部署职责转移到平台,服务网格将网络职责转移到平台。但这不是最终状态;这些更改只是为实现 serverless 世界铺平了道路,在这个世界中,部署和基于流量的即时可伸缩性是先决条件。
serverless 的概念
所有问题都关乎视角
为了讨论 serverless 的特性,我将使用云原生计算基金会(CNCF)的 serverless 工作组(serverless working group)的定义,因为在许多不同软件供应商给出的定义中,它是得到最广泛认可的其中一个:
serverless 计算的概念是指构建和运行不需要服务器管理的应用程序。它描述了一个更细粒度的部署模型,在这个模型中,捆绑为一个或多个函数的应用程序被上传到一个平台上,并根据所需的确切需求执行、伸缩和计费。
如果我们作为将从 serverless 平台中获益的开发人员来看待这个定义,那么我们可以将 serverless 总结为一种架构,它支持“在无需服务器管理的情况下按需运行更细粒度的函数”。通常都是从开发人员的角度来考虑 serverless ,但是还有另一个很少讨论的角度。每个 serverless 的平台都有管理平台和服务器的供应商:它们必须管理粗粒度的计算单元,并且无论需求如何,它们的平台都要花费 24x7 的成本。供应商是 AWS Lambda、Azure Functions 和谷歌云函数背后的团队,或者是您公司中使用 Knative 管理 Apache OpenWhisk、Kubernetes 或其他东西的团队。在这两种情况下,这些供应商都允许开发人员将计算和存储作为一种工作负载类型来使用,而这种工作负载类型没有任何服务器的概念。根据组织和业务因素,供应商可以是同一组织中的另一个团队/部门(想象一支使用 AWS Lambda 满足其需求的 Amazon 团队),也可以是另一个组织(如果 AWS 客户使用 Lambda 和其他服务)。无论供应商和使用者之间的业务如何安排,使用者不对服务器负责,负责的是供应商。
serverless 架构
上面的定义仅指“serverless 计算”。但是应用程序的架构是由计算和数据组合而成的。更完整的 serverless 架构定义包括 serverless 计算和 serverless 数据。通常,此类应用程序将合并云托管服务来管理状态和通用服务器端逻辑,如身份验证、API 网关、监视、警报、日志记录等。我们通常将这些托管服务称为“后端即服务”(BaaS)——可以将服务视为 DynamoDB、SQS、SNS、API 网关、CloudWatch 等。事后看来,术语“serviceful”相比“serverless ”可以更准确地描述得出的架构。但并不是所有的东西都可以被第三方服务取代;如果是这样,有了针对于您的业务逻辑的服务,那么您还做什么业务呢!因此,serverless 架构通常还具有“功能即服务”(functions as a service, FaaS)元素,这些元素允许执行由事件触发的自定义无状态计算。这里最流行的例子是 AWS Lambda。
一个完整的 serverless 架构由 BaaS 和 FaaS 组成,无需从消费者/开发人员的角度考虑服务器的概念。不需要管理或提供服务器还意味着基于消费的定价(不提供容量)、内置的自动伸缩(直到某个上限)、内置的可用性和容错性、内置的补丁和安全性增强(带有内置的支持策略约束)、监控和日志记录(作为附加的付费服务)等。所有这些都由 serverless 开发人员使用,并由 serverless 供应商提供。
纯 serverless
如果 serverless 架构听起来这么好,为什么不拥有一个纯 serverless 架构,所有组件都 100% serverless 且完全没有服务器的概念?原因可以用艾伦·乌尔曼(Ellen Ullman)的这句话来解释:“我们建造计算机系统的方式就像我们建造城市一样:随着时间的推移在一片废墟之上修修补补,无需一份完整的计划。”企业制度就像一座老城;通常,它已经存在了十多年,这就是它的价值和临界点的来源。这就是它“进取”的原因。想象一下伦敦,一个存在了 2000 多年的城市,有着百年历史的地铁系统、狭窄的街道、宫殿、维多利亚时代的社区和供应系统;这样一个正在使用的复杂系统永远不会完全被一个新的系统所替代,并且它总是处于恢复和更新的过程中(用 IT 术语来说,就是重构、升级、迁移和重新构建平台)。在这样的体系中,变化是常态,新旧并存是常态。这就是这些系统应该存在的方式。
serverless 1.0
容器技术以各种形式存在了许多年,但是 Docker 使其得以普及,Kubernetes 使其成为部署的规范。类似地是,serverless 技术已经存在很多年了,但是 AWS Lambda 使其流行开来,我们还没有看到谁将把它带到下一个层次。
serverless 1.0 基本上是由 AWS 定义的,并由用于 FaaS 组件的 AWS Lambda 和用于 BaaS 组件的其他 AWS 服务(如 API 网关、SQS、SNS 等)表示。随着 AWS 定义了这一趋势,而谷歌和 Azure 等其他厂商也在努力追赶,下面是当前一代中一些不太理想的 serverless 特性,这些特性可能需要改进。
非确定性执行模型具有:
不可预测的容器生命周期和影响冷启动的重用语义;
对编程模型的约束,它会影响代码初始化逻辑,导致回调泄漏,产生递增的递归调用成本等;
不可预测的资源生命周期,例如/tmp文件存储;
对内存、超时、有效负载、包、临时文件系统和环境变量的任意限制;以及
跨行业的整体非标准化执行模型,带有影响特定serverless 供应商编程模型的非标准化约束。
有限的运行时支持意味着:
serverless 软件栈是操作系统、语言运行时和库版本的组合,受限于OS、JDK和应用程序库(如AWS SDK)的(单个版本)。
平台支持策略通常规定在哪些条件下可以废弃和更新任一serverless 栈组件,从而迫使所有serverless 用户以相同的速度遵循严格的时间表。
编程API可能会导致问题。虽然我有着很好的针对其他服务使用AWS SDK的体验,但我不喜欢所有函数都必须与com.amazonaws.services.lambda.runtime包及其编程模型紧密耦合。
我们使用非标准的打包。使用.zip、uber-JAR或lib目录,并使用自定义的特定于AWS的分层和依赖关系模型,并不能使我对这种打包的未来抱有信心,或者它能在没有服务器的平台上工作抱有信心。
自定义环境变量(以AWS_开头)不能在其他serverless 的平台上工作。
专用的数据格式
专用的数据格式是一个障碍。事件是 serverless 架构中函数的主要连接机制。它们将每个函数与其他函数和 BaaS 连接起来。它们实际上是函数的 API 和数据格式。在所有函数中使用 com.amazonaws.services.lambda.runtime.events 包中定义的事件保证零互操作性。
不支持 Java
虽然 AWS Lambda 有一个 Java 运行时,但是 Java 和 Lambda 之间的不匹配非常严重,甚至 AWS 都不推荐使用它。在 Tim Bray 的“AWS内幕:现代应用的技术选择”演讲中,他建议使用 Go 和 Python。我希望 serverless 的供应商能做得更好,并改进它们的运行时,而不是试图对数百万 Java 开发人员进行再教育,改变由数百万 Java 类库组成的生态系统。Java 已经像 Go 一样轻便和快速(即使没有更好的话),所以用这种语言构建 serverless 是无可回避的。
可能的影响
回顾一下,不确定性和非标准化执行模型、专有运行时、专有数据格式、专有 API 和缺乏 Java 支持,它们组合起来意味着所有这些问题都会泄漏到应用程序代码中,并影响我们实现业务逻辑的方式。这是把控制权从一个组织移交给另一个组织的最终结果。由于今天的 serverless 提供了构建时构件(以 sdk 和打包格式、事件格式和软件栈的形式),并提供了运行时环境,所以 serverless 消费者对支持策略和供应商强加的专有限制做出承诺。这些消费者承诺紧紧跟随供应商的语言运行时、SDK、升级和弃用。他们通过编写跨 serverless 平台具有零互操作性的函数来遵守这些术语。如果我们对所有事情都使用 BaaS,那么我们只能将组织的业务逻辑(用函数编写)与专有的执行模型、运行时、API 和数据格式耦合起来,否则我们将别无选择。虽然我们可能不想有其他选择,但对某些人来说,拥有这样的选择权很重要。
耦合和锁定本身并不是坏事,但是迁移的高成本是坏事。使用 AWS AMI、AWS RDS、流行的开源项目作为托管服务,甚至选择 SQS 都是不介意被锁定的消费者的例子,因为迁移到替代服务或供应商是一个可行的替代方案。这与将我们的业务逻辑耦合到不成熟的 serverless 技术和特定的 serverless 供应商的特性是不一样的。在这里,迁移工作是完全重写和测试业务逻辑及粘合代码,考虑到 serverless 架构的高度分布式特性,这些事的成本尤其高昂。
微服务以代码复杂度换取运维复杂度。serverless 为了速度交出了控制力。选择一个流程的架构时,要留意阅读旁边的小字。每一个架构选择都是一种权衡。
serverless 1.5
AWS 做了一件了不起的工作,将 serverless 带到了目前的位置上,但是如果现在的 serverless 就是它的最高点,那就太遗憾了。如果只有 AWS 能够在 serverless 的环境中进行创新并定义其未来,那将令人很是遗憾。考虑到 AWS 在开源生态系统中的背景相对有限,期望 AWS 将 serverless 范式标准化,在如此基础的层面上影响整个行业,这是不现实的。AWS 的业务模型和市场地位有助于识别市场趋势和最初的封闭创新,但是开源模型更适用于泛化、标准化,并不强制行业范围接受。我希望下一个 serverless 的时代将使用开放源码模型来建设,并在整个行业中进行更广泛的协作,这将有助于其采用和互操作性。这一过程已经开始,业界正在慢慢探索可互操作和可移植的替代方案,以替代目前专有的 serverless 产品。
让我们来讨论一些行业趋势(排名不分前后),我认为这些趋势将推动和影响未来的 serverless 技术。
统一的打包和执行模型
容器已确立为应用程序打包和运行时的行业标准。如前所述,将容器化的应用程序与功能强大的编排引擎相结合,使丰富的工作负载成为可能。没有理由让 serverless 的工作负载成为例外,因为这将使我们回到混合打包格式和执行模型。Knative是多个供应商共同开发的开放产品,它通过提供 Kubernetes 上的基于容器的工作负载的 serverless 特性(伸缩至零、基于 HTTP 请求的自动伸缩、订阅、交付、绑定和事件管理)来挑战这一现状。基于容器的打包和基于 kubernets 的执行将允许一个开放的执行模型,该模型可以跨多个 serverless 供应商予以标准化。它将支持一系列更丰富的运行时,更好地支持 Java、自定义软件栈、限制和自定义的可能性。
有些人可能会争辩说,在函数包中包含语言运行时和事件处理程序并不符合 serverless 的原始定义,但这是一个实现细节,这是将来 serverless 非常需要的选项。
行业认可的事件格式
按定义来看,serverless 架构是事件驱动的,事件起着中心作用。在 serverless 的环境中,事件类型越多,开发人员的体验就越丰富,可以用现成服务替换的逻辑也就越多。但是,这是有代价的,因为业务逻辑与事件格式和结构耦合了起来。虽然您可以读读AWS的最佳实践,看它们如何将核心业务逻辑和事件处理逻辑分离为单独的方法,但这离解耦还很远。业务逻辑与 serverless 平台的数据格式之间的这种耦合阻碍了互操作性。CloudEvents致力于创建可以跨所有 serverless 平台运行的标准化事件格式。除了是一个很棒的想法,它还对包括AWS 在内的行业抱有巨大的兴趣,行业可能是对其重要性和采用潜力的最终验证。
可移植性和互操作性
一旦有了标准的打包格式和标准事件,下一个级别的自由是能够在公共或私有云、场所或边缘上跨服务器提供程序运行 serverless 工作负载,并根据需要将所有工作负载混合和匹配到混合服务器中。一个函数应该可以在多云、混合云、任何云、非云或混合云上运行,并且应该只需要一些配置和映射。我们以同样的方式编写 Java 应用程序来实现抽象接口并把它们部署到不同的 Web 容器,我希望能够写非专有的 API 函数、事件和编程模型,并将它们部署到任何 serverless 平台,使它以可预测的、确定性的方式运行。
除了可移植性,我还希望看到互操作性,使函数能够使用来自任何平台的事件,而不管函数本身运行在何处。像KEDA这样的项目允许我们运行如 Azure 函数之类的自定义函数,以响应 AWS、Azure 和其他事件触发器。TriggerMesh等项目允许我们在 Kubernetes 和OpenShift之上部署 AWS lambda 兼容的功能。这些迹象表明,未来的功能将在多个级别上具有可移植性和互操作性:打包、执行环境、事件格式、事件源、工具等等。
将 Java 视为一类公民
虽然 serverless 工作负载适用于许多用例,但是避免使用 Java(企业最流行的编程语言)成为一个主要限制。感谢有Substrate VM 和Quarkus之类的框架,Java 已经是轻量级、快速、云原生的,并且是 serverless 友好的。而且有迹象表明,这种 Java 运行时很快也将适用于 serverless 的环境,包括 AWS Lambda,希望如此。
具有 serverless 特性的容器工作负载、具有标准化事件的功能可移植性和互操作性,以及为云原生环境和 serverless 环境创建的超轻型和快速 Java 运行时,这些都是 serverless 即将发生改变的信号。我还不想将这些指示器标记为“第二代 serverless ”,但它肯定不是第一代 serverless ,所以大概应该称为 1.5 吧。
我记得当许多人认为 Cloud Foundry 赢得了 PaaS 之战时,但后来又冒出了 Kubernetes。现在许多人声称 AWS Lambda 赢得了 FaaS 之战。我希望 Kubernetes(或者更好的东西)证明他们错了。
serverless 工作负载
我们了解了微服务架构如何通过围绕业务域建模服务并将更改封装在服务中来改进大型独体应用程序的部署周期。serverless 有一种简单的说法,是将其表示为更小的微服务,其中每个操作都是一个函数。虽然这在技术上是可能的,但这将是两种架构中最糟糕的一种,导致大量函数以同步方式彼此调用,而不会从得到的架构中获得任何好处。
微服务的价值在于,它可以用基于 Web 的 API(通常是 REST 风格)封装一系列请求/响应风格操作背后的复杂业务域逻辑和持久性逻辑。而反之,serverless 和函数主要关注事件和触发器。虽然函数可以放在 API 网关的后面,并以请求/响应的方式执行操作,但是 API 并不是主要接口:事件和触发器才是主要接口。当应用程序是异步的(单向的即发即弃(fire-and-forget)样式,而不是请求/响应样式)并通过队列或其他数据和事件源连接时,serverless 的应用程序往往工作得最好。因此,每个函数应只预计执行一个操作,应该避免直接调用其他函数,并将其结果写入事件存储。由于执行模型的原因,函数是短生命周期的,并且应该与其他不面向连接的 serverless 数据源一起使用(典型的 RDBMS 就属于此种情况),并且在部署规模方面比较轻,启动速度也比较快。以下所有用例都使 serverless 更适合,因为它充当连接各种事件驱动系统的粘合代码:
按需功能,如批处理、流处理和提取-转换-加载(ETL);
可分解为短时间工作的任务调度,如批处理任务;
事件驱动的架构,它执行响应数据源变化的逻辑;
处理不统一的通信,例如不经常发生的不一致的通信,或负载不可预测的通信;
运维中的通用“胶水”代码;
针对构建作业具有按需资源的持续集成流水线;以及
自动化运维任务,例如在事件发生时触发操作或通知随时待命的人员。
我讨论了一些 serverless 世界中正在发生的主要创新,但没有说明它们在 Kubernetes 上会是什么样子。大家做了许多努力,试图将 serverless 引入 Kubernetes,但是拥有最广泛行业支持和最佳成功机会的项目是 Knative。Knative 的主要目标是为常见的 serverless 用例提供具有更高层次抽象的专注的 API。它仍然是一个年轻的项目(编写本文时是 0.5 版),并且变化很快。我们来研究一下 Knative 当前支持的 serverless 工作负载。
请求伺服
从微服务到 serverless 的低阻力的转换方法是使用单一操作的函数来处理 HTTP 请求。我们希望 serverless 平台能够在几秒钟内支持无状态、可伸缩的功能——这正是Knative Serving的目标,它为 serverless 工作负载提供了一个通用的工具包和 API 框架。在此上下文中,serverless 工作负载是一个单容器、无状态 pod,主要由应用程序级(L7)请求流量驱动。
Knative Serving 项目提供了基本的支持:
通过提供高级的、自定义的原语,快速部署serverless 容器;
由请求驱动的激活、上下伸缩到零;
底层原语的自动路由与配置;以及
修订的不可变快照(已部署的代码和配置)。
所有这些都可以在特定的限制范围内实现,比如每个 pod 只有一个容器、一个端口、无持久化以及其他一些限制。
事件
Knative Eventing 处理项目为创建可靠、可伸缩、异步事件驱动的应用程序提供了构建模块。它的目标是围绕使用 CloudEvents 标准消费和事件的创建制定标准体验。Knative Eventing 的高级功能包括:
可扩展和可插拔的架构,允许不同的导入实现(如GitHub、Kafka、SQS、Apache Camel等)和通道实现(如Kafka、谷歌Pub/Sub、NATS、在内存中等);
事件注册表,用于维护事件类型的目录;
通过绑定事件源、触发器和服务来实现事件编排的声明式API;以及
触发器功能,允许从特定代理订阅事件,并可以选择将事件路由到下游Knative Serving之前进行筛选。
这些特性很有趣,但是它们如何帮助云原生开发人员在 Kubernetes 上更为高效呢?
假设我们已经实现了一个函数,将其构建为一个容器,并对其进行了充分的测试。它基本上是一个只有一个操作的服务,通过 HTTP 接受 CloudEvents。使用 Knative,我们可以将容器部署到 Kubernetes 中,作为具有 serverless 特性的工作负载。例如,使用 Knative Serving 原语,容器只能在有 HTTP 请求时激活,并在必要时快速伸缩。此外,还可以通过订阅通道将相同的 pod 配置为接受来自一个代理的 CloudEvents。该 pod 还可以作为通过 Knative 序列定义的更复杂的事件编排流程中的一个步骤。所有这些无需修改已经构建的容器,只需要使用声明性的 Knative 配置都可能做到。Knative 将确保 serverless 基础设施的路由、激活、可伸缩性、可靠性、订阅、重新交付和代理弹性。这还不是全部,但也接近全部了。
自定义工作负载
以上还不是全部。如果您的应用程序具有非常特定的需求,而又没有标准工作负载原语提供给你,Kubernetes 提供了更多的选项。在这种情况下,自定义控制器可以主动监视和维护一组处于所需状态的 Kubernetes 资源,从而向集群的行为添加定制的功能。
控制器是一个在高层次上执行“观察->分析->行为”的主动协调过程。它监视感兴趣的对象的预期状态,并将它们与现实的实际状态进行比较。然后,该流程发送试图将当前状态更改为更接近所预期的状态的指令。
处理定制工作负载的更高级方法是使用另一种出色的 Kubernetes 扩展机制:CustomResourceDefinitions。将 Kubernetes Operator 与 CustomResourceDefinitions 相结合,可以将针对特定应用程序需求的运维性知识封装在“算法”表单中。Operator 是一个理解 Kubernetes 和应用程序域的 Kubernetes 控制器,通过结合这两个领域的知识,它可以自动执行通常需要人类运维人员执行的任务。
控制器和 Operator 正在成为扩展平台和在 Kubernetes 上启用复杂应用程序生命周期的标准机制。因此,正在形成管理更复杂的云本地工作负载的完整生命周期的控制器生态系统。
Operator 模式使我们可以扩展控制器模式,以获得更大的灵活性和更强的表达能力。本文中讨论的所有工作负载类型,以及其他相关的模式在我最近合著的《Kubernetes 模式》一书中都有所涉及。有关这些主题的更多细节,请阅读该书。
云原生趋势
在 Kubernetes 生态系统中,正在向将越来越多不属于业务逻辑的商品特性迁移到平台层的趋势演变:
部署、放置、健康检查、恢复、伸缩、服务发现和配置管理都转移到了Kubernetes层。
通过将与网络相关的职责(如弹性通信、跟踪、监控、传输级安全性和流量管理)转移到平台上,服务网格延续着这一趋势。
Knative添加了专门的serverless 原语,并将快速伸缩、伸缩到零、路由、事件基础设施抽象、事件发布、订阅机制和工作流组合的职责转移到平台上。
这主要是为让应用程序开发人员来实现业务逻辑的关注点。剩下的由平台来处理。
职责转移到平台上。更多的抽象使各类工作负载成为可能
通过向 Kubernetes 添加更高级别的抽象,我们将越来越多的共性职责从应用程序层转移到平台中。例如,Istio 提供了依赖于低层 Kubernetes 原语的高级网络抽象。Knative 添加了更高级别的 serverless 抽象,这些抽象依赖于 Kubernetes 和 Istio 中的较低级别抽象(请注意,可能很快就不是这样了,Knative 将不再依赖于 Istio,尽管它将需要实现类似的功能)。
这些额外的抽象使 kuberntete 能够统一地支持各种工作负载,包括具有同样开放的打包和运行时格式的 serverless 。
运行时和应用程序设计
随着大型独体架构向微服务和 serverless 的过渡,运行时也在不断发展;发展如此之快,以至于已有 20 年历史的 Java 运行时正在从“一次编写,随处运行”的口号转向首先实现本机、轻量级、快速和 serverless 。
Java 运行时和应用程序设计趋势
多年来,摩尔定律和不断增长的计算能力带领 Java 构建了最先进的运行时之一,包括高级垃圾收集器、JIT 编译器和许多其他东西。摩尔定律的终结使得 Java 引入了从多处理器和多核系统中获益的非阻塞原语和库。本着同样的精神,随着云原生化和 serverless 化等平台趋势的出现,分布式系统组件变得更轻、更快,并且针对单个任务进行了优化。
Severless
serverless 范式被认为是下一个基础性架构演化。但是,当前这一代的 serverless 已经从早期采用者和阻挠者中脱颖而出,他们的首要任务是速度。这不是具有复杂业务和技术限制的企业的首要任务。在这些组织中,行业标准是容器编排。Kubernetes 正试图通过 Knative 计划将云原生和 Serverless 结合起来。其他项目,如 CloudEvents 和 Substrate VM 也在影响着 Serverless 生态系统,并将其推向一个开放、可移植性、互操作性、混合云和全球采用的时代。
2019 年 10 月 10 日,我的 Red Hat 组织的虚拟大会上演讲了“用Kubernetes模式设计云原生应用”。如果你对这个话题和内容感兴趣,请查看本次大会的相关内容。
作者简介
Bilgin Ibryam 是 Red Hat 的首席架构师,也是多个 Apache 软件基金会项目的提交者。他经常写博客、宣传开源软件、热衷于区块链和演讲。他写过《Camel设计模式》和《Kubernetes模式》两本书。他在设计和构建高度可伸缩和弹性的分布式系统方面拥有超过十年的经验。在日常工作中,Ibryam 喜欢指导企业中的团队通过经过验证和可重复的模式和实践大规模地构建成功的开源解决方案。他目前的兴趣包括企业区块链、云原生和 serverless 范例。@bibryam 关注他,定期阅读他在这些话题上的更新内容。
原文链接:
Kubernetes Workloads in the Serverless Era: Architecture, Platforms, and Trends
评论