打造仅执行一次处理、容错且无服务器的数据聚合管

4916d253eddc3f334838238b3f18367f.gif

我们会在本文中介绍如何强化该架构,在提供仅执行一次处理的语义情况下,使其能够在收到新报文后对交易的风险值进行调整。为实现此目标,我们为管道额外添加了两个组成部分:Amazon Lambda 函数,从输入的数据流中提取报文,并在 Amazon DynamoDB 表中添加或更新每笔交易的风险值。

此外,我们还引入了新的 Version 属性,用于检测乱序送达的报文。我们将 Version 属性添加到风险报文模式(参见下例),所以如果管道提取到交易的 Version : 2,但已经处理过 Version : 3,则可以直接忽略该报文。每笔交易的 Version 初始值是零,并且 Version 会随着该交易后续的每次修改进行递增。

"RiskMessage": {
   TradeID   : '0d957268-2913-4dbb-b359-5ec5ff732cac',
   Value     : 34624.51,
   Version     : 3,
   Timestamp : 1616413258.8997078,
   Hierarchy : {'RiskType': 'Delta', 'Region': 'AMER', 'TradeDesk': 'FXSpot'}
}

*左滑查看更多

633ad56e5a19d1846f7422ab24229b66.gif

架构概述:有状态、仅执行一次的聚合管道

以下图表显示了这个有状态、仅执行一次的聚合管道的架构。它采用映射和归约方法,多个并行的映射 Amazon Lambda 函数对数据进行预聚合,并将其归约到可管理的体量,以便单个归约 Amazon Lambda 函数对数据一致地进行聚合。总体来看,数据从上游数据源穿过管道后进入 Amazon DynamoDB 聚合表,并准备好供前端使用。

9bb8648a5e2c638c4b1eb028ab24ecbc.png

该映射和归约模式中新增的部分以虚线包围。在此架构中,Amazon Kinesis 数据流会触发一系列的状态 Amazon Lambda 函数。这些函数将输入风险报文的所有属性写入 Amazon DynamoDB 状态表。风险报文的 TradeID 属性被用作 Amazon DynamoDB 表的分区键。

每个状态 Amazon Lambda 函数实例由批量的风险报文激活,其中含有来自数据流的多达 100 条报文。对于批次中的每条报文,如果表中尚无 TradeID 属性,则状态函数会在 Amazon DynamoDB 状态表中写入新行。如果表中已记录有先前版本的 TradeID 属性,则会把现有行更新为风险记录的新版本。

如下文的阐述,状态 Amazon Lambda 函数与 Amazon DynamoDB 状态表的组合可避免对任何报文执行多次处理,并持续记录单条风险报文的最新状态(根据其 TradeID 属性确定),借此我们可在有新版本的风险输入时计算风险值的变动。

随后,状态表的 Amazon DynamoDB 流会借助与我们在相关文章中介绍的架构相似的映射和归约模式,激活管道的第二部分。

根据对此架构的实现,我们可确保摄取到 Amazon Kinesis 的每条报文都经过仅执行一次的处理并添加到聚合。此过程有两大支柱作为基础。

首先,我们使用的亚马逊云科技服务可实现至少执行一次的交付和处理:作为发生器,托管在亚马逊云科技 Cloud 中的样本风险记录生成器会生成风险记录报文,并将其发送到数据流。如果发生故障,则会重新尝试提交请求,直至发生器收到报文成功交付的确认。Amazon Kinesis Data Streams 原生可提供至少执行一次的交付语义。这意味着 Amazon Kinesis 可确保被成功摄取到数据流的每条报文都会交付到终点。

该架构依靠事件来驱动:数据流中有风险报文抵达时,会触发状态 Amazon Lambda 函数。默认情况下,被某个流(Amazon Kinesis 数据流以及 Amazon DynamoDB 流)激活的 Amazon Lambda 函数会使用相同批次的报文进行重试,直至该批次成功得到处理。映射和归约 Amazon Lambda 函数同样如此(由相应的 Amazon DynamoDB 流激活),因此我们可确保管道端到端对每条报文至少处理一次(前提是已启用自动重试)。

其次,对三个 Amazon DynamoDB 表的写入操作具有幂等性,也就是说任何操作都可重复任意次数,且不会影响结果。根据此管道的具体要求,我们利用三种不同的技术来确保管道的三个 Amazon Lambda 函数各自具备幂等性。

我们来深入了解管道的不同部分是如何实施,进而确保对每条报文的容错和仅执行一次的处理。

状态 Amazon Lambda 函数

状态 Amazon Lambda 函数会尝试将每条收到的风险报文的新状态写入 Amazon DynamoDB 状态表。如前文所述,我们将唯一的 TradeID 属性用作状态表的分区键,由此可按照以下方式使用 update_item API 调用:

table.update_item(
    Key = {
        STATE_TABLE_KEY: record_id
    },
    UpdateExpression = 'SET  #VALUE     = :new_value,' + \
                            '#VERSION   = :new_version,' + \
                            '#TYPE      = :new_type,' + \
                            '#TIMESTAMP = :new_time',
    ConditionExpression = 'attribute_not_exists(' + ID_COLUMN_NAME + ') OR ' + \
                           VERSION_COLUMN_NAME + '< :new_version',
    ...
    )

*左滑查看更多

这个有条件的更新可解决两个潜在问题:重复报文以及无序报文。仅当表中不存在具有相同 TradeID 的项目,或者如果存在具有相同 TradeID 的项目,但现有项目的版本号低于当前所处理报文中的版本号时,具有特定 TradeID 和 Version 的报文才被写入 Amazon DynamoDB。

映射 Lambda 函数

批量的状态表添加或更新操作会激活映射 Amazon Lambda 函数。按照由数据流激活的函数的默认重试模式,在发生故障时会使用相同批次的报文进行重试。因此,为确保幂等性,我们会对 Amazon Lambda 事件中的记录列表计算 SHA256 哈希值,并且仅当表中不存在此哈希值时,才会将聚合写入归约表(参见以下代码):

def Lambda_handler(event, context):

   # Calculate unique hash over the incoming batch of messages
   message_hash = hashlib.sha256(str(event['Records']).encode()).hexdigest()

   # Compute aggregate  
   message = json.dumps(compute_aggregate(event['Records']))

   # Conditional put if message_hash does not exist
   table.put_item(
       Item={
            'MessageHash': message_hash,
            'Message': message
            },
        ConditionExpression='attribute_not_exists(MessageHash)'
        )
    ...

*左滑查看更多

请注意我们如何将唯一标识符与有条件赋值相结合,实现对 Amazon DynamoDB 的幂等写入。

归约 Amazon Lambda 函数

最后,来自 Amazon DynamoDB 归约表流中的批量项目会激活归约 Amazon Lambda 函数。此函数会更新聚合表中的总聚合值。聚合存储在表内的多个行中,每行代表某个特定类别的值。我们要确保每次更新都以原子方式执行:要么更新所有项目,要么都不更新。我们需要消除出现状态不一致的可能性,因为局部更新后函数会出现故障。因此,我们利用单个 Amazon DynamoDB 事务,通过单一的原子操作来使所有项目的值发生递增:

def Lambda_handler(event, context):

   # Calculate hash over batch of messages to be used as ClientRequestToken
   message_hash = hashlib.md5(str(event['Records']).encode()).hexdigest()

   # Compute the aggregate over all records
   delta = compute_aggregate(event['Records'])

   # Prepare a batch of items to be written in DynamoDB format
    batch = [ 
        { 'Update': 
            {
                'TableName' : AGGREGATE_TABLE_NAME,
                'Key' : {AGGREGATE_TABLE_KEY : {'S' : entry}},
                'UpdateExpression' : "ADD #val :val ",
                'ExpressionAttributeValues' : {
                    ':val': {'N' : str(delta[entry])}
                },
                'ExpressionAttributeNames': { 
                    "#val" : "Value" 
                }
            }
        } for entry in delta.keys()]

   # Write to DynamoDB in a single transaction
   response = ddb_client.transact_write_items(
            TransactItems = batch,
            ClientRequestToken = record_list_hash
        ) 
   ...

*左滑查看更多

Amazon DynamoDB 事务可包含 25 个唯一的项目或最多 4 MB 数据(含条件)。在我们的具体案例中,由于报文大小不到 1 KB 且未达到 25 个不同的聚合类别,因此未受到此类限制的影响。显然,其他用例可能会有所不同。如果您的具体用例包含超过 25 个不同的聚合类别,或者报文较大,则我们建议要限制映射 Amazon Lambda 函数结果的最大值:如果该映射函数的任何预聚合含有超过 25 个不同的值或大小超过 4 MB,则可在归约表中将其拆分为多个条目。

除了原子性以外,Amazon DynamoDB 事务还通过写入或事务请求包含的 ClientRequestToken 来实现幂等性。ClientRequestToken 可确保使用过去 10 分钟内用过的令牌继续进行事务调用时,不会导致 Amazon DynamoDB 表更新。

