BFF困境与思考

时间:2024-03-23 19:33:18

BFF困境与思考

一、BFF在整体微服务架构中的位置

微服务化带来的最大问题是服务边界的划分。可以从两个维度看:纵向职能和横向业务的划分。
  纵向划分,BFF在网关与领域服务之间比较容易确定,起到桥梁的作用,一来可以快速支持前端的迭代;二来可以让下游的领域服务更加纯粹。
  横向的划分是最大的难点,后面会有分析。
BFF困境与思考
  上图是我公司目前的服务架构图(并不是说这样的分层是最合理的,仅作文章后面的铺垫),从上到下分别是:前端服务、BFF服务、领域服务(有些会分为两层:领域聚合和领域服务)、中台服务(基础服务)。


二、BFF是什么

BFF困境与思考
  Backend For Frontend(服务于前端的后端)。初衷是在后台服务与前端之前添加一层,主要负责快速跟进UI迭代,从后台取数据并进行数据封装,让前端依赖更少更专注。
  为了做到这些,BFF需要遵循一些规则:

1.前端与BFF服务由同一个团队负责

同一个团队内可以更高效沟通和协调资源,快速响应迭代,同时也可以减少协作流程和依赖。

2.BFF层应该足够薄

不负责业务实现,只负责最基本的参数校验、数据调用和组装。其它的逻辑可以向上浮到网关层(比如鉴权或用户会话)或向下下沉到后台服务。后台服务以领域设计模式DDD的话还可以有效防止乱窜,让服务更规整。

3.BFF有明确的服务终端

每个BFF层创建时,必须很明确它的上游是哪些(一个或多个)终端,android / ios / web / h5 / 小程序…明确的服务终端,可以更好的设计BFF层的功能。让BFF层更小更容易维护,也更容易提供特定的能力,不需要考虑兼容太过广泛的设备。
  
  当然,还有其它一些比较基础的原则,比如应该暴露出去的是尽量通用的接口,比如RESTful接口、JSON结构数据等。

三、BFF服务如何划分

纵向划分非常容易,但是横向划分就没这么容易了。可以先了解下下面三种划分方式:

1. 按产品拆分

每个产品一个BFF层,这个是在一些刚做前后端分离时喜欢用的模式。
  
优点:

  1. 接口复用,代码复用,各端数据统一
  2. 服务器资源利用率高

缺点:

  1. 接口数据冗余。有时需要做些各端适配,APP的接口经常还需要做版本兼容。
  2. 维护成本很高,多个团队共同开发时还容易发生冲突。
    BFF困境与思考

2. 按模块拆分

如果一个产品规模很大,由多个团队共同完成时,不太可能按一个BFF服务来实现整个产品功能,这时候,更可能会按产品的模块来划分BFF服务。
  一个模块使用一个BFF服务去支撑一个产品不同的端。这样做的好处是可以缓解一个大BFF服务带来的复杂性,但实际上也没有太大的变化,只是将BFF的规模变小了而已。
优点:

  1. 按产品模块划分,功能会更专注,有利于团队分工。
  2. 代码的复用利依然很高

缺点:

  1. 依然存在兼容多个终端的情况

BFF困境与思考

3. 按终端划分

BFF最初的理念其实是按终端来划分。但是又涉及到一个BFF服务对应一个还是多个终端的问题。这个暂且不要纠结,后面会说到一些BFF划分的推荐原则问题。

优点:

  1. 快速响应终端迭代
  2. 支撑终端差异化功能
  3. 无需过多考虑终端间的兼容问题

缺点:

  1. 各端BFF存在重复代码、重复劳动。
  2. 容易造成数据不一致。
  3. 服务器资源利用率低。  
    BFF困境与思考

四、BFF困境

1. 服务如何划分

上面的BFF服务划分仅仅是从不同的维度进行的讲解,但实际工作中通常可没有这么简单直接。往往,一个产品终端太多,模块太多时,仅仅是按产品、模块或终端来划分会显得很不协调。但又无法提供一个更明确的划分方法,所以最终还是将划分的重任落在“经验丰富”的开发者身上,这明显是不靠谱的一件事(不同的人可能会有不同的划分方法)。

2. 代码复用和资源利用率如何平衡

架构设计没有银弹,只有适合的。所以在代码复用和资源利用率上,往往会与灵活、自主性相违背,就像CAP原则一样,只能取其二而舍其一。

3. 如何避免平级调用或跨级调用

BFF层为了保持它的独立性,禁止BFF之间调用(即是平级调用)。BFF只对自己明确的终端负责,不关心周边的产品/终端。特别是按终端划分时,往往不同端的页面是比较接近的,开发人员有时为了省事,会直接调用“拿”其它端的接口来用,这应该是明令禁止的!

五、BFF拆分的原则

以下是我们公司制定的一些BFF拆分原则,供大家参考:

1. [强制]所有终端必须与BFF服务对接

如果UI直接调用底层服务(领域服务或基础服务),会造成领域服务或基础服务无法稳定,经常需要跟随UI变动。所以每个前端必须有BFF层,这时条红线。BFF存在的目的,除了快速响应前端迭代外,还有一个目的是为了隔离前端后端,让后端更稳定,有沉淀。

2. [建议]按终端 + 模块划分,一个产品可以有一个或多个BFF服务

一个产品比较大时,需要按终端划分,同时还需要按模块划分。这个还取决于各自公司使用的技术栈,如果使用容器,分服务的成本会显示小很多,次之是虚拟机,再次之是直接在物理机上。

3. [建议]按产品中长期终端UI的差异性拆分

这应该是最重要的一条拆分原则了。按终端拆分并不是每一个端都需要一个BFF服务,这样会有太多的服务,不利于维护。BFF原则上是跟着UI走的,无论是哪个端的UI,如果差异性不大,大可对接同一个BFF服务。UI差异性体现在:

  1. 展示的数据和数据逻辑。如果不同终端,展示的数据和取数据逻辑是一致的,那么从同一个接口取数据完全没有问题。
  2. 终端功能。不同的终端,可以提供的用户交互是完全不一样的,比如app可以直接拨打电话,而PC端是无法做到,而提供二维码,让手机扫码拨号,这就是很大的差异性。原生app能够提供顺滑的切换页面的交互,而H5很难做到,app有推送…
    BFF困境与思考
    (大的产品,可以按终端 + 模块来划分BFF)

4. [建议]尽量为APP、小程序提供最终的数据

APP、小程序的更新需要发版,有的还需要审核,所以更改成本很高。如果在BFF层提供足够保真的数据(比如UI上显示的金额,小数点保留几位;比如一个列表的排序等,直接在BFF计算好直接吐给APP),能够快速的修改BFF的数据而快速更新APP、小程序上的数据,不用发版。

5. [建议]尽量保持服务无状态

这其实是微服务的设计原则。无状态即是可以*水平扩展,而不需要去同步、维护一些状态,比如用户的登录态;用户的会话等。

六、BFF层代码复用原则

1. 向上网关层抽取

用户的登录态、会话,应该抽取到网关层。而BFF层只保留与底层用户服务的注册、登录、查询通道(通用网关甚至可以不需要)。

2. 抽取公共包

只有一种情况可以抽取公共包在不同的BFF服务间共用,那就是抽取的功能应该是完全没有业务逻辑的,比如Utils包。注意,==公共库通常是耦合的主要原因。==所以要抽取公共库时,必须非常小心,更多时候应该考虑开源社区是否已经有类似的包了。

3. 向下领域服务下沉业务

公共的业务代码不能向上或平行抽取,最好的解决方案是向底层的领域服务下沉业务能力。向下沉,直接面对的是领域服务,所以下沉到哪个地方,怎样下沉直接遵循DDD原则。这样不会把底层服务搞乱,也会越来越规整。

BFF困境与思考

七、BFF层破局者一:Serverless

