为了账号安全,请及时绑定邮箱和手机立即绑定

从50分钟到50秒:Runa公司一年内将AWS无服务器应用的停机时间大幅减少的方法与经验

简介

在 Runa,我们始终致力于构建性能卓越的产品。我所在的 Ledger 团队是工程部门的一个子团队。我们的重点是让团队能够快速、准确且轻松地转账汇款。

我们通过在核心账本之上创建一个接口来解决问题。这避免了其他团队被核心账本的复杂性困扰,让他们能够快速高效地行动,同时确保核心账本的准确性和完整性。

我们通过一组API实现这一点。这些API构成了“支付意图”服务。简单来说,它们就像银行通常的授权及结算交易的概念。

这项服务对我们平台的功能来说是至关重要的,从根本上说,Runa的宗旨是让价值转移变得轻松。除了必须保证正确性以外,两个关键要求是高可靠性(> 99.999%)和快速响应。

我们正遭受所谓的“幽灵”请求的困扰。虽然接收到这些请求,但它们既没有到达目标资源也没有返回。其服务水平协议仅为 99.95%,这意味着在高流量端点,你几乎可以肯定会有请求在网络中丢失。

以下是我们的架构组件的高层次架构图。我们经常看到API网关的请求有时无法成功到达步函数(Step Function),导致请求超时失败。

高级架构图

可以参考这篇帖子:Yan Cui — AWS组件的各种服务等级协议(SLA)如何组合,了解AWS组件的不同SLA如何组合的一个示例。

我们用了以下方法,将API的可用性从99.99%提升至99.997%(在排除无法控制的假错误之后,我们达到了99.999%的可用性)。这基于四百万次请求,我们通过这些成果实现了上述目标。

可观测性

这里的一切都从可观察性开始,它包括我们所有的性能指标数据、日志和事故处理。

我们必须采用基于数据的科学方法来提高表现。常常,很多被认为能提升表现的方法其实不起作用,反之亦然也可以是这样。没有这些初始的基准数据,你就无法判断你的改变是否真的有效。

总体上来说,我们关心的这两个指标是p95响应时间以及你的4xx/5xx错误码率。服务器错误和超时的原因很明显。但是,4xx错误则比较复杂。是授权失败了吗?应该存在的资源缺失了吗?你是否达到了速率限制?这些问题需要具体问题具体分析。

我们现在用什么工具来获取这些基准?

性能测试

要到达你想要的地方,首先,你需要知道自己从哪里开始。要做到这一点,你需要执行一次性能测试,它将提供一个在特定工作负载下的响应性和稳定性基准。这样我们就可以评估所做的任何更改是否真正有所改善。没有这个基准,你可能会恶化响应时间,或在某些特定的工作负载下增加错误率。

在 Runa,我们大量使用了 Artillery 来进行负载测试。它是一款非常容易上手的工具。它可以帮助你进行分布式负载测试,并提供详细的统计信息,如响应时间、错误代码等。

它不仅提供给你这些数据指标的优秀可视化效果,还让你可以对比之前的运行。

炮兵报到

我们使用Artillery[^1]来测量我们支付意图系统性能提升的逐步改进。它揭示了我们之前未曾察觉的多个竞争条件问题和瓶颈,直到我们进行了广泛的测试。

[^1] 注:Artillery 是一个用于性能测试的工具。

可视化

性能监控工具给我们提供了多个性能快照,但它并没有告诉我们 API 用户的长期体验感受。

在这个情况下,我们在Runa有两个选择,Datadog或AWS CloudWatch。我们使用Datadog作为长期仪表板,但在处理临时问题或事件时,我们会转向使用CloudWatch。

总结

上述两个工具应该能让你了解系统的性能。每个系统都有可能出现故障和运行缓慢的问题,准确诊断这些问题可能有点棘手。我们将在下一节进一步探讨如何应对这个挑战。

诊断工具集
追踪

追踪能给我们提供详细的执行诊断信息。它会展示每个组件,从网关到数据库的所有环节。它可以帮助你识别故障位置、慢速组件,并更好地了解组件的布局。

只需在所有组件(如 API Gateway、状态机、Lambda 等组件)上启用 AWS X-Ray 跟踪功能。

只需几行配置,你可以获得服务的详细视图,如下面的详细信息所示,包括详细的执行时间。

AWS X-Ray 追踪图

AWS X-Ray 追踪瀑布

你可以使用 trace map 分析功能来汇总和分析持续的时长,从而深入了解每个组件的表现。这有助于确保你获得组件响应时长的整体视图,而不仅仅是一些孤立的案例。

AWS X-Ray 聚合统计信息

日志分析

如果您在服务中使用了 Lambda 函数,日志洞察可以帮你提取影响性能表现的指标:

    筛选 @type = 'REPORT'   
    | 计算   
     count(@type) as countInvocations ,   
     count(@initDuration) as countColdStarts ,   
     (count(@initDuration)/count(@type))*100 as percentageColdStarts,  
     max(@initDuration) as maxColdStartTime,  
     avg(@duration) as averageDuration,  
     max(@duration) as maxDuration,  
     min(@duration) as minDuration,  
     avg(@maxMemoryUsed) as averageMemory占用,  
     max(@memorySize) as 分配内存,  
     (avg(@maxMemoryUsed)/max(@memorySize))*100 as 内存使用百分比   
    按每小时时间间隔分组

你可以生成关于你Lambda函数的最大冷启动持续时间的洞察,如下图所示。将这段时间里的冷启动次数与之结合,这能帮助你更好地理解是什么因素影响了你的响应时间。

AWS Logs Insights 图表功能显示 lambda 统计

简要概述如下

这些工具帮助我们找出性能不佳或不可靠的地方。有了这些基准线,我们可以采用更科学的方法来调整、测试并不断迭代改进,以实现优化。

学到的教训
尽量避免使用 lambda 层。尽量不要使用 lambda 层,这样更口语化。但在正式文档中,保持“lambda 层”术语一致,语气正式即可。

在 Lambda 层共享代码被认为是一种简单的方法来减少部署的代码量,并改善冷启动时间。然而,这是一个常见的误解,对此有更详细的解释可以参见此文章。不过,我建议您单独阅读这篇文章,您可以在这篇文章中找到更详细的解释。以下是一些关键点:

  • Lambda 层不会影响冷启动时间的长短。
  • 冷启动时间主要取决于加载的代码大小,无论这些代码是在您的Lambda函数还是在层里。
  • Lambda 层在开发过程中可能是个头疼的问题,因为部署时会出现自动加载的依赖项,调试这些依赖项会让人很头疼。
  • Lambda 层不支持语义版本控制,只支持 LATEST 和增量版本。这意味着,如果你对多个Lambda函数进行了重大版本变更,所有这些函数必须同步准备好来处理这个变更。
  • 使用Lambda层更容易遇到依赖冲突的问题。

值得一提的是,启用 APM 跟踪的 Datadog 扩展会延长冷启动时间,具体可参见这篇文章:https://medium.com/@adtanasa/the-latency-overhead-of-application-performance-monitoring-with-aws-lambda-extensions-896582da9b51

Datadog最近发布了一个更新版本的这一层,将APM的冷启动的影响从800毫秒减少到了400毫秒。然而,这仍然是一项显著的延迟,因此账本组选择在必要的关键低延迟Lambda函数中使用AWS X-Ray跟踪,以降低延迟。

提升你的冷启动时间

包裹的大小

你zip文件的大小是影响冷启动时间的最关键因素。很难不通过减小zip文件大小来缩短冷启动时间。下面的图表显示了包大小如何影响冷启动时间。

不同部署大小下的 AWS Lambda 冷启动时长,详细了解

你可以采取以下措施来处理问题:

  • 检查所有非必要的依赖项,并尽可能移除它们。
  • 将内部依赖项拆分为更小的模块,以便选择性地打包这些模块。
  • 只需使用你真正需要的依赖项部分,例如 @aws-sdk@aws-sdk/dynamodb/client
  • 将开发和测试所需的依赖项移至 dev-dependencies。
  • 使用一个经过修改的脚本来裁剪所有非必需文件,可以在此处找到该脚本的版本。我们使用此脚本将我们的 node 模块大小从大约 14MB 减少到大约 400KB。

