已同步到个人博客,欢迎访问。
问题:服务端设计的接口究竟是面向UI,还是面向通用服务?
解决方案: Backends For Frontends, 简称BFF。
BFF最适合的场景,为第三方提供定制API等差异化场景,每个用户体验(客户端)对应一个后端,
BFF作为用户体验适配层,对后端接口进行组合、处理,对数据进行:裁剪、格式化、聚合、编排。
BFF理念中,最重要的一点是:服务自治,谁使用谁开发,所以一般由前端维护。
BFF实现不限制具体技术,可以自由选型:Java/Node/PHP/Python,但大部分前端团队都会选择Node.js。
BFF和网关(API Gateway)是微服务架构中两个重要概念,可以通过下面的例子讲解BFF的出现过程
实现单块应用的解构拆分,微服务初步完成,前端用户体验层主要是传统的服务端Web应用,服务化架构如下所示:
随着无线应用的流行,除了Web应用外,服务还需要为新的无线原生App来提供接口和数据,先采用下面的服务架构:
这样的架构的问题是:
为了解决这个问题,在用户体验层和内部微服务层之间引入了BFF层,将后端的微服务进行适配,想用户体验层暴露有号和统一的API,方便无线设备接入访问后端服务
这种架构解决了上面的问题:
但是随着接入设备的增加,也会有一些问题:
为了解决这个问题,引入API Gateway
在这种架构下:
可以对V3架构进行优化,将传统的服务端Web应用模式改为前后分离架构,前端采用H5单页技术提供给用户更好体验。
同时增加了下手是哪个第三方应用开放API的能力
拓展新的接入渠道,形成了一个完成的现代微服务架构,从外到内依次为:端用户体验层->网关层->BFF层->微服务层
要求把一个大后端拆分为多个小后端,有三种方式
一般建议按照用户体验级来拆分
(1)如何对接多个技术栈不同的下游服务
RPC协议调用
(2)如何管理、组合调用
借助RXJava、Finagle等事件机制来简化这些异步流程控制
(3)某个调用失败时,如何保障可用性
在BFF层容错,同时前端保证可接受不完整相应内容
拆分后,多个BFF间会产生冗余代码,这是不可避免的。
可以添加API网关层,将通用的后端逻辑(授权、认证、限流)放进去
想消除冗余,又不想因为抽离可复用代码而导致BFF间紧耦合,所以就有了一种折衷的态度:容忍BFF间冗余、消除单BFF内冗余。也就是允许一定程度的BFF间冗余
2016年时,汤尧对BFF在蚂蚁财富项目中的落地实践进行了介绍。
在引入BFF之前,蚂蚁财富的业务也遇到了上面提到的问题,主要是体验层API经常变化,导致开发效率低下。
于是引入了BFF,主要的目的是对数据进行:裁剪、格式化、聚合、编排。
一个理想的模型如下:
在实际落地过程中,形成了如下的架构:
他们在实战过程中,主要解决了一下几个问题:
(1)Node.js与Java通信
这个调用过程一般是通过RPC协议完成的,而非通过HTTP协议。
由于我对Java没有接触,而且资料只是他当时的PPT,没有详细的讲解,没有办法理解具体是如何实现的。而且恐怕对于大多数前端开发工程师来说,与Java的通信也是BFF落地过程中一个比较严峻的挑战,而且这是在前端开发人员有比较好的Node开发能力的基础上。
(2)多App适配
在API网关层处理多个App的通用的逻辑,比如错误码管理、数据一致性、免登、业务日志等。
(3)聚合
(4)接口设计准则
基础服务接口:(微服务?)
BFF API设计
在落地BFF过程中,从技术角度来看:
在2018年的SEE Conf,蚂蚁金服的不四(知乎ID:死马)对在BFF基础上发展出来的TWA(Techless Web Application)开发体系进行了介绍(视频和PPT)。
传统的分层:
BFF负责聚合底层业务数据,给客户端提供接口,秉承谁使用谁维护的理念,一般由前端团队维护。
业务实际上分为三层:前端(HTML/CSS/JS)+ BFF(Node.js接口聚合层)+ 后端服务(Java)
BFF on Chari
是蚂蚁自研的,基于Egg.js的BFF框架,打通了Node到Java的RPC通信链路
Egg.js已经开源(Egg.js是以Koa作为基础框架),需要基于Egg.js去打造属于自己的BFF框架
RPC的意思是不在一个内存空间的两个应用,借助网络来实现,像调用本地的函数一样去调用远程函数
BFF不是银弹,有着自己的问题:
尽管职责划分越来越清晰,但是由于前后端发布系统不一致,前端团队仍需要在基于不同的代码仓库进行研发,走不同的发布流程
理想的开发流程:
蚂蚁金服推出的解决方案TWA(Techless Web Application),是一个全栈的研发框架
TWA是为了提升开发者研发体验而推出的渐进式解决方案,开发者在一个代码仓库下,基于TWA的框架,完成客户端和BFF层的研发,通过Basement研发平台提供的流程支持,不用再关注应用、构建、部署、流程等细节,可以一键将应用部署到各个运行终端,同时在研发平台上完成应用的自主运维和监控
将TWA拆解开来,分为了三个大的方向:框架、研发平台、运行时
是一个渐进式框架,可以选择部分能力使用
客户端、服务端目录、依赖都是相互独立的。
复杂的终端环境,有不同的接入链路(可能通过网关走TCP长连接通信)、不同的鉴权方案。如果每个业务的BFF系统要对接到每个系统完成接入、鉴权是非常复杂的。
解决方案是:TWA网关层 + 客户端RPC Client,
TWA网关层统一完成所有终端的接入、鉴权,BFF不需要对接到每个终端,只需要对接到网关,让BFF接口统一
客户端的RPC Clinet是一个请求库,对接到网关,将客户端的鉴权、接入的细节隐藏,并且可以自动选择接入链路
这就可以在client目录下,像调用本地方法一样,调用server中目录下的服务
Basement,一站式运维平台,基于Docker,支撑整个应用的研发流程,完成自动化的测试和部署
与使用的微服务框架有关,可以采用HTTP的方式,也可以通过RPC调用。
开源框架:
可以采用服务注册中心:
首先,Java应用服务启动的时候,会往服务注册中心注册服务,这里的服务注册中心可能是ETCD或者Zookeeper,然后,Node应用在启动的时候,会先从服务注册中心拉取服务列表,接着Node会跟Java服务建立一条TCP长链接,除此之外,Node还需要负责Hession协议解析以及负载均衡等。
上面的方式Node职责比较重,对Node开发的要求高,有赞在此基础上做了改进:
在Node和Java之间添加了一层中间代理层Tether,Tether是用Go语言写的一个本地代理,Tether会对外暴露一个HTTP的服务,对Node来说,只需要通过HTTP方式调用本地的服务即可,其他服务化相关的服务发现、协议解析、负载均衡、长链建立维护都交由Tether来处理。这样,Node这一层就非常轻量了,Node是调用Java服务时:
const Service = require('../base/BaseService');
class GoodsService extends Service {
/**
* 根据商品 alias 获取商品详情
* @param {String} alias 商品 alias
*/
async getGoodsDetailByAlias(alias) {
const result = this.invoke(
'com.youzan.ic.service.GoodsService',
'getGoodsDetailByAlias',
[alias]
);
return result;
}
}
module.exports = GoodsService;
这种方式的优点:
TWA是蚂蚁金服自研的,整合了BFF的一整套渐进式的前端+Node聚合层的开发体系,它不仅包括了BFF的引入,还包括了前后端代码管理、部署、运维等功能框架。
但是很遗憾的是,TWA没有开源,对于想要从0开始引入BFF,想实现TWA的功能,还有一个努力的过程。
实际上我们引入BFF,不是要完整的开发TWA,而是首先实现Chari BFF的功能。
对于团队来说:
对于个人来说: