[关闭]
@levinzhang 2021-11-29T22:41:38.000000Z 字数 6343 阅读 582

借助Amazon S3实现异步操作状态轮询的Serverless解决方法

by

摘要:

本文提出了一个将轮询重定向到Amazon Simple Storage Service(S3)的解决方案,S3是一个由公有云提供商Amazon Web Services(AWS)管理的高可用、可扩展和安全的对象存储服务。我们将会展现一个使用AWS Lambda函数的serverless实现,但是如果你想使用S3的话,并不强制要使用AWS Lambda函数。


核心要点

异步API会有很多的优势,比如解耦、可扩展和弹性等。但是,正如俗话所言,“世上没有免费的午餐”,我们需要考虑在客户端和服务器端所增加的复杂性。

要获取异步操作的状态往往需要客户端定期轮询结果。这种操作会导致客户端和服务器端的资源浪费。

本文提供了一种将轮询部分重定向到Amazon Simple Storage Service(S3)的方案。

S3是一个由公有云提供商Amazon Web Services(AWS)管理的高可用、可扩展和安全的对象存储服务。

我们将会展现一个使用AWS Lambda函数的serverless实现,但是如果你想使用S3的话,并不是强制要使用AWS Lambda函数。

举例来说,你还可以使用Docker容器。

Serverless异步API

在AWS平台上,异步API的典型的serverless实现会涉及到Amazon API Gateway、一些lambda函数、一个SQS队列以及我们本例中所用到的NoSQL键-值数据库:DynamoDB。在下图中,我们可以看到整体的架构:

为了简单起见,我们的API只有一个资源,通过POST到“/order”可以创建一个新的订单,通过GET到“/order/{id}”可以检索订单。我们假设创建订单会消耗一定的时间,所以请求是异步的。客户端调用该端点并得到一个订单的id。借助这个id,它们必须要轮询GET端点来检查该订单何时创建完成。当然,如果客户端有一个可以被调用的回调端点或者它们能够在订单创建完成之后,接收到通知的话,那就没有必要使用轮询了。

尽管每隔一秒钟或差不多的时间去调用一个端点是很容易的,但这是一个无效的过程,会浪费客户端和服务器端的资源。除此之外,有些客户端无法实现webhook端点,无法消费通知,或者没有足够的时间来实现这些机制。

消除服务器端资源浪费的一种方式就是将轮询委托给AWS提供的托管服务。我们可以使用Amazon Simple Storage Service(S3)来实现这一点。

使用AWS S3实现轮询

Amazon S3是Amazon Web Services云供应商最早提供的服务之一。它是一个对象存储服务,提供了高可扩展性、高可用性和高性能。它的结构在某种程度上模拟了一个文件系统,其中会使用桶来盛放对象,所谓的对象也就是文件以及描述该文件的元数据。

我们可以使用S3将异步操作的状态存储为一个JSON文件,API的客户端会调用该服务,而不是轮询我们的API。通过这种方式,客户端检查状态更新的所有流量会被重定向到S3 API上,而不是我们自己的API上。

为了避免向我们的API客户端传播证书或其他的认证机制,我们将会使用S3的预签名URL(presigned URL)特性。默认情况下,所有的桶和文件都是私有的。但是,在限定的时间内,我们可以使用预签名URL共享一些文件(不需要暴露AWS安全凭证和权限)。

收到POST请求的lambda函数会生成包含操作状态的预签名URL,并将其返回给客户端。这个S3的文件名也会作为一个属性添加到要发送至SQS的消息中,这样的话,负责进行处理的部分在需要更新状态的时候就可以引用它的值。

AWS SDK提供了生成这些预签名URL的功能。在下面Python代码的样例中,我们会得到一个访问对象的GET URL,对象的key是OBJECT_KEY且位于BUCKET_NAME S3桶中,该URL会在十分钟内过期:

import boto3
url = boto3.client('s3').generate_presigned_url(
ClientMethod='get_object',
Params={'Bucket': 'BUCKET_NAME', 'Key': 'OBJECT_KEY'},
ExpiresIn=600)

使用其他编程语言的样例,请参考AWS文档

注意,这个功能也可以在Docker容器和自托管的应用中使用。如果你无法使用某种AWS SDK(Java、.NET、Ruby、PHP、Node.js、Python或Go)的话,还可以采用AWS S3 REST APIAWS Command Line Interface。并不是必须要使用serverless lambda函数。

