Serverless的微服务持续交付案例

本文是GitChat《Serverless 微服务的持续交付》部分内容已做修改。文章聊天实录请见:“顾宇:Serverless 微服务的持续交付解析

Serverless 风格微服务的持续交付(上):架构案例”中,我们介绍了一个无服务器风格的微服务的架构案例。这个案例中混合了各种风格的微服务。

架构图如下:

在这个架构中,我们采用了前后端分离的技术。我们把 HTML,JS, CSS 等静态内容部署在 S3 上,并通过 CloudFront 作为 CDN 构成了整个架构的前端部分。我们把 Amazon API Gateway 作为后端的整体接口连接后端的各种风格的微服务,无论是运行在 Lambda 上的函数,还是运行在 EC2 上的 Java 微服务,他们整体构成了这个应用的后端部分。

从这个架构图上我们可以明显的看到 前端(Frontend)和后端(Backend)的区分。

持续部署流水线的设计和实现

任何 DevOps 部署流水线都可以分为三个阶段:待测试,待发布,已发布

由于我们的架构是前后端分离的,因此我们为前端和后端分别构造了两条流水线,使得前后端开发可以独立。如下图所示:

(整体流水线)

在这种情况下,前端团队和后端团队是两个不同的团队,可以独立开发和部署,但在发布的时候则有些不同。由于用户是最后感知功能变化的。因此,为了避免界面报错找不到接口,在新增功能的场景下,后端先发布,前端后发布。在删除功能的场景下,前端先发布,后端后发布。

我们采用 Jenkins 构建我们的流水线,Jenkins 中已经含有足够的 AWS 插件可以帮助我们完成整个端到端的持续交付流水线。

前端流水线

前端持续交付流水线如下所示:

前端流水线的各步骤过程如下:

  1. 我们采用 BDD/ATDD 的方式进行前端开发。用 NightWatch.JS 框架做 端到端的测试,mochachai 用于做某些逻辑的验证。
  2. 我们采用单代码库主干(develop 分支)进行开发,用 master 分支作为生产环境的部署。生产环境的发布则是通过 Pull Request 合并的。在合并前,我们会合并提交。
  3. 前端采用 Webpack 进行构建,形成前端的交付产物。在构建之前,先进行一次全局测试。
  4. 由于 S3 不光可以作为对象存储服务,也可以作为一个高可用、高性能而且成本低廉的静态 Web 服务器。所以我们的前端静态内容存储在 S3 上。每一次部署都会在 S3 上以 build 号形成一个新的目录,然后把 Webpack 构建出来的文件存储进去。
  5. 我们采用 Cloudfront 作为 CDN,这样可以和 S3 相互集成。只需要把 S3 作为 CDN 的源,在发布时修改对应发布的目录就可以了。

由于我们做到了前后端分离。因此前端的数据和业务请求会通过 Ajax 的方式请求后端的 Rest API,而这个 Rest API 是由 Amazon API Gateway 通过 Swagger 配置生成的。前端只需要知道 这个 API Gateway,而无需知道API Gateway 的对应实现。

后端流水线

后端持续交付流水线如下所示:

后端流水线的各步骤过程如下:

  1. 我们采用“消费者驱动的契约测试”进行开发,先根据前端的 API 调用构建出相应的 Swagger API 规范文件和示例数据。然后,把这个规范上传至 AWS API Gateway,AWS API Gateway 会根据这个文件生成对应的 REST API。前端的小伙伴就可以依据这个进行开发了。
  2. 之后我们再根据数据的规范和要求编写后端的 Lambda 函数。我们采用 NodeJS 作为 Lambda 函数的开发语言。并采用 Jest 作为 Lambda 的 TDD 测试框架。
  3. 和前端一样,对于后端我们也采用单代码库主干(develop 分支)进行开发,用 master 分支作为生产环境的部署。
  4. 由于 AWS Lambda 函数需要打包到 S3 上才能进行部署,所以我们先把对应的构建产物存储在 S3 上,然后再部署 Lambda 函数。
  5. 我们采用版本化 Lambda 部署,部署后 Lambda 函数不会覆盖已有的函数,而是生成新版本的函数。然后通过别名(Alias)区分不同前端所对应的函数版本。默认的 $LATEST,表示最新部署的函数。此外我们还创建了 Prod,PreProd, uat 三个别名,用于区分不同的环境。这三个别名分别指向函数某一个发布版本。例如:函数 func 我部署了4次,那么 func 就有 4个版本(从1开始)。然后,函数 func 的 $LATEST 别名指向 4 版本。别名 PreProd 和 UAT 指向 3 版本,别名 Prod 在 2 版本。
  6. 技术而 API 的部署则是修改 API Gateway 的配置,使其绑定到对应版本的函数上去。由于 API Gateway 支持多阶段(Stage)的配置,我们可以采用和别名匹配的阶段绑定不同的函数。
  7. 完成了 API Gateway 和 Lamdba 的绑定之后,还需要进行一轮端到端的测试以保证 API 输入输出正确。
  8. 测试完毕后,再修改 API Gateway 的生产环境配置就可以了。

部署的效果如下所示:

(API Gateway + Lambda 配置)

无服务器微服务的持续交付新挑战

在实现以上的持续交付流水线的时候,我们踩了很多坑。但经过我们的反思,我们发现是云计算颠覆了我们很多的认识,当云计算把某些成本降低到趋近于 0 时。我们发现了以下几个新的挑战:

  1. 如果你要 Stub,有可能你走错了路。
  2. 测试金字塔的倒置。
  3. 你不再需要多个运行环境,你需要一个多阶段的生产环境 (Multi-Stage Production)。
  4. 函数的管理和 Nanoservice 反模式。

Stub ?别逗了

很多开发者最初都想在本地建立一套开发环境。由于 AWS 多半是通过 API 或者 CloudFormation 操作,因此开发者在本地开发的时候对于AWS 的外部依赖进行打桩(Stub) 进行测试,例如集成 DynamoDB(一种 NoSQL 数据库),当然你也可以运行本地版的 DynamoDB,但组织自动化测试的额外代价极高。然而随着微服务和函数规模的增加,这种管理打桩和构造打桩的虚拟云资源的代价会越来越大,但收效却没有提升。另一方面,往往需要修改几行代码立即生效的事情,却要执行很长时间的测试和部署流程,这个性价比并不是很高。

这时我们意识到一件事:如果某一个环节代价过大而价值不大,你就需要思考一下这个环节存在的必要性。

由于 AWS 提供了很好的配置隔离机制,于是为了得到更快速的反馈,我们放弃了 Stub 或构建本地 DynamoDB,而是直接部署在 AWS 上进行集成测试。只在本地执行单元测试,由于单元测试是 NodeJS 的函数,所以非常好测试。

另外一方面,我们发现了一个有趣的事实,那就是:

测试金字塔的倒置

由于我们采用 ATDD 进行开发,然后不断向下进行分解。在统计最后的测试代码和测试工作量的的时候,我们有了很有趣的发现:

  • End-2-End (UI)的测试代码占30%左右,占用了开发人员 30% 的时间(以小时作为单位)开发和测试。
  • 集成测试(函数、服务和 API Gateway 的集成)代码占 45%左右,占用了开发人员60% 的时间(以小时作为单位)开发和测试。
  • 单元测试的测试代码占 25%左右,占用了10%左右的时间开发和测试。

一开始我们以为我们走入了“蛋筒冰激凌反模式”或者“纸杯蛋糕反模式”但实际上:

  1. 我们并没有太多的手动测试,绝大部分自动化。除了验证手机端的部署以外,几乎没有手工测试工作量。
  2. 我们的自动化测试都是必要的,且没有重复。
  3. 我们的单元测试足够,且不需要增加单元测试。

但为什么会造成这样的结果呢,经过我们分析。是由于 AWS 供了很多功能组件,而这些组件你无需在单元测试中验证(减少了很多 Stub 或者 Mock),只有通过集成测试的方式才能进行验证。因此,Serverless 基础设施大大降低了单元测试的投入,但把这些不同的组件组合起来则劳时费力 。如果你有多套不一致的环境,那你的持续交付流水线配置则是很困难的。因此我们意识到:

你不再需要多个运行环境,你只需要一个多阶段的生产环境 (Multi-Stage Production)

通常情况下,我们会有多个运行环境,分别面对不同的人群:

  1. 面向开发者的本地开发环境
  2. 面向测试者的集成环境或测试环境(Test,QA 或 SIT)
  3. 面向业务部门的测试环境(UAT 环境)
  4. 面向最终用户的生产环境(Production 环境)

然而多个环境带来的最大问题是环境基础配置的不一致性。加之应用部署的不一致性。带来了很多不可重现问题。在 DevOps 运动,特别是基础设施即代码实践的推广下,这一问题得到了暂时的缓解。然而无服务器架构则把基础设施即代码推向了极致:只要能做到配置隔离和部署权限隔离,资源也可以做到同样的隔离效果。

我们通过 DNS 配置指向了同一个的 API Gateway,这个 API Gateway 有着不同的 Stage:我们只有开发(Dev)和生产(Prod)两套配置,只需修改配置以及对应 API 所指向的函数版本就可完成部署和发布。

然而,多个函数的多版本管理增加了操作复杂性和配置性,使得整个持续交付流水线多了很多认为操作导致持续交付并不高效。于是我们在思考:

对函数的管理和“ Nanoservices 反模式 ”

根据微服务的定义,AWS API Gateway 和 Lambda 的组合确实满足 微服务的特征,这看起来很美好。就像下图一样:

但当Lambda 函数多了,管理众多的函数的发布就成为了很高的一件事。而且, 可能会变成“Nanoservice 反模式”:

Nanoservice is an antipattern where a service is too fine-grained. A nanoservice is a service whose overhead (communications, maintenance, and so on) outweighs its utility.

如何把握微服务的粒度和函数的数量,就变成了一个新的问题。而 Serverless Framework ,就是解决这样的问题的。它认为微服务是由一个多个函数和相关的资源所组成。因此,才满足了微服务可独立部署可独立服务的属性。它把微服务当做一个用于管理 Lambda 的单元。所有的 Lambda 要按照微服务的要求来组织。Serverless Framework 包含了三个部分:

  1. 一个 CLI 工具,用于创建和部署微服务。
  2. 一个配置文件,用于管理和配置 AWS 微服务所需要的所有资源。
  3. 一套函数模板,用于让你快速启动微服务的开发。

此外,这个工具由 AWS 自身推广,所以兼容性很好。但是,我们得到了 Serverless 的众多好处,却难以摆脱对 AWS 的依赖。因为 AWS 的这一套架构是和别的云平台不兼容的。

所以,这就又是一个“自由的代价”的问题。

CloudNative 的持续交付

在实施 Serverless 的微服务期间,发生了一件我认为十分有意义的事情。我们客户想增加一个很小的需求。我和两个客户方的开发人员,客户的开发经理,以及客户业务部门的两个人要实现一个需求。当时我们 6 个人在会议室里面讨论了两个小时。讨论两个小时之后我们不光和业务部门定下来了需求(这点多么不容易),与此同时我们的前后端代码已经写完了,而且发布到了生产环境并通过了业务部门的测试。由于客户内部流程的关系,我们仅需要一个生产环境发布的批准,就可以完成新需求的对外发布!

在这个过程中,由于我们没有太多的环境要准备,并且和业务部门共同制定了验收标准并完成了自动化测试的编写。这全得益于 Serverless 相关技术带来的便利性。

我相信在未来的环境,如果这个架构,如果在线 IDE 技术成熟的话(由于 Lambda 控制了代码的规模,因此在线 IDE 足够),那我们可以大量缩短我们需求确定之后到我功能上线的整体时间。

通过以上的经历,我发现了 CloudNative 持续交付的几个重点:

  1. 优先采用 SaaS 化的服务而不是自己搭建持续交付流水线。
  2. 开发是离不开基础设施配置工作的。
  3. 状态和过程分离,把状态通过版本化的方式保存到配置管理工具中。

而在这种环境下,Ops工作就只剩下三件事:

  1. 设计整体的架构,除了基础设施的架构以外,还要关注应用架构。以及优先采用的 SaaS 服务解决问题。
  2. 严格管理配置和权限并构建一个快速交付的持续交付流程。
  3. 监控生产环境。

剩下的事情,就全部交给云平台去做。


更多精彩洞见,请关注微信公众号:思特沃克

Share

无服务器架构下的运维

前言

在介绍运维之前,大家先来快速了解一下无服务器(serverless)的概念。由于笔者的实战经验是在AWS平台上,本文中出现的无服务器均指使用AWS Lambda构建的serverless应用。Serverless的特点是用户无需预配置或管理服务器,只需要部署功能代码,服务会在需要的时候执行代码并自动伸缩,从每天几个请求到每秒数千个请求,轻松地实现FaaS(Function as a Service)。如下图所示:

(图片来自网络)

在传统的应用中,开发团队除了需要编写功能代码,还要监控实时负载,并相应地对应用进行伸缩,还要处理一些因非功能性故障导致的停机(硬盘、内存等)。而无服务器架构则将开发团队从服务器维护的工作中解放出来,继而能更专注在功能代码上(图中的Function)。在实际的项目里,开发者只需将功能代码打包上传到AWS Lambda,再进行少量配置(环境变量,触发条件,内存,超时时间等)即可将应用/服务上线。

以上是无服务器架构的基本概念。接下来,笔者将从日志,指标,监控及报警,灾备这四个维度来介绍无服务器架构下的运维。

日志

默认情况下,应用运行时产生的日志会保存在应用服务器本机,在需要查看日志的时候,需要运维人员远程登录到这台服务器获取日志信息。这种方式操作起来稍显繁琐,而且当应用服务器的数量增多后,由于需要先找出产生错误信息的那台服务器,会严重降低查找日志的效率。

一种解决办法是ELK(ElasticSearch, Logstash, Kibana),这三个开源工具各司其职,Logstash负责日志的推送和转换,ElasticSearch作为数据库与搜索引擎,Kibana作为图形界面。好处是搭建容易,良好的伸缩性,以及免费。但带来的额外成本是,独立出来的日志服务也需要做好全方位的监控(应用状态,硬盘,网络等),避免因为基础服务的问题导致系统全面故障。