BFF困境与思考
  Serverless的概念越来越火,但是什么是Serverless呢?上图是AWS lambda的经典应用,开发者不需要再关心服务器,不需要关心内存、CPU、磁盘空间…所有这些,都交给云计算去完成。
  BFF困境与思考
  Serverless是云计算基金会下面的产品之一,它的定义是提供了其中一种或同时提供两种以下服务:
  FaaS(Functions-as-a-Service) – 用户自己提供的
  BaaS(Backend-as-a-Service) – 三方提供的
  说白了就是把某些接口包装成sdk,以函数或本地方法的方式提供云端的计算能力,这个能力可以是三方提供或用户按照云端的规范自己写,然后发布到云端。
  那么,这是不是一个完美的BFF层实现呢?不一定!

  1. 它没有根本上改变BFF层的,至多是把服务/接口的概念打破了,以函数的方式提供。
  2. 引入了另外的复杂度,比如函数包(serverless也是以引入包的方式来提供对应的函数服务的),同样避不开版本和底层服务(函数)调用的问题。
  3. 云原生。如果是新项目,倒是个不错的技术选型,不过一旦开始,可能后面就得一直依赖这朵云了。
  4. Serverless 更多是云厂家的卖点。

八、BFF层破局者二:GraphQL

BFF困境与思考

1. GraphQL是一种规范

它定义了:

  1. 统一的图(图可以理解为一个业务实体对象,以及它们的关联关系)
  2. 统一的数据逻辑(图的每个类型、字段都需要定义它们的取数据逻辑)
  3. 统一的查询语法(有点像SQL,一种查询语言)

2. GraphQL是统一的数据接口

所有的数据都是由一个统一的http接口提供。使用gql语法,将页面需要的数据一次性查询出来。不多不少,一来减少http请求;二来不会有多余数据;三者可以UI的变换可以快速修改查询脚本实现。

3. GraphQL带来的好处

领域:
  在BFF层几乎看不到领域的概念,所有的数据都是按UI所需进行拼装。所以GraphQL的出现算是眼前一亮,简直就是BFF的一股清流呀。
  领域对象很重要,它是对象的定义,在哪里出现含义都是一样的。特别是配合GraphQL的关联特性,可以将需要的数据都关联出来,这也是GraphQL的魔力所在,拼数据在GraphQL里面是不存在的!

成本
  GraphQL不需要考虑拆分的问题,因为它就是集中管理的。另外,对于开发人员来说,如果底层服务也是使用DDD,那用GraphQL简直就是爽得不要不要的,因为它不需要再去定义领域了,拿来即用…
  从运维角度来说,也是一种高效的部署方式,还节约网络成本。

监控
  GraphQL的特色之一,监控粒度可以细到字段。字段取值耗时、字段是否有被使用等一目了然。这对于需要下线的字段来说,是个非常有用的工具。

4. GraphQL的困境

把GraphQL吹上天了,那么GraphQL是否就真的是个灵丹妙药呢?为什么现在用的企业并不是很多呢?
  确实,GraphQL的初衷很好,但是在具体实施的时候却有很多的问题,主要是:

维护图的成本很高
  定义图,需要对业务、对UI的把握非常到位,不然再好的东西,没有设计好,最后也会变成一团糟。图的定义说白了就是领域模型、领域能力的定义。
  另外,多个团队维护同一个产品时,分工协作也是一个很大的问题。

维护数据逻辑成本很高
  图中的每个类型、字段取值都需要指定,比如从API中取…
  GraphQL在定义之初是无法知道会被怎样使用的,而是由GraphQL引擎自动完成,虽然一些GraphQL引擎的实现中帮你准备了很多性能优化策略,但是最终还是需要人工去定义。很容易出现循环调用PRC、N+1等性能问题。
  即便是当前已经商业化了的Apollo GraphQL,也躲不开这两大块问题。

5. Duo-GraphQL

把GraphQL吹上天,接着又泼了盆冷水,还让不让人愉快地玩耍了?
  这里介绍一个Java的GraphQL实现:Duo-GraphQL,它是github上的一个开源项目,基于apache-2.0协议开源,是个很有意思的项目。
 BFF困境与思考
看看它提供的主要功能:

  • 由普通的RESTful服务实现
  • 自动生成Schema,即图的维护是自动的
  • 自动绑定Type / Field的数据逻辑
  • 自动实现了多种性能优化。比如合并请求、自动解决N+1问题、智能缓存等

Duo-GraphQL保留了GraphQL的所有优势,自动化完成了GraphQL的需要人工维护的工作。同时又以传统的微服务的理念协作,保留了微服务团队敏捷协作的特点。最大程度匹配现代的互联网开发模式。
  它,会是BFF的破局者吗?

