当前位置: 首页 > Web前端 > HTML

基于GraphQL的云音乐BFF构建实践

时间:2023-03-27 23:10:39 HTML

图片来自: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结构来描述。这两个数据逻辑上几乎完全等价,我们写一个转换器定义一些从Java类型到GraphQL类型的映射关系来完成转换,最终生成GraphQL需要的类型定义并保存在我们的Git仓库中。在数据图的展示上,我们直接采用了开源库graphql-voyager,并通过一些扩展使其具备了一些开发过程中经常用到的能力,比如字段编辑能力、字段搜索等。基于AST创建LowCodeGraphQL编辑模式类型定义完成后,我们开始思考如何编写GraphQL语句。下图是我们的编辑界面。编辑器实际上使用了LowCode和ProCode的双重模式。大多数时候,开发者只需要过滤字段和配置一些命令形式就可以完成GraphQL语句的编辑。那么我们是如何实现这个效果的呢?GraphQL官方提供了graphiql这个语句编辑器,已经很强大了,提供语法提示、错误检查、语句调试等基础能力。不过为了在团队内部大规模推广,这种使用方式还是比较原始的。我们需要进一步降低开发者的使用成本,提供LowCode编辑模式。这里会提到为什么GraphQL天生适合LowCode编辑模式。我们知道,所有的低代码编辑模式都需要定义一套标准的协议,界面上的大部分操作都可以映射到对协议的操作更改。对于一条GraphQL语句,我们可以很容易的通过官方引擎提供的内置方法获取到它的AST结构,而且这个AST结构非常易读易懂:{song@param(from:"$query.id"){name}}通过调用转换方法,可以得到如下结构:import{parse}from'graphql';constast=解析(查询);对于这部分结构,我们可以和接口建立映射关系,比如当我们通过数据图文档检查字段的时候,其实是生成对应的selection结构,插入到指定路径的selections中.而我们通过表单配置指令时,修改的是对应路径的指令结构。并且由于我们是对AST本身进行操作,开发者也可以自己编写GraphQL语句,对语句的改动也可以在操作面板上体现出来。除了低代码编辑能力,该编辑器还提供了一些辅助功能,可以让GraphQL接口的开发更加流畅和方便,例如:自动生成接口文档:GraphQL查询结果属于数据图的一个子集,所以我们完全可以根据开发者的GraphQL语句,生成响应结构,解析依赖参数,从而自动生成接口文档,让GraphQL接口也能有清晰的定义。追踪请求链接:在线开发最大的难点在于问题定位和调试。为了帮助开发者更容易定位问题,我们对离线环境下GraphQL语句的每一步都进行了管理,包括每一次RPC调用。执行脚本,记录每次操作的输入输出,方便开发者查询后查看完整的请求链接,在请求出错时定位问题。基于指令和脚本加强原生GraphQL能力我们刚刚提到,必须使用GraphQL才能满足至少70%的BFF编排场景。如果我们只使用开源引擎,我们很快就会发现以下问题。第一个问题是,我们如何传递复杂的RPC参数?在实际业务场景中,由于RPC和HTTP接口已经解耦,我们往往需要通过一些逻辑构造来构造RPC需要的参数,比如下面的RPC接口:ClassSearchDto{IntegerpageSize;整数游标;整数用户名;//需要获取登录用户的uesrIdStringsearch;}...SongService.searchSongByUser(SearchDtoparams){...}该接口入参为结构化对象,其他3个参数来自HTTP接口透传的查询参数,需要从请求的cookie中解析出userId。GraphQL提供了一些参数透传的可变机制,但是不够灵活,无法完成上述参数构造。第二个问题,如何对响应结果做更灵活的数据转换?GraphQL的响应结果必须与Schema结构严格一致。虽然我们可以对某些字段进行裁剪和重命名,但是对于不同的页面,我们需要更加灵活的数据转换,这样才能复用同一套Schema来面对更多的场景。以上两个问题的共性是GraphQL默认的DSL表达式很难满足复杂场景的需求。幸运的是,GraphQL提供了一种称为指令的扩展机制。指令可以附加到字段或片段中包含的字段,然后以服务器期望的任何方式更改查询执行。以下是使用GraphQL引擎内置的@skip指令的示例。{song{name@skip(if:true)}}上面指令的意思是当判断条件为真时跳过该字段的查询。GraphQL允许我们自定义指令。我们可以在GraphQL解析器中获取查询语句附带的指令描述,从而修改执行逻辑来完成指令的执行。针对上述问题,我们提供了两个自定义指令。@param指令:传递复杂的RPC参数指令@param(from:Stringdest:StringscriptName:StringscriptMethod:String)@param指令主要在执行查询操作之前运行,负责收集参数来源,并传入多个参数来源。脚本进行处理,处理结果最终传递给RPC参数。其执行流程如下图所示:@convert指令:对响应结果指令进行更灵活的数据转换@convert(from:StringscriptName:StringscriptMethod:String)@convert指令主要在查询操作执行后执行,并负责收集响应结果。也输入到脚本中进行处理,最后返回脚本处理后的结果。其执行过程如下图所示:扩展这两条指令后,开发者可以在查询操作前后插入自定义脚本,构造参数和处理响应结果。我们目前正在实现基于Java的GraphQL引擎,因此脚本语言使用Groovy语法。虽然不是前端同学熟悉的语言,但是处理一些常规的数据转换逻辑还是绰绰有余的。在完成这部分之后,我们真的成功地覆盖了几乎大部分的BFF场景。标准的研发流程控制我们期望GraphQL应用的研发流程应该和普通应用一样。当开发者接到一个需求,他需要在平台上创建迭代,我们会分配分支和环境给它。当他完成测试regression最后,需要做一些卡点。我们将在卡点链接中提供一些语法验证和更改审查。卡点流程结束后,开发者将进入专属线上渠道。完成线上发布和验证后,开发者可以一键将开发分支合并到master中。目前在云音乐,所有的前端应用开发都遵循这样一套流程,极大的保证了我们开发过程的安全性和规范性。对于GraphQL,我们在应用卡片链接中提供了语法校验,基于graphql-language-service-interface提供的getDiagnostics,可以帮助我们快速定位错误位置。consterrList=getDiagnostics(查询,模式);总结最后总结一下,在本文中,我们简单介绍了GraphQL和云音乐落地的背景,介绍了云音乐Febase平台GraphQL研发能力的整体架构设计,一些关键模块的实现思路(数据图结构,低代码GraphQL编辑器),以及GraphQL引擎的扩展设计,GraphQL应用的开发过程控制。后续我们也会考虑分享更多GraphQL引擎的实现细节和GraphQL应用案例。目前,基于GraphQL的BFF研发模式已经在云音乐落地半年左右。期间大前端同学独立制作了160+个数据接口,其中不乏一些大流量的核心场景。当然,对于BFF的研发模式,我们确实还处于初步探索阶段。未来随着GraphQL接口在云音乐业务中的覆盖率越来越高,希望能总结一些设计数据图模型的经验,帮助前后端同学建立更高效的协同关系。本文由网易云音乐技术团队发布。未经授权禁止任何形式的转载。我们常年招聘各种技术岗位。如果你要跳槽,又恰好喜欢云音乐,那就加入我们吧grp.music-fe(at)corp.netease.com!