目录标题
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层,这个是在一些刚做前后端分离时喜欢用的模式。
优点:
- 接口复用,代码复用,各端数据统一
- 服务器资源利用率高
缺点:
- 接口数据冗余。有时需要做些各端适配,APP的接口经常还需要做版本兼容。
- 维护成本很高,多个团队共同开发时还容易发生冲突。
2. 按模块拆分
如果一个产品规模很大,由多个团队共同完成时,不太可能按一个BFF服务来实现整个产品功能,这时候,更可能会按产品的模块来划分BFF服务。
一个模块使用一个BFF服务去支撑一个产品不同的端。这样做的好处是可以缓解一个大BFF服务带来的复杂性,但实际上也没有太大的变化,只是将BFF的规模变小了而已。
优点:
- 按产品模块划分,功能会更专注,有利于团队分工。
- 代码的复用利依然很高
缺点:
- 依然存在兼容多个终端的情况
3. 按终端划分
BFF最初的理念其实是按终端来划分。但是又涉及到一个BFF服务对应一个还是多个终端的问题。这个暂且不要纠结,后面会说到一些BFF划分的推荐原则问题。
优点:
- 快速响应终端迭代
- 支撑终端差异化功能
- 无需过多考虑终端间的兼容问题
缺点:
- 各端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差异性体现在:
- 展示的数据和数据逻辑。如果不同终端,展示的数据和取数据逻辑是一致的,那么从同一个接口取数据完全没有问题。
- 终端功能。不同的终端,可以提供的用户交互是完全不一样的,比如app可以直接拨打电话,而PC端是无法做到,而提供二维码,让手机扫码拨号,这就是很大的差异性。原生app能够提供顺滑的切换页面的交互,而H5很难做到,app有推送…
(大的产品,可以按终端 + 模块来划分BFF)
4. [建议]尽量为APP、小程序提供最终的数据
APP、小程序的更新需要发版,有的还需要审核,所以更改成本很高。如果在BFF层提供足够保真的数据(比如UI上显示的金额,小数点保留几位;比如一个列表的排序等,直接在BFF计算好直接吐给APP),能够快速的修改BFF的数据而快速更新APP、小程序上的数据,不用发版。
5. [建议]尽量保持服务无状态
这其实是微服务的设计原则。无状态即是可以*水平扩展,而不需要去同步、维护一些状态,比如用户的登录态;用户的会话等。
六、BFF层代码复用原则
1. 向上网关层抽取
用户的登录态、会话,应该抽取到网关层。而BFF层只保留与底层用户服务的注册、登录、查询通道(通用网关甚至可以不需要)。
2. 抽取公共包
只有一种情况可以抽取公共包在不同的BFF服务间共用,那就是抽取的功能应该是完全没有业务逻辑的,比如Utils包。注意,==公共库通常是耦合的主要原因。==所以要抽取公共库时,必须非常小心,更多时候应该考虑开源社区是否已经有类似的包了。
3. 向下领域服务下沉业务
公共的业务代码不能向上或平行抽取,最好的解决方案是向底层的领域服务下沉业务能力。向下沉,直接面对的是领域服务,所以下沉到哪个地方,怎样下沉直接遵循DDD原则。这样不会把底层服务搞乱,也会越来越规整。
七、BFF层破局者一:Serverless
Serverless的概念越来越火,但是什么是Serverless呢?上图是AWS lambda的经典应用,开发者不需要再关心服务器,不需要关心内存、CPU、磁盘空间…所有这些,都交给云计算去完成。
Serverless是云计算基金会下面的产品之一,它的定义是提供了其中一种或同时提供两种以下服务:
FaaS(Functions-as-a-Service) – 用户自己提供的
BaaS(Backend-as-a-Service) – 三方提供的
说白了就是把某些接口包装成sdk,以函数或本地方法的方式提供云端的计算能力,这个能力可以是三方提供或用户按照云端的规范自己写,然后发布到云端。
那么,这是不是一个完美的BFF层实现呢?不一定!
- 它没有根本上改变BFF层的,至多是把服务/接口的概念打破了,以函数的方式提供。
- 引入了另外的复杂度,比如函数包(serverless也是以引入包的方式来提供对应的函数服务的),同样避不开版本和底层服务(函数)调用的问题。
- 云原生。如果是新项目,倒是个不错的技术选型,不过一旦开始,可能后面就得一直依赖这朵云了。
- Serverless 更多是云厂家的卖点。
八、BFF层破局者二:GraphQL
1. GraphQL是一种规范
它定义了:
- 统一的图(图可以理解为一个业务实体对象,以及它们的关联关系)
- 统一的数据逻辑(图的每个类型、字段都需要定义它们的取数据逻辑)
- 统一的查询语法(有点像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协议开源,是个很有意思的项目。
看看它提供的主要功能:
- 由普通的RESTful服务实现
- 自动生成Schema,即图的维护是自动的
- 自动绑定Type / Field的数据逻辑
- 自动实现了多种性能优化。比如合并请求、自动解决N+1问题、智能缓存等
Duo-GraphQL保留了GraphQL的所有优势,自动化完成了GraphQL的需要人工维护的工作。同时又以传统的微服务的理念协作,保留了微服务团队敏捷协作的特点。最大程度匹配现代的互联网开发模式。
它,会是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层,这个是在一些刚做前后端分离时喜欢用的模式。
优点:
- 接口复用,代码复用,各端数据统一
- 服务器资源利用率高
缺点:
- 接口数据冗余。有时需要做些各端适配,APP的接口经常还需要做版本兼容。
- 维护成本很高,多个团队共同开发时还容易发生冲突。
2. 按模块拆分
如果一个产品规模很大,由多个团队共同完成时,不太可能按一个BFF服务来实现整个产品功能,这时候,更可能会按产品的模块来划分BFF服务。
一个模块使用一个BFF服务去支撑一个产品不同的端。这样做的好处是可以缓解一个大BFF服务带来的复杂性,但实际上也没有太大的变化,只是将BFF的规模变小了而已。
优点:
- 按产品模块划分,功能会更专注,有利于团队分工。
- 代码的复用利依然很高
缺点:
- 依然存在兼容多个终端的情况
3. 按终端划分
BFF最初的理念其实是按终端来划分。但是又涉及到一个BFF服务对应一个还是多个终端的问题。这个暂且不要纠结,后面会说到一些BFF划分的推荐原则问题。
优点:
- 快速响应终端迭代
- 支撑终端差异化功能
- 无需过多考虑终端间的兼容问题
缺点:
- 各端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差异性体现在:
- 展示的数据和数据逻辑。如果不同终端,展示的数据和取数据逻辑是一致的,那么从同一个接口取数据完全没有问题。
- 终端功能。不同的终端,可以提供的用户交互是完全不一样的,比如app可以直接拨打电话,而PC端是无法做到,而提供二维码,让手机扫码拨号,这就是很大的差异性。原生app能够提供顺滑的切换页面的交互,而H5很难做到,app有推送…
(大的产品,可以按终端 + 模块来划分BFF)
4. [建议]尽量为APP、小程序提供最终的数据
APP、小程序的更新需要发版,有的还需要审核,所以更改成本很高。如果在BFF层提供足够保真的数据(比如UI上显示的金额,小数点保留几位;比如一个列表的排序等,直接在BFF计算好直接吐给APP),能够快速的修改BFF的数据而快速更新APP、小程序上的数据,不用发版。
5. [建议]尽量保持服务无状态
这其实是微服务的设计原则。无状态即是可以*水平扩展,而不需要去同步、维护一些状态,比如用户的登录态;用户的会话等。
六、BFF层代码复用原则
1. 向上网关层抽取
用户的登录态、会话,应该抽取到网关层。而BFF层只保留与底层用户服务的注册、登录、查询通道(通用网关甚至可以不需要)。
2. 抽取公共包
只有一种情况可以抽取公共包在不同的BFF服务间共用,那就是抽取的功能应该是完全没有业务逻辑的,比如Utils包。注意,==公共库通常是耦合的主要原因。==所以要抽取公共库时,必须非常小心,更多时候应该考虑开源社区是否已经有类似的包了。
3. 向下领域服务下沉业务
公共的业务代码不能向上或平行抽取,最好的解决方案是向底层的领域服务下沉业务能力。向下沉,直接面对的是领域服务,所以下沉到哪个地方,怎样下沉直接遵循DDD原则。这样不会把底层服务搞乱,也会越来越规整。
七、BFF层破局者一:Serverless
Serverless的概念越来越火,但是什么是Serverless呢?上图是AWS lambda的经典应用,开发者不需要再关心服务器,不需要关心内存、CPU、磁盘空间…所有这些,都交给云计算去完成。
Serverless是云计算基金会下面的产品之一,它的定义是提供了其中一种或同时提供两种以下服务:
FaaS(Functions-as-a-Service) – 用户自己提供的
BaaS(Backend-as-a-Service) – 三方提供的
说白了就是把某些接口包装成sdk,以函数或本地方法的方式提供云端的计算能力,这个能力可以是三方提供或用户按照云端的规范自己写,然后发布到云端。
那么,这是不是一个完美的BFF层实现呢?不一定!
- 它没有根本上改变BFF层的,至多是把服务/接口的概念打破了,以函数的方式提供。
- 引入了另外的复杂度,比如函数包(serverless也是以引入包的方式来提供对应的函数服务的),同样避不开版本和底层服务(函数)调用的问题。
- 云原生。如果是新项目,倒是个不错的技术选型,不过一旦开始,可能后面就得一直依赖这朵云了。
- Serverless 更多是云厂家的卖点。
八、BFF层破局者二:GraphQL
1. GraphQL是一种规范
它定义了:
- 统一的图(图可以理解为一个业务实体对象,以及它们的关联关系)
- 统一的数据逻辑(图的每个类型、字段都需要定义它们的取数据逻辑)
- 统一的查询语法(有点像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协议开源,是个很有意思的项目。
看看它提供的主要功能:
- 由普通的RESTful服务实现
- 自动生成Schema,即图的维护是自动的
- 自动绑定Type / Field的数据逻辑
- 自动实现了多种性能优化。比如合并请求、自动解决N+1问题、智能缓存等
Duo-GraphQL保留了GraphQL的所有优势,自动化完成了GraphQL的需要人工维护的工作。同时又以传统的微服务的理念协作,保留了微服务团队敏捷协作的特点。最大程度匹配现代的互联网开发模式。
它,会是BFF的破局者吗?