BFF困境与思考

一、BFF在整体微服务架构中的位置

微服务化带来的最大问题是服务边界的划分。可以从两个维度看:纵向职能和横向业务的划分。
  纵向划分,BFF在网关与领域服务之间比较容易确定,起到桥梁的作用,一来可以快速支持前端的迭代;二来可以让下游的领域服务更加纯粹。
  横向的划分是最大的难点,后面会有分析。
BFF困境与思考
  上图是我公司目前的服务架构图(并不是说这样的分层是最合理的,仅作文章后面的铺垫),从上到下分别是:前端服务、BFF服务、领域服务(有些会分为两层:领域聚合和领域服务)、中台服务(基础服务)。


二、BFF是什么

BFF困境与思考
  Backend For Frontend(服务于前端的后端)。初衷是在后台服务与前端之前添加一层,主要负责快速跟进UI迭代,从后台取数据并进行数据封装,让前端依赖更少更专注。
  为了做到这些,BFF需要遵循一些规则:

1.前端与BFF服务由同一个团队负责

同一个团队内可以更高效沟通和协调资源,快速响应迭代,同时也可以减少协作流程和依赖。

2.BFF层应该足够薄

不负责业务实现,只负责最基本的参数校验、数据调用和组装。其它的逻辑可以向上浮到网关层(比如鉴权或用户会话)或向下下沉到后台服务。后台服务以领域设计模式DDD的话还可以有效防止乱窜,让服务更规整。

3.BFF有明确的服务终端

每个BFF层创建时,必须很明确它的上游是哪些(一个或多个)终端,android / ios / web / h5 / 小程序…明确的服务终端,可以更好的设计BFF层的功能。让BFF层更小更容易维护,也更容易提供特定的能力,不需要考虑兼容太过广泛的设备。
  
  当然,还有其它一些比较基础的原则,比如应该暴露出去的是尽量通用的接口,比如RESTful接口、JSON结构数据等。

三、BFF服务如何划分

纵向划分非常容易,但是横向划分就没这么容易了。可以先了解下下面三种划分方式:

1. 按产品拆分

每个产品一个BFF层,这个是在一些刚做前后端分离时喜欢用的模式。
  
优点:

  1. 接口复用,代码复用,各端数据统一
  2. 服务器资源利用率高

缺点:

  1. 接口数据冗余。有时需要做些各端适配,APP的接口经常还需要做版本兼容。
  2. 维护成本很高,多个团队共同开发时还容易发生冲突。
    BFF困境与思考

2. 按模块拆分

如果一个产品规模很大,由多个团队共同完成时,不太可能按一个BFF服务来实现整个产品功能,这时候,更可能会按产品的模块来划分BFF服务。
  一个模块使用一个BFF服务去支撑一个产品不同的端。这样做的好处是可以缓解一个大BFF服务带来的复杂性,但实际上也没有太大的变化,只是将BFF的规模变小了而已。
优点:

  1. 按产品模块划分,功能会更专注,有利于团队分工。
  2. 代码的复用利依然很高

缺点:

  1. 依然存在兼容多个终端的情况

BFF困境与思考

3. 按终端划分

BFF最初的理念其实是按终端来划分。但是又涉及到一个BFF服务对应一个还是多个终端的问题。这个暂且不要纠结,后面会说到一些BFF划分的推荐原则问题。

优点:

  1. 快速响应终端迭代
  2. 支撑终端差异化功能
  3. 无需过多考虑终端间的兼容问题

缺点:

  1. 各端BFF存在重复代码、重复劳动。
  2. 容易造成数据不一致。
  3. 服务器资源利用率低。  
    BFF困境与思考

四、BFF困境

1. 服务如何划分

上面的BFF服务划分仅仅是从不同的维度进行的讲解,但实际工作中通常可没有这么简单直接。往往,一个产品终端太多,模块太多时,仅仅是按产品、模块或终端来划分会显得很不协调。但又无法提供一个更明确的划分方法,所以最终还是将划分的重任落在“经验丰富”的开发者身上,这明显是不靠谱的一件事(不同的人可能会有不同的划分方法)。

2. 代码复用和资源利用率如何平衡

