一、背景介绍
随着互联网和移动设备的普及,用户对于应用的需求也越来越多样化和个性化,这就要求应用程序在不同的端类型上表现出差异化的交互和UI。同时,在后端微服务化的变革浪潮下,后端开发人员需要专注在领域服务及建模的工作中。而交互/UI的变更频率往往要高于领域服务及相关模型,加之前/后端本身的差异性也会无形中加重后端开发人员的心智负担。这就使得后端开发人员既无法专注于领域模型的抽象,又无法敏捷灵活地适应不同的用户界面差异,限制了整体的研发效率提升。
为了解决这个问题,BFF作为一种研发模式被提出。其作为“面向前端的后端服务”,作为中间层在软件架构上隔离了领域服务层及前端UI层。随着架构被隔离,相关的开发人员及开发角色也随之被清晰的分开:前端研发负责BFF的视图逻辑,后端研发则可以专注在领域服务层当中。
这种模式改变了原来的生产关系,令后端开发人员可以更专注于特定领域模型及服务的搭建,不必过多关注频繁的UI层变动。而BFF开发人员则可以与前端开发人员更紧密的合作(甚至前端同时兼任BFF开发工作),提供更符合前端场景需求的接口及数据结构。BFF不是一项突破性的生产力变革,更多是一种生产关系变革,通过前后端协作模式的调整来适应互联网领域的敏捷开发节奏。
传统的API层包含了视图逻辑和业务逻辑,统一由后端开发进行维护。BFF层承担其中视图逻辑的职责,而业务逻辑则“下沉”到领域微服务层进行维护。
在现代微服务架构模式下,BFF作为一类后端服务也被部署在微服务架构中。它作为整个架构中前/后端应用的中间层,也需要考虑其内部各个BFF应用的职责分工问题。通常,可以按照‘领域驱动设计’来进行微服务职责的划分:把特定领域范围内的功能划分到一个微服务上做到聚合,把彼此关联性较小的功能划分到不同的服务上满足解耦。在携程酒店业务BFF架构实践过程中,针对BFF微服务职责的划分问题,出现过2种模式:“一码一端”和“一码多端”。
1.1 一码一端
这个模式针对特定客户端提供一个单体BFF应用,该应用提供整个酒店预定主流程的全部服务能力。为各端提供了充分的控制端差异的能力,独立开发独立部署。在Ctrip小程序端的BFF上有过使用,特点是能够快速部署,独立迭代,适合小团队运维和快速上线。
但缺点是出现了单体巨型应用,在一个服务内承载了列表/详情/填写全流程的前端服务能力,整个应用架构显的臃肿。并且针对同一个产品需求,各端的BFF应用内部需要各自实现相似的视图控制逻辑,在实现业务需求的时候存在重复开发的弊端,不利于提高人效。
1.2 一码多端
这个模式将预定主流程划分为若干关键阶段,分别由不同的BFF提供服务,且一个BFF俱备服务多端的能力并在内部处理不同端的差异性,避免了重复开发,从而提升研发侧整体的生产效率。且由于按页面流程维度进行了微服务划分,架构上更为清晰,迭代也更加灵活。
从两种模式的实践结果来看,“一码多端”的模式更符合微服务职责划分的原则,也更有利于提高研发人效。因此,当前酒店的BFF层的实践方向主要采用了“一码多端”的模式。曾经采用“一码一端”模式的BFF应用也将向“一码多端”模式重构迁移。
二、基于NestJS的多端架构
前述已经谈到了BFF的概念以及酒店业务BFF的微服务划分方式,接下来谈具体实现层面的问题。
首先,我们选择了NestJS作为酒店BFF基础框架进行二次开发。NestJS是一个用于构建高效、可扩展的 NodeJs服务器端应用的框架。它使用渐进式 JavaScript,构建并完全支持 TypeScript。并结合了 OOP(面向对象编程)、FP(函数式编程)的特点。相比其他框架,有如下一些优势:
1)强大的模块化和可扩展性:使用模块化的结构来组织代码,这使得代码更易于组织和维护。此外,NestJS还提供了一种插件系统,允许开发者扩展框架的功能。
2)内置的依赖注入容器:内置了一个强大的依赖注入(DI)容器,使得服务和控制器之间的解耦变得简单,同时也提高了代码的可测试性。
3)TypeScript的支持:基于TypeScript编写的,这意味着可以利用TypeScript的静态类型检查和最新的ECMAScript特性。也可以使用纯JavaScript。
4)装饰器和元数据反射:大量使用了装饰器,这使得代码更易于理解和维护。同时,通过元数据反射,NestJS可以自动处理很多常见的模式,如路由、依赖注入等。
5)支持微服务:提供了一套完整的微服务解决方案,包括消息模式、传输策略等。
6)测试工具:提供了一套强大的测试工具,使得单元测试和端到端测试变得简单。
7)与其他库的集成:可以轻松地与其他流行的JavaScript库和工具集成,如TypeORM、Passport.js、GraphQL等。
在Nest框架提供的IOC能力基础上,酒店BFF研发组构建了专用于支持”一码多端“研发场景的BFF服务框架,试图通过统一的状态数据管理及策略流程模式支持多端适配能力的开发:
1)提供了标准的开发模板,令多端处理逻辑的开发过程趋向标准化。
2)帮助开发者在编写接口时通过流程的横向拆分和数据组件的模块化提高代码可维护性。
3)通过框架模板的约束,促使开发者在系统设计之初就考虑如何处理一致性与差异性。
具体来说,BFF层作为前后端中间层主要的作用是调用下游微服务接口并进行请求参数的处理并最终组装出视图模型数据返回给前端,典型的模式是:
当前端请求到达BFF之后,BFF会解析参数并做出数据结构转换来向下游微服务发起请求,获得返回后再重复这一过程。
面对这样的调用链路,非常容易写成面向过程的逻辑代码。通过一个个工具函数不停对入参/出参进行处理并传递给后一个下游服务接口。
整个程序控制流通过工具函数及函数传参被耦合在一起,这种模式在”一码一端“下由单一团队维护尚能接受,同一批开发人员确保自己端的BFF链路不出错即可。
但当多个端团队共同维护"一码多端"BFF应用时,针对同一个BFF接口服务的不同特性被耦合在一起,将导致团队协作的困境:
1)下游传参不一致:
由于下游领域服务针对各端可能存在特殊逻辑配置,因此给下游的传参因端而异。
这一点通过IF/ELSE/SWITCH在BFF层面控制差异尚且能够接受,但多个端的BFF开发人员需要共同修改同一段分枝逻辑。
2)调用链路不一致:
各端会存在调用时序差异,例如端A某接口强依赖获取用户等级,而端B某接口仅依赖用户是否登陆,端C同时依赖用户登录态和用户等级。
这种程序控制流程的差异相比与参数的差异更难以处理,可能需要更大更复杂的代码块分枝处理来解决,并更容易引起冲突提高协作成本。
3)视图模型不一致:
由于各个端的BFF在不同的时间由不同的团队开发维护,各个版本BFF的接口契约难免存在差异,当“一码一端”合并为“一码多端”时,为了视图模型的一致性,必须将(1)(2)中的不一致在BFF层处理为一致的视图模型。
良好的代码组织结构和清晰的调用链路带来可读性和可维护性,降低迁移重构过程中的摩擦成本。
为了降低“一码多端”迁移过程中的协作成本,降低各端分支逻辑之间的耦合性,我们尝试提出一种BFF的多端开发模式:
通过多端策略模式将不同端的处理逻辑在接口内进行分流,并将一个接口逻辑内原本对下游的整体链路横向拆分为互不耦合的独立逻辑处理实例,让每一个实例仅需关注自身与下游服务的调用关系。且针对不同端的差异处理可以被文件级的分开,很大程度减少了冲突的发生。基于这个架构,多个端侧团队可以高效安全的进行协同开发。
多个调用下游的逻辑实例可以被并行执行,多个并行阶段可以被串行,数据流最终到达装饰器实例被处理后返回给前端。
在携程酒店某二级页面BFF一码多端项目中,采用前文提到的多端架构模式对原本多套BFF应用进行了合并及重构,将原本分散的视图控制逻辑整合收口到了一个BFF应用内,大幅减少了后续的迭代维护成本,提升了研发效率。原本由多个BFF分别来支持多个端,通过多端开发模式重构之后,多个BFF合并成为一个,内部通过多端策略实例来支持各端。原本的过程化代码被横向解耦并拆分成可复用的数据组件被这多个策略分别组合调用,最后再通过视图模型变化映射成统一的视图模型返回给各端。
在应用代码重构的同时,携程酒店BFF团队与携程公共平台团队合作,在他们的支持与帮助下实现了BFF的云函数化。
三、云函数平台
携程Node.js云函数平台从2021年正式立项已开始第四个年头。俱备轻量化的运行时,中间件能力及其他丰富的函数中间件接入能力。
轻量化运行时
云函数的轻量化主要体现在基础镜像、业务代码、发布运维三个方面:
- 云函数的基础镜像相比传统应用更加轻量化,在最新迭代的函数镜像中仅包含微型系统、js运行时、函数运行时。相比此前的最小化镜像大小减少59%,这有利于函数平台更快速的拉起函数镜像和实例。
- 云函数的代码结构相比传统应用更为简化,只需在定义json中定义函数入口。函数内置的运行时将启动一个监听服务器,将必要的请求负载交由函数入口文件运行,并将处理后的函数返回以响应负载的形式返回给客户端。
- 函数开发平台提供开箱即用的基础设施建设、管理与运维,帮助用户脱离繁冗的开发配置工作,只需关注业务代码逻辑的编写即可快速上线业务服务。
中间件能力
云函数运行时集成了 Tripcore 核心中间件集,并为云函数自动启用部分基础能力,由云函数接管核心中间件的升级和维护,具有以下优势:
- 自动接入基础能力,无需复杂接入和配置
- 精选常用组件SDK,无需安装,开箱即用
- 运行时内置,减少项目安装和构建时间
- 定期跟进维护组件升级,保障核心功能稳定性
函数中间件
由于函数代码结构和传统应用有较大差异,为了提升传统应用迁移到云函数的效率,框架团队设计了函数中间件机制,提供了将传统的Web应用快速接入到云函数的能力。
使用@ctrip/serverless-app模块可以将常见的Express、Koa、Nest.js、Egg.js、NFES、Remix、GraphQL等Web框架的应用使用云函数进行部署,而无需对应用代码进行大刀阔斧的改动。
3.1 函数能力
触发器
云函数通过触发器提供给外部服务进行调用。在部署函数时即可生成函数URL,用户可以直接使用函数URL访问云函数进行测试。此外云函数平台还提供了定时触发器、QMQ消息队列触发器、SLB触发器、SOA触发器满足多种业务场景需求。
层管理
层提供了一种全新的管理依赖和静态文件的方法。通过将不经常变更的依赖库打包成层,可以减少部署镜像的大小,并加快代码的部署速度。目前在云函数平台上提供了AlmaLinux的常用运维工具层、Puppeteer和Playwright层、FFCreator层等,并提供相关能力的解决方案,帮助开发人员快速上线业务功能,而无需过分关注这些依赖库在Linux上运行的各种问题。
弹性扩缩
云函数产品支持快速弹性伸缩能力,能帮助业务提升资源利用率,在业务流量高峰时,业务的计算能力、容量自动扩容,承载更多的用户请求,而在业务流量下降时,所使用的资源也能同时收缩,避免资源浪费。
云函数平台支持基于RPS和并发度指标进行弹性扩缩,相比传统应用只能按照QPM指标和CPU指标进行分钟级扩容,云函数平台依靠底层流量监测组件,对流量变化的响应时间最快达到秒级,可以最大限度提升资源利用率。云函数平台支持最小可以将实例缩容到零,在请求到达时,云函数平台挂起请求,并实时拉起函数,待函数实例就绪后再将请求发送给函数实例进行处理。这对于一些不需要一直运行的函数或者对响应时间不敏感的非业务核心应用来说,可以最大化节省资源。
版本和流量切换
- 云函数使用蓝绿部署帮助业务提升系统可用性。蓝绿部署时,并不停止掉老版本,而是直接部署一套新版本,等新版本运行起来后,再将流量切换到新版本上。使用蓝绿部署可以更平滑地进行流量切换。
- 云函数支持快速更改堡垒流量指向,便于开发测试人员在上线前进行堡垒验证。在正式发布前,通过堡垒测试工具验证和观察预发布版本的各项业务与性能指标。
- 云函数支持灰度发布,通过预先设置的灰度批次和流量比例,云函数可以实现无人值守的灰度发布,在监控到发布产生异常告警时可自动执行发布刹车并通知开发人员进行排查。
- 云函数支持多集群部署,通过部署在不同地域和机房实现容灾。在单个集群因演练、发布或其他原因导致故障时,可在函数平台进入流量切换页面更改流量指向。
微型实例
云函数平台的实例规格最小可以到0.125C和128MB内存,可以根据实际业务需求指定不同的实例规格。云函数鼓励通过更精细化的资源管理和快速弹性能力相结合来提升资源利用率。
函数场景
云函数提供了丰富的函数场景和代码模板,预先定义好的代码和流水线模板可以使开发人员在创建函数后等待数分钟即可在测试环境创建指定的场景函数。
目前提供的基础场景有 SOA微服务、NFES Web应用函数、QMQ消息队列函数、定时函数等,此外还有酒店、度假、火车票等业务BU提供的定制化函数场景可供选择。
在云平台的函数场景能力支持下,前文提到的基于NestJS的多端BFF框架被标准化为云函数应用模板提供给其他BU开发者使用。
BFF云函数模板框架采用了分层架构风格:
1)最底层为公司基建,包括SOA、Clog、NodeJS基础设施、存储模块等
2)在公司基建之上,通过NestJS模块化组织方式,封装常用NPM PKG,PKG可拓展,方便业务定制;支持Template和脚手架等提效工具,实现开箱即用
3)最上层为业务模块,使用公共模块开发各BU具体API,实现各自业务逻辑;其中多端模块可支持各端共用一套功能接口
使用方在实现业务逻辑的时候可以做到开箱即用,无需重复造轮子对接公共基础设施。
前文提到的某二级页面BFF上云之后在性能方面获得了一定的提升,云函数的基础能力有效提高了BFF应用的资源利用率。
在相同QPS的运行条件下,云函数应用的响应时间,内存占用,冷启动时间都分别得到了不同程度的优化。
3.2 研发流程和函数生态
迭代管理
云函数迭代管理是具有探索性的一种研发流程一体化的实现方式,旨在为研发人员提供更便捷和灵活的研发流程体验。
- 研发流程可视化
将开发流程所涉及的各个节点,以流水线的方式展示给用户,使用户能够清楚地知晓整个开发流程所需要经历的节点。
- 详细的开发引导
在开发过程中,用户可以清晰地知晓当前进度。在开发过程的不同阶段,系统都会提供详细的指导,在遇到发布卡点时可以明确告知用户接下来应该做什么,用户只需按照指引一步一步操作,即可完成整个开发流程。
- 更专一的开发体验
创建 iDev 任务、创建分支、镜像关联 iDev 需求、分支合并等操作由系统接管,用户可以更加专注于研发工作本身。
运维监控和故障诊断
云函数平台提供的监控面板包含系统指标、Node.js运行时指标等。开发人员可以实时观测到各个函数实例的基本情况,也可以通过Web控制台远程登录机器后进行调试。
云函数平台集成Node.js Astro监控面板,提供为Node.js增强的监控数据和能力:
- 实时监测到js应用的运行情况、点火报告
- 查询应用的node_modules依赖、Tripcore依赖、层依赖等
- 支持镜像依赖和仓库提交比对,快速定位因依赖和代码变更引起的问题
- 支持实时的CPU性能分析和内存快照,快速排查性能和内存故障
- 支持在线断点调试,使用开发人员熟悉的调试工具连接到函数实例
- 支持离线调试,将函数镜像拉取到本地进行调试,定位发布环境的问题
Docker化本地开发
由于Node.js 函数内置了运行时和中间件,在本地开发时只能通过单元测试的形式测试业务逻辑。
因此框架团队提供了 Mars – Docker化本地开发工具,Mars利用Docker 容器能力在本地完全模拟了函数实例真实在CI流水线和测试环境的运行情况,
使用Mars可以帮助开发人员更好地在本地进行云函数的开发调试工作。
四、前端动态化能力
在上述的BFF单体应用多端框架能力的基础上,大前端团队还在客户端和BFF微服务群中间设计了动态业务网关。如果说多端框架在应用内进行赋能,提供了支持多端UI差异的能力,这种差异我们可以称之为“接口内逻辑差异”。那么动态网关层则提供了处理“接口间组合差异”的能力。
动态网关层支持:跨应用接口组装裁剪,白名单及灰度发布能力以及场景动态能力,支持针对不同参数维度组合场景来提供定制化的接口组装服务,增强接口服务的灵活性和适配性。
“多端模式” + “动态网关”的组合架构模式,分别在应用内,应用间2个层级来处理多端的视图控制逻辑差异,从架构维度再一次对多端的差异处理进行了解耦。
在携程酒店某一级页面技改项目中,为了能实现“一码多端”的技术验证目标,采用了”一码多端“+“动态网关”的架构方案。目前C&T Web侧共用一套BFF功能接口,通过在动态层对接口进行模块化组装来支持差异化的页面UI数据需求。
改造范围涉及Ctrip H5、小程序、Trip Online、Trip H5(CSR/SSR)共5个终端,实现17个功能模块接口的改造,多端功能模块收口落地BFF层,实现多端一致和复用,提高研发能效。
各平台流量在动态层入口处通过请求参数进行分流到不同的BFF编排逻辑中,动态层会根据编排配置访问BFF接口。
五、One More Thing
前文已经讲到了在“一码一端”到“一码多端”的前端能效变革背景下的BFF整体解决方案:应用内多端+动态网关+云函数能力。
这套方案除了能够用来支持将多个BFF合并为1个之外,还能够提供更强大的UI动态化能力:
结合酒店前端团队正在研发的跨端组件库,能够支持以极少的代码量支持多种用户场景,做到组件的跨场景复用,跨端复用。