在返回预签名URL以便于进行轮询的lambda函数中,我们还可以在响应中包含一个预估的时间,即客户端在什么时候可以开始询问操作的状态。这个时间预估可以基于SQS队列中消息的大致数量、in-flight状态的消息的大致数量(业已发送到客户端但尚未删除,或尚未达到消息的可见性过期时间),以及处理一个请求的平均时间。下面我们可以看到一个Python的例子,说明如何从SQS队列中获得这些数字:

import boto3
response = boto3.client(‘sqs’).get_queue_attributes(
     QueueUrl='QUEUE_URL',        
AttributeNames=['ApproximateNumberOfMessages'|'ApproximateNumberOfMessagesNotVisible'])

当使用S3来存储异步操作的状态时,较新的状态会被更频繁地查询,而旧的状态在一段时间后可能就完全不会再被读取了。因此,根据使用情况,你可以利用S3提供的不同存储类别。在写这篇文章的时候,AWS提供的不同类别和成本如下所示(仅限于Ireland区域):

存储类别 所设计的适用场景 可用性(按照设计) 可用区 最小存储时间 最小计费对象大小 其他考虑因素 每月每GB价格
S3 Standard 经常访问的数据 99.99% >= 3 0.023美元
S3 Standard-IA 长期存在,不经常访问的数据 99.99% >= 3 30天 128 KB 根据每GB的检索计费 0.0125美元
S3 Intelligent-Tiering 数据的访问模式是未知的、变化的或不可预测的 99.99% >= 3 对每个对象的监控和自动化会计费。没有检索的费用。 0.023至0.00099美元(取决于所使用的层)
S3 One Zone-IA 长期存活的、不经常访问、非关键性的数据 99.5% 1 30天 128 KB 根据每GB的检索计费。对可用区造成的丢失缺乏弹性 0.01美元
S3 Glacier 长期数据的存档,检索时间从几分钟到几个小时不等 99.99%(还原对象之后) >= 3 90天 40 KB 根据每GB的检索计费。在访问之前,我们必须先还原归档的对象。 0.004美元
S3 Glacier Deep Archive 用于归档很少访问的数据,默认检索时间为12小时 99.99%(还原对象之后) >= 3 180天 40 KB 根据每GB的检索计费。在访问之前,我们必须先还原(restore)归档的对象。 0.00099美元

资料来源

对象存储的管理是通过S3生命周期规则实现的。例如,我们可以声明一个规则,让文件在S3 Standard中存在十天,然后转移到S3 Standard-IA,30天后将其删除或者转移至S3 Glacier Deep Archive中。生命周期可以通过Amazon S3控制台、REST API、AWS SDK和AWS CLI进行配置。关于这方面的更多信息,请参阅文档

安全方面的考虑因素

虽然在默认情况下,S3中所有的文件和桶都是私有的,但是创建预签名URL会允许在限定的时间范围内访问这些文件。获取了预签名URL的所有人都能读取状态文件。因此,与API的通信应该只允许通过HTTPS来实现,状态文件中不要存储任何的敏感数据,并且这些文件的时间限制要设置地越短越好,当然,不能短于实际操作所要占用的时间。

另外一个额外的安全防护可以在S3侧执行,也就是只允许特定IP范围进行访问。这可以通过在桶上添加策略来实现,在AWS文档页面我们可以看到相关的例子。

如果预签名URL的机制对你的使用场景来说不够安全的话,那么在这种情况下,你可以使用AWS Security Token Service(AWS STS)创建临时的安全凭证,并将其提供给你的客户端,这种临时安全凭证可以控制对S3操作状态文件的访问。对于联合身份验证(identity federation),AWS STS支持企业级联合身份验证(自定义身份代理或SAML 2.0)和Web联合身份验证(使用Google、Facebook、Amazon或任意兼容OpenID Connect的身份识别供应商)。关于这方面的更多信息,请查阅他们的文档

收益分析

将轮询功能委托给S3能够让主服务只处理实际的业务逻辑请求,而不用持续地检查更新。这样的话,我们的serverless样例就会产生更少的函数调用,而且对DynamoDB的读取容量单元消耗也会更少。