架构设计没有银弹,只有适合的。所以在代码复用和资源利用率上,往往会与灵活、自主性相违背,就像CAP原则一样,只能取其二而舍其一。

3. 如何避免平级调用或跨级调用

BFF层为了保持它的独立性,禁止BFF之间调用(即是平级调用)。BFF只对自己明确的终端负责,不关心周边的产品/终端。特别是按终端划分时,往往不同端的页面是比较接近的,开发人员有时为了省事,会直接调用“拿”其它端的接口来用,这应该是明令禁止的!

五、BFF拆分的原则

以下是我们公司制定的一些BFF拆分原则,供大家参考:

1. [强制]所有终端必须与BFF服务对接

如果UI直接调用底层服务(领域服务或基础服务),会造成领域服务或基础服务无法稳定,经常需要跟随UI变动。所以每个前端必须有BFF层,这时条红线。BFF存在的目的,除了快速响应前端迭代外,还有一个目的是为了隔离前端后端,让后端更稳定,有沉淀。

2. [建议]按终端 + 模块划分,一个产品可以有一个或多个BFF服务

一个产品比较大时,需要按终端划分,同时还需要按模块划分。这个还取决于各自公司使用的技术栈,如果使用容器,分服务的成本会显示小很多,次之是虚拟机,再次之是直接在物理机上。

3. [建议]按产品中长期终端UI的差异性拆分

这应该是最重要的一条拆分原则了。按终端拆分并不是每一个端都需要一个BFF服务,这样会有太多的服务,不利于维护。BFF原则上是跟着UI走的,无论是哪个端的UI,如果差异性不大,大可对接同一个BFF服务。UI差异性体现在:

  1. 展示的数据和数据逻辑。如果不同终端,展示的数据和取数据逻辑是一致的,那么从同一个接口取数据完全没有问题。
  2. 终端功能。不同的终端,可以提供的用户交互是完全不一样的,比如app可以直接拨打电话,而PC端是无法做到,而提供二维码,让手机扫码拨号,这就是很大的差异性。原生app能够提供顺滑的切换页面的交互,而H5很难做到,app有推送…
    BFF困境与思考
    (大的产品,可以按终端 + 模块来划分BFF)

4. [建议]尽量为APP、小程序提供最终的数据

APP、小程序的更新需要发版,有的还需要审核,所以更改成本很高。如果在BFF层提供足够保真的数据(比如UI上显示的金额,小数点保留几位;比如一个列表的排序等,直接在BFF计算好直接吐给APP),能够快速的修改BFF的数据而快速更新APP、小程序上的数据,不用发版。

5. [建议]尽量保持服务无状态

这其实是微服务的设计原则。无状态即是可以*水平扩展,而不需要去同步、维护一些状态,比如用户的登录态;用户的会话等。

六、BFF层代码复用原则

1. 向上网关层抽取

用户的登录态、会话,应该抽取到网关层。而BFF层只保留与底层用户服务的注册、登录、查询通道(通用网关甚至可以不需要)。

2. 抽取公共包

只有一种情况可以抽取公共包在不同的BFF服务间共用,那就是抽取的功能应该是完全没有业务逻辑的,比如Utils包。注意,==公共库通常是耦合的主要原因。==所以要抽取公共库时,必须非常小心,更多时候应该考虑开源社区是否已经有类似的包了。

3. 向下领域服务下沉业务

公共的业务代码不能向上或平行抽取,最好的解决方案是向底层的领域服务下沉业务能力。向下沉,直接面对的是领域服务,所以下沉到哪个地方,怎样下沉直接遵循DDD原则。这样不会把底层服务搞乱,也会越来越规整。

BFF困境与思考

七、BFF层破局者一:Serverless

