图片来自:https://bz.zzzmh.cn/本文作者:cgt背景:如何解耦大前端与服务端的适配层依赖当它说到BFF,相信大家不认识都会太陌生。以往,在云音乐中,前后端协同架构一直保持着比较传统的前后端协同模式。每一端所需的接口完全依赖于服务器。服务器上的同学除了完成微服务的业务逻辑外,还需要对前端页面调度的各个领域的微服务进行编排,根据前端的数据需求进行一定程度的组装和适配。.年初,我们计划改版云音乐的P0页面和个人主页。云音乐的个人主页聚合了各个页面字段的数据,比如用户个人信息、Mlog、云圈、卡拉OK等,这些数据来自各个服务端团队。对于前端大同学来说,我们期望通过尽可能少的接口调用来获取这些数据,以保证页面性能。同时,我们希望得到的数据接口与页面UI高度兼容,对端面要求不高。数据转换。所以服务端同学给我们分离了一层独立的中间服务,负责聚合各个业务的接口数据。同时,我们需要每个业务服务器将业务领域的DTO转换成VO,保证能适配UI。在改版过程中,我们发现了这个模型的一些问题:大前端需要的接口的契约定义对服务端有很深的依赖。很多时候,一个页面字段的变化,需要对平台服务器和业务服务器进行评估和排名。期间由于功能差异,中间会产生大量的沟通成本。由于前端UI的可变性,各业务服务器为该场景提供的接口难以复用。一旦更换了其他场景,服务端同学就得封装新的接口了。针对这些问题,我们发现业界其实已经给出了比较成熟的解决方案,即在架构中引入BFF层。BFF的全称是“BackendForFrontend”,顾名思义就是后端给前端。它的主要职责是调度微服务,为页面的数据需求组装适配数据。这部分我们以前是通过微服务来完成的,现在已经从微服务中拆解出来了,获得了独立性。在BFF架构中,我们不再需要平台服务器为我们提供数据聚合,解决了我们之前提到的问题:大前端同学可以开始自己完成这一层的数据组装工作,从而在适配中配合服务端完成分发层的解耦,大部分的字段变更都可以由前端同学闭环完成,不需要大量的沟通成本。服务器端的同学不需要进行DTO到VO的数据转换,这样可以提供一个可复用性更强的接口,微服务的职责也会更清晰。在云音乐中,类似的场景还有很多。我们期望在这些场景中实现BFF架构。最终随着可复用接口的沉淀和通信成本的降低,可以帮助我们提升整体的业务吞吐量。那么,问题来了,如何让前端大佬在大量类似场景下接管BFF层呢?我们为什么选择GraphQL?FaasVSGraphQL目前业界主流的BFF实现有两种。首先,它是基于NodeJS+Faas的形式。这种模式是基于大多数web前端同学对NodeJS有一定的基础,可以快速上手。同时,它的布局非常灵活,基本可以满足BFF的所有需求,甚至可以超越BFF的界限。最初,我们也期望依靠这种模式落地BFF,但很快我们发现了这种模式面临的一些挑战:基础设施要求高:这种模式对团队的Node基础设施和云原生基础设施都有一定的要求。毕竟掌握NodeJS开发是一方面。服务监控、运维、部署、线上问题调试都需要有相应的解决方案,我们需要保证这些保障能够覆盖所有的NodeJS服务。有一定的学习成本:去除Web前端对于原生客户端的同学来说,NodeJS虽然比较轻量级,但也是一门全新的语言。第二种方式就是我们今天要说的基于GraphQL的方式。GraphQL为API定义了一套查询语言。开发者可以通过一些低代码的安排,快速完成查询语言的定义,这给我们带来了以下好处:与技术栈解耦:开发者只需要认识GraphQL的DSL,而不需要学习其他语言,而GraphQL的DSL是相对好用多了。复杂度更可控:我们可以统一实现GraphQL的执行引擎,所有开发者基于我们的引擎服务执行查询。只能自定义数据图和查询语句,这样我们就可以在引擎上附加一些服务开发的最佳实践。什么是GraphQL?好的,那么GraphQL到底是什么?GraphQL一般分为两部分:一组用于API的查询DSL:也称为GraphQL语句,你可以在这组DSL中描述你的查询需要的字段,以及需要调用的接口,参数需要通过等等。基于图形数据的服务端运行时:执行这套查询DSL,其执行逻辑是根据GraphQL语句的描述,从一个完整的数据图中找到需要的节点,调度涉及的接口,最后返回对应的查询语句的数据。例如:在上图所示的案例中,我们在图的左侧写了一条查询语句,图的右侧是引擎执行完查询语句后在数据图上命中的节点。由此可见,实现GraphQL的关键在于实现其服务端运行时,而GraphQL的运行时也可以拆解为三部分:GraphQL引擎:解析GraphQL语句。目前,社区已经提供了各种开源版本的GraphQLEngines,包括NodeJS、Java、Python等,我们可以选择适合自己的版本。类型定义:GraphQL的类型系统其实和其他类型系统类似。GraphQL提供了一些基本标量,你可以在这些基本标量的基础上不断扩展你的业务模型,最终生成图数据结构。解析器:我们需要描述这些类型的节点需要执行的查询。当然,并非所有节点都需要执行查询。我们只需要保证查询结果与节点的类型定义一致即可。比如上图中的节点,我们分别对歌曲节点和专辑节点进行了查询,它们会调用获取歌曲详情和专辑详情的RPC接口返回相应的数据。云音乐如何实现?了解了GraphQL的运行机制后,我们开始思考如何在CloudMusic中实现。在程序设计阶段,我们提出了一些问题:如何让前端大牛搭建一个稳定可靠的GraphQLruntime?大前端同学大多没有服务端开发经验,对服务开发、部署、运维基本一窍不通。从头开始构建GraphQL运行时会带来巨大的运营成本。如何快速上手GraphQL语句?GraphQL语句虽然上手并不复杂,但不在前端同学的知识体系中,上手还是有一定的学习成本的。如何对接云音乐现有的研发体系?如何尽可能地扩展GraphQL的边界?针对前两个问题,我们认为可以用低代码的方式进行GraphQL应用开发。Low-code可以说是目前业界非常流行的。它是一种可以打破功能边界的手段。很多团队使用低代码的方式让服务端的同学具备搭建前端页面的能力。那么反过来想,前端同学也可以具备通过低代码来编排服务端逻辑的能力。考虑到这个方向后,我们发现GraphQL天生就很适合低代码构建。它的DSL设计可以很容易地转换成结构化数据,可以映射成接口操作。对于第三个问题,GraphQL应用应该有和传统云音乐应用一样的发布流程控制,避免乱发布导致上线事故。我们通过Git仓库管理GraphQL应用的语句、类型定义、解析器,并集成到云音乐前端研发平台Febase中,对发布过程进行管控。最后一个问题,我们希望GraphQL能够解决至少70%的BFF编排场景。如果仅仅依靠自身的能力,实现的场景将是有限的,意义不大。扩展以处理更多场景。分布式架构设计我们整体采用分布式架构设计:从流量的角度来看,前端还是通过Restful请求获取页面需要的数据。这样做的目的是我们所有的请求仍然可以依赖云音乐的通用API,网关具有流量控制、异常降级、静态的能力,大大提高了接口的稳定性。当请求经过API网关后,会被转发到GraphQL应用所在的集群。GraphQL应用内置的引擎会将接口URL转化为GraphQL语句,从而执行GraphQL语句,调度服务端RPC接口进行数据组装,最终返回页面所需数据。我们会为每个GraphQL应用分配一个独立的云原生容器。依托云音乐的云原生基础设施,我们可以灵活安排每个GraphQL应用所需的Pod数量,甚至可以根据CPU进行扩容,从而减轻前端同学的运维负担。在Febase平台上,我们提供了低代码的GraphQL编辑器、编写Groovy脚本的能力、发布过程的控制以及可视化数据图的能力。最后,基于这些能力,平台可以输出一个GraphQL应用配置。内容包括:URL到GraphQL语句的映射关系。执行查询语句所需的数据图中的查询节点的解析器配置引擎通过监控zookeeper获取该配置并更新。这个过程就是GraphQL应用的部署过程。因为整个部署过程不涉及服务的重启,只是配置文件的热更新,所以它的日常发布会很快,几秒就能完成,进一步提高了我们的研发效率.除了这些基础能力,我们还对接了云音乐的一些基础平台。例如:通过Mock平台,我们允许开发者自由配置接口的Mock数据,只需要在请求头中添加一个flag,让请求走Mock链接即可。所有GraphQL语句、数据图和脚本都将保存在Gitlab中,通过分支进行管理和编辑。云音乐的合约管理平台为我们提供了Java服务端RPC数据模型,让我们可以几乎零成本的构建数据图。基于Serverless部署应用容器,保证了我们的服务可以灵活伸缩。打通了性能、日志等多种服务监控平台,具备完备的服务运维能力。基于契约快速构建GraphQLSchema了解了我们的整体架构之后,我们继续看看Febase是如何以近乎零成本构建GraphQL数据图的。下面是一个非常简单的数据图的构建过程。GraphQL构建数据图的方式是从根节点开始,进入字段和字段对应的model,我们可以在任意model下插入一个新的字段,并定义字段的model。在插入字段时,我们需要定义该字段对应的解析器,即如何获取该字段对应的数据。我们发现在传统的GraphQL数据图布局中,开发者需要定义自己的模型和解析器。事实上,大多数时候,这个过程只是在移动服务器端模型定义。所以这里我们同意解析器做的只是调用服务端的RPC接口,这样只要开发者选择了RPC接口,我们就可以根据响应的元信息拉取服务端的数据模型,从而构建数据图。比如上面的例子,当我们要导入song字段时,系统实际上在仓库的约定路径下创建了两个文件:resolver.json:描述了引擎如何调用接口,比如类名RPC接口、方法名等type.schema:保存根据接口响应生成的GraphQL模型信息。下面是最简单的resolver.json例子:{"type":"rpc",//调用的协议类型"clzName":"com.netease.music.api.SongService",//RPC类名"methodName"":"getSongById",//RPC方法"params":[]//RPC参数类型列表}type.schema其实就是GraphQL的模型定义:typeQuery{song:Song}typeSong{id:IDname:String那么,我们如何生成这个模型信息呢?通过研究GraphQL引擎的源码,我们发现通过官方引擎提供的内置方法,可以将GraphQL的模型定义等价转换为标准的JSON结构。那么,相比于生成模型定义的源代码,生成这个JSON结构要简单的多。比如上面提到的Song模型可以这样转换:import{introspectionFromSchema,buildSchema}from'graphql';constschema=introspectionFromSchema(buildSchema(schema));转换后可以生成如下结构:在云音乐中,所有服务端的接口模型定义都会维护在云音乐的合约管理平台上,同样有一个JSON结构来描述。
