
本文由 Xavier Lefèvre 发表在 medium.com,经原作者授权由 InfoQ 中文站翻译并分享。
Lambdas 如此吸引人有两个原因:自动缩放功能(扩容、减容)以及按使用量计价的模型。为了利用这些能力 ,并从无服务器架构的优势中获得最大利益 ,我们需要其他基础架构组件也具有同样的灵活性。
在一个 Web 项目中,这样的架构是怎么样的呢?
在 Theodo,我们热爱无服务器架构技术,并将这项技术应用到越来越多的项目中。 今天, 一些服务和模式开始被行业广泛使用。所以,我们决定分享 Web 应用的基础架构最佳实践。如果你不了解无服务器架构,并且希望找到回答这些问题的高级指南,那么你就来对地方了!
先来看看这个简单粗暴的全文“剧透”吧!(别担心,我们将深入讲解其中各个方面)

在上述图表中, 方框代表的是存在于大多数无服务器架构中的典型并且明确界定的领域或技术功能。他们不一定代表微服务或 CloudFormation 术语中的堆栈(Stack)(我们在下文会提到) 。
我们的实践
我们的目标是拥有一个稳健且完全托管的系统,并使开发者拥有舒适的开发体验。为了实现这个目标,我们选择了:
AWS
云技术的竞争很激烈:亚马逊云服务(AWS)、谷歌云服务(GCP)、微软云服务(Azure)、IBM 云服务、阿里云服务。他们都提供了一些令人惊艳的产品,并且以前所未有的速度在快速发展。 Ben Ellerby 在这篇文章中比较了前三名的云服务提供商,而我们更青睐于 AWS 的解决方案。在无服务器架构方面,AWS 是最先进的。借助 AWS 的解决方案,我们可以尽可能接近无服务器架构。为了说明这一点,我们将在下文中详细介绍构成我们架构模块的每个 AWS 服务。
TypeScript 中的 Node
JavaScript 是世界上最受欢迎的编程语言之一,因此它有一个庞大的社区(请参阅 GitHub统计信息)!根据 Datadog的调查,无服务器架构的世界中也是如此。尽管 Python 以 47%的占有率领先,但目前已部署的 Lambdas 中有 39% 是运行 JavaScript 的。 TypeScript 使该语言更进一步,增加了一层额外的保护。最后, 在绝大多数用例中, Lambdas 中的 JavaScript 都运行得很好。
Serverless Framework
无服务器架构框架完成了大部分基础架构即代码(Infrastructure as Code, IaC)的工作(基于 CloudFormation)。你定义一个对 HTTP 事件做出响应的 Lambda 函数,无服务器架构框架将自动部署相关的 API Gateway资源、相应的路由以及新的 Lambda 函数。当我们达到框架的极限,并且需要更复杂的服务配置时,我们只需简单地添加一些 CloudFormation 即可 。
细粒度的 Lambda 函数
Lambda 是一个函数,它有自己的工作任务,并且能做得很好。我们的前端需要获得一个项目列表吗?为这个功能新建一个 Lambda 函数吧。当用户注册后,我们需要发送确认电子邮件吗? 为这个功能新建另一个 Lambda 函数吧。当然,某些特定的代码(例如数据实体)可以分解成小的单元,并在专用的 utilities 文件夹中共享。但一定要非常小心这些代码,因为任何更改都会影响所有相关的 Lambda 函数。而且因为每个 Lambda 是可以独立测试和部署的,因此我们可能会遗漏一些内容(这时 TypeScript 就派上用场了)。
分解成微服务
为了不让团队互相影响,并且避免巨大的 package.json 和 serverless.yml (CloudFormation 的资源限制数量为 200)以及过长的 CloudFormation 部署时间,同时也为了方便我们在代码库中定位,并在所有 Lambdas 函数之间明确清晰的团队职责:我们定义了微服务中划分项目的边界。Ben Ellerby 在这篇文章中写了一个方法, EventBridge Storming,来帮助定义这些界限。
在我们的单体代码库中:一个微服务=一个 CloudFormation 堆栈=一个 serverless.yml + package.json。此外,微服务会属于自己的数据实体,这些数据实体不会与其他微服务共享。
我们早时推荐完全使用 JavaScript,但出于种种原因,你可能想要使用另一种语言,或者可能希望逐步迁移到 JavaScript 中的无服务器架构。在无服务器架构中,微服务的极端优势是你可以在架构中轻松混合多种技术栈,同时也能够保持微服务之间的抽象接口的简单性和一致性。
以事件驱动的方式交流

这些微服务需要完全独立,如果其中一个微服务出现事故,或者我们正在对另一个微服务进行重大改动,这对于系统其他部分的影响应该越小越好。为实现这个目标,Lambdas 函数仅通过 EventBridge 这个无服务架构的事件总线来和其他 Lambdas 交流。在这篇文章中, Ben Ellerby 详细叙述了为什么 EventBridge 用途这么大 。
详解每个架构模块
现在我们已经介绍了一些背景知识,接下来让我们讲解上面那副令人望而却步的无服务器架构图中的每一个架构模块,并且详细介绍在 AWS 中最具有无服务器架构精神的服务。
前端开发