BFF困境与思考
  Serverless的概念越来越火,但是什么是Serverless呢?上图是AWS lambda的经典应用,开发者不需要再关心服务器,不需要关心内存、CPU、磁盘空间…所有这些,都交给云计算去完成。
  BFF困境与思考
  Serverless是云计算基金会下面的产品之一,它的定义是提供了其中一种或同时提供两种以下服务:
  FaaS(Functions-as-a-Service) – 用户自己提供的
  BaaS(Backend-as-a-Service) – 三方提供的
  说白了就是把某些接口包装成sdk,以函数或本地方法的方式提供云端的计算能力,这个能力可以是三方提供或用户按照云端的规范自己写,然后发布到云端。
  那么,这是不是一个完美的BFF层实现呢?不一定!

  1. 它没有根本上改变BFF层的,至多是把服务/接口的概念打破了,以函数的方式提供。
  2. 引入了另外的复杂度,比如函数包(serverless也是以引入包的方式来提供对应的函数服务的),同样避不开版本和底层服务(函数)调用的问题。
  3. 云原生。如果是新项目,倒是个不错的技术选型,不过一旦开始,可能后面就得一直依赖这朵云了。
  4. Serverless 更多是云厂家的卖点。

八、BFF层破局者二:GraphQL

BFF困境与思考

1. GraphQL是一种规范

它定义了:

  1. 统一的图(图可以理解为一个业务实体对象,以及它们的关联关系)
  2. 统一的数据逻辑(图的每个类型、字段都需要定义它们的取数据逻辑)
  3. 统一的查询语法(有点像SQL,一种查询语言)

2. GraphQL是统一的数据接口

所有的数据都是由一个统一的http接口提供。使用gql语法,将页面需要的数据一次性查询出来。不多不少,一来减少http请求;二来不会有多余数据;三者可以UI的变换可以快速修改查询脚本实现。

3. GraphQL带来的好处

领域:
  在BFF层几乎看不到领域的概念,所有的数据都是按UI所需进行拼装。所以GraphQL的出现算是眼前一亮,简直就是BFF的一股清流呀。
  领域对象很重要,它是对象的定义,在哪里出现含义都是一样的。特别是配合GraphQL的关联特性,可以将需要的数据都关联出来,这也是GraphQL的魔力所在,拼数据在GraphQL里面是不存在的!

成本
  GraphQL不需要考虑拆分的问题,因为它就是集中管理的。另外,对于开发人员来说,如果底层服务也是使用DDD,那用GraphQL简直就是爽得不要不要的,因为它不需要再去定义领域了,拿来即用…
  从运维角度来说,也是一种高效的部署方式,还节约网络成本。

监控
  GraphQL的特色之一,监控粒度可以细到字段。字段取值耗时、字段是否有被使用等一目了然。这对于需要下线的字段来说,是个非常有用的工具。

4. GraphQL的困境

把GraphQL吹上天了,那么GraphQL是否就真的是个灵丹妙药呢?为什么现在用的企业并不是很多呢?
  确实,GraphQL的初衷很好,但是在具体实施的时候却有很多的问题,主要是:

维护图的成本很高
  定义图,需要对业务、对UI的把握非常到位,不然再好的东西,没有设计好,最后也会变成一团糟。图的定义说白了就是领域模型、领域能力的定义。
  另外,多个团队维护同一个产品时,分工协作也是一个很大的问题。

维护数据逻辑成本很高
  图中的每个类型、字段取值都需要指定,比如从API中取…
  GraphQL在定义之初是无法知道会被怎样使用的,而是由GraphQL引擎自动完成,虽然一些GraphQL引擎的实现中帮你准备了很多性能优化策略,但是最终还是需要人工去定义。很容易出现循环调用PRC、N+1等性能问题。
  即便是当前已经商业化了的Apollo GraphQL,也躲不开这两大块问题。

5. Duo-GraphQL

把GraphQL吹上天,接着又泼了盆冷水,还让不让人愉快地玩耍了?
  这里介绍一个Java的GraphQL实现:Duo-GraphQL,它是github上的一个开源项目,基于apache-2.0协议开源,是个很有意思的项目。
 BFF困境与思考
看看它提供的主要功能:

  • 由普通的RESTful服务实现
  • 自动生成Schema,即图的维护是自动的
  • 自动绑定Type / Field的数据逻辑
  • 自动实现了多种性能优化。比如合并请求、自动解决N+1问题、智能缓存等

Duo-GraphQL保留了GraphQL的所有优势,自动化完成了GraphQL的需要人工维护的工作。同时又以传统的微服务的理念协作,保留了微服务团队敏捷协作的特点。最大程度匹配现代的互联网开发模式。
  它,会是BFF的破局者吗?