导读:业界有不少 FaaS/Serverless 方面讨论,不少的架构师对引入类似的架构仍然存在一些顾虑,今天文章介绍一篇国外 Hootsuite 是用 Serverless 的案例,供考虑引入 FaaS 的同行参考。
Harry Huang,目前是 Hootsuite 的一名全栈工程师。Hootsuite 是一家创立于 2008 年来自加拿大的著名社交媒体管理工具。
背景
随着业界公司都将架构演进到面向服务的架构 (Service oriented Architecture),越来越多的服务会转移到“云”中。
数据库、文件存储、各种服务器都正在慢慢地转移到云端,他们在虚拟容器中运行,而不是像以前一样跑在专用的主机上。
最近流行起来的 FaaS(Function as a Service)可以使开发人员将“应用的业务逻辑”上传到云端,不用考虑服务器或其他外部依赖,实质上是被抽象成了函数(Function)。
尽管如此,开发人员依然需要处理和“云”密切相关的环节,比如上传、部署、版本控制等等,这些复杂性与 FaaS 其他一些局限性共同构成了 Serverless 架构。
动机
Serverless 框架允许我们将开发过程中和“云”相关的部分抽象出来,开发人员可以直接编写应用程序实际的业务逻辑代码。除此之外,Serverless 架构还具备其他优点,比如更低成本和易扩展。
既然这样,我们会思考是否可以完全使用 Serverless 架构替代现有的架构模式呢?下面我们就着手构建一个功能齐全的,基于云的,符合 Serverless 架构的应用。
我们构建了什么?
我们创建了一个服务于 Hootsuite 内部工程师的小系统,工程师们可以列出自己的技能和兴趣,可以创建、搜索内部项目,也可以根据技能和兴趣加入项目。
整个系统的后端完全基于 AWS Lambda,它是 Amazon 的事件驱动 FaaS 平台,服务以 Lambda 函数的形式提供。
设计阶段,我们需要函数能够完成:
-
将用户和项目信息存储在数据库中
-
检索数据库中的用户和项目信息
-
修改用户和项目信息
-
搜索用户和项目
-
发送邮件/通知给用户
-
根据投票数和时间排列项目
另外,函数还要能够集成其他服务。
整个项目中我们使用了以下 AWS 的服务[1]:
-
Lambda - 函数计算平台
-
IAM&Cognito - 权限和认证
-
DynamoDB - 数据存储
-
S3 - 文件存储
-
SES - 邮件服务
-
Cloudwatch - 定时任务
-
Cloudfront - CDN,用来托管单页应用程序(SPA)
-
API 网关 - 作为 Lambda 的 HTTP endpoint
以及一些第三方服务:
-
Algolia - 搜索服务[2]
-
S3_website - 开源项目,用于在S3上部署静态网站[3]
对于单页应用的前端:
-
React - 响应式 UI
-
React-Bootstrap - 集成 React 的 Bootstrap 模版
同时,我们在项目中使用 Serverless Framework[4] 来测试和部署我们的基础设施。这样我们就不用操心 AWS 相关的配置或任何 DevOps 相关的事情,我们能够专注于编写应用的逻辑代码,照着里程碑快速推进。
约束
在正式开始之前,我们最好明确和统一一下认知。 Lambda,意如其名,就是匿名函数。因此,我们的后端实现基本上就限制于单一独立的函数调用,我们可用的其他资源就是云上的存储和服务。这种 Serverless 架构模式下我们就会面对一些约束:
无状态性
Lambda 函数本身是事件驱动的,所以自然是无状态的。来自客户端的每个事件就是一个函数的单个调用,所以通常将状态存储于内存(如Redis)。在我们的例子中,我们使用DynamoDB 进行存储和检索。因此,我们的流程必须设计成单一的,独立的,无状态的函数组成。
长耗时的任务
目前,Lambda 函数有5分钟的执行时长限制,显然传统的架构中是没有这样限制的。这使得需要长耗时的任务,比如处理数据流面临挑战性。如果有任务需要耗时5分钟以上,那么任务必须被拆分,同时还需要追踪数据块和状态。对于定时任务或计划任务,可以借助 Cloudwatch 来设置时间间隔。
冷启动
当 Lambda 函数无调用时,云端通常会 Spin down 该函数。当再有调用时,函数的运行时容器会先将函数 Spin up。这意味着设置执行环境所产生的任何开销,比如 JVM 层面的初始化等,都会增加函数执行的额外延迟。Neil Powers 和 Paul Cowles 之前给出了一个函数启动时间的分析报告[5]。
对于后台任务或者守护任务这倒没关系。但是,对于我们正在构建的应用,冷启动造成的延迟将会直接影响用户的前端体验。比如 Java 实现的函数,冷启动时可以通过类似“ping 请求”,来尽可能降低延迟。对于我们的应用,最终选择 Node.js 作为函数载体,因为它的冷启动延迟最小[6]。
依赖
Lambda 的优点和缺点基本都源于 AWS Lambda 执行模型的工作原理。
-
首次调用函数时,AWS Lambda 将启动一个执行容器并根据配置执行函数。
-
对于后续的调用,AWS Lambda 会尝试重用该容器来执行函数。
-
如果流量较大请求较多时,AWS Lambda 将直接启动更多的容器来提高吞吐,但每个新的执行容器都会产生冷启动开销。
由此可见,AWS Lambda 会自动进行水平扩展。但由于每个函数都有自己独立的执行容器,所以每个容器都有创建开销。这也意味着简单得将一个 Lambda 函数拆分成几个小的 Lambda 函数会进一步增加开销。
由于执行容器是按需创建的,因此AWS Lambda 不能保证这些函数将在同一个内存空间甚至同一台机器上运行。这与传统架构模型不同,Lambda 函数不是“本地(locality)”的,在云上需要来回传递状态。
如果若干函数必须链式执行,那应该完全在云端执行此操作,而不是在客户端(比如 user facing 的功能)。这里可以借助 Amazon SNS 进行消息传递,也可以使用 AWS Step Functions[7] 来执行函数工作流。
认证
这部分与 Serverless 架构关系不大,主要展示我们的应用如何通过 BaaS(Backend as a Service)来实现认证。所有认证逻辑都由 BaaS 提供。
我们应用用户将和几个 AWS 服务进行交互,我们需要一种方式来管理身份验证以及不同 AWS 服务权限和 API 访问权限的控制。借助 Amazon IAM 和 Cognito,我们可以在控制台内配置和管理权限。
通过 IAM 策略,可以指定允许或不允许的操作,比如上传到 S3
-
策略可以绑定到角色
-
将角色添加到 Cognito Identities 中,就创建一类用户
这样用户通过验证身份,获得用户所属角色,继而获得各种权限。我们能够处理,维护用户目录,我们也可以通过令牌进行身份验证,同时所有内容都保存在云中,工作流程如下所示。
-
客户端将身份验证信息发送给 IdP(Identity Provider)
-
我们的 IdP(Cognito 用户池)向客户端和 Amazon Federated Identities 发送确认令牌
-
客户端将令牌发送给 Federated Identities 并验证令牌
-
Federated Identities 从 IAM 查询角色并返回临时访问凭证
-
用户就可以使用凭证访问我们应用的 API 和服务了
完整的 Serverless 架构
客户端通过 Amazon API Gateway 来调用我们的 Lambda 函数。 Lambda 函数再调用其他服务的 API 并返回相应的结果。这是一个典型的 Lambda 函数工作流:
还有其他几种调用 Lambda 函数的方式。 S3,DynamoDB,SES 甚至 Amazon 的 Alexa 都可以触发 Lambda 函数。例如,我们使用 Cloudwatch 设置计划事件来触发 ranking 函数。
另一个常用的是 Amazon Cognito 的预注册触发器(pre-signup trigger)。这样在注册或登录工作流程的每个步骤都可以触发事件并调用函数,我们就可以设计灵活的流程来定制身份验证。
搜索
Serverless 架构很棒的一个特点就是很易于和其他服务集成。 基于数据库提供快速,复杂搜索的能力通常需要类似 Elasticsearch 这样的系统。这与我们的 Serverless 架构相违背,因此我们将搜索的能力委托给 SaaS(Software as a Service)服务商 Algolia。这样我们就能够使用它的 API 来索引我们的数据,并通过 Lambda 函数执行查询。
如下图:
我们的 Lambda 函数基本上取代了传统架构中的“服务”层。
前端
对于单页的 React 应用,通过 API 网关与背后的各个函数通行。所有静态资源托管在 S3 上,并通过 Cloudfront 提供服务。通过Serverless 架构,我们希望以完全独立的方式来构建应用中的组件,就像前面罗列的做法。
开发 Lambda 函数
设置好 AWS Lambda 之后的实际开发过程简单且高效。将你的函数添加到 Serverless Framework 的配置文件后,唯一需要去做的就是编写实际的业务逻辑函数。
Serverless Framework 可以模拟请求来测试本地的 Lambda 函数。任何输出和异常都呈现在终端里,就像通过 IDE 运行和调试其他程序一样方便。
在云端,任何异常、日志或调用都会被自动记录下来。在 Cloudwatch 上可以查看部署的每个函数的每个版本产生的日志和统计信息。因此如果需要追踪生产环境中出现错误的请求,就可以通过 Cloudwatch 轻松完成。
至于上传和部署,就像在终端中执行单个命令一样简单。以前,每次要上传函数时都需要进行一些 Lambda 上的配置和操作。这要求开发人员自己编写调试一个脚本来自动执行这个过程。但使用 Serverless Framework,所有这些过程都是内置的和自动化的。与服务器有关的任何事情都被抽象出来了,真的感觉开发过程是“无服务器”(server-less)的。
优点
低成本
AWS 的许多服务都有免费套餐,可以满足开发阶段的使用。即便是投入生产环境,Lambda 的定价是基于使用量的,所以如果一个函数没有被使用,那就不会产生任何费用。Serverless 架构/FaaS 是开发产品原型和轻度使用量应用的更便宜选择。
降低 Devops 的负担
随着 FaaS 的推出,DevOps 大部分负担已经从“维护主机以及搭建基础设施”转移到“维护代码和云服务商”。
随着 Serverless Framework 的诞生,开发人员的代码越来越接近生产环境,减少了大量 DevOps 工作。正如我们这个项目所体现的,设计架构和搭建服务器的时间较少,而大部分精力被用来开发实际应用。
确保微服务化
FaaS 模式的后端会客观上确保微服务化。比如身份验证和搜索服务,需要一个连续运行的服务,并不适合直接构建在 AWS Lambda 上,我们必须将它们作为单独的微粒度的服务来构建,或者委派给其他已经存在的服务(就像前面说的 BaaS 服务)。
然后我们的应用可以通过一个函数和它们的 API 集成,它们服务的治理是独立于我们的应用,但集成用的函数就可作为一个微服务来管理。
微服务架构的优点不必多言,建立一个 Serverless 架构的应用会客观上确保你构建真正的微服务,并从中受益。
下一步计划?
Serverless/FaaS 技术在过去一年中获得了显著的增长和发展[8]。
我们的项目以及这篇文章都已经证明,Serverless 是开发和部署微服务应用或是 Web 应用的廉价,快速的最佳方法。随着相关技术的不断发展,未来容器技术和 Serverless 趋势很可能融合。即使没有,拥抱 Serverless 都是值得的。
参考信息
[1] https://aws.amazon.com/
[2] https://www.algolia.com/
[3] https://github.com/laurilehmijoki/s3_website
[4] https://serverless.com/
[5] http://code.hootsuite.com/accelerating-cross-platform-development-with-serverless-microservices/#more-4730
[6] https://www.bouvet.no/bouvet-deler/comparing-java-and-node.js-on-aws-lambda
[7] https://aws.amazon.com/step-functions/
[8] https://serverless.com/blog/ultimate-list-serverless-announcements-reinvent/?rd=true/