AWS无服务器架构中的日志是一个开箱即用的服务,所有日志自动采集到AWS CloudWatch Logs中,只要根据服务名称找到对应的日志组,即可进行查询搜索,不需要任何配置,也没有任何维护成本。

指标

通常情况下,运维工作会包含采集线上应用的运行指标,来反映应用的健康状况,故障率,性能,访问量,访问频率等。这里以一个使用Spring Boot构建的API服务来举例,Spring Boot中的Actuator扮演了采集指标的角色。默认配置下,对于每个API,Actuator会自动采集以下几个指标:

  • uri,例如/api/person/{id}
  • method,例如GET或POST
  • status,例如200或500

当然我们可以通过实现一些接口来扩展/自定义采集指标,这里就不展开了。有了指标数据,还需要对应的报表或仪表盘工具,以便更好地查询和展示,可以选择像Prometheus,Grafana这样的工具。

那么AWS无服务器架构是否提供了类似的指标采集呢?答案是肯定的,AWS CloudWatch Metrics自动采集了Lambda function的以下四个指标:

  • Invocations(实际调用量)
  • Errors
  • Duration(执行时间)
  • Throttles(超过并行限制而被阻止的调用的数量)

Invocations和Errors取一段时间的总数,结合二者可以得出应用的错误率,如下

Duration则通过取平均数来反映一段时间的性能表现,在笔者的项目中Lambda function的耗时主要集中在SQL的查询上,这个数字可以相应地反映技术人员对查询优化的效果。当然,在实际情况中,这些检验都可以在预发布环境下进行,这个例子只是为了方便理解。

在笔者目前的项目中,Throttle并未被使用到,默认的并发限制是1000/秒,而用量最大的Lambda function的调用频率也不过每分钟150次,距离超限差得很远,不过这一数据对于并发高的应用有很重要的意义。

除了开箱即用的几个指标以外,还可以结合CloudWatch metrics的API,在相应的功能代码中埋点,定制化采集指标。例如,对于一个Lambda function,代码里三个子task,默认提供的Duration只能反映总体的运行效率,如果需要统计每个task的消耗,就需要用到AWS CloudWatch metrics API。

监控&报警

监控的意义在于全面了解应用的资源使用率,性能和运行情况,这些数据可以用来帮助团队及时作出调整,保证应用程序顺畅运行。这通常包括CPU使用率,数据传输,磁盘使用等。在突发状况导致系统不可用的时候,团队的响应速度,往往取决于监控和报警的及时性,全面性和准确度。如果能在对历史数据的分析之上对监控系统进行合理的配置,团队甚至能预测不好的事情将要发生,提前做好防范,未雨绸缪。

同上,这里还是以一个Spring Boot应用为例,在上一小节指标数据的采集中提到过Actuator,事实上Actuator除了可以记录上面提到的指标,还可以用来收集监控数据。这里我们只需要设置一个Spring Boot Admin应用,给需要进行监控的应用加上Spring Boot Admin client配置,监控数据就会通过Actuator暴露的API传递给Spring Boot Admin。

报警功能一般则要根据实际情况自行实现。Spring Boot Admin中实现了对Pagerduty,Slack等第三方工具的集成,如果只是需要简单的邮件提醒,实现起来也不复杂,这里就不展开了。

随着云上基础设施的普及,上面提到的监控和报警早已是各个平台的标准配置,根本轮不到开发者去操心如何实现及维护,运营团队可以把更多的精力放在配置优化的工作中去。

AWS默认提供了非常完备的监控数据,也允许自定义监控dashboard,通过把一系列重要的指标添加到创建好的dashboard中,应用的运行状况一目了然。

前面已经提到过,在出现错误,或性能底下时,根据某些关键指标的变动情况发送警告通知非常必要。笔者所在的项目的做法是使用AWS CloudWatch和AWS SNS提供的告警通知功能,只需要先选择指标然后设定触发阈值和检查间隔时间即可,AWS SNS支持HTTP、SMS、Email等多种订阅方式。下图展示了如何设定当某个Lambda在过去5分钟内发生了5次以上错误的时候发送通知。

灾难备份&恢复

在系统镜像,构建工具还有容器技术越来越普及的今天,灾难备份的意义很大程度上是为了有效保护重要数据。通常的做法是设定一些定期任务,将数据传输到远端的灾备中心,从物理上抵御不可抗灾难。如果数据量过大,出现网络传输效率跟不上的情况,可以参考AWS用卡车拉数据的解决办法。

真正需要用到灾难备份的情况在笔者有限的经历中还没有发生过,但是如果不未雨绸缪,真正发生时的后果将难以设想。笔者项目中用到的AWS RDS默认启用了以7天为周期的自动备份,这个配置可以手动调整也可以将配置写入构建基础设施的脚本中去。 如果灾难真的发生,光有数据备份是不够的,还需要能够快速重建应用运行时的基础设施。笔者所在的团队(下文简称团队)分别使用了AWS CloudFormationServerless framework,CloudFormation用来重建数据库、网络等基础设施,Serverless framework用来重建Lambda function,在重建数据库的时候,通过持续集成流水线,以环境变量的方式传入最近一次数据备份快照的Id,15分钟以内即可重建一套产品环境。

总结

笔者所在的团队是10个人左右的配置,采用结对编程的方式,3对pair,包含web端、业务层、数据层。从产品原型确定到第一次上线(MVP)耗时30天,每周至少发布一次新版本,story的平均交付时间(cycle time,从需求确定到上线)为8天。这样的速度也许不能算快,但是如果没有Serverless架构在运维端提供的支持,我们想要在交付速度上有更高的突破会困难得多。

最后来谈一下成本,俗话说抛开商业化谈技术都是耍流氓,大部分人看到一个强大易用的工具都会下意识里觉得开销会很大。实际上并不是这样,我们做了一个粗算,选用双核CPU,8G内存的M4型服务器,开销是$72每月。dev,staging,prod三个环境都用同样的配置就是$216每月,而实际上Lambda每个月的开销包含所有环境在$20左右,需要注意的是Lambda的计费是根据使用量来的,我们的API访问大约在150万每月的量级。可以预见到当访问达到一定数量的时候Lambda的开销会和使用服务器的方案持平甚至更大,但是在量小的时候优势明显。

得益于强大的AWS生态,利用Lambda构建的无服务器应用经过少量甚至无需任何配置,即可以极低的价格获得完整的运维功能和体验。与自己利用开源工具进行搭建的方式相比,研发团队可以从繁琐的运维工作——特别是基础工程搭建——中解脱出来,更加专注于产品本身,极大的提高软件交付速度,可用性、可靠性和可扩展性也相当有保障。换来的代价是更高的迁移成本,某些功能的不可定制化可能成为瓶颈,以及对底层实现原理的屏蔽也可能对开发者的学习和成长有影响。


更多精彩洞见,请关注微信公众号:思特沃克

Share

Serverless的微服务架构案例

本文首发于GitChat《Serverless 风格微服务的持续交付(上):架构案例》,部分内容已做修改。文章聊天实录请见:“顾宇:构建Serverless 风格微服务实战解析(上)

一次微服务架构的奇遇

2016年12月初,当时我正在以一名 DevOps 咨询师的身份参与某客户的 DevOps 转型项目。这个项目是提升该部门在 AWS (Amazon Web Services)云计算平台上的 DevOps 能力。

自助服务的应用系统基于 Ruby on Rails 框架开发,前端部分采用 AngularJS 1.0,但是没有采用前后端分离的设计,页面代码仍然是通过 ERB 组合而成。移动端则采用 Cordova 开发。为了降低开发难度和工作量, 移动端的应用内容实际上是把 AngularJS 所生成的 Web 页面通过响应式样式的方式嵌入到移动端。但因为经常超时,所以这款 APP 体验并不好。

整套 Rails 应用部署在 AWS 上,并且通过网关和内部业务系统隔离。BOSS 系统采用 SOAP 对外暴露服务,并由另外一个部门负责。因此,云上的应用所做的业务是给用户展现一个使用友好的界面,并通过数据的转化和内部 BOSS 系统进行交互。系统架构如下图所示:

应用的交互流程如下

  1. 浏览器或者移动端通过域名(由 AWS Route 53托管)转向 CDN(采用 AWS Cloudfront)。
  2. CDN 根据请求的内容类别进行区分,静态文件(图片,JS,CSS 样式等),会转向 AWS S3 存储。动态请求会直接发给负载均衡器 (AWS Elastic Load Balancer)。
  3. 负载均衡器会根据各 EC2 计算实例的负载状态将请求转发到不同的实例上的 Ruby On Rails 应用上。每一个应用都是一个典型的 MVC Web 应用。
  4. EC2 上的应用会将一部分数据存储在关系型数据服务(AWS RDS,Relational Database Service)上,一部分存储在本地文件里。经过应用的处理,转换成 SOAP 请求通过 网关发送给 BOSS 系统处理。BOSS 系统处理完成后会返回对应的消息。

根据业务的需要,一部分数据会采用 AWS ElastiCache 的 Redis 服务作为缓存以优化业务响应速度。

团队痛点

这个应用经历了多年的开发,前后已经更换过很多技术人员。但是没有人对这个应用代码库有完整的的认识。因此,我们对整个团队和产品进行了一次痛点总结:

组织结构方面

运维团队成为瓶颈,60 个人左右的开发团队只有 4 名 Ops 支持。运维团队除了日常的事务以外,还要给开发团队提供各种支持。很多资源的使用权限被限制在这个团队里,就导致各种问题的解决进度进一步拖延。

随着业务的增长,需要基础设施代码库提供各种各样的能力。然而 Ops 团队的任何更改都会导致所有的开发团队停下手头的进度去修复更新所带来的各种问题。

应用架构方面

应用架构并没有达到前后端分离的效果,仍然需要同一个工程师编写前后端代码。这样的技术栈对于对于开发人员的要求很高,然而市场上缺乏合适的 RoR 工程师,导致维护成本进一步上升。经过了三个月,仍然很难招聘到合适的工程师。

多个团队在一个代码库上工作,新旧功能之间存在各种依赖点。加上 Ruby 的语言特性,使得代码中存在很多隐含的依赖点和类/方法覆盖,导致了开发进度缓慢。我们一共有 4 个团队在一个代码库上工作,3个团队在开发新的功能。1 个团队需要修复 Bug 和清理技术债,这一切都要同时进行。

技术债方面

代码库中有大量的重复 Cucumber 自动化测试,但是缺乏正确的并行测试策略,导致自动化测试会随机失败,持续集成服务器 (Jenkins)的 slave 节点本地难以创建,导致失败原因更加难以查找。如果走运的话,从提交代码到新的版本发布至少需要 45 分钟。如果不走运的话,两三天都无法完成一次成功的构建,真是依靠人品构建。

基础设施即代码(Infrastructure as Code)建立在一个混合的遗留的 Ruby 代码库上。这个代码库用来封装一些类似于 Packer 和 AWS CLI 这样的命令行工具,包含一些 CloudFormation 的转化能力。由于缺乏长期的规划和编码规范,加之人员变动十分频繁,使得代码库难以维护。

此外,基础设施代码库作为一个 gem 和应用程序代码库耦合在一起,运维团队有唯一的维护权限。因此很多基础设施上的问题开发团队无法解决,也不愿解决。

我参与过很多 Ruby 技术栈遗留系统的维护。在经历了这些 Ruby 项目之后,我发现 Ruby 是一个开发起来很爽但是维护起来很痛苦的技术栈。大部分的维护更改是由于 Ruby 的版本 和 Gem 的版本更新导致的。此外,由于 Ruby 比较灵活,人们都有自己的想法和使用习惯,因此代码库很难维护。

虽然团队已经有比较好的持续交付流程,但是 Ops 能力缺乏和应用架构带来的局限阻碍了整个产品的前进。因此,当务之急是能够通过 DevOps 提升团队的 Ops 能力,缓解 Ops 资源不足,削弱 DevOps 矛盾。

DevOps 组织转型中一般有两种方法:一种方法是提升 Dev 的 Ops 能力,另一种方法是降低 Ops 工作门槛。在时间资源很紧张的情况下,通过技术的改进,降低 Ops 的门槛是短期内收益最大的方法。

微服务触发点:并购带来的业务功能合并

在我加入项目之后,客户收购了另外一家业务相关的企业。因此原有的系统要同时承载两个业务。恰巧有个订单查询的业务需要让当前的团队完成这样一个需求:通过现有的订单查询功能同时查询两个系统的业务订单。

这个需求看起来很简单,只需要在现有系统中增加一个数据源,然后把输入的订单号进行转化就可以。但由于存在上述的痛点,完成这样一个简单的功能的代价是十分高昂的。几乎 70% 的工作量都和功能开发本身没有关系。

在开发的项目上进行 DevOps 转型就像在行进的汽车上换车轮,一不留心就会让所有团队停止工作。因此我建议通过设立并行的新团队来同时完成新功能的开发和 DevOps 转型的试点。

这是一个功能拆分和新功能拆分需求,刚好订单查询是原系统中一个比较独立和成熟的功能。为了避免影响原有各功能开发的进度。我们决定采用微服务架构来完成这个功能。

构建微服务的架构的策略

我们并不想重蹈之前应用架构的覆辙,我们要做到前后端分离。使得比较小的开发团队可以并行开发,只要协商好了接口之间的契约(Contract),未来开发完成之后会很好集成。

这让我想起了 Chris Richardson 提出了三种微服务架构策略,分别是:停止挖坑,前后端分离和提取微服务。

停止挖坑的意思是说:如果发现自己掉坑里,马上停止。

原先的单体应用对我们来说就是一个焦油坑,因此我们要停止在原来的代码库上继续工作。并且为新应用单独创建一个代码库。所以,我们拆分策略模式如下所示:

在我们的架构里,实现新的需求就要变动老的应用。我们的想法是:

  1. 构建出新的业务页面,生成微服务契约。
  2. 根据 API 契约构建出新的微服务。
  3. 部署 Web 前端到 S3 上,采用 S3 的 Static Web Hosting (静态 Web 服务) 发布。
  4. 部署后端微服务上线,并采用临时的域名和 CDN 加载点进行测试。
  5. 通过更新 CDN 把原应用的流量导向新的微服务。
  6. 删除旧的服务代码。

