继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

Serverless的微服务架构案例

BIG阳
关注TA
已关注
手记 429
粉丝 70
获赞 457

一次微服务架构的奇遇

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 系统进行交互。系统架构如下图所示:

image


应用的交互流程如下

  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 支持。运维团队除了日常的事务以外,还要给开发团队提供各种支持。很多资源的使用权限被限制在这个团队里,就导致各种问题的解决进度进一步拖延。

image


随着业务的增长,需要基础设施代码库提供各种各样的能力。然而 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 比较灵活,人们都有自己的想法和使用习惯,因此代码库很难维护。

image


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

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

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

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

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

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

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

构建微服务的架构的策略

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

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

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

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

image


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

  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 规范,就很容易做前后端集成。

image


部署前端部分到 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 完全满足需求!

image


部署后端微服务

选择了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 个人一个月就可以完成工作。

最后的架构如下图所示:

image


在上图中,请求仍然是先通过域名到 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 架构的微服务如何更好的进行持续交付。

本文首发于Gitchat    作者:ThoughtWorks顾宇



作者:ThoughtWorks中国
链接:https://www.jianshu.com/p/491b4262cfa3


打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP