当前位置: 首页 > 科技观察

说说微服务中的BFF架构

时间:2023-03-19 14:11:54 科技观察

在我们之前设计的一个供应链系统中,包括商品、销售订单、加盟商、门店运营、门店工单等服务,涉及到各种用户角色,比如总部商品管理、总部门店管理、加盟商员工、门店人员等,每个部门的角色将被细分。并且这个系统还包括两个客户端应用程序:一个是针对客户的,另一个是针对公司员工和加盟商的。此时整个供应链系统的架构如下图所示:上图中网关层主要负责路由、鉴权、监控、限流熔断等。路由:所有的请求都需要经过通过网关层处理,网关层会根据URI将请求指向相应的后台服务。如果同一个服务有多个服务器节点,网关层也会承担负载均衡的工作。Authentication:对所有请求进行集中认证和鉴权。监控:记录所有API请求数据,API管理系统可以对API调用的性能进行管理和监控。限流熔断:当流量过大时,我们可以在网关层实现限流。如果后台服务响应延迟或失败,我们可以在调用端主动融合上游服务,在不影响用户体验的情况下保护后端服务资源。在这一点上,我们的架构看起来很完美,不是吗?而市面上标准的SpringCloud架构就是这样做的。但是,这种架构会存在一些问题。我们先来看几个例子。案例一在这个供应链系统中,很多界面需要展示多种业务数据。例如,在一个APP的首页,对于门店经营者来说,需要展示工单数量、近期工单、销售订单数据、近期待处理订单、低于库存安全值的产品等信息至此,第一个问题来了。在接口设计的过程中,我们常常纠结于用哪个服务来存储两个客户端应用程序调用的接口?导致决策效率低下,职责分工不统一。最后,我们决定将第一个接口存储在商店服务中。此时的调用关系如下图:而第二个接口存放在工单服务中。此时的调用关系如下图所示:案例2一个用户的提交操作往往需要修改多个业务数据,比如一个操作提交工单,我们需要修改库存,销售订单等数据状态和工作顺序。这时候,第二个问题就出现了。因为这样的需求非常多,服务往往会被其他服务调用,导致服务之间的依赖关系非常混乱。最终的服务调用关系如下图所示:通过上图,我们发现服务之间的依赖问题给技术迭代带来了地狱般的体验。关于这一点,我们在第十五讲中已经详细说明了,这里不再赘述。为了解决这两个问题,我们最终决定抽象出一个API层。API层一般来说,客户端的接口需要满足聚合、分布式调用、装饰三个需求。聚合:一个接口需要将多个后台服务返回的数据进行聚合,返回给客户端。分布式调用:一个接口可能需要依次调用多个后台服务,实现多个后台服务的数据修改。装饰:接口需要对后台返回的数据进行重新装饰,比如删除一些字段或者封装一些字段,然后组合成客户端需要的数据。因此,我们决定在客户端和后台服务之间增加一个新的API层,专门用来满足以上三个需求。此时整个架构如下图所示。从图中我们发现,所有的请求在经过网关之后,都是由一个共享的API层来处理的,而API层并没有自己的数据库,其主要职责是调用其他后台服务。采用这样的设计方案后,以上两个问题得到了多方面的解决。减少了服务应该放在接口中的纠结:如果是聚合、装饰、分布式调用逻辑,我们直接放在API层。如果是存储或者查询数据库的逻辑,目标数据在哪个服务里面,我们就把数据和逻辑放在那个服务里面。后台服务之间的依赖也大大减少:目前的依赖只是调用各个后台服务的API层。至此,我们的设计就完美了吧?不要太兴奋,新的问题会出现。客户端适配问题在这个供应链系统中,一系列的接口主要被各种客户端(如App、H5、PC网页、小程序等)调用,此时的调用关系如图所示下图:但是,这种设计方案会存在三个问题:不同的客户端对页面细节的要求可能不同。它会需要更少的信息,以至于后台服务中的同一个API需要针对不同的客户端做不同的适配;客户端经常需要做一些细微的改动,比如增加一个字段/删除一个字段,这时候我们必须采取数据最小化原则来降低客户端界面的响应速度。而且,对于客户端细微而频繁的变更,后台服务往往需要同步发布版本;结合#1和#2,我们发现在发布后台服务版本的过程中,往往需要综合考虑不同客户端的兼容性问题。无形中增加了不同客户端API层兼容的复杂度。这个时候怎么解决呢?我们可以考虑使用BFF。BFF(BackendforFront)BFF不是一种架构,而是一种设计模式。它的主要职责是为前端设计一个优雅的后台服务,即API。一般来说,每个客户端都有自己的API服务。此时整个架构如下图所示:从上图我们可以看出,不同的客户端请求经过同一个网关后,会被重定向到客户端设计的对应的API服务。因为每个API服务只能针对一种类型的客户端,所以它们可以针对特定客户端进行专门优化。去掉兼容逻辑的API更轻量,响应速度比一般的API服务更快(因为不需要判断不同客户端的逻辑)。另外,每个客户端也可以实现自己的发布,不需要和其他客户端一起调度。这个时候的计划很完美吧?目前还不完善,因为上面的方案属于通用架构。在实际业务中,我们还需要结合实际业务来确定,下面深入讲解实际业务需求。前面我们列出了5个服务,实际上整个供应链系统中有将近100个服务。因为它是一个非常大的系统,整个业务链条的所有工作都包含在这个系统中,比如新零售、供应链、财务、加盟商、售后、客服等,需要数百个研发人员维修人员同时工作。因为我们共同维护一个App,PC接口,新零售,售后,加盟商,还有自己的小程序和H5,为了实现业务解耦和单独调度,每个部门需要维护自己的API服务,而App和PC前端也需要按照部门进行组件化,此时的架构如下图所示。针对以上需求,我们在技术架构上如何实现呢?让我们详细看一下。在技??术架构上如何实现?我们整个架构还是基于SpringCloud来设计的,如下图所示:下面简单介绍下图中网关、API服务、后台服务的功能。网关:网关使用SpringCloudZuul。Zuul将拉取的注册存储在ZooKeeper的API服务中,然后通过Feign调用API服务。API服务:API服务其实就是一个SpringWeb服务。它没有自己的数据库。其主要职责是聚合、分发调用和装饰数据,通过Feign调用后台服务。后台服务:后台服务其实就是一个SpringWeb服务,有自己的数据库和缓存。这个时候的解决方案看起来很完美,但是会造成API之间的代码重复。这个时候应该怎么解决呢?下面我们来看看如何解决API之间代码重复的问题?H5的布局虽然和小程序不同,但是页面中的很多功能是相同的,也就是说重复的代码逻辑主要存在于PCAPI和AppAPI中。但是,对于重复代码的问题,不同的部门在设计的时候会呈现出三种不同的逻辑:有的部门将这些重复代码存储在一个JAR中,这样可以共享几个API服务;一些部门将这些重复的代码提取出来并存储在一个名为CommonAPI的独立API服务中。其他API服务可以直接调用这个CommonAPI;一些部门的重复逻辑较少。通过评估后,他们发现维护这些重复代码的成本低于维护#2中的#1JAR或#2中的CommonAPI服务,因此您将继续拥有重复代码。如果有些API服务提供接口访问参与后台服务的一致性怎么办?此时API服务的接口不需要做任何事情,因为它只是一个简单的代理层。于是,一位同事提出:“每次看到这些纯代理API接口,心里都不舒服,能不能想办法去掉。”有几种方法,让我们来看看。网关直接绕过API服务调用后台服务,但是这样会破坏层次感,所以很快就被否决了。在API服务层创建拦截器。如果URI在对应的API服务中找不到controller映射,则直接通过URI找到后台服务并调用。但是这种方式会大大增加系统的复杂度,而且当出现问题时,排查起来比较麻烦,收益也不大。写这些无脑代码,不仅成本低,整体界面列表也更可控。经过综合考虑,我们最终决定保留无脑代码。后台服务和API服务的开发团队如何划分?最终我们的分工是这样的:一个专门的API开发团队负责API服务,而后台服务需要按照领域来划分团队的职责。这种划分方式的好处是API团队可以对所有服务有一个整体的了解,不会出现后台服务分工不清和重复工作的情况。缺点是API团队整体业务逻辑比较简单,不能长期留人。