我们原本要在原有的应用上增加一个 API 用来访问以前应用的逻辑。但想想这实际上也是一种挖坑。在评估了业务的复杂性之后。我们发现这个功能如果全新开发只需要 2人2周(一个人月)的时间,这仅仅占我们预估工作量的20%不到。因此我们放弃了对遗留代码动工的念头。最终通过微服务直接访问后台系统,而不需要通过原有的应用。

在我们拆微服务的部分十分简单。对于后端来说说只需要修改 CDN 覆盖原先的访问源(Origin)以及保存在 route.rb 里的原功能访问点,就可以完成微服务的集成。

构建出新的业务页面,生成微服务契约

结合上面的应用痛点和思路,在构建微服务的技术选型时我们确定了以下方向:

  1. 前端框架要具备很好的 Responsive 扩展。
  2. 采用 Swagger 来描述 API 需要具备的行为。
  3. 过消费者驱动进行契约测试驱动微服务后端开发。
  4. 前端代码库和后端代码库分开。
  5. 前端代码框架要对持续交付友好。

因此我们选择了 React 作为前端技术栈并且用 yarn 管理依赖和任务。另外一个原因是我们能够通过 React-native 为未来构建新的应用做好准备。此外,我们引入了 AWS SDK 的 nodejs 版本。用编写一些常见的诸如构建、部署、配置等 AWS 相关的操作。并且通过 swagger 描述后端 API 的行为。这样,后端只需要满足这个 API 规范,就很容易做前后端集成。

部署前端部分到 S3 上

由于 AWS S3 服务自带 Static Web Hosting (静态页面服务) 功能,这就大大减少了我们构建基础环境所花费的时间。如果你还想着用 Nginx 和 Apache 作为静态内容的 Web 服务器,那么你还不够 CloudNative。

虽然AWS S3 服务曾经发生过故障,但 SLA 也比我们自己构建的 EC2 实例处理静态内容要强得多。此外还有以下优点:

  1. 拥有独立的 URL,很容易做很多 301 和 302 的重定向和改写操作。
  2. 和 CDN(CloudFront)集成很好。
  3. 很容易和持续集成工具集成。
  4. 最大的优点:比 EC2 便宜。

根据 API 契约构建出新的微服务

在构建微服务的最初,我们当时有两个选择:

采用 Sinatra (一个用来构建 API 的 Ruby gem) 构建一个微服务 ,这样可以复用原先 Rails 代码库的很多组件。换句话说,只需要 copy 一些代码,放到一个单独的代码库里,就可以完成功能。但也同样会面临之前 Ruby 技术栈带来的种种问题。

采用 Spring Boot 构建一个微服务,Java 作为成熟工程语言目前还是最好的选择,社区和实践都非常成熟。可以复用后台很多用来做 SOAP 处理的 JAR 包。另一方面是解决了 Ruby 技术栈带来的问题。

然而,这两个方案的都有一个共同的问题:需要通过 ruby 语言编写的基础设施工具构建一套运行微服务的基础设施。而这个基础设施的搭建,前前后后估计得需要至少 1个月,这还是在运维团队有人帮助的情况下的乐观估计。

所以,要找到一种降低环境构建和运维团队阻塞的方式避开传统的 EC2 搭建应用的方式。

这,只有 Lambda 可以做到!

基于上面的种种考量,我们选择了 Amazon API Gateway + Lambda 的组合。而 Amazon API Gateway + Lambda 还有额外好处:

  1. 支持用 Swagger 规范配置 API Gateway。也就是说,你只要导入前端的 Swagger 规范,就可以生成 API Gateway。
  2. 可以用数据构建 Mock API,这样就可以很大程度上实现消费者驱动契约开发。
  3. 通过 Amazon API Gateway 的 Stage 功能,我们无需构建 QA 环境,UAT 环境和 Staging 环境。只需要指定不同的 Stage,就可以完成对应的切换。
  4. Lambda 的发布生效时间很短,反馈很快。原先用 CloudFormation 构建的 API 基础设施需要至少 15 分钟,而 Lambda 的生效只需要短短几秒钟。
  5. Lambda 的编写很方便,可以采用在线的方式。虽然在线 IDE 并不很好用,但是真的也写不了几行代码。
  6. Lambda 自动根据请求自扩展,无需考虑负载均衡。

虽然有这么多优点,但不能忽略了关键性的问题:AWS Lambda 不一定适合你的应用场景!

很多对同步和强一致性的业务需求是无法满足的。所以,AWS Lambda 更适合能够异步处理的业务场景。此外,AWS Lambda 对消耗存储空间和 CPU 很多的场景支持不是很好,例如 AI 和 大数据。(PS: AWS 已经有专门的 AI 和大数据服务了,所以不需要和自己过不去)

对于我们的应用场景而言,上文中的 Ruby On Rails 应用中的主要功能(至少60% 以上)实际上只是一个数据转换适配器:把前端输入的数据进行加工,转换成对应的 SOAP 调用。因此,对于这样一个简单的场景而言,Amazon API Gateway + Lambda 完全满足需求!

部署后端微服务

选择了Amazon API Gateway + Lambda 后,后端的微服务部署看起来很简单:

  • 更新 Lambda 函数。
  • 更新 API 规范,并要求 API 绑定对应 Lambda 函数处理请求。

但是,这却不是很容易的一件事。我们将在下一篇文章《Serverless 风格微服务的持续交付》中对这方面踩过的坑详细介绍。

把原应用的请求导向新的微服务

这时候在 CDN 上给新的微服务配置 API Gateway 作为一个新的源(Origin),覆盖原先写在 route.rb 和 nginx.conf 里的 API 访问规则就可以了。CDN 会拦截访问请求,使得请求在 nginx 处理之前就会把对应的请求转发到 API Gateway。

当然,如果你想做灰度发布的话,就不能按上面这种方式搞了。CloudFront 和 ELB 负载均衡 并不具备带权转发功能。因此你需要通过 nginx 配置,按访问权重把 API Gateway 作为一个 upstream 里的一个 Server 就可以。

删除旧的服务代码

不要留着无用的遗留代码!

不要留着无用的遗留代码!

不要留着无用的遗留代码!

重要且最容易被忽略的事情要说三遍。斩草要除根,虽然我们可以保持代码不动。但是清理不再使用的遗留代码和自动化测试可以为其它团队减少很多不必要的工作量。

最终的架构

经过6个人两个月的开发(原计划8个人3个月),我们的 Serverless 微服务最终落地了。当然这中间有 60% 的时间是在探索全新的技术栈。如果熟练的话,估计 4 个人一个月就可以完成工作。

最后的架构如下图所示:

在上图中,请求仍然是先通过域名到 CDN (CloudFront),然后:

  1. CDN 根据请求点的不同,把页面请求转发至 S3 ,把 API 请求转发到 API Gateway。
  2. 前端的内容通过蓝绿部署被放到了不同的 S3 Bucket 里面,只需要改变 CDN 设置就可以完成对应内容的部署。虽然对于部署来说蓝绿 Bucket 乍看有一点多余,但这是为了能够在生产环境下做集成在线测试准备的。这样可以使环境不一致尽可能少。
  3. API Gateway 有自己作用的 VPC,很好的实现了网络级别的隔离。
  4. 通过 API Gateway 转发的 API 请求分成了三类,每一类都可以根据请求状况自扩展。
  5. 身份验证类:第一次访问会请求 ElastCache(Redis),如果 Token 失效或者不存在,则重新走一遍用户验证流程。
  6. 数据请求类:数据请求类会通过 Lambda 访问由其他团队开发的 Java 微服务,这类微服务是后台系统唯一的访问点。
  7. 操作审计类:请求会记录到 DynamoDB (一种时间序列数据库)中,用来跟踪异步请求的各种日志。
  8. API Gateway 自己有一些缓存,可以加速 API 的访问。
  9. 消息返回后,再有三类不同的请求的结果统一通过 API Gateway 返回给客户端。

Serverless 风格微服务架构的优点

由于没有 EC2 设施初始化的时间,我们减少了至少一个月的工作量,分别是:

  1. 初始化网络配置的时间。
  2. 构建 EC2 配置的时间。
  3. 构建反向代理和前端静态内容服务器的时间。
  4. 构建后端 API 应用基础设施的时间。
  5. 构建负载均衡的时间。
  6. 把上述内容用 Ruby 进行基础设施即代码化的时间。

如果要把 API Gateway 算作是基础设施初始化的时间来看。第一次初始化 API Gateway 用了一天,以后 API Gateway 结合持续交付流程每次修改仅仅需要几分钟,Serverless 风格的微服务大大降低了基础设施配置和运维门槛。

此外,对于团队来说,Amazon API Gateway + Lambda 的微服务还带来其它好处:

  1. 开发效率高,原先至少 45 分钟的开发反馈周期缩短为 5 分钟以内。
  2. 无关的代码量少,需要维护的代码量少。除了专注业务本身。上游和 API Gateway 的集成以及下游和后端服务的集成代码量很少。
  3. 应用维护成本低。代码仅仅几十行,且都为函数式,很容易测试。避免了代码库内部复杂性的增加。

此外,我们做了 Java 和 NodeJs 比较。在开发同样的功能下,NodeJS 的开发效率更高,原因是 Java 要把请求的 json 转化为对象,也要把返回的 json 转化为对象,而不像 nodejs 直接处理 json。此外, Java 需要引入一些其它 JAR 包作为依赖。在 AWS 场景下开发同样一个函数式微服务,nodejs 有 4 倍于 java 的开发效率提升。

最后

Serverless 风格的微服务虽然大大减少了开发工作量以及基础设施的开发维护工作量。但也带来了新的挑战:

  1. 大量函数的管理。
  2. SIT,UAT 环境的管理。
  3. 持续交付流水线的配置。
  4. 面对基础设施集成带来的测试。

这让我们重新思考了 Serverless 架构的微服务如何更好的进行持续交付。


更多精彩洞见,请关注微信公众号:思特沃克

Share

Serverless实战:打造个人阅读追踪系统

阅读习惯和个人知识管理体系

进入互联网时代,知识的获取成本变得前所未有的低廉,但是无论再好的知识,若是没有对个人产生价值的话,那也只不过是一种信息噪音而已。我在《个人知识管理:知识的三种形态》这篇文章中使用“材料 -> 资料 -> 知识”这样的路径来诠释信息的流通,如何方便快捷并且有效地收集材料,再将其整理转化为有价值的个人知识体系结构,在这个信息极度碎片化的时代变得尤为重要。而在《去伪存真的知识管理之路》一文中也详细阐述了如何将网络上的碎片化文章纳入统一的稍后阅读体系,比如有时候在朋友圈看到一篇好文章,但暂时没时间直接看,或是这篇文章值得再读一遍,细读一遍,那么我就会将其存入稍后阅读工具即Instapaper当中,诸如此类的还有Pocket收趣等等。

稍后阅读中永远读不完的痛点:缺乏追踪

随着时间的推移,Instapaper里面的文章将会变得越来越多,就像我们在代码中所注释的TODO:可能就变成了Never Do,“稍后读”也是一样地被广为诟病:Read it Later=Read Never。其实我发现文章堆积的一个永恒痛点就是没有有效的方式追踪自己的阅读需求与能力,其核心原因在于阅读的速度赶不上添加的速度。从而没办法可视化的评估阅读进度、合理安排阅读计划,也就没办法给予自己适当的奖励,长此以往必然将失去阅读的动力。

在之前的一篇文章——《基于GitHub的敏捷学习方法之道与术》,其中提到使用GitHub Issue来管理自己的学习计划,于是就产生了这么一个想法——将我的稍后阅读列表跟GitHub结合起来,使用ZenHub丰富的图表功能将阅读体系进行追踪与可视化。

可视化Cumulative Flow Diagram

首先让我们直接来看一下最终的具体效果图,在这里简单介绍一下CFD(Cumulative Flow Diagram)即累积流图,这是一种能让你快速了解项目或产品工作概况的图表,关注的是价值的流动效率,价值的流动最直接的体现就是需求卡片在各个队列中的数量。

里特定律(Little’s law)告诉我们,交付时间(Delivery time)依赖于在制品数量(Work In Progress, WIP)。WIP是指所有已经初始但还未完成的工作,例如:所有在分析(Analysis)与完成(Done)之间的工作。首先需要留意的就是WIP,如果WIP增加了,交付日期就会有风险。ZenHub所提供的Release Report中最有效果的就是预测完成日期,总之就是跟敏捷方法结合起来,使用项目管理的方式来管理自己的阅读列表,虽然我还处在进一步的探索之中,但是每次看到这个走势图就能对自己的阅读列表有更多的掌控和理解,至少减少了因文章堆积而产生的焦虑感。

IFTTT与Serverless架构

那么这是怎么通过APIs来实现的呢?在真正进入正题之前我们先来简单介绍一下Serverless架构。Serverless指的是在构建Web应用程序的时候,不用担心如何配置服务器,但是这并不意味着应用程序不会在服务器上运行,而是说服务器的管理都可以尽可能地交给相应的云平台,从而最大程度地减轻开发人员的部署与配置工作。与之对应的一个名词可能就是Function As a Service(FAAS),由AWS Lambda这个命名就能想到,当我们在构建Serverless架构时,实际上我们是在写一个个的Function,即函数而已。

流程化:APIs即服务

首先让我们来介绍一下IFTTT即if this then that。通俗的来讲,IFTTT的作用就是当一件事情被触发时,能够执行设定好的另一件事。所谓的「事」,指的是各种应用、服务之间可以进行有趣的连锁反应。IFTTT的宗旨是Put the internet to work for you(让互联网为你服务)。用户可以在IFTTT里设定任何一个你需要的条件,当达成条件时,便会触发下一个指定好的动作。它就像是一座神奇的桥梁,能连接我们日常所用的各种网络服务。

而我们现在遇到的这个串联式的场景是特别合适Serverless架构的,使用IFTTT并且将它跟Instapaper账号绑定,设置文章添加、高亮、归档等行为作为trigger条件,然后将相关信息发到某一个指定API endpoint。先把操作GitHub Issue和ZenHub的各种APIs准备好,结合IFTTT的触发器与Marker工具能够非常方便地与之相集成,最后我们可以产出这样一个APIs交互流程图:

初始化Webtask项目

虽然AWS Lambda是Serverless架构的典范,但它也有一些槽点,而且我觉得已经被人说得足够多了,所以我们今天就来尝尝鲜,着重介绍和使用一下Webtask。你可能没有听说过推出该服务的这家公司——Auth0,但你一定知道大名鼎鼎的JWT即JSON Web Token,这是一种开放标准(RFC 7519),通常被运用在身份验证(Authentication)和信息交换等需要安全传输信息的场景下。

首先让我们来安装工具初始化项目以及注册账号,然后使用电子邮件进行登录:

npm install -g wt-cli
wt init <YOUR-EMAIL>

创建项目目录,添加index.js文件并添加以下内容:

module.exports = function (cb) {
  cb(null, 'Hello World');
}

然后在该目录中运行以下命令,进行应用程序部署之后,点击控制台中输出的URL就能看到编程史上最有名气、没有之一的HelloWorld!:

wt create index

Webtask的上下文绑定

Webtask有一个实用工具webtask-tools,可以将应用程序绑定到Webtask上下文,让我们将之前所export的简单函数修改为绑定到Webtask的Express app,然后就可以愉快地使用Express进行开发,一切就又回到了熟悉的轨道:

app.use(bodyParser.urlencoded({ extended: false }))
app.use(bodyParser.json())

require('./routes/reading')(app)

module.exports = Webtask.fromExpress(app)

Webtask context还有一个非常重要的用途,就是在部署时传输一些敏感信息,比如安全Token,从而在应用程序当中随时使用它们。下面的部署命令中–secret后面所传入的ACCESS_TOKEN都会在后续与GitHub和ZenHub APIs交互时被用到。

wt create index --bundle --secret GITHUB_ACCESS_TOKEN=$GITHUB_ACCESS_TOKEN 
--secretZENHUB_ACCESS_TOKEN=$ZENHUB_ACCESS_TOKEN --secret
ZENHUB_ACCESS_TOKEN_V4=$ZENHUB_ACCESS_TOKEN_V4

# ./routes/reading.js
module.exports = (app) => {
  app.post('/reading', (req, res) => {
    const { GITHUB_ACCESS_TOKEN, ZENHUB_ACCESS_TOKEN, ZENHUB_ACCESS_TOKEN_V4 } = req.webtaskContext.secrets
    }
}

使用GitHub Issue追踪阅读列表

IFTTT:添加Instapaper文章后自动创建GitHub Issue

得益于IFTTT非常丰富的第三方服务,IFTTT可以直接创建Instapaper与GitHub Issue相集成的Applet:If new item saved, then create a new issue – IFTTT,就可以在当Instapaper新增文章的时候,自动在GitHub所指定的仓库Issues · JimmyLv/reading 中创建一个新的Issue并添加相应的标题、链接以及描述等相关信息。

但仅仅只是添加一个Issue还不够,这时候还需要将这个Issue加入到指定的Milestone以便利用ZenHub的图表功能,使用GitHub的Webhooks功能就可以轻松帮我们把Issue更新的状态转发到我们所指定的Webtask地址:

使用GitHub Webhook 更新Issue的Milestone

所以我们的Webtask就需要处理GitHub Webhook所转发的POST请求,其中包括了Issue的类型和内容,在拿到’opened’即新建Issue类型的action之后我们可以对其进行相应的处理,即添加到Milestone当中:

if (action === 'opened') {
  fetch(`${url}?access_token=${GITHUB_ACCESS_TOKEN}`, {
    method: 'PATCH',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      milestone: 1
    })
  })

}

结合ZenHub的Milestone燃尽图我们可以清晰地看到剩余阅读量,并且能够跟理想中的阅读速度进行对比,从而判断自己什么时候能够全部读完所有的文章。可能有些小伙伴看到这里会有所疑问,这些所谓的Story Point是从哪儿来的呢?接下来就要提到我们将要集成的ZenHub API了。

集成ZenHub API:阅读可视化

更新Issue的估点和Release

GitHub Issue的任何变动都会触发Webhook,因此我们可以在Issue被加入Milestone之后再处理下一个’milestoned’ action,即:

if (action === 'milestoned') {
fetch(`https://api.zenhub.io/p1/repositories/${REPO_ID}/issues/${number}/estimate?access_token=${ZENHUB_ACCESS_TOKEN}`, {
    method: 'PUT',
    body: JSON.stringify({ estimate: 1 })
  })
    .then(() => {
      return 
fetch(`https://api.zenhub.io/v4/reports/release/591dc19e81a6781f839705b9/items/issues?access_token=${ZENHUB_ACCESS_TOKEN_V4}`,
        {
          method: 'POST',

        })
    })
}

这样我们就完成了对每个GitHub Issue的估点,以及设置了对应的Release,接下来所有的变动都将体现在ZenHub的图表当中。

归档Instapaper文章后关闭GitHub Issue

说了这么多,不要忘了整个阅读系统最最核心的部分依然还是「阅读」啊!在众多的稍后阅读工具中我无比喜爱Instapaper并迟迟没有转到Diigo的原因就在于它优秀、简洁、纯粹的阅读体验,让人可以专注在阅读本身这件事情上,在被Pinterest收购之后更是将所有的诸如全文搜索、无限高亮/笔记、速读等Premium功能都变成了免费,岂不美哉?

那么在完成阅读归档之后,最后一步就是在GitHub当中将Issue关闭掉,但是IFTTT的GitHub服务并没有提供close Issue的接口,于是乎我们就只有利用IFTTT新推出的Maker自己创建一个,即将Instapaper规划作为一个IF trigger,然后用Maker发出一个Web请求,可以是GET、PUT、POST、HEAD、DELETE、PATCH或者OPTIONS之中的任何一种,你甚至还可以制定Content Type和Body。

app.get('/reading', (req, res) => {
    const { GITHUB_ACCESS_TOKEN } = req.webtaskContext.secrets
    const title = req.query.title
    let keyword = encodeURIComponent(title.replace(/\s/g, '+'))

fetch(`https://api.github.com/search/issues?q=${keyword}%20repo:jimmylv/reading`)
    .then(data => {
          if (data.total_count > 0) {
        data.items.forEach(({ url, html_url }) =>
          fetch(`${url}?access_token=${GITHUB_ACCESS_TOKEN}`, {
              method: 'PATCH',
              headers: { 'Content-Type': 'application/json' },
              body: JSON.stringify({ state: 'closed' }),
            })
                   }
    })
}

上述代码就可以用于处理IFTTT Marker所发送的GET请求,我们从query参数中取到文章标题之后再去搜索相对应的Issues,再通过GitHub API将其关闭。

而与此同时,我们在文章的阅读过程中,有时候也会想要对文章中的亮点部分进行高亮,甚至添加自己的一些想法和总结,那我们也可以用IFTTT Marker和Webtask的套路添加至GitHub Issues的comments当中。具体的详细代码就不贴了,更多内容都已经放在我的GitHub上:JimmyLv/demo.serverless-mern,与此同时我的阅读列表也公开在GitHub上:Issues · JimmyLv/reading,欢迎围观。

总结与后续计划

随着时间的推移,日常你只需要在Instapaper添加并阅读文章即可,而背后利用Serverless所搭建的整套阅读追踪系统将会任劳任怨的帮你记录下所有的踪迹和笔记,你只需要在特定的时候定期review、分析阅读的效果与预测效果,与此同时结合自己的时间统计系统,持续不断地改进自己的阅读目标与阅读计划。

最后再来考虑一下后续计划,比如说我现在只是简单把Instapaper中高亮部分和阅读笔记作为评论放到GitHub的comments里面,但是最终我需要把它收藏到自己的个人知识库即Diigo,这也是可以通过API自动实现的,以及最终需要被刻意记忆的部分还需要与Tinycards或者QuizletAPI相集成,对抗艾宾浩斯遗忘曲线。

与此同时,还需要根据文章类型和难易程度具体划分一下估点,而不是现在简简单单的1点,比如说Instapaper也有根据字数来预测的阅读分钟数,以及根据中文或英文、技术或鸡汤等不同种类文章阅读难度进行区分,从而使整套追踪系统更具有效性与参考性。


更多精彩洞见,请关注微信公众号:思特沃克

Share

Serverless架构实践初探

随着云计算技术的进步,软件系统的架构方式也因此发生着一些变化,其中Serverless架构就是这里的一个典型的例子。

(图片来自:http://t.cn/RadEFqr)

什么是Serverless架构

目前关于Serverless架构的准确定义,业界并没有一个统一的标准。那么我们从字面上来分析,所谓Serverless架构,翻译过来也就是无服务器架构。那么似乎可以涵盖以下两个方面:

  1. BaaS(Backend as a Service)即后台即服务。后台即服务出现有很长一段的时间了,例如Parse,Firebase都是典型的代表。具体来说就是服务器端的逻辑和状态是完全依赖于云平台进行管理的。
  2. FaaS(Function as a Service)即函数即服务。函数即服务,意味着这些函数中的后台逻辑是由我们开发者自己实现的。但是这些函数是执行在一个无状态的计算容器中的,函数的执行是基于事件驱动的,关于这些函数的部署、执行、触发是由云平台来管理的。其最典型的例子就是AWS Lambda。

我们这篇文章中的所讨论的Serverless,是指的第二种,也就是FaaS。在我们Thoughtworks最新一期的技术雷达中,Serverless架构位于试验象限,下文就介绍下我们在Serverless架构下的一些实践经验。

数据处理业务的Serverless架构演进

所谓的数据处理业务,是指我们的系统需要每天定时获取一些外部数据与我们自身的数据结合,生成一些数据报表。那么最初我们是怎么设计技术方案的呢?

传统架构方式

我们将业务拆分为3个独立的服务,2个Data Collector,1个Data Loader,都分别部署在AWS服务器上,将中间数据存储在一个外部S3(AWS的数据存储)上。最后将数据保存在数据库中,在数据库之上使用专门的BI工具来制作报表。我们第一个数据服务就是按照这样的架构进行设计和实践的。当系统上线服务以后,我们发现了里边的一些问题。

在这套系统中,Data Collector 2每天的执行时间较长,需要1个小时左右的时间,而Data Collector 1每天的执行时间较短,通常执行时间不会超过1分钟,但是由于外部数据源的更新时间是不确定的,所以虽然我们服务实际有效时间只有仅仅一到两分钟,但是也不得不让服务器全天运行。

可以看到,这个系统每天的有效时间只有一个小时,其他23个小时实际上是在浪费资源,如何改善这样的情况呢?首先想到了让服务定点运行的方法。由于我们外服数据源的更新特点,虽然它的更新时间是不确定的,但是它在某个特定的时间点前是一定会更新的。基于这样的前提,我们将服务运行时间改为定点运行,这样是不是就能解决问题了呢?

然而现实并不总是那么美好,因为我们服务间是有依赖关系的,Data Loader是依赖于我们Data Collector的处理结果的,当我们把运行方式改为定点运行后,带来的问题是,一旦Data Collector的运行状态出现了问题,例如运行时间过长,运行中出现错误,那么Data Loader必然出错。同时改为定点运行后,我们的数据更新必然有延迟。

那么如何解决这些问题呢?

Serverless的系统架构

我们引入了Lambda,将Data Collector 和 Data Loader用Lambda进行了替换,带来了下面这些好处:

由于Lambda是由事件驱动的,S3上一个数据的变化可以触发一个事件,SNS的一条消息可以触发一个时间等等,在使用Lambda后,我们就可以讲原来基于时间的数据处理流程,转变为基于事件的数据处理流程,这样一方面可以保证我们数据更新的实时性,另一方面可以大大节省资源,由于Lambda是按照触发次数收费的,所以在我们的这个用例下,可以大大减少花费。

可能细心的读者想问为什么我们Data Collector 2没有使用Lambda进行替换呢?这是因为它的业务逻辑比较复杂,每次运行的时间较长,而Lambda的最长执行时间是5分钟,所以在这种情况下,就不适合使用Lambda进行替换了。

实时数据处理下的Serverless架构

在初识Serverless架构的好处之后,我们开始在其他方面的应用尝试,比较典型的一个例子就是在实时数据处理业务下的Serverless架构。在我们业务下,我们需要实时跟踪一个外部的数据源API,根据它的数据变化来实时更新我们的数据。

在我们的架构设计中,我们使用一个Lambda来跟踪外部数据源的数据变化,并将其推到AWS Kinesis Stream里,AWS Kinesis会触发第二个Lambda进行相应的数据处理,并把数据存储到数据库中,值得注意的是由于Lambda是可以根据需求自动伸缩的,所以Lambda会根据Kinesis的需求来自动扩展。这就体现了Serverless架构下的另一个好处,可以相对简单的,自动进行伸缩扩展。

Web系统的Serverless架构

对于Web系统这种我们最为熟悉和常见的IT系统来说,它能不能用Serverless的架构来实现呢?我们来看下边的例子。我们先来看看传统的例子。

在传统实现中,我们会利用Load Blancer来做负载均衡,然后后续的应用会部署在AutoScaling Group中,根据流量来做自动伸缩,这种模式已经是十分成熟了。那么在Serverless的架构下该如何设计呢?

在Serverless架构下,一般我们的前端应用的资源文件包括Html,JS,CSS,都是部署在S3(AWS的文件存储)上的。前端应用通过AJAX请求向后台请求数据。后台通过API GateWay定义对外的Endpoint,同时每个Endpoint会触发一个Lambda进行数据操作,例如图中的GET,和POST请求会触发两个不同Lambda。这样的Serverless架构可以让开发者不必担心水平扩展的问题。

Serverless架构的未来

目前AWS Lambda似乎已经成为了Serverless的代名词,为了帮助开发者更好的构建Serverless应用,市场上出现了一些工具和框架,例如Serverless Framework。但是同样我们还可以看到一些其他的云平台和开源框架也在提供类似的服务,例如webtaskOpenWhisk,以及其在IBM Bluemix上的实现。

Serverless架构作为一种新的架构方式,还在不断的发展中。希望本文能给您带来一些思考。


更多精彩洞见,请关注微信公众号:思特沃克

Share