BFF
前面处理了服务间数据依赖的场景。
除了这种频繁需要其他服务的数据的场景,其实还会碰到服务间依赖太杂乱的问题。
本篇讨论的就是如何缓解服务依赖复杂度的问题。
先把整个业务场景描述一下。
业务场景:如何处理好微服务之间千丝万缕的关系
本节所讲的系统包含商品、订单、加盟商、门店(运营)、工单(门店)这几个服务,其他服务就不细说了。
除了一个App面向客户以外,还有一个App是给公司的员工和加盟商的员工使用的。里面有各种角色的用户,比如总部商品管理、总部门店管理、加盟商员工、门店人员等。当然,每个部门里面还会细分角色。
后台服务架构如图15-1所示。
• 图15-1 后台服务架构
其中,网关层负责如下工作。
1)路由:所有的请求都会通过网关层,网关层再根据URI把请求指向对应的后台服务,如果同一个服务有多个服务器节点,网关层还会做一些负载均衡的工作。
2)认证:对所有的请求进行集中认证鉴权。
3)监控:记录所有的API请求数据,API管理系统可以对API调用进行管理和性能监控。
4)限流熔断:当流量过大时,可以在网关层做限流。当后台服务出现响应延时或者故障时,可以主动熔断,保护后端的服务资源,同时,防止影响用户体验。
该架构看起来非常完美,有些类似于Spring Cloud标准架构,但它也存在一些问题。下面举两个例子。
1)有很多页面需要显示多个服务的数据。比如App首页,它要根据用户的不同来显示不同的信息。如果是门店运营人员,就要显示工单数量、最近的工单、销售订单数据、最近待处理的订单、低于库存安全值的商品等。
2)很多时候,用户的一个提交操作需要修改多个服务的数据。比如一个工单操作要修改库存、销售订单状态、工单的数据。
那么,第一个问题出现了:这两种情况要调用的接口做在哪个服务上?
接口设计过程中,经常需要纠结这个问题。当然,最终总能达成共识——第一个接口做在门店服务上,变成图15-2所示的调用关系;
第二个接口做在工单服务上,变成图15-3所示的调用关系。
• 图15-2 门店服务接口
• 图15-3 工单服务接口
接下来讲第二个问题。因为这样的需求非常多,所以服务经常会来回调用,最终服务调用关系就会变得纠缠不清,如图15-4所示。
• 图15-4 服务调用关系
这种复杂的依赖给迭代带来了地狱般的感受,这一点在第12章中有详细的描述,这里不再赘述。
所以总结一下,目前要解决两个问题。
1)对于很多页面要用的接口,都要考虑放在哪个后台服务,这导致决策效率低下,也导致一些职责划分不统一。
2)服务之间的依赖非常混乱。
为了解决这两个问题,项目组决定抽象出一个API层。
API层
一般来说,客户端的接口会有以下需求。
1)聚合:一个接口需要聚合多个后台服务返回的数据,然后再返回给客户端。
2)分布式调用:一个接口可能需要依次调用多个后台服务,去修改多个后台服务的数据。
3)装饰:一个接口需要重新装饰一下后台返回的数据,删除一些字段,或者对某些字段再加一个封装,组成客户端需要的数据。
项目组决定在客户端和后台服务之间增加一个新的API层,专门来做这些事情,此时架构如图15-5所示。
• 图15-5 API层架构
所有的请求经过网关后都由一个共用的API层进行处理,这个API层没有自己的数据库,它做的事情就是去调用其他后台服务。
这样的设计至少解决了两个问题。
1)纠结某个接口该放在哪个服务的情况大幅减少了。如果是聚合、装饰、分布式调用的逻辑,就都放在API层;如果是要落库或者查询数据库的逻辑,就看目标数据放在哪个服务,数据在哪里,逻辑就在哪里。
2)后台服务之间的依赖也大幅减少了。目前的依赖关系只有API层去调用各个后台服务,后台服务之间的调用关系减少了。
架构看起来更完美了一些,但是会面临新的问题。
客户端适配问题
一般来说,有一系列的接口给各种客户端调用,比如App、H5、PC网页、小程序等。正常来说,调用关系如图15-6所示。
• 图15-6 多种客户端调用关系
但是,这样的设计会有3个问题。
1)不同客户端的页面可能是不一样的,比如App的功能比较多,就会要求页面当中包含一些信息;小程序要求比较轻量化,同样的页面就会少一些数据。这样的问题会导致后台服务的同一个API需要为不同的客户端做不同的适配。
2)客户端经常做一些轻微的改动,比如加一个字段、减一个字段。客户端的接口都要求降低响应速度,为此需要遵循数据最小化原则。所以,伴随客户端这些细微但频繁的改动,后台服务也经常要发布新版本。
3)结合1)和2),后台服务的版本发布又要同时考虑不同客户端的兼容问题,无形中又增加了复杂度。
为了解决这些问题,可以考虑使用BFF。
BFF
(BackendforFront)BFF不是一个架构,而是一个设计模式。
它的主要理念是专门为前端设计优雅的后台服务(也就是API)。换句话说,就是每一种客户端有自己的API服务。这样调用关系就变成图15-7所示。
• 图15-7 使用BFF的调用关系
不同的客户端请求经过同一个网关后会分别重定向到专门为这种客户端设计的API服务(WX API即用于微信小程序的API)。
因为每个API服务只针对一种客户端,所以它们可以为特定的客户端进行优化,使得逻辑更轻便,而且响应速度会比一个通用的API服务更快(因为不需要判断不同客户端的逻辑)。
另外,每种客户端就可以自己发布,而不需要跟其他的客户端一起排期。
图15-7中的架构是通用的,但还需要通过深入研究具体业务来完善。
这次项目所针对的系统非常庞大,整个业务链条所涉及的工作都包含在这个系统中。前面列出了6个服务,但实际上系统的服务有近百个,由几百人组成的研发团队在维护这个系统,分为新零售、供应链、财务、加盟商、售后、客服等几个部门。
大家共同维护一个App,共同维护一个用户界面,新零售、售后、加盟商、客服还有各自的小程序和H5。
为了解耦和分开排期,每个部门肯定会维护自己的API服务,App与PC前端也要按部门实现组件化,此时的调用关系如图15-8所示。
• 图15-8 组件化后使用BFF的调用关系
这个架构基本上就是每个部门都会维护自己的一系列API服务。
接下来展开讨论一些细节问题。
技术架构上怎么实现
整套架构还是基于Spring Cloud实现,如图15-9所示。主要的3层分别如下。
1)网关:网关使用Spring Cloud Zuul。Zuul拉取注册到ZooKeeper的API服务,然后通过Feign调用API服务。
2)API服务:API服务是一个Spring Web服务。它没有自己的数据库,主要的逻辑就是聚合、分布式调用以及装饰数据。它通过Feign调用后台服务。
3)后台服务:后台服务也是Spring Web服务,它有自己的数据库和缓存。
• 图15-9 基于Spring Cloud的分层架构
API之间的代码重复怎么解决
一般来说,H5、小程序之间的需求都是不一样的。重复的代码逻辑主要存在于PC和App的API,因为它们有些页面功能是一样的,只不过布局不一样。针对这一点,几个部门有不一样的逻辑。
1)有的部门是将这些重复的代码放在一个JAR里面,让几个API服务共用。
2)有的部门是将这些重复的代码抽取在一个独立的称为CommonAPI的API服务中,其他API服务调用这个CommonAPI。
3)有的部门因为重复逻辑占少数,所以他们的做法就是保留这些重复代码。根据他们的评估,维护这些重复代码的成本会小于维护上述JAR或者CommonAPI服务的成本。如果有些API服务的出入参和后台服务提供接口的出入参一摸一样,该怎么办?
针对这种情况就会使用API服务的接口,其实就是一个简单的代理层,什么事都不用做。
那这些仅为代理的API接口能不能直接去掉呢?如果需要,有几个办法可以实现。
1)网关可以绕过API服务,直接调用后台服务,但是这样做就破坏了分层。
2)在API服务层做一个拦截器,如果这个URI找不到对应API服务中的controllermapping,就尝试直接通过URI去找后台的服务,有的话就直接调用。
第一个办法因为破坏了分层,很快就被否决了。项目组对第二个办法争执了很久,最终的结论是,这样做会增加系统的复杂度,出问题后调查起来很麻烦,而其好处只是去掉了一些看起来有些累赘的代码,从收益来说,并不会很大。而且这些代码的编写成本非常低,对整体的接口列表来说是可控的。综合考虑后,项目组决定,不去掉这些接口代码。
后台服务与API服务的开发团队如何分工
最后的分工是这样的:有一个专门的API团队负责这些API服务,后台的服务再根据领域来划分小组职责。
这样做的好处就在于,API团队对所有的服务有个整体的认识,由一个中心团队控制接口的划分,就不会出现后台服务划分不清楚、服务重复的情况。
当然,坏处就在于API团队整体业务逻辑偏简单一些,无法让人员长久在岗,所以也会定期进行岗位轮换。
小结
BFF这一章就讲完了。本章并不是介绍一个技术方案,而是整体接口开发的管理和设计方案,所以其内容基本都是一些设计思路和具体会碰到的场景。
另外,虽然本章关于BFF的内容只占一小部分,大部分是后台服务的分层设计,但是BFF的理念贯彻始终。
至此,微服务相关的架构已经讲完了,接下来将会进入开发运维场景实战,讨论如何让开发更高效。