使用无服务器(Serverless)技术构建应用程序并不完全是为了各种目的以实现函数。
过度使用与其他服务相结合的相互依赖的函数会导致架构变成一团乱麻。
必须培养一种将函数视为代码负担的思维方式。尽可能地消除它们可以降低成本和复杂性。
在某些用例中,函数并不是最适合的。在这种情况下评估和避免函数有助于构建架构良好的应用程序,从而优化成本和效率,并促进可持续性。
在现代分布式和事件驱动系统中,业务逻辑并不总是被整合为函数。例如,服务编排非常适合处理分布式业务逻辑。
由云提供商运营和管理服务(称为全托管服务)并不是什么新鲜事了。例如,Amazon Simple Queue Service (SQS)和 Amazon Simple Storage Service (S3)是 AWS 提供的全托管服务,已有近二十年的历史了。
随着平台和基础设施以亚马逊网络服务(AWS)、微软 Azure、谷歌云平台(GCP)以及其他云提供商提供的服务的形式出现,促进了独立软件供应商(ISV)的增长,这些供应商以软件即服务(Software as a Service,SaaS)的形式提供多种产品。
尽管“无服务器”(serverless)这一术语出现在 2012 年——其灵感来自于不断发展的云环境以及 SaaS 应用程序如何消除拥有和管理物理硬件的需要,但它与现有的全托管云服务的关联并不明显。
虽然 SaaS 市场的增长推动了创新,但该行业在托管服务库中缺少一个关键部分——云上的全托管计算服务。2014 年,亚马逊发布了 AWS Lambda——云上的全托管计算服务,俗称函数即服务(Function as a Service,FaaS)——它彻底地改变了云计算行业,立即将无服务器提升到了一个新的高度!不久之后,微软和谷歌分别推出了他们自己的 FaaS 产品 Azure Functions 和 Google Cloud Functions。
作为业界的首个 FaaS 产品,AWS Lambda 迅速流行起来。因此,关于无服务器开发的多个案例研究、经验教训、模式和反模式都是围绕着 Lambda 函数展开的。因此,它的名字与本文中提到的一些早期行业思想联系在一起。然而,这些想法同样适用于每个 FaaS 产品。
随着 API 网关和来自头部云提供商托管服务的发布,世界开始接受了消费函数的计算逻辑。因此,无服务器革命变得势不可挡,FaaS 的过度使用也随之而来!
编程纯粹主义者通常不太关注非计算性托管云服务,认为这些服务是面向配置的,适用于基础设施工程师。然而,这些纯粹主义者现在配置了云计算服务,以探索函数背后的编程模型。
随着 FaaS 采用率的不断提高,云提供商增加了各种语言的运行时来满足不同的计算需求、技能等,为大多数程序员提供了一些东西。语言运行时,如 Java、.NET、Node.js、Python、Ruby、Go 等是最受欢迎且最被广泛采用的。然而,这也给采用无服务器技术的组织带来了一些挑战。这些挑战不仅仅是技术的挑战,更是工程师思维方式的挑战。
下面将重点介绍在无服务器应用程序中过度使用 FaaS 的一些副作用。
“如果你唯一的工具是锤子,那么所有的东西看起来都像是钉子。” ——Abraham Maslow
编写 FaaS 函数对工程团队来说非常容易,可以快速实现业务逻辑和功能特性的发布,从而提高团队速度。这种新获得的名声和开发速度不可避免地创造了一种将一切都当作函数来处理的思维——这是解决所有业务问题的唯一解决方案。在 AWS 上,Lambda 函数被认为是解决所有问题的唯一方法。因此,我们称之为 Lambda 锤子(Lambda Hammer)!
工程师形成 Lambda 锤子思维的主要原因之一可以归因于他们的行业经验和编程能力。许多在非云环境中开发传统应用程序的工程师主要关注软件工程的编程方面。在这样的环境中,孤立的团队共同承担将应用程序投入生产或向客户发布功能的责任。开发人员(仅限于编程)根据架构师的愿景实现解决方案,由 QA 工程师进行测试,然后将其移交给平台或基础设施团队进行部署和运维。
当以编程为中心的工程师转向使用无服务器技术时,Lambda 编程模型和 FaaS 的概念自然就成了吸引他们的点——它们被认为是解决所有问题的工具!
在弹球游戏中,球被推进一个柜子里,在那里它会在各种各样的灯、保险杠、斜坡和其他障碍物上反弹起来。 ——维基百科。
Thoughtworks 最初提出了“Lambda 弹球架构”(Lambda Pinball Architecture)这一术语,是为了警告这样一种情况:相互依赖且纠缠在一起的 Lambda 函数以及 S3 存储桶和 SQS 队列等服务组成的网络,会导致请求在系统中四处传递反弹,从而导致形成一个复杂且分布式的单体应用程序,该应用程序难以测试和维护。
尽管 Thoughtworks 强调的是 Lambda 和其他 AWS 服务,但这种弹球情况也可能发生在 Azure、谷歌云和其他提供商身上。所有主要的云提供商都提供了具有类似功能的服务。因此,无服务器开发最佳实践是相关的,适用于所有人。
出现上述情况的原因是,在实现计算逻辑、数据摄取、数据转换、数据传输、服务中介和其他活动时采用了设计不佳的架构方法,没有明确的服务边界。长期处于弹球状态的可怕结果是会导致一种纠结的架构,即“无服务器泥球”,如图 1 所示。
图 1:纠缠在一起的无服务器架构
使用 FaaS 函数,你只需为执行付费(如果你不使用一些高级特性的话)。然而,实施超过必要的函数会增加你的云成本和总体拥有成本(TCO)。
对于大多数云提供商来说,一个函数的成本主要有两部分组成:
为调用该函数而向云服务发出的每个请求的价格
完成每次调用的时间和分配的内存(RAM)
假设你的函数
分配了 1024 MB 内存
每月调用 300 万次(每天 100,000 次调用)
每次调用平均执行 10 毫秒(ms)
对于部署在欧洲中部地区的 AWS Lambda 函数,上述费用为每月 0.40 美元(含免费套餐限额)。对于在德国中西部地区运行具有上述配置的 Azure 函数,费用是相同的。
但是,如果该函数的平均执行时间为 10 秒(而不是 10 毫秒),那么 AWS Lambda 函数的每月成本将跃升至 493 美元,而 Azure 函数的成本将增至 474 美元。
现在,想想如果有几个这样的函数可以执行通过其他方式(即以无函数的方式)实现的非计算任务,可以节省多少成本!
注意 :上述 Lambda 成本是针对使用 x86 处理器的。如果换成 Arm 处理器,价格将降至 395 美元。
可持续性是现代云操作的一个关键方面。使用可再生能源、减少碳足迹和实现绿色能源目标是云提供商的首要任务。云提供商投资于高效的电力和冷却技术,并运营高效的服务器群,以实现更高的利用率。因此,AWS 建议使用托管服务进行高效的云操作,作为其架构良好的框架(Well-Architected Framework)的可持续性最佳实践的一部分。
作为服务消费者,你也需要对可持续性负责。因此,主要的云提供商已经制定了架构良好的可持续性最佳实践,以促进提供商和消费者之间共担责任。
虽然云提供商管理 FaaS 的资源以实现云的可持续性,但你也有同等的责任在云上可持续地运行这些函数。这包括:
以最优的方式利用多个函数
优化每个函数的内存分配
将性能优化到所需的水平,而不是使用最大化
当你为不合适的目的实现 FaaS 函数或以次优的方式运行时,你会间接影响云的三个核心元素:计算、存储和网络。每个元素的运行都会消耗能量。
“你写的所有代码都是业务逻辑”。 ——Werner Vogels 博士,亚马逊首席技术官。
作为一名软件工程师,你会在行业中听到类似的关于程序代码的陈述和争论。
“对于一个组织来说,代码是一种负担,而不是资产”。
“你今天写的代码明天就会成为遗产”。
“一家公司拥有的代码越多,它花在维护上的钱就越多,从而增加了 TCO”。
不管这些争论如何,我们都无法避免编写代码——无论是函数代码、数据代码、集成代码还是基础设施代码。然而,这里的重点是不必要的和不需要的函数代码。
如前所述,Lambda 锤子思维通常会驱使工程师实现一个函数,而不是一个可替代的云服务或特性。该函数现在成为了团队维护的不必要负担。
当你考虑一个函数时,不要只想着编写它的乐趣,还要考虑测试、CI/CD 流程、发布、观测和云帐户的并发限制等。
对于刚接触无服务器的工程师来说,让他们的思维适应无服务器的需求可能是一个挑战。因此,你会听到无服务器思维是采用无服务器的先决条件。这是因为使用无服务器需要一种新的思维方式,在云上开发和操作应用程序。你必须将无服务器应用程序视为与基础设施代码编织在一起的托管云服务的编排。
通常,工程师用编程的眼光看待无服务器环境,忽略了托管服务的功能以及相关人员的角色和职责。
在我与 Luke Hedger 合著的《AWS 上的无服务器开发》(“Serverless Development on AWS” O’Reilly, 2024)一书中,我们建议将无服务器技术视为一个包含云提供商、FaaS 和其他托管服务、工具、框架、工程师、利益相关者等的生态系统。这为技术带来了 社会技术思维,这对于使用现代技术和工程实践来说至关重要。
图 2:无服务器生态系统的视图
另一种看待它的方式是通过系统思维。Diana Montalion 的新书《学习系统思考》(“Learning Systems Thinking”O’Reilly, 2024)指出,系统是一组相互关联的硬件、软件、人员、组织和其他元素,它们相互作用和 / 或相互依存,以服务于共同的目的。
当你构建微服务时,根据你的领域和团队的有界上下文,正如我在 QCon 2024 上的演讲“The Set Piece Strategy”中所解释的那样,并非所有的服务都会执行需要函数的计算。正如你将会在下面学习到的,服务的业务逻辑可以通过原生集成和事件驱动的通信方式来进行编排并与其他服务协作。
快速流程团队寻求对事件的快速周转处理。对于这样的团队来说,少即是多。
现代产品团队是遵循流对齐的(正如团队拓扑所倡导的),并在快速迭代中自主开发和发布新功能和更新。无服务器是一项能够实现快速流的伟大技术。
当团队在云上操作他们的工作负载时,观测和修复生产中的问题是至关重要的。平衡正方形(The square of balance)是《AWS 上的无服务器开发》(“Serverless Development on AWS” O’Reilly, 2024)中使用的一个术语,用来解释测试、交付、观测和恢复这四个活动是如何成为实现平衡的关键的。
图 3:快速流动的无服务器平衡正方形
当无服务器团队在快节奏的开发环境中运行时,拥有最小的代码负担是有利的。它减少了潜在的错误和故障点,并将更多的责任转移给了云提供商。
在无服务器上下文中,“无函数”(Functionless)、“无 Lambda”(Lambda-less)、“无代码”(Codeless)、“低代码”(Low-Code)等术语表达了对通过原生服务集成来减少应用程序中 FaaS 函数数量的想法。
我第一次听说 Functionless 是在 2019 年赫尔辛基的无服务器日(Serverless Days)会议上。我意识到有必要让工程师了解一下过多函数的副作用,因此一直在社区中倡导,重申在可行的情况下消除函数的好处。
除了了解云计算的细微差别之外,构建和开发无服务器应用程序的一个关键部分是超越 FaaS 的思维,并记住函数是无服务器生态系统的一部分,而不是全部。
当企业决定使用云提供商的服务或供应商的产品时,就会引入某种形式的锁定。企业意识到了这一点,并有意识地选择与提供商建立稳定而友好的合作关系来发展业务,而不是频繁地更换产品、服务和提供商。
使用原生服务集成和特定于云平台的功能的确会增加依赖性。即使使函数的计算逻辑脱离特定的云平台,也很难做到完全独立。如上所述,一个函数必须与一个或多个云服务协作才能提供业务功能。当你专注于使函数不依赖于特定的云平台时,实际上你并未充分利用云计算的优势。如果企业选择了某个云提供商,然后又决定违背其核心利益,这是违反直觉的,会损害业务。
“简单胜过繁复的修饰。有时,简单的东西比高级或复杂的东西更好”。 —— 来自网络。
虽然提倡简单很容易,但做到简单并不总是那么容易。它需要有处理复杂架构的经验和知识,以及对可以实践简单性领域的认识。
你在构建无服务器应用程序方面的经验使你能够从简单的角度看待架构。本节将研究可以使用模式来避免使用函数并使架构 Functionless 的领域。
无服务器社区的一条建议是:你应该使用函数来转换数据,而不是传输数据。如果你使用函数在服务之间移动数据,就说明你将它用在了错误的目的上,或者云提供商缺乏原生功能。
数据处理是云计算的核心部分,现代数据集的数量和增长率呈指数级增长。从成本和规模的角度来看,在这种情况下,不建议对每个数据操作都使用函数。因此,你应该设法将繁重的工作留给核心云服务。
在传统上,数据库主要是指关系型数据库系统(RDBMS),在较小程度上指代其他存储变体,如向量和图形数据存储等。然而,现在云上提供了多种数据存储选项,如关系型或 SQL、NoSQL、对象、图、向量、文档等。
除此之外,包含消息和事件摄取、缓冲和路由的服务还提供了临时存储。你可以对许多此类服务操作应用 Functionless 和原生操作,这也是你必须要在你的架构思维中加以考虑的。
许多工程师都有一种先入为主的思维模式,即认为需要使用函数来执行数据操作。有了这样的想法,函数就会悄悄地出现在它们没什么用的地方。函数通常用于传输数据的两个地方是 API 网关后端和工作流编排。
下面将介绍几种无需函数帮助即可处理数据操作的方法。
将 API 请求的有效负载存储在数据表中:图 4 显示了一个简单的无服务器模式,其中函数接收 API 请求的有效负载,例如一本新书的标题,并将其保存在表中。大多数 API 网关都提供了对传入数据的模式验证。验证通过后,你可以使用原生服务集成将 API 中的数据直接存储到表中。因此,中间的函数就没有必要了。
图 4:消除数据操作中不必要的函数
和上面一样,你可以从数据表中获取数据,然后将其作为 API 的响应负载发送,中间无需使用函数。
没有函数的原子数据计数器操作:例如,Amazon DynamoDB 没有像传统关系型数据库那样内置生成唯一序列号的功能。但是,这可以通过 DynamoDB 中的原子计数器概念实现,如图 5 所示。如果你有一个服务,需要为订单、客户注册、网站访问者等生成唯一值,就可以在 API 和 DynamoDB 表之间实现。
图 5:API 网关和 DynamoDB 表之间的原生服务集成,以生成序列号
{
"TableName": "sequence-numbers",
"Key": {
"id": {
"S": "visitor"
}
},
"ExpressionAttributeValues": {
":val": {
"N": "1"
}
},
"UpdateExpression": "ADD sequence :val",
"ReturnValues": "UPDATED_NEW"
}
执行原子计数器增量的 VTL 脚本示例。
缓冲大容量的 API 请求:另一个被广泛使用的案例是处理尖峰 API 请求并异步处理它们,如图 6 所示。在这种情况下,控制负载以保护下游系统至关重要。在 API 后面添加队列可以作为控制数据流的缓冲区。在这种情况下,数据在处理之前先被存储,这称为存储优先模式(Storage First pattern)。这种模式在其他数据存储中很常见。
图 6:在队列中缓冲的 API 请求用于异步处理
从数据库中清除过期数据的一种传统方法是在调度器上运行一个任务,以查询具有特定参数的数据并执行删除操作。根据数据量的不同,此类清理任务可能需要几分钟或几小时才能完成。
当工程师将上述经验引入到无服务器开发时,查询和删除数据的明显选择是使用一套专门构建的函数。然而,现代数据存储提供了免费且不影响性能的自动数据清除。选择这种原生特性可以避免实现函数。
例如,Amazon DynamoDB 可以选择为表中的每个数据项(记录)设置生存时间(TTL)。尽管某些删除可能长达 48 小时,但这也比你自己以编程的方式处理要高效得多。Azure Cosmos DB 还提供了一个类似的 TTL 功能来删除不需要的数据。
云上最受欢迎的对象存储 Amazon S3 支持数据存储桶的数据保留策略。借助数据保留策略,你可以让 S3 管理数据对象的生命周期,以删除过期对象或将其移动到归档或低成本存储中,以满足你的业务需求。
使用服务的原生特性对你使用的每个数据存储执行数据清理。除了处理的函数较少这一好处之外,这还为你的云操作带来了可持续性的好处。虽然数据是我们所做的一切的核心,但并非所有数据都是有价值的,或者某个点之后仍然是有价值的。因此,考虑数据生命周期对于删除不需要的数据、减少计算存储和网络使用至关重要。
API 实现包括执行典型操作的端点,例如在数据库系统中创建、读取、更新和删除(CRUD)数据,并与适当的 HTTP 方法保持一致。尽管每个 API 契约和调用类型(同步或异步)都可以归类为这四种不同的数据操作之一,但在现代分布式系统中,后端操作并不总是严格的 CRUD。
如前所述,当同步 API 获取新订单号时,它将会执行原子数据操作。但是,在异步调用中,行为不必相同。例如,当客户下订单时,来自多个业务领域的多个团队拥有和运营的一个或多个服务将协同来完成请求。在这种情况下,并非各个服务内部或跨各种服务的每个活动都需要是原子性的。
从模块化和可扩展性的角度来看,由与多个服务交互以协调任务组成的复杂逻辑单体函数并不是一个理想的解决方案。在这种情况下,服务编排是一种值得考虑的恰当设计模式。服务工作流不再是一个函数,而是成为后端编排器,如图 7 所示。AWS Step Functions 和 Azure Logic Apps 等云服务是目前可用的流行工作流编排器。
图 7:启动异步操作并处理 API 请求的工作流
流行的全托管云服务可以采集和交付事件,提供了与许多其他服务的原生集成。事件过滤、转换、归档和向多个目的地传递交付是这些服务的一些核心特性,它们消除了对自定义函数的需求。
Amazon EventBridge、 Azure EventGrid、 Google Cloud Pub/Sub 等云服务,都提供了事件传输功能,并充当事件代理,以松耦合的事件驱动的通信方式轻松地与多个应用程序协调。
使用云服务的一个主要动机是将尽可能多的工作委托给云提供商。在这方面,减少不必要的函数代码的原生服务集成必须是构建无服务器和云应用程序时的一个因素。
然而,在培养 Functionless 思维的同时,同样重要的是要意识到它的一些局限性和权衡,如下所示。
大多数原生服务集成都是黑盒的,在运行时无法看到集成的代码。这可能会使调试缺陷变得更具挑战性。
例如,在 AWS 中,集成代码是使用 Velocity 模板语言(VTL) 编写的。由于 VTL 的语法不同于我们熟悉的编程语言,学习和熟悉它需要时间。
尽管 API 网关和托管服务支持与多个云服务的原生集成,但每个目标服务的集成格式并不统一。
了解源和目标服务之间允许的数据有效负载大小的差异对于防止处理失败至关重要。
与有效负载的大小一样,集成服务之间的服务超时和节流限制也可能不同。
有时,你可能需要一个函数来执行非功能性操作,例如对部分请求上下文数据进行安全检查。
“改变的第一步是意识。第二步是接受”。 ——Nathaniel Branden。
作为云计算的演变,无服务器技术使组织能够通过消除不必要的服务器和硬件管理负担来快速增加价值。然而,在使用无服务器技术构建应用程序时,你必须继续探索简化架构和运维开销的方法。
想通过本文来捕获每一个减少 FaaS 占用的用例是不可能的。它旨在提高人们的意识。根据行业反馈,新采用的无服务器团队可能会犯下代价高昂的架构和实施错误。虽然这在某种程度上是可以理解的,但动机应该是从一开始就采用恰当的模式和原则来避免这种错误。
一刀切的方法并不适合所有人。业务领域和用例因团队而异。在实施模式(无函数或其他模式)时,你必须首先分析其适用性。当了解了各种可能性之后,你就可以采取必要的措施来简化你的架构了。
Sheen Brisals 是独立顾问、AWS 无服务器英雄、团队拓扑倡导者,也是《AWS 上的无服务器开发:构建企业级无服务器解决方案》(O'Reilly,2024)的合著者。作为一名技术专家,Sheen 曾在领先的软件组织担任过多个职位。他的想法帮助了多个团队和组织走上云和无服务器采用之路。他是一位主题演讲者,住在英国伦敦的郊外。
声明:本文为 InfoQ 翻译,未经许可禁止转载。