我们优秀的无服务器后端需要以某种方式为前端提供数据。为简化与 AWS 耦合的前端开发,我们利用了 Amplify。Amplify 囊括了几个不同的东西:一个命令行工具、一个基础架构即代码(IaC)工具、一个 SDK 和一套 UI 组件。我们利用前端的 JS 的 SDK 来加快与其他资源(比如用于认证的 Cognito)的集成,这些资源通常是通过其他 基础架构即代码工具(比如无服务架构框架)来部署的。
网站托管

如今,大多数网站是单页应用(Single Page Application,SPA),它们是功能齐全的动态应用程序,被打包在一组静态文件中。这些文件是在用户的浏览器首次访问 URL 时下载的。在 AWS 环境中,我们在 S3(一个文件存储服务)中托管这些静态文件,并通过 CloudFront(一个内容分发网络,即 CDN)来公开。
话虽如此,现在的趋势显然在朝着诸如 Next.js 之类的(无)服务端渲染(Server Side Rendering,即 SSR)发展。要在无服务器架构中运行一个 SSR 网站,我们可以利用 CloudFront 中的 Lambda @ Edge。这使我们能够使用尽可能更接近用户端的 Lambdas 函数来进行服务端渲染。请查看这篇文章以深入探讨该主题。
域名与证书

在我们网站中,我们希望使用相比于原始自动生成的 S3 URL 更好的 URL,为了做到这一点,我们使用 Certificate Manager 来生成证书,并将其绑定在 CloudFront,并使用 Route 53 来管理域名。
业务逻辑接口

现在,我们的网站需要连接后端,以获得和推送数据。为此,我们使用 API Gateway来处理 HTTP 连接和路由,并为每个路由同步触发一个 Lambda 函数。我们的 Lambda 函数包含与 DynamoDB 通信的业务逻辑,以便存储和使用数据。
如上文所示,我们是事件驱动的,这意味着我们会立即回复用户请求,然后,继续在后台异步地处理请求。例如,DynamoDB 提供了流(Streams),它可以对任何数据改动作出反应,并且异步地触发 Lambda 函数。大多数无服务器架构的服务都有类似的功能。
DynamoDB 本身是一个非常大的话题,在这篇文章中,Rob Cronin解答并化解了 了一些关于“臭名昭著的”NoSQL 数据库的一些经典问题和疑虑。
异步任务

我们的架构是事件驱动的,所以我们许多的 Lambda 函数都是异步的,通常是由 EventBridge 事件、S3 事件、DynamoDB 流等事件触发。例如,我们可以有一个异步 Lambda 函数,负责在成功注册后发送欢迎电子邮件。
在分布式异步系统中,故障处理是非常重要的。所以对于异步的 Lambda 函数,我们使用它们的死信队列(Dead Letter Queue,DLQ),并且将最终的故障信息首先传递给 Simple Notification Service(SNS),然后再传给 Simple Queue Service(SQS)。我们现在必须这样做,因为 AWS 暂时还不支持将 SQS 直接连接到 Lambda DLQ 上。
后端向前端的推送

有了异步的操作,前端不能在等待一个 XHR 响应时仅仅显示一个加载页面。我们需要后端的预备状态和数据推送。为此,我们利用了 API Gateway 的 WebSocket,这个 API 可以使 WebSocket 保持连接状态,并且仅在在收到消息时触发 Lambdas 函数。我写了 一篇文章深入讨论了相比较于其他解决方案,为什么我们选择了 WebSocket,以及如何实现它。
文件上传

处理 Lambda 的文件上传流(Stream)可能会造成较大的开销。相较于这个方案,S3 还提供了一个功能,使得前端能使用由 Lambda 生成的、签名(安全)的上传 URL 来直接上传文件到 S3。
用户与认证

Cogito有我们所需要的所有东西:认证、用户管理、访问控制以及外部身份提供商集成。尽管大家知道它使用起来有些复杂,但它确实可以为我们做不少事情。和其他服务一样,它由专用的 SDK 来与 Lambda 交互,并且可以分发事件以触发 Lambda。在我们的例子中,我们说明了将 原生Cogito授权者绑定在我们的API Gateway路由上的可能性。我们也暴露了一个用于刷新身份验证令牌的 Lambda 函数和一个用于获取用户列表的 Lambda 函数。
然而,还有一个忠告:Cogito 现在还不是用于管理你整个用户数据的数据库不二之选。它有一些不足,例如,你可以拥有的属性数量是存在限制 的 。如果你需要一些灵活性,你最好将自定义的属性存在 DynamoDB 中。
状态机

