在本文中,我将深入探讨 Lambda 的成功之处,我们面临的挑战,以及为什么我们最终会决定将一些服务从 Lambda 迁移到 AWS 弹性容器服务(Elastic Container Service,ECS)。
我们要解决的问题是什么?
简单介绍一下背景,我们的产品是 B2B 软件公司的一个集成平台,我们帮助软件公司构建集成,并将这些集成部署给他们的客户。一次简单的集成可能如下所示:
步骤一:在 Dropbox 中下拉 XML 文档。
步骤二:使用一些自定义的 JavaScript 代码处理 XML。
步骤三:使用一些存储的凭证,向第三方 API 发布经过处理的数据。
用户可以按计划配置集成以运行,也可以通过 webhook 触发集成,而平台则负责运行、记录和监控集成(以及一大堆其他内容)。
早期情况
Prismatic 的第一个实现使用 LocalStack。我们希望最终把 Prismatic 托管在 AWS 上(根据需要可能会迁移到 Azure、GCP 等),所以需要在本地启动我们的平台来模拟 AWS。类似 AWS Lambda 的 LocalStack 服务易于迭代,并且在运行中不会出现大问题。这为我们提供了一个非常好的开发反馈回路,我们很快进行了原型设计和测试。
在使用 Lambda 执行集成的每一个步骤时,步骤利用 SQS 传递数据并触发下一个步骤。所以,集成的执行情况如下所示:
执行 Dropbox 的“获取文件”动作以抓取 XML 文件。
在 SQS 中保存 XML 文件的内容,触发下一步骤。
运行客户自定义 JavaScript 代码以处理 XML。
在 SQS 中保存生成的转换数据,并触发下一步骤。
执行一个动作,将处理后的数据发布到第三方 API。
保存上一步骤的结果,触发集成结束。
这对于 LocalStack 来说是一个非常快速的过程。我们可以定义一个 6 步集成,运行它,然后在几秒内就能看到结果。
向实际的 AWS Lambda 迁移
当我们的概念被证明可行之后,我们就将 Prismatic 转到实际生产环境中,使用实际的 Lambda、队列、数据库等等。我们还只是一个小团队,不想花太多的时间来解决 DevOps-y 的基础设施问题。我们希望花更多的时间在核心商品上,而 Lambda 能让我们做到这一点。
此外,Lambda 在很多方面都比较有优势。比如,我们无需担心 CPU 或内存分配、服务器监控或自动扩展,因为这些都是内置的。还可以将包含 JavaScript 代码的 .zip 文件放到 Lambda 上,AWS 负责剩下的工作。Lambda 让我们将代码分成一系列服务(一个用于日志记录、一个用于 OAuth 密钥更新、一个用于集成出错时短信 / 邮件提醒等),这样我们就能很好地理解由哪些代码负责哪些任务。成本也很合理:你只需支付计算时间的费用。我们无需全天候运行服务器,只需在我们的原型执行某些任务时付费。
在 Terraform 上运行几天之后,我们在 AWS 上有了 Prismatic 的第二个实现。集成运行器运行在实际的 Lambda 上,并且被 SQS 触发。此时,我们就要面对集成运行器的性能问题了。
为何 Lambda 无法工作
在 Lambda 中,我们遇到了速度、SQS 大小的限制以及缺少进程隔离等问题。这让我们重新考虑它作为集成运行器的有效性。下面我们依次对这些问题进行讨论:
速度。我在前文提到过,在 LocalStack 内运行 6 步集成只需几秒。但是在实际的 Lambda 和 AWS 中,这花了整整一分钟。实际上,Lambda 调用非常快,通常为几毫秒。但是,在向 SQS 写入步骤结果的过程中,以及在接下来执行下一步骤的过程中,每一步骤都需要几秒钟。对于更为复杂的集成来说,比如那些循环了 500 多个文件的集成,这就成为一个障碍:谁想要花上几分钟(几小时)来完成他们的集成?
为了加快 Lambda 调用的速度,我们尝试了许多方法。根据指导原则,我们为一些 Lambda 实例保留了“热度”,并将驱动 Lambda 的虚拟 CPU 数量增加到了当时能够达到的最高水平(6 个虚拟 CPU/10 GB 内存),但这只会降低我们集成运行时间的个位数百分比。
SQS 的大小限制。SQS 限制消息大小为 256 KB。在集成的各步骤之间传递的数据量通常会超过这个大小(毕竟,集成开发人员现在一个数兆的 JSON 文件进行处理是完全合理的)。要想绕过这个大小限制,推荐的解决方案是向 S3 写入有效载荷,然后通过 SQS 传递对 S3 对象的引用——但是这个对 S3 的 API 调用只会使速度更慢。
结果表明,如果客户在其集成中编写了像 global.XMLHttpRequest = null; 这样不好的代码,依赖 XMLHttpRequest 库的集成随后将在同一个 Lambda 上运行,这将导致错误。这个问题很大,因为一个客户可能会破坏其他客户的 axios。有些客户甚至可能会恶意执行 global.console.log = (msg) => { nefariousCode(); } 之类的操作,同时,当调用 console.log() 时,同一 Lambda 上执行的其他集成将运行 nefariousCode()。
为了解决共享执行空间的问题,我们做了一些尝试。我们曾强迫 Lambda 每次都冷启动(这是一个坏主意,原因显而易见),也曾试过在 chroot jail 中启动不同的 Node 进程。这两种方法都不起作用:在 Lambda 中启动子 Node 进程需要 3~5 秒的时间,并且在一定程度上与 Lambda 的初衷背道而驰。
向 ECS 迁移
在开发方面,Lambda 为我们提供了很好的服务:我们可以快速迭代,并获得一个原型,但是由于 Lambda 面临的各种问题,我们决定咬紧牙关,花一些开发时间在云基础设施上。
为了扩展已有的 Terraform 脚本,并将集成运行器迁移到 AWS 弹性容器服务(ECS),我们的团队已经开始了工作。使用 ECS 容器,我们可以轻松地、快速地使用 chroot,并将 Node 进程彼此隔离,从而解决了 Lambda 中出现的进程隔离问题。为解决 SQS 的大小限制问题,我们改用了 Redis 支持的队列服务。虽然我们重新发明一些 Lambda 免费提供的“轮子”,比如日志、自动扩展和健康检查,但是最终,我们的 6 步测试集成将在 2 秒内恢复运行。
但是,ECS 也并不完美,有些东西需要进行取舍。比如,ECS 的自动扩展似乎没有 Lambda 快。“扩展”似乎要花费一分钟时间,从 API 调用到 AWS Fargate 获取并初始化一个准备好接受工作的容器。我们不得不让一名开发者从产品开发中抽调出来,参与云基础设施的工作,同时在 CPU 和内存使用、自动扩展规则和监控方面也有许多问题需要解决,但在产品开发的这一阶段,这种痛苦是值得的,因为它能让我们为客户提供一个快速的集成运行器。
Lambda 还有什么?
在 Lambda 中,我们并没有移出所有的微服务:许多微服务仍然保留在无服务器的生态系统中,并且在可预见的将来也是如此。集成运行器并不适用于 Lambda,但是对于其他任务,它似乎是一个明确的选择。在 Lambda 中,我们保留了所有重要的集成服务,而这些服务对 Lambda 的实际执行并不重要。其中包括:
从 ECS 中提取日志并将其发送到 DataDog 的记录器服务。
向 PostgreSQL 数据库写入关于集成执行的元数据的服务。
跟踪和排队调度的集成服务。
如果用户的集成出错,就会向用户发送短信或电子邮件通知的警报服务。
针对第三方服务的 OAuth 2.0 密钥更新授权服务。
我们不希望让这些服务妨碍集成的运行,如果它们需要更多的一两秒钟来运行也没有问题,因为对于 Lambda 来说,这样做是很合适的。
总结
当然,随着时间的流逝,我们的基础设施肯定会发生变化,但是我认为我们一直在做正确的决定。通过 LocalStack 的“Lambda”服务,我们可以进行快速地开发和迭代,我们的 AWS 首次部署非常简单,我们的小型开发团队可以更改我们的基础设施,而无需花费大量的开发时间。
Lambda 看起来是一个很有吸引力的解决方案,可以用于托管和扩展我们的微服务,而且对于许多这样的服务,尤其是那些可能需要一到两秒才能运行的一步服务,仍然是正确的选择。但是,对于我们的集成运行器,我们了解到 Lambda 的规模、速度和进程隔离限制使得 ECS 成为更好的选择,并且值得为这个特定服务创建 ECS 部署所需的开发时间。
Lambda 让我们在早期关注产品开发,当时机成熟时,向 ECS 迁移将会非常顺利。尽管我们在 Lambda 遇到了很多困难,但我很高兴我们走上了正规。
作者介绍:
Taylor Reece,Prismatic 开发技术推广工程师,具有 DevOps/ 云的背景。
原文链接:
评论