尽管AWS Lambda函数的扩展速度非常快,并且可以处理大量的并发请求,但是你依然需要考虑并发的限制。根据AWS区域的不同,初始的流量暴增限制是500到3000,这一限制适用于账户中的所有函数。我们让轮询不去消耗并发量,这样就会为其他的函数留下更多的容量。关于lambda函数限制的完整列表,请查阅AWS的文档

其他浪费的资源是DynamoDB的读取请求单元。每个读取单元代表了一次强一致性的读取请求,或者两个最终一致的读取请求,因为每个条目最多只能有4KB。另外,如果你的表配置成了provisioned模式的话,这意味着你会声明读取容量单元的数量,这样的话,有些请求可能会被限流。DynamoDB还有一种On-Demand模式,在这种模式下,容量会随着流量进行调整。令人遗憾的是,轮询只会产生带来副作用的业务流量。

成本的收益会在请求达到100万的时候开始显现。对于几十万级别的请求来讲,差异并不大。我们下面会看到一个成本计算的样例。

我们以10万个请求为例,并假设每个请求平均会有10个轮询请求,因此共有100万个轮询请求。如下的计算是使用AWS Pricing Calculator针对Ireland AWS区域进行的计算。

API Gateway REST API的成本计算很简单:1,000,000个请求 x 0.0000035000美元 = 3.50美元

对于lambda函数,我们假设平均执行时间是500毫秒,并分配256MB的内存:

lambda的总成本:2.08美元 + 0.20美元 = 2.28美元

对于DynamoDB,我们估算的平均条目大小是10KB,我们将会使用最终一致的读取。

从Dynamo进行读取的总成本:总的读取请求单元1,500,000.00 x 0.000000283美元=0.42美元的读取请求成本

轮询请求的总成本将会是:3.50(API Gateway) + 2.28(Lambda) + 0.42(从DynamoDB的读取) = 6.2美元

这个成本略微有些高估了,因为lambda函数的响应时间可能会少于500毫秒,为它们提供128MB的内存可能就足够了。

对于S3,我们预估使用每月1GB(100,000 x 10 KB)的Standard存储:

S3数据传输,outbound的互联网流量,1 GB的tiered价格:

S3总成本:0.92美元 + 0.00美元 = 0.92美元

请注意,为了尽可能让对比更接近实际情况,这些计算只包含了实际请求相关的成本。因此,所有其他的额外成本没有包含进去,比如DynamoDB的存储成本。

成本差异不是很大。但是,我们将它列在了这里,这样你可以大致了解如何进行计算。

缺点

将轮询转移到S3有这么多的好处,但它也给整个解决方案增加了额外的复杂性。我们需要涉及另一个服务,即S3,并为每个操作创建一个预签名的URL。如果状态文件包含任何敏感信息的话,这个解决方案可能会增加更高的风险,因为任何得到预签名URL的人都可以访问这些信息。如果有来自许多客户端的大量调用,并且他们会在很短的间隔内进行轮询时,本文所提到的大部分的收益将会兑现。在只有少量调用的情况下,主API也可以处理轮询流量,而不需要使用S3。

总结

这篇文章展示了如何使用AWS S3来处理来自异步API的轮询流量。如果你无法实现通知策略,并且客户端需要轮询来获取操作结果的话,那么S3可以是一个很好的候选方案,它能够将轮询的调用从主API中迁移出来。我们需要为每个操作生成一个S3预签名的URL,并将其返回给客户端,以便于客户端调用它,这样的话,计算资源就能处理应用程序的主业务逻辑,而不必通过API调用检查操作的状态。

文章中的例子展现了一个serverless的API。但是,这种机制也可以用于其他类型的应用中,比如托管在Docker容器、虚拟机中的应用,甚至自托管的应用。对于短时间内大量调用的场景,其好处会显现出来。如果只是几个客户端不时地进行调用,那么在解决方案中再增加一个系统可能并不是高效的办法。

关于作者

Cristian Gherghinescu自2006年以来一直在软件开发领域工作。他目前在挪威的Visma公司担任软件架构师。Cristian从C#和Java EE开始其职业生涯,现在专注于将当前的解决方案迁移到AWS平台上。最近,他开始热衷于serverless的解决方案。

查看英文原文:Serverless Solution to Offload Polling for Asynchronous Operation Status Using Amazon S3

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注