DDD系列文章第10篇:BFF架构
全文约5180字,预计阅读时间11分钟。
导读
不管是通过DDD方法论设计新服务还是梳理老服务,绕不开的一点就是接口设计。接口设计时很容易犯的一个错就是经常会根据接口调用方的个性化场景(比如多种界面展示)设计出很多类似且重复性的接口,且接口的实现逻辑割裂、复用性差。为了让业务服务更加聚焦领域能力,根据领域能力设计对外接口,同时又要满足多样化的接口消费场景如前端展示,架构里往往需要引入BFF这一层。
01
—
BFF要解决什么问题
1.1 问题
在用户体验至关重要的今天,程序展示界面丰富多样。比如同一个界面可能同时存在极简版、专业版,一个界面要展示多种数据,要连接的设备也层出不穷如小程序、APP、网页、客户端等等。归纳起来有这几类:
- 字段裁减
- App端往往只能展示少量信息,浏览器网页则需要全部信息,因此需要适配各端而裁减字段
- 聚合和编排多种数据
- 一个页面要展示多个服务端的数据,因此需要聚合多个API的数据(串行或并行编排多个API)
- 接入平台工作
- 通过组合多个已有系统的接口对外提供更具业务语义的能力,提供统一接入层服务
- 数据格式转换
- 面对服务端不同的数据格式,甚至有历史遗留系统,需要适配成前端展示更友好的格式
- 前端的业务效果试验
- 试验不同的展示方案效果,需要快速支持新业务方案上线
1.2 分析
这些需求如果都在前端完成,则前端需要多次网络请求服务端数据,并且接口相关的适配逻辑不适合用前端技术来处理,效率不会高。但是如果一股脑丢到服务端来处理,则服务端模块的接口频繁修改带来稳定性下降。模块界限会变模糊,接口数量膨胀厉害。展示逻辑和领域逻辑混杂在一起,久而久之业务逻辑将变得难以维护。
从DDD角度看,提倡围绕领域业务能力进行接口设计,服务端应该聚焦领域自身能提供什么样的能力来设计接口,而展示相关的处理逻辑不应该是领域业务。因此可以得出这里的主要矛盾是前端的本质是提供良好的用户体验,必然会高频迭代,而服务端的业务逻辑和关键业务数据字段有行业特性,相对比较稳定,不会随着体验变化很大。那应该怎么办呢?
既然错不在前端也不在服务端,那么一般架构的做法是引入新的一层。业界的叫法是BFF(Backend For Frontend),即后端为前端编程。BFF源于Sam Newman在微服务模式里提出。BFF其实也是微服务架构下的产物,通过引入BFF解耦了前后端,也对组织架构做了归属和指导。下图来自他的BFF文章:
02
—
BFF定位和技术选型
增加一层永远是解耦的大招,但BFF只是逻辑分层的名称,不是具体的技术。实现BFF的技术框架有多种。从BFF层的组织架构归属看,由前端团队或后端团队负责则会引入各自不同的技术选型。前端有NodeJS、GraphQL框架等,后端有Java、GraphQL框架、网关框架等。因此很多大厂的产品都有BFF层,但具体的实现各有不同。不妨来看一下BFF技术要考虑的4个方面的关键诉求。
2.1 BFF的组织定位
康威定律是架构首要考虑到的,前端模式或者后端模式会决定BFF由不同技术栈的团队来承接,也直接影响着BFF层的技术架构演进方向。相应地这两种模式各有优缺点。但不管是前端团队来承接BFF还是后端团队,没有对错,应该根据组织自身的现状和发展来思考。
- 前端模式
- 优点:前端团队负责,自主取数,减少跨团队沟通
- 缺点:需要深入业务逻辑、需要掌握基础设施建设的能力
- 后端模式
- 优点:复用后端现有基础设施
- 缺点:前端服务、bff服务和业务服务三者的职责和边界把控
2.2 接口开发与编排
BFF层的接口编排技术包含几层含义:
- 传统接口方式连接供给和消费
即消费方每次都必须按照接口供给方规定的请求参数发起请求,获得请求出参数据。
- 供给方如何高效开发一个接口。正常的接口开发需要考虑接口端点暴露、请求参数和返回参数格式、请求路由和状态等。不管是NodeJS、Java、Go等语言都有现成的Web框架集成
- 供给方如何高效支持串并行调用下游接口。BFF层的一个接口往往需要调用下游多个微服务接口,有串行调用也有并行调用。这点在各种编程语言也有比较好的支持,比如NodeJS的Promise等异步控制对象,Java里的CompleteFuture异步调用、Go里的协程等
从上面两点来看其实编程语言不是关键。如果业务和团队规模不是很大,建设BFF层到这里就够用了。但一旦业务和团队规模变大,展示和取数逻辑涉及的团队人员和接口数量激增,如何高效地完成重复性的接口开发和取数调用将变得关键。突破口往往都是某种意义上的元数据驱动。做到声明式的接口开发体验效果。
因此BFF层的建设将分化出底层框架开发和业务开发两类工作。业务开发以类似DSL(领域专属语言)的方式(比如XML/Json表达、甚至界面拖拽形式)定义出需要从哪些接口取哪些数据字段,做哪些数据加工动作。底层框架负责以标准化的步骤实现接口的定义、组装和调用。
- 消费方按需查询
即消费方可以在调用接口时表达自己想使用哪部分数据,从供给方获得按需的出参数据。通俗地讲就是消费方调用同一个接口时可以控制接口的出参数据。
- 业界的实现方案是Facebook开源的GraphQL接口查询标准,不同语言下有不同的实现框架。其中NodeJS下的Apollo框架使用较广,Java也有社区的开源实现。
- 元数据驱动,如第一点所述。
按需查询的能力听上去很好,但实现的成本也高。因此需要深入理解产品和业务的形态,先回答到底有没有这种需求,值不值得去支持。
2.3 BFF与API网关的关系
这个问题关系到BFF服务在整体架构里的定位。在没有BFF之前,微服务架构里一般都有一个反向代理层,即负责路由和转发来自外网的http和tcp请求给内部的微服务集群,也起到了不把微服务对外暴露的作用。同时可能还会有API网关这一层,负责对接入层的请求进行鉴权、协议转换、流量控制等。相当于把每个微服务要做的事情集中做一次。如果引入BFF层,需要考虑和API网关的职责和协作。
上面提到的GraphQL服务端也有鉴权和流量限流的设计,甚至还有普通微服务的数据库访问的设计。因此BFF层的定位要清晰,要想清楚BFF服务仅仅是微服务接口的聚合层?还是叠加了API网关功能?还是一竿子到底的全栈微服务?
2.4 BFF的非功能需求
BFF服务作为服务端的一份子,必然需要考虑如何无缝地融入现有微服务架构,满足微服务的横向非功能需求。包括:
- 服务注册和发现:BFF服务应该融入现有微服务注册发现的机制
- 服务治理:如何跟其他微服务(不限语言)互相调用、接口流量的控制如限流
- 可观测能力:日志、链路、指标等方面的采集、监控、告警
- 融入现有开发流程:接口文档在线化、测试(Mock、自动化)
- 按照什么粒度划分BFF模块:根据不同的展示端、不同的业务模块、不同的业务团队等
2.5 BFF的能力层次
根据上面提到的关键诉求满足程度以及当前业务发展情况,笔者结合自己经历过的项目实践以及了解到的其他团队的实践,把BFF服务能力划分为如下几个不同层次。方便你根据团队和业务现状进行BFF技术选型和建设力度的决策。
- 第一层:BFF独立存在,解耦了前端和后端微服务。仍旧按照后端开发接口的方式一个查询场景对应一个接口,开发效率中等。
- 前端模式,用NodeJS开发,基于express/koa等框架在MVC模式下开发查询接口
- 后端模式,用Java开发,和API网关合体,或者独立存在。但BFF服务还是按照正常后端接口的开发流程进行。
- 第二层:在第一层基础上具备更高的接口开发效率和模型复用。
- 前端模式,基于GraphQL,但模型的复用没落实到字段级别。意味着模型和取数接口复用度有优化空间。
- 后端模式,用Java开发,低层次的元数据驱动。即基于自定义DSL(Json格式)来表达接口编排细节。
- 第三层:在第二层基础上具备更高的复用,最大程度地提高了接口开发效率和模型复用。
- 前端模式,基于GraphQL,Schema的定义拆解到每一个字段级别,并能够在组织范围内共享使用。这意味着模型和取数接口的复用度极高,带来查询接口开发效率和微服务接口使用效率高。
- 后端模式,通过元数据驱动,低代码方式开发查询接口,附带可视化后台开发界面。美团有一篇文章介绍他们的做法(传送门)。
03
—
BFF的实践
前面多次提到了GraphQL,下面笔者结合经历过的一个生产项目实践来聊一下GraphQL的落地细节。为什么需要建设BFF这一层就不多说了,因为是ToB项目,界面展示逻辑普遍复杂,产品同时支持浏览器、App、小程序、M站等
3.1 选型考虑
- 选择了前端模式,为什么?
- 希望提高前端团队理解业务逻辑的水平。前后端对业务逻辑的理解水平对产品的演进很关键,而前端比较缺乏业务理解。
- 拓宽前端团队的技术栈,增加NodeJS、TypeScript等服务端开发能力,以及服务部署和运维的基本能力。
- 当前团队是按照业务模块划分敏捷小组,因此BFF服务根据业务模块来划分。
- 选择了GraphQL,为什么?
- GraphQL的模型优先思想很契合团队在服务端的DDD实践。产品、前端、后端一起沟通需求时都习惯先弄清楚领域模型,GraphQL的Schema包括数据结构以及数据对应的取数接口。这和DDD的领域模型很类似,因此有利于加强这种习惯,落地起来也很顺畅。
- 基于GraphQL的开发心智简单。GraphQL开发只需要完成三个步骤,定义Schema、Resolver、Service。而BFF的核心功能就是考虑取哪些数,从哪些接口取数,取完数要做哪些加工。另外结合TypeScript也能加强JS代码的质量,提高调试体验。
- 另外前端也有一些按需查询的场景。即针对同一个Shema有不同的查询需求。
- 和API网关的关系?
- BFF服务和API网关独立存在,BFF和API GW都是正常的微服务,可以互相发现,通过http协议互相调用。
- 如何保证NodeJS服务的非功能需求支持能力?
- 虽然服务端主要基于Java开发,但整体集群底座都已经service mesh化。服务之间的调用基于K8s service,流量控制能力基于Istio控制面。加上APM可观测组件也已经mesh化,因此NodeJS服务可以快速融入到现有的服务集群里。
3.2 落地架构
- 基于Egg的NodeJS框架
- 基于Apollo的GraphQL client和server框架
- 基于GraphQL server的Schema、Query、Service三步开发
- NodeJS服务端工程化改造,融入现有微服务架构,比如服务发现调用、日志、链路、监控
3.3 基于GraphQL的开发流程
- 定义Schema,相当于接口约定。这也很契合API First理念,先定义接口然后并行开发。Schema的几个知识点:
- 标量与类型:Schema里除了使用基本数据类型外,你也可以自定义标量类型。定义类型时可以嵌套其他类型,好比面对对象编程里对象的某个属性可以是另一个对象。
- 读写类型:GraphQL里查询接口是Query,改写接口是Mutation。一般BFF服务主要针对查询。
- 强弱类型:最好采用强类型形式把所有层次上的对象都定义出来直到对象字段都是标量类型。你也可以在Schema里使用自定义Json标量然后笼统用Json表示,但不建议这种弱类型方式。
举例来说,下方CorpWechatQRCodeDetail是一种对象类型,包括数据格式和对应的查询接口。
- 定义Resolver。作用是绑定Query和Service,起到路由作用。
- 定义Service。编排调用需要的微服务接口,加工处理数据。
- 文档与调试
- 用Apollo graphql的playgroud插件可以方便查看schema和接口文档,模拟按需查询请求并调试。
3.4 实践经验小结
前面都说了BFF和GraphQL的优点,下面说下缺陷部分。
- 警惕BFF层对前端团队的挑战
- 大部分前端团队的服务端经验很欠缺,起步阶段如何运维和治理BFF会比较吃力,需要有计划有目标地培养。
- 要衡量好业务开发的收益以及非业务功能需求的支持成本,这块的成本往往会比预计的要多。
- 模块和接口职责的划分与分工
- 需要明确前端、BFF、后端微服务三者的定位、职责、界限。以及相应的保障措施比如设计和编码规范。
- 警惕业务逻辑往BFF服务蔓延,前后端在责任划分时容易偷懒,不进行深入思考,往BFF写不必要的代码逻辑。
- BFF服务的项目模块组织和命名规范需要重视。代码量多了容易带来维护问题。基于GraphQL来实现的话路径目录、文件拆分、代码命名等相对比较清晰。
- 业务模块在什么时机需要独立拆分出BFF?看起来只能凭经验,应根据实际需求开发过程中数据编排和加工的逻辑占比看。
- 警惕GraphQL的缺点
- 同一个GraphQL服务的请求都共用一个请求url端点,并基于请求体里的内容来做路由。比如线索BFF服务的请求端点都是/bff/leads。这意味着传统基于请求url做的事情比如鉴权、路由、限流、监控等工作会失效。一个解决办法是在请求头里植入实际url端点,按需从请求头里解析获得。
- GraphQL的请求状态码只有400和200,而传统RESTFul或者普通http的前后端框架里都很依赖500请求状态码。一个解决办法是对GraphQL请求的钩子函数里特殊处理。
- GraphQL服务端如果要做网关和数据访问等事情,目前看框架提供的鉴权、限流、缓存等能力不能很好满足。建议委托给其他组件或服务来实现,GraphQL专注做接口编排和数据裁减工作。
- 如果前端对同一个schema的按需查询场景少,GraphQL的性价比就低了。选型时需要谨慎考虑GraphQL这些缺点。
04
—
结语
微服务DDD化后需要尽可能地保持领域模型和领域接口的纯洁性和稳定性,如何应对多样化且高频的前端展示需求是一大挑战。引入BFF是一个解法,但架构需要权衡。BFF服务的存在本身有利有弊,BFF的不同落地实现也有利有弊。GraphQL听起来很炫酷但也不尽然。实践能出真知,但对所支撑业务的理解也很关键。很多时候还得回到业务和团队中去看。
感谢阅读,本篇首发于微信公众号:非写不可