相关阅读:
从微服务转为单体架构、成本降低 90%,亚马逊内部案例引发轰动!
在构建分布式系统时,松散耦合是一个主要的考虑因素。关于耦合及其在分布式系统设计中的作用,我们可以为其写一整本书。许多集成模式都与耦合有关。十多年前,我对耦合进行了定义:
耦合描述了互连的系统的独立可变性,即系统 A 中的变化是否会对系统 B 产生影响。如果有影响,那么 A 和 B 就是耦合的。
以下几个重要的推论可以用来支撑这一定义:
这两个维度的耦合尤为明显:
通用数据类型和稳定的接口是减少设计时耦合的常用方法,而异步消息传递和断路器通常用于减少运行时耦合。
在我的一次 re:Invent 演讲中,我也强调了解耦系统是有成本的。
例如,通过通用数据格式进行解耦需要在端点做转换,这会导致运行时和内存成本增加。
通过注册中心进行位置解耦需要额外的查询操作,消息路由通常由中央消息 Broker 负责处理,这会导致运行时成本和延迟增加。
因此,从某种程度上讲,云端的解耦也是需要付出代价的,这一点也就不足为奇了。然而,当我们看着月账单上的成本费用时,我们的反应可能是这样的:这真的值得吗?让我们来看一个实际的例子。
在一个无服务器研讨会上,我看到了下面这段代码(为简单起见,我省略了对象的许多字段):
dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table(DYNAMODB_TABLE)
event_bridge = boto3.client("events")
domain_object = # set fields
table.put_item(Item=domain_object)
domain_object_status_changed_event = # set fields
event_bridge.put_events(
Entries=[{
'Time': get_current_date(request_id),
"Source": SERVICE_NAMESPACE,
"DetailType": "DomainObjectStatusChanged",
"Detail": json.dumps(domain_object_status_changed_event),
"EventBusName": EVENT_BUS
}]
)
代码非常简单,你不需要成为 AWS 无服务器专家就能理解它做了什么。这段 Python 代码接收来自 API Gateway(这里未显示)的传入请求,执行一些逻辑,然后将业务领域对象存储在 DynamoDB 表中。它还将事件发布到 EventBridge(AWS 的无服务器事件路由器),通知其他组件发生了变更。发送事件这种方式可以避免直接与对变更事件感兴趣的组件发生耦合。
交互的架构图如下所示:
这段代码的优点是:
它的缺点是:
无服务器的伟大之处在于它不只是代码的运行时,而是一套完整的全托管服务,可以帮助减少代码量。为了展示这种平台的强大功能,我把用自动化代码(以及相应的资源)替换应用程序代码的无服务器重构过程记录了下来。
上面的应用程序是一个理想的重构场景:不通过编写代码来发送事件,而是让 DynamoDB 为你发送事件。DynamoDB Streams 是一个很棒的特性,它可以发布变更日志,供其他系统使用。这非常适用于我们的场景!
有了 Streams,我们就可以避免编写所有与准备和发送应用程序事件相关的应用程序代码。但实际上 Streams 并没有发送事件,而是让轮询消费者主动读取。这也就是为什么 EventBridge 不能直接从 DynamoDB Streams 中获取到事件。但 AWS Lambda 可以,最近发布的 EventBridge Pipes 也可以:
Pipes 可以将事件发布到各种目的地,包括 EventBridge。
Pipes 还提供了另一个方便的特性:消息转换。我们需要这个特性,因为 DynamoDB Streams 发布的事件格式使用了 DynamoDB 数据结构,因此不适合作为业务领域事件(为了简单起见,这里的数据被截短了):
{
"version": "0",
"id": "89961743-7396-b27b-1234-1234567890",
"region": "ap-southeast-1",
"detail": {
"eventID": "faae270c2d33f15567451234567890",
"eventName": "INSERT",
"eventSource": "aws:dynamodb",
"awsRegion": "ap-southeast-1",
"dynamodb": {
"ApproximateCreationDateTime": 1675870861,
"Keys": {
"object_id": {
"S": "usa/anytown/main-street/152"
}
},
"NewImage": {
"object_last_modified_on": {
"S": "08/02/2023 15:41:00"
},
"address": {
"M": {
"country": { "S": "USA" },
"number": { "N": "152" },
"city": { "S": "Anytown" },
"street": { "S": "Main Street" }
}
},
"contract_id": { "S": "d4745ecb-ac96-4764-ba88" }
},
},
"eventSourceARN": "arn:aws:dynamodb:ap:12345:table/xyz/stream/"
}
}
你可以使用 EventBridge Pipes 的便捷转换编辑器来构建转换:
多亏了 Pipes,发送到 EventBridge 的事件看起来就像是最初由应用程序代码发送的事件:
{
"version": "0",
"id": "7ba242f7-5a2c-81d3-1234-1234567890",
"detail-type": "ContractStatusChanged-Pipes",
"source": "unicorn.contracts",
"account": "1234567890",
"time": "2023-02-08T16:06:56Z",
"region": "ap-southeast-1",
"detail": {
"object_last_modified_on": "08/02/2023 16:06:56",
"property_id": "usa/anytown/main-street/153",
"contract_id": "25a238b6-3143-41e2-1234-1234567890",
"contract_status": "DRAFT"
}
}
我配置了不同的 detail-type,这样就可以区分事件(因为还不能在控制台上设置 EventBridge 目标参数,所以我通过命令行来设置)。
架构就是一个(有意识地)做出权衡的过程,因此我们有必要看看这个解决方案需要做出哪些权衡。
新的解决方案似乎更加优雅,或者我可以说它们就是“云原生”的吗?没有与发送事件相关的代码,也不需要在 Lambda 函数中包含 EventBridge 库(或了解它的 API)。AWS 运行时负责管理事务完整性和重试逻辑并异步执行,这让 Lambda 函数变得更小、更快。
那么新的解决方案的成本如何呢?云账单会因为使用了额外的服务而增加吗?可能会,但云账单并不是你唯一要考虑的成本。
在大多数地区,EventBridge Pipes 的定价范围从每百万事件 0.40 美元到 0.50 美元,所以账单中将包含这项费用。从 DynamoDB Streams 中读取数据需要收费,但从 Lambda 或 Pipes 中读取时是没有费用的。
一个更小更快的 Lambda 函数抵消了部分 Pipes 成本。
另一方面,Lambda 函数由于消除了所有 EventBridge 代码而变得更小更快。为了估算这样能节省多少钱,我做了一个不是那么科学的测试,用 Postman 多次调用这个函数。从 Lambda 函数的指标中可以看到,原始版本发送事件在大约 65 毫秒(左边的蓝点)时触底,而 DynamoDB 处理事件将其降到了大约 14 毫秒(右下角的蓝点)——由于 DynamoDB 的异步处理,减少了 75%:
这 50 毫秒的时间值多少钱?Lambda 函数的成本为每 BG 秒 0.000016667 美元(每月 90 亿 GB 秒后可以获得批量折扣,也有按请求收费的,不过这也不会影响我们的比较)。数这么多个零并不容易,所以我们来快速计算一下:
$0.000016667美元每GB/秒
$0.016667美元每GB/毫秒(对于百万个请求)
$0.10416875美元128MB/50毫秒(对于百万个请求)
因此,我们这种基于经验数据的数学算法可以估算出:Lambda 执行时间大约相当于 Pipes 的 1/4(尽管我没有检查内存占用减少了多少,但这方面的成本也会进一步降低)。
那么,事件发送的解耦是否也会消耗成本?按照每百万请求额外 0.3 美元的粗略数字计算,开发人员花费 1 小时(150 美元)编写、测试和调试与发布事件相关的代码(还有重试和错误处理逻辑)相当于会生成 5 亿个事件。如果你运行的不是世界上首屈一指的电子商务网站,那么这些事件已经算很多了。
我已经在“Cloud Strategy - A Decision-based Approach to Successful Cloud Migration”一书的“It’s Time to Increase Your ‘Run’ Budget”章节里提到,你为开发工作付出的不是实际成本,而是机会成本,即开发人员在 1 小时内可以创造的价值。在一个运行良好的软件交付组织中,这些价值应该是工资成本的高倍数。这意味着你很容易就会被淹没在数十亿个事件中。
此外,计算云端成本可能会产生所谓的“地下室效应”:
当你把更亮的灯装到地下室里,可能会看到更多乱糟糟的东西。但你不能因此责怪光线太亮。
所以,不要责怪云计算让成本问题显露无疑。你所运行的任何一段应用程序代码都会产生基础设施成本,只是你在购买硬件之前看不到而已。在我最喜欢的例子中(见“The Software Architect Elevator”一书),应用程序的绝大多数 CPU 和内存都花在了解析 XML 和回收它所创建的无数对象上。这确实耗费了大量的成本,但这些成本都被隐藏在了硬件采购中(在将应用程序被迁移到弹性基础设施上时,这个问题就暴露出来了)。
了解成本细节是件好事,但要确保考虑到了总体成本,包括调试和解决数据不一致的问题、将代码升级到新的运行时或更新库、增加新的开发人员、更长的构建和测试周期等等所花费的时间。人们之所以会(错误地)认为成本上升,考虑范围太窄是其中的一个常见原因。架构师既能纵览全局也能着眼于细微处,所以你要确保把问题放大到合适的规模:
仅仅因为有形成本上升,并不意味着总体成本的上升。而恰恰因为成本变得可见,你才可以看到并管理好它们。
在改变系统的运行时架构时,成本并不是唯一需要考虑的问题。例如,性能也可能受到影响。我们已经注意到 Lambda 执行时间减少了大约 50ms,这对于这个示例应用程序的 Web 前端来说是非常了不起的。
但是,异步发送事件会增加发布事件所需的时间吗?我们通常应该优化同步执行时间(在我们的例子中是 Lambda 函数及其前面的 API 网关),即使它们会导致更长的异步执行时间。
为了了解我们节省的 50 毫秒是用什么换来的,Luc van Donkersgoed 发布了一份 AWS 无服务器消息延迟的比较(这里只显示 50 和 90 百分位):
P50 P90
SNS Standard 73 ms 125 ms
EventBridge 148 ms 241 ms
DynamoDB Streams 213 ms 341 ms
可以看到,DynamoDB Streams 处于频谱的较高水平,可能是因为它们使用了轮询 / 拉取模式。
架构是一个关于边界的问题,无论是软件架构还是物理(建筑)架构。在下图中,我悄悄定义了“应用程序”和“集成”之间的边界:
这看起来似乎就是自然的职责分离——你几乎可以说表示服务的图标就是按照颜色进行分组的。但将架构画成一组表示服务的图标通常并不能说明全部情况,甚至可能会导致想法变得狭隘。
如果我们思考的是服务的意图,而不是它们的颜色,就会看到略微不同的视图。将 Pipes 视为应用程序的一部分实际上更有意义,因为它依赖于私有数据源,即应用程序数据库:
出站转换(Outbound Transform)是基于规范化数据模型的架构的一个常见元素,它确保发布的消息(包括事件)不会将实现细节泄漏到消息总线中:
拦截过滤器是一种更为常见的模式,在 Deepak Alur、John crubi 和 Dan Malks 所著的《J2EE 核心模式》中有详细说明。模式中的“过滤器”指的是管道和过滤器架构风格。早在 2005 年,我就在博客上写过出站过滤器和入站过滤器。还有一点值得注意的是,我们正在使用 Pipes 服务实现过滤器。
按照我自己的建议,将模式作为更加突出的前景,将服务作为装饰,那么画出来的架构图是这样的:
为了更好玩一些,我加入了“Sync or Swim”模式装饰(“鼻子”形状的东西),用以显示哪个组件在“推送”或“拉取”事件。我们通过这种方式让架构图变得更加丰富,包含了相关的分布式设计考虑因素而不会看起来很乱。
为了让分布式系统架构锦上添花,我们需要思考最后一个问题:
如果我们使用了出站过滤器,并假设实现了高度的自动化,那么我们还需要事件代理(Broker)吗?
这是一个很好的问题,关于这个问题,可以从 API308 这个视频中找到关于这个设计决策的一些想法:
基于这个比较,在端点附近添加 Pipes 并将 Amazon SNS 作为发布订阅通道来路由事件可能是一种可行的架构,并且实际上可以降低运行成本:从 SNS 到 Lambda 不收取通知费用,数据的收费为每 GB(即 100 万个 1KB 的消息)0.09 美元。因为它会随消息的大小而变化,所以你得自己动动手指头算一算,但这对我来说是确实是一个不错的买卖。此外,你还可以获得更高的扇出能力(同一种事件类型可以有更多的订阅者),并通过为要路由的每种事件类型配置事件代理来避免潜在的开发瓶颈。
因此,我们发现:
将所有东西变得松散耦合实际上可以让你云账单上的数字降下来。
这篇文章比我原先计划的要长一些。但跟往常一样,有很多值得反思的地方:
原文链接:https://architectelevator.com/cloud/cloud-decoupling-cost/
领取专属 10元无门槛券
私享最新 技术干货