在某些情形下,我们的逻辑和数据流可能会非常复杂。如果直接在 Lambda 函数内部手动维护和操作这些数据流,你可能会难以跟踪和理解正在发生的事情。因此,AWS 为我们提供了专门提供了一个服务:可视化工作流服务 ( Step Functions)。
我们通过 CloudFormation 来声明状态机:包括每个子步骤和状态、每个希望得到的结果和不希望得到的结果,并且将一些原生操作(例如等待、选择)或者一个 Lambda 挂载在这些步骤上。然后,我们可以通过 AWS 界面实时看到我们的服务可视化地运行(并且还能查看日志)。在其中的每个步骤中,我们可以定义重试和失败处理逻辑。Ben Ellerby 在 这篇文章中进一步详细介绍了该服务。
举一个更加具体的例子,假设我们希望通过 SaaS 发送一个电子邮件广告,并且能够确保该广告已经发送完毕:
步骤 1,Lambda:要求 SaaS 发送电子邮件广告系列并获取广告的 ID。
步骤 2,任务令牌 Lambda:从 Step Function 中获得回调令牌,将其链接到广告 ID,然后等待来自 SaaS 的回调。
步骤 3,(在任务流之外的)Lambda:在广告的状态发生改变时(待定、存档、失败、成功),从 SaaS 通过一个钩子函数调用,然后通过对应的回调令牌根据新的广告状态来继续任务流。
步骤 4,选择(Choice):基于状态选择,如果这个广告还没有成功,回到第二步。
步骤 5,(结束)Lambda:在广告发送后,更新用户。
这篇文章深入讲述了任务令牌(Task Tokens )是如何工作的。
安全

Identity and Access Management (IAM) 帮助我们非常细粒度地管理任何 AWS 访问,不 管是开发人员的访问、持续集成与持续交付流程 , 还是 AWS 的服务调用另一个服务。它乍看之下可能令人望而却步,但它的优点是十分先进和精细,要求我们认真思考某个特定的“消费者”被允许操作的每个微小行为。这意味着我们基础架构中的每一层都是受到保护的。 Ben Ellerby在 ServerlessDays在Nashville的峰会中提到了这个话题。
对于非常敏感的数据,比如 SaaS 的 API 密钥,我们将其安全地存储在 System Manager中的 Parameter Store 中。无论是来自我们的无服务器架构框架还是 CloudFormation 文件,甚至是来自我们的代码的访问,都必须通过对应的 SDK 来请求它们。值得一提的是, AWS Secrets Manager 也可以完成类似的工作。并且 Key Management System也可以帮助我们管理加密密钥。
如果对该话题感兴趣,我推荐来自 Sat G 的 这篇文章,关于无服务器架构中的安全问题在此文章中有更详细的概述。
监控

CloudWatch 是监控服务的业界标准。所有 AWS 服务都有基本的自动指标和日志发送到发送到 CloudWatch,以向我们提供一些基本信息。我们可以做更多的事情:将自定义的指标和日志发送到 CloudWatch,创建指标仪表板,在超过阈值后触发警报,进行复杂查询来挖掘数据并且把它们显示到自定义的图表中。
我们仍然在寻找其他选择,比如 X-Ray,它的目标是在我们整个分布式系统中端到端地追踪请求,然后以非常直观和动态的方式表示它。只不过现在这个追踪服务时不时会失败,因为它还不支持某些 AWS 服务,比如 EventBridge(而这在我们的架构中是核心)。
另一个服务,基于 X-Ray 和 CloudWatch 构建的 ServiceLens,看起来棒极了 。此外,还有一些有前景的外部解决方案,比如 Thundra, Epsagon 或 Lumigo,但我们现在还没有机会完全尝试它们。
Theodo 自豪地将其工具添加到了生态系统中:如果你想改善你的本地开发和可观察性,那么你应该尝试 Serverless-Dev-Tools。
总结
无服务器世界正在飞速发展。一旦开始了解它,感觉就像是发现一个全新的宇宙,充满了万物俱兴的一切可能性,这太令人兴奋了!
在 Theodo,我们每周都会发现新的服务、工具和模式。这就是为什么我希望本文尽可能是最新的,以跟上步伐并分享我们当前的最佳做法。因此,别忘记在 Twitter 上关注我,那里还会有更多更新。
现在你已经了解了无服务器架构的基础知识,我们打算证明它与经典的服务器架构相比便宜得多!因此,我们为此构建了成本计算器。只需要少量“简单的”输入,你将看到它可能要花多少钱。如果有兴趣,请通过 Twitter 与我联系。当然,我会在未来几周内(译者:在本文翻译完毕时,该推文已经发送)发布有关此消息的推文。
作者介绍:
Xavier Lefèvre 现任Theodo技术副总裁(VP Engineering)。Theodo 是一家软件咨询和开发机构,在巴黎(作者居住地)、伦敦和纽约设有办公地点。 他从小就热爱高科技,如今他有机会从事自己所热爱的技术行业。 他最近专注于无服务器架构,坚信这项技术将在行业中取得重大飞跃,并将他的关注和精力放在于客户更相关的话题上。 如果您想就此话题进行交流,请随时在 Twitter 上关注/联系他。
英文原文:
评论