我们建议使用https://pnpm.io/,它确保您的lambda函数仅打包所需的组件和依赖项,从而显著减小了您的打包大小。

这些步骤使我们关键端点的P95时间大约减少了60毫秒,接近20%的减少。这一改进完全来自于更快的冷启动时间,这意味着突发负载受到冷启动的影响大大减小。你可以在artillery中通过配置不同阶段并设置快速增加时间来测试这些请求量的突发。

尽可能不要将Lambda函数放在VPC内

VPC是访问某些资源(例如Redis和Postgres集群)所必需的。在你的服务里,你可能会想把所有的Lambda都放在VPC里,这样开发起来更方便,但这会增加你的冷启动时间。

当然这并不总是能做到的,但是要记住这一点哦!

冷启动时间(在VPC内和VPC外):这里:https://mikhail.io/serverless/coldstarts/aws/

使用 AWS Lambda 的性能调优

这是一篇关于AWS Lambda性能调优的文章的链接:https://github.com/alexcasalboni/aws-lambda-power-tuning

AWS Power Tuning 是一个工具,可以用来优化 Lambda 使用的内存大小。

在AWS Lambda中,内存是您用来调节CPU、RAM和网络优先级的主要手段。在一定范围内,它能带来显著的性能提升。如图所示,我们通过调整一个配置变量,将初始化时间减少了50ms(33%!)。

AWS Lambda 性能调优

上面的图表显示,在4096附近有一个最优点,调用时延和成本在那里达到平衡。

在我们的情况下,再增加内存不值得。性能提升的效果微乎其微——据猜测,内存增加可能使 AWS 分配时间变长。

也值得注意的是,成本图并不完全准确。它没有把执行时间缩短这一因素考虑进去,这里有一个例子,取自AWS进行的一千次执行的实例。

按执行时长和内存的成本表格,点击这里查看:https://repost.aws/knowledge-center/lambda-memory-compute-cost

在外(如启动时)初始化客户端,而不是在Lambda函数内部初始化。

提高Lambda性能的关键一步是,在不同请求间重用类似的客户端比如Redis或DynamoDB。要做到这一点,请在外层代码中初始化这些客户端,而不是在Lambda处理函数中。

这是因为 AWS Lambda 使用了一个叫做“执行上下文”的概念来提高性能。每当有 Lambda 函数被调用时,AWS Lambda 会创建一个执行上下文来运行这个函数。这个上下文会保持一段时间,以便应对后续函数调用。

例如,下面这个小小的改动使我们的这个端点的第95百分位响应时间比之前快了80毫秒。

在 Lambda 函数之外启动 Redis 客户端

这个改动会让 Lambda 第一次启动时初始化时间稍微变长,但之后的请求会变得更快。

直接集成优于通过代码的通用调用

尽可能地,建议直接调用步进函数,而不是使用Lambda代码和boto/aws-sdk进行集成。这种方法可以减少Lambda调用次数的延迟和持续时间,最终降低总体成本。

对于简单的端点,使用一个 Lambda 函数通常就足够了。然而,当你需要协调多个步骤、组件和服务调用时,强烈建议将操作包装在一个 Step Function 中。AWS Step Functions 可以提供内置的可观测性、扩展服务的灵活性和自由度,最重要的是,它提供了内置的错误处理和自动重试机制。

不断重试

这是我们 Ledger 团队在初次发布支付意图的早期版本时学到的艰难教训。那时,我们每周因为各种故障被叫醒超过 20 次。所有可能出现故障的地方都出现了故障,甚至从 SecretsManager 中检索密钥也出问题了。

为了修复这个问题,团队付出了巨大努力,为所有调用增加了重试,并确保所有创建资源的调用都是幂等的。如前所述,例如,API Gateway的服务水平协议(SLA,即服务等级协议)仅为99.95%,但我们的客户端团队需要满足4个9的指标,所以我们必须至少达到99.999%的水平。

这意味着任何涉及网络调用的操作——无论是API Gateway调用、获取密钥值还是发布事件数据——都可能失败,因此你需要捕获并重试。此外,你的服务应该被设计为具有幂等性,以便依赖这些API的消费者也可以安全重试。

通过这种方法,我们把每周超过20个错误减少到几乎10天内无错误。

步函数的重试机制更加健壮且易于实现,相比将这些功能构建到自定义的 Lambda 函数中来说。我们强烈推荐这种方法,因为它能提供更可靠且可观察的服务。

我们最大的体会是从之前的版本中了解到这种方式的重要性。我们在最新的实现中大量采用了这种方法来处理那些可能失败的网络请求。只要有网络请求,它就可能出错,甚至肯定会出错。

在设置重试的时候,请仔细考虑您的配置设置。考虑您要重试的内容和具体对象。例如,我们对 Redis 操作会在五毫秒内重试五次,但是在处理遗留系统时,我们会采用更加渐进的方法,使用指数回退。每种情况都有其独特性。

你能异步来吗?

我们无法避免的是,由于各种原因,比如依赖老旧的服务、第三方处理器或者其他缓慢的依赖项,我们的操作变得非常慢且耗时。

由于这些操作虽然关键但不紧急,并且需要生成对客户端的回应,因此应该异步完成。这可以通过异步调用步骤函数或使用AWS提供的多种消息代理之一来实现。您可以在下面的链接中找到put和交付延迟的比较:https://lucvandonkersgoed.com/2022/09/06/serverless-messaging-latency-compared/

关键是让关键操作尽量简单,并尽量减少在回应客户端之前的准备工作。

我们通过移除慢操作的关键路径,显著降低了P95延迟。

请使用正确的组件来保存您的设置。

这篇文章提供的信息比我在这儿提到的更详尽:https://aaronstuyvenberg.com/posts/ultimate-lambda-secrets-guide

总之,对于那些不需要频繁更新且不包含敏感信息的配置,建议将其存储在环境变量中,而不是存放在参数存储中——这样会更快,并且参数存储则提供了静态加密。

对于需要快速检索的重要机密,建议使用 Secrets Manager 而不是 Parameter Store 的安全字符串(Secure strings)。

此外,在使用 Step Function 获取密钥时,请记得使用前面提到的重试策略。如果您在 Lambda 中使用密钥,可以考虑像初始化客户那样,在处理函数之外的初始化阶段获取它们,正如我们在初始化客户时所做的一样。

关于预置并发的一点注释

简单的说,预置并发意味着始终保持一定数量的Lambda在运行,从而避免冷启动的费用。

在配置 Lambda 时,可能会倾向于使用预置并发作为默认选项。然而,这可能会非常昂贵。例如,一个具有 10 个预置并发的 4096MB Lambda 每月费用可能超过 300 英镑。如果您有多个 Lambda,成本会很快累积起来。

如果你的 Lambda 处理大量请求,实际上你并不真的需要预置并发量——Lambda 在当前执行完成后会保持一段时间的空闲时间,但是当吞吐量非常大时,你很可能会再次使用已经预热的实例。

云Watch指标 ProvisionedConcurrencySpilloverInvocations 可以很好地指示您的预配置并发是否没有被充分利用,您可能可以减少预配置并发的规模。

尽可能使用IAM策略

尽可能使用基于资源的IAM策略

使用资源基础的IAM策略,而不是基于身份和角色的策略,可以显著减少API网关与计算资源(如AWS的Step Functions或Lambdas)之间的响应延迟。这种简单的改动就能带来显著的效果。想了解更多,请阅读这篇文章。

摘要
  • 要从基准开始,如果你不知道起点,你就不知道自己是否真的有所改进。
  • 跟踪可以为你提供高度详细的诊断能力。
  • 日志洞察能帮助你深入了解你的Lambda启动、冷启动时间以及内存使用情况。
  • 考虑你的整体Lambda大小,以及其大小是否因影响响应时间而有问题。
  • 初始化客户端以便在请求间重复使用。
  • 由于其重试和直接集成能力,步骤函数可以协调即使是看似简单的小操作。
  • 如果可以异步处理,尽量采用异步处理。
  • 使用基于资源的IAM策略设置。
推荐的一些资源
点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消