我们对激活 Amazon Lambda 函数所用批次中的所有报文计算哈希值,以用作 ClientRequestToken 。Amazon Lambda 可确保在发生故障时,函数使用相同批次的报文进行重试。因此,在保证 Amazon Lambda 函数中的所有代码路径都已确定后,我们可确保事务的幂等性,并在管道的最后阶段实现仅执行一次的处理,但是任何重试都要发生在首次成功写入后的 10 分钟内(因为 ClientRequestToken 仅在 10 分钟内有效)。

b73082ca98c47a48b37ea8481e45aa38.gif

吞吐量和容量要求

状态函数可每次向 Amazon DynamoDB 写入一个项目(更多详情参见上一部分),因此单个实例的最大吞吐量约为每秒 100 条报文。为实现每秒处理 50,000 条报文,我们需要建立约 500 个并行的状态函数实例。此外,由于每条报文被写入状态表时,每次写入占用 1 个写入容量单位 (WCU),因此该表的 WCU 占用总数会与管道的吞吐量相当。管道的其余部分所占用的资源会少得多

评估

以下是按每秒 50,000 条报文的稳定速率,向管道摄取 1,000 万条报文时的测量数据(总吞吐量按 20 秒内的滚动平均值计算)。横轴表示时间,纵轴则在以下每张图的顶部注明。我们可观察到在最长约 20 秒的范围内,端到端的平均延迟为 3–4 秒。

a231561b2ea959e9a229ab5e48f92ae6.png

f90d5062879552c519f20152189996f2.png

30753c74b943d849d37f8017f475b0d0.gif

测试弹性

考虑到金融服务业对一致性的严格要求,我们专门开发了一款具有容错性的解决方案,我们还更进一步,基于 Lambda 函数故障时结果的一致性以及数据发生器有意为之的报文重复来进行测试,借此检验其弹性。

我们为三个 Amazon Lambda 函数都添加了以下代码片段:

if random.uniform(0,100) < FAILURE_XXX_LAMBDA_PCT:
    raise Exception('Manually Introduced Random Failure!')

*左滑查看更多

此代码可按照预设的概率使每个 Amazon Lambda 函数发生故障,由此我们能够验证在故障下的操作行为。此外,某条特别不走运的报文可能会在三个 Amazon Lambda 函数中分别重试多次。我们为这三个函数选择了以下故障概率:

FAILURE_STATE_LAMBDA_PCT            = 1
FAILURE_MAP_LAMBDA_PCT              = 2
FAILURE_REDUCE_LAMBDA_PCT           = 2

*左滑查看更多

最后,我们对发生器进行了修改,为管道引入重复和无序的报文。

下图所示的测试中,在有意引入故障的情况下,我们在 200 秒内一致地对 1,000 万条报文进行了处理,并达到了每秒 50,000 条报文的吞吐量。

a132adc3fddec11a61e9971669a95cbc.png

365925240084af58c0bff91632ef5d29.png

通过有意引入故障,我们观察到管道的延迟变量扩大。在极端情况下,单条记录的端到端处理时间超过 60 秒。这种情况在预料之中,原因在于特定(“不走远”)报文在管道的多个阶段都经历了故障。这种连续的故障可导致端到端处理时间显著增加。

但尽管有故障引入,但此管道仍然实现了每秒 50,000 条报文的吞吐量,同时端到端延迟平均值保持在 3–4 秒,这体现了所述架构的弹性和可扩展性。实验结束时,我们验证发现,没有一条报文重复或丢失,证实了此管道遵循仅执行一次处理的语义。

abd994b40329f4d99bb808edd4544cac.gif

在您的亚马逊云科技帐户探索此管道

您可使用所提供的 Amazon CloudFormation 模板,在您自己的亚马逊云科技帐户中轻松部署本文所讨论的架构。您可借助此模板部署的管道,测试并探索无服务器数据聚合。它带有可运行发生器的 Amazon Cloud9 实例以及前端。

在您的帐户中运行所提供的 Amazon CloudFormation 模板可能会产生费用。在您所选的任何地区精确按照本文所述的步骤操作,产生的费用不会超过 1 美元,但使用后务必要清理所有资源。

如要部署该解决方案架构,请完成以下步骤:

  1. 下载下方的 Amazon CloudFormation 模板。

  2. 在您所选的地区,进入 Amazon CloudFormation 控制台。

  3. 选择 Create stack(创建堆栈),然后选择 With new resources (standard)(使用新资源(标准))。

  4. 选择 Upload a template file(上传模板文件)并选择您下载的文件。

  5. 选择 Next(下一步)。

  6. 为堆栈输入名称,例如 ServerlessAggregationStack

    注意:如果您已部署过以往文章中介绍的无状态管道,则要为此堆栈指定不同的名称。

  7. 选择 Next(下一步)。

  8. 其他所有选项保留默认值,然后选择 Next(下一步)。

  9. 选择 I acknowledge that Amazon CloudFormation…(我确认该 Amazon CloudFormation...),然后选择 Create stack(创建堆栈)。

    创建堆栈需要 1–2 分钟。完成后,运行管道就准备就绪了。

  10. 在 Amazon Cloud9 控制台找到实例 StatefulDataProducer

    如未找到,则要确定您所在的地区与创建 CloudFormation 堆栈时所选的地区相同。

  11. 选择 Open IDE(打开 IDE)。

  12. 打开一个终端并运行以下命令,使管道准备就绪:

    cd ~/environment/ServerlessAggregation
    chmod +x prepare_stateful.sh 
    ./prepare_stateful.sh

    *左滑查看更多

    管道已准备就绪!

  13. 使用以下代码启动前端:

    cd ~/environment/ServerlessAggregation/Frontend
    python3 frontend.py

    *左滑查看更多

  14. 打开另一个终端,启动发生器:

    cd ~/environment/ServerlessAggregation/Producer
    python3 producer.py

*左滑查看更多

来自发生器的数据应该开始分批次到达前端。首次运行管道时,可能会有持续若干秒的初始延迟。您可比较两组数据来确认聚合的精确性。

再次运行发生器前,您可能要运行以下命令,重置在前端显示的聚合表:

cd ~/environment/ServerlessAggregation/Producer
python3 clearTables.py

*左滑查看更多

ed08f9702da41e18dad035f8b798bfc8.gif

清理

清理您使用的资源,以免产生不必要的费用:

  1. 在 Amazon CloudFormation 控制台选择 Stacks(堆栈)。

  2. 选择您创建的堆栈 (ServerlessAggregationStack)。

  3. 选择 Delete(删除)。

  4. 选择 Delete stack(删除堆栈)进行确认。

您应该看到状态 DELETE_IN_PROGRESS,而且在 1–2 分钟后,应该会完成删除,该堆栈从列表中消失。

269256d86055e3e1b7bc2137b232fe23.gif

结语

在本系列文章中,我们探讨了在 Amazon Cloud 中构建近实时、可扩展、无服务器的数据聚合管道的两种架构模式。我们首先介绍了通用的映射和归约模式,利用 Amazon Lambda 函数和 Amazon DynamoDB 可轻松承担每秒 50,000 条报文的吞吐量,同时对聚合保持单一且一致的视角。接下来,我们介绍了状态以及在摄取后修改报文值的能力,并将管道扩展到不仅能进行修改,而且还可应付重复和无序报文,对管道中任何部分的故障实现容错,以及确保对每条报文仅执行一次处理。

通过本文描述的案例,您应该能很好地理解如何在 Amazon Cloud 部署具备不同程度一致性的无服务器数据聚合管道。借助所提供的 Amazon CloudFormation 模板,您可对该模式和聚合逻辑进行调整,以适应您的工作负载,然后仅需几分钟即可完成部署、运行和测试。

本篇作者

b710f6757294c520f945155b9e2dc52e.png

Lucas Rettenmeier 

解决方案架构师

最初加入亚马逊云科技从事业务拓展工作。在海德堡大学完成机器学习方向的理科硕士课程后,他于 2020 年再度加入,担任解决方案架构师。他是全球金融服务部迁移加速团队的成员,该部门的使命是加快客户的云进程。Lucas 尤其热衷于定制数据库和无服务器技术。在工作之外,他的大部分时间都是在大自然中寻找乐趣,喜欢骑车、远足、滑雪或尝试一些新鲜的事物。

ed4083fd525049c84aa22568ec88185d.png

Kirill Bogdanov

全球金融服务部 亚马逊云科技高级解决方案架构师

负责提供云原生架构设计,并构建各类实现原型,以打造高度可靠、可扩展、安全且具有成本效益的解决方案,确保客户的长期业务目标和战略得以实现。Kirill 拥有瑞典皇家理工学院的计算机科学博士学位,尤其擅长分布式系统和高性能计算 (HPC)。他在研发、云迁移、利用云技术开发大规模创新解决方案和推动数字化转型方面拥有 12 年的经验。

437cf383f0f7447e6ccf7de7a3d10c58.png

扫描上方二维码即刻注册

ed498e1dd2ef68ae09643d4db4b3f5ec.gif

52a50a284561a7b479820bdbf454ab7d.gif

听说,点完下面4个按钮

就不会碰到bug了!

75cd878f067d45c44b85a585f2d3b7b6.gif

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值