共计 15248 个字符,预计需要花费 39 分钟才能阅读完成。
GraphQL 是 Facebook 提出的一种数据查询语言,外围个性是数据聚合和按需索取,目前被广泛应用于前后端之间,解决客户端灵便应用数据问题。本文介绍的是 GraphQL 的另一种实际,咱们将 GraphQL 下沉至后端 BFF 层之下,联合元数据技术,实现数据和加工逻辑的按需查问和执行。这样不仅解决了后端 BFF 层灵便应用数据的问题,这些字段加工逻辑还能够间接复用,大幅度晋升了研发的效率。本文介绍的实际计划曾经在美团局部业务场景中落地,并获得不错成果,心愿这些教训可能对大家有帮忙。
1 BFF 的由来
BFF 一词来自 Sam Newman 的一篇博文《Pattern:Backends For Frontends》,指的是服务于前端的后端。BFF 是解决什么问题的呢?据原文形容,随着挪动互联网的衰亡,原适应于桌面 Web 的服务端性能心愿同时提供给挪动 App 应用,而在这个过程中存在这样的问题:
- 挪动 App 和桌面 Web 在 UI 局部存在差别。
- 挪动 App 波及不同的端,不仅有 iOS、还有 Android,这些不同端的 UI 之间存在差别。
- 原有后端性能和桌面 Web UI 之间曾经存在了较大的耦合。
因为端的差异性存在,服务端的性能要针对端的差别进行适配和裁剪,而服务端的业务性能自身是绝对繁多的,这就产生了一个矛盾——服务端的繁多业务性能和端的差异性诉求之间的矛盾。那么这个问题怎么解决呢?这也是文章的副标题所形容的 ”Single-purpose Edge Services for UIs and external parties”,引入 BFF,由 BFF 来针对多端差别做适配,这也是目前业界宽泛应用的一种模式。
在理论业务的实际中,导致这种端差异性的起因有很多,有技术的起因,也有业务的起因。比方,用户的客户端是 Android 还是 iOS,是大屏还是小屏,是什么版本。再比方,业务属于哪个行业,产品状态是什么,性能投放在什么场景,面向的用户群体是谁等等。这些因素都会带来面向端的性能逻辑的差异性。
在这个问题上,笔者所在团队负责的商品展现业务有肯定的发言权,同样的商品业务,在 C 端的展现性能逻辑,粗浅受到商品类型、所在行业、交易状态、投放场合、面向群体等因素的影响。同时,面向消费者端的性能频繁迭代的属性,更是加剧并深入了这种矛盾,使其演化成了一种服务端繁多稳固与端的差别灵便之间的矛盾,这也是商品展现(商品展现 BFF)业务零碎存在的偶然性起因。本文次要在美团到店商品展现场景的背景下,介绍面临的一些问题及解决思路。
2 BFF 背景下的外围矛盾
BFF 这层的引入是解决服务端繁多稳固与端的差别灵便诉求之间的矛盾,这个矛盾并不是不存在,而是转移了。由原来后端和前端之间的矛盾转移成了 BFF 和前端之间的矛盾。笔者所在团队的次要工作,就是和这种矛盾作奋斗。上面以具体的业务场景为例,联合以后的业务特点,阐明在 BFF 的生产模式下,咱们所面临的具体问题。下图是两个不同行业的团购货架展现模块,这两个模块咱们认为是两个商品的展现场景,它们是两套独立定义的产品逻辑,并且会各自迭代。
在业务倒退初期,这样的场景不多。BFF 层零碎“烟囱式”建设,性能疾速开发上线满足业务的诉求,在这样的状况下,这种矛盾体现的不显著。而随着业务倒退,行业的开辟,造成了许许多多这样的商品展现性能,矛盾逐步加剧,次要体现在以下两个方面:
- 业务撑持效率:随着商品展现场景变得越来越多,API 呈爆炸趋势,业务撑持效率和人力成线性关系,零碎能力难以撑持业务场景的规模化拓展。
- 零碎复杂度高:外围性能继续迭代,外部逻辑充斥着
if…else…
,代码过程式编写,零碎复杂度较高,难以批改和保护。
那么这些问题是怎么产生的呢?这要联合“烟囱式”零碎建设的背景和商品展现场景所面临的业务,以及零碎特点来进行了解。
特点一:内部依赖多、场景间取数存在差别、用户体验要求高
图例展现了两个不同行业的团购货架模块,这样一个看似不大的模块,后端在 BFF 层要调用 20 个以上的上游服务能力把数据拿全,这是其一。在下面两个不同的场景中,须要的数据源汇合存在差别,而且这种差别普遍存在,这是其二,比方足疗团购货架须要的某个数据源,在丽人团购货架上不须要,丽人团购货架须要的某个数据源,足疗团购货架不须要。只管依赖上游服务多,同时还要保障 C 端的用户体验,这是其三。
这几个特点给技术带来了不小的难题:1)聚合大小难管制,聚合性能是分场景建设?还是对立建设?如果分场景建设,必然存在不同场景反复编写相似聚合逻辑的问题。如果对立建设,那么一个大而全的数据聚合中必然会存在有效的调用。2)聚合逻辑的复杂性管制问题,在这么多的数据源的状况下,不仅要思考业务逻辑怎么写,还要思考异步调用的编排,在代码复杂度未能良好管制的状况下,后续聚合的变更批改将会是一个难题。
特点二:展现逻辑多、场景之间存在差别,共性共性逻辑耦合
咱们能够显著地辨认某一类场景的逻辑是存在共性的,比方团单相干的展现场景。直观能够看出基本上都是展现团单维度的信息,但这只是表象。实际上在模块的生成过程中存在诸多的差别,比方以下两种差别:
- 字段拼接逻辑差别:比方以上图中两个团购货架的团购题目为例,同样是题目,在丽人团购货架中的展现规定是:[类型] + 团购题目 ,而在足疗团购货架的展现规定是: 团购题目。
- 排序过滤逻辑差别:比方同样是团单列表,A 场景依照销量倒排序,B 场景依照价格排序,不同场景的排序逻辑不同。
诸如此类的 展现逻辑 的差异性还有很多。相似的场景实际上在外部存在很多差别的逻辑,后端如何应答这种差异性是一个难题,上面是最常见的一种写法,通过读取具体的条件字段来做判断实现逻辑路由,如下所示:
if(category == "丽人") {title = "[" + category + "]" + productTitle;
} else if (category == "足疗") {title = productTitle;}
这种计划在性能实现方面没有问题,也可能复用独特的逻辑。然而实际上在场景十分多的状况下,将会有十分多的差异性判断逻辑叠加在一起,性能始终会被继续迭代的状况下,能够设想,零碎将会变得越来越简单,越来越难以批改和保护。
总结 :在 BFF 这层,不同商品展现场景存在差别。在业务倒退初期,零碎通过独立建设的形式反对业务疾速试错,在这种状况下,业务差异性带来的问题不显著。而随着业务的一直倒退,须要搭建及经营的场景越来越多,呈规模化趋势。此时,业务对技术效率提出了更高的要求。 在这种场景多、场景间存在差别的背景下,如何满足场景拓展效率同时可能控制系统的复杂性,就是咱们业务场景中面临的外围问题。
3 BFF 利用模式分析
目前业界针对此类的解决方案次要有两种模式,一种是后端 BFF 模式,另一种是前端 BFF 模式。
3.1 后端 BFF 模式
后端 BFF 模式指的是 BFF 由后端同学负责,这种模式目前最宽泛的实际是基于 GraphQL 搭建的后端 BFF 计划,具体是:后端将展现字段封装成展现服务,通过 GraphQL 编排之后裸露给前端应用。如下图所示:
这种模式最大的个性和劣势是,当展现字段曾经存在的状况下,后端不须要关怀前端差异性需要,按需查问的能力由 GraphQL 反对。这个个性能够很好地应答不同场景存在展现字段差异性这个问题,前端间接基于 GraphQL 按需查问数据即可,后端不须要变更。同时,借助 GraphQL 的编排和聚合查问能力,后端能够将逻辑合成在不同的展现服务中,因而在肯定水平上可能化解 BFF 这层的复杂性。
然而基于这种模式,依然存在几个问题:展现服务颗粒度问题、数据图划分问题以及字段扩散问题,下图是基于以后模式的具体案例:
1)展现服务颗粒度设计问题
这种计划要求展现逻辑和取数逻辑封装在一个模块中,造成一个展现服务(Presentation Service),如上图所示。而实际上展现逻辑和取数逻辑是多对多的关系,还是以前文提到的例子阐明:
背景 :有两个展现服务,别离封装了商品题目和商品标签的查问能力。
情景 :此时 PM 提了一个需要,心愿商品在某个场景的题目以“[类型]+ 商品题目”的模式展现,此时商品题目的拼接依赖类型数据,而此时类型数据商品标签展现服务中曾经调用了。
问题:商品题目展现服务本人调用类型数据还是将两个展现服务合并到一起?
以上形容的问题的是展现服务颗粒度把控的问题,咱们能够狐疑上述的示例是不是因为展现服务的颗粒度过小?那么反过来看一看,如果将两个服务合并到一起,那么势必又会存在冗余。这是展现服务设计的难点,外围起因在于,展现逻辑和取数逻辑自身是多对多的关系,后果却被设计放在了一起。
2)数据图划分问题
通过 GraphQL 将多个展现服务的数据聚合到一张图(GraphQL Schema)中,造成一个数据视图,须要数据的时候只有数据在图中,就能够基于 Query 按需查问。那么问题来了,这个图应该怎么组织?是一张图还是多张图?图过大的话,势必带来简单的数据关系保护问题,图过小则将会升高计划自身的价值。
3)展现服务外部复杂性 + 模型扩散问题
上文提到过一个商品题目的展现存在不同拼接逻辑的状况,在商品展现场景,这种逻辑特地广泛。比方同样是价格,A 行业展现优惠后价格,B 行业展现优惠前价格;同样是标签地位,C 行业展现服务时长,而 D 行业展现商品个性等。那么问题来了,展现模型如何设计?以题目字段为例,是在展现模型上放个 title
字段就能够,还是别离放个 title
和titleWithCategory
?如果是前者那么服务外部必然会存在 if…else…
这种逻辑,用于辨别 title
的拼接形式,这同样会导致展现服务外部的复杂性。如果是多个字段,那么能够设想,展现服务的模型字段也将会一直扩散。
总结:后端 BFF 模式可能在肯定水平上化解后端逻辑的复杂性,同时提供一个展现字段的复用机制。然而依然存在未决问题,如展现服务的颗粒度设计问题,数据图的划分问题,以及展现服务外部的复杂性和字段扩散问题。目前这种模式实际的代表有 Facebook、爱彼迎、eBay、爱奇艺、携程、去哪儿等等。
3.2 前端 BFF 模式
前端 BFF 模式在 Sam Newman 的文章中的 ”And Autonomy” 局部有特地的介绍,指的是 BFF 自身由前端团队本人负责,如下示意图所示:
这种模式的理念是,原本能一个团队交付的需要,没必要拆成两个团队,两个团队自身带来较大的沟通合作老本。实质上,也是一种将“敌我矛盾”转化为“人民内部矛盾”的思路。前端齐全接手 BFF 的开发工作,实现数据查问的自力更生,大大减少了前后端的合作老本。然而这种模式没有提到咱们关怀的一些外围问题,如:复杂性如何应答、差异性如何应答、展现模型如何设计等等问题。除此之外,这种模式也存在一些前提条件及弊病,比方较为齐备的前端基础设施;前端不仅仅须要关怀渲染、还须要理解业务逻辑等。
总结:前端 BFF 模式通过前端自主查问和应用数据,从而达到升高跨团队合作的老本,晋升 BFF 研发效率的成果。目前这种模式的实际代表是阿里巴巴。
4 基于 GraphQL 及元数据的信息聚合架构设计
4.1 整体思路
通过对后端 BFF 和前端 BFF 两种模式的剖析,咱们最终抉择后端 BFF 模式,前端 BFF 这个计划对目前的研发模式影响较大,不仅须要大量的前端资源,而且须要建设欠缺的前端基础设施,计划施行老本比拟昂扬。
前文提到的后端 GraphQL BFF 模式代入咱们的具体场景尽管存在一些问题,然而总体有十分大的参考价值,比方展现字段的复用思路、数据的按需查问思路等等。在商品展现场景中,有 80% 的工作集中在数据的聚合和集成局部 ,并且这部分具备很强的复用价值,因而信息的查问和聚合是咱们面临的主要矛盾。因而,咱们的思路是: 基于 GraphQL+ 后端 BFF 计划改良,实现取数逻辑和展现逻辑的可积淀、可组合、可复用,整体架构如下示意图所示:
从上图可看出,与传统 GraphQL BFF 计划最大的差异在于咱们将 GraphQL 下放至数据聚合局部,因为数据来源于商品畛域,畛域是绝对稳固的,因而数据图规模可控且绝对稳固。除此之外,整体架构的外围设计还包含以下三个方面:1)取数展现拆散;2)查问模型归一;3)元数据驱动架构。
咱们通过取数展现拆散解决展现服务颗粒度问题,同时使得展现逻辑和取数逻辑可积淀、可复用;通过查问模型归一化设计解决展现字段扩散的问题;通过元数据驱动架构实现能力的可视化,业务组件编排执行的自动化,这可能让业务开发同学聚焦于业务逻辑的自身。上面将针对这三个局部逐个开展介绍。
4.2 外围设计
4.2.1 取数展现拆散
上文提到,在商品展现场景中,展现逻辑和取数逻辑是多对多的关系,而传统的基于 GraphQL 的后端 BFF 实际计划把它们封装在一起,这是导致展现服务颗粒度难以设计的根本原因。思考一下取数逻辑和展现逻辑的关注点是什么?取数逻辑关注怎么查问和聚合数据,而展现逻辑关注怎么加工生成须要的展现字段,它们的关注点不一样,放在一起也会减少展现服务的复杂性。因而,咱们的思路是将取数逻辑和展现逻辑拆散开来,独自封装成逻辑单元,别离叫取数单元和展现单元。在取数展现拆散之后,GraphQL 也随之下沉,用于实现数据的按需聚合,如下图所示:
那么取数和展现逻辑的封装颗粒度是怎么样的呢?不能太小也不能太大,在颗粒度的设计上,咱们有两个外围考量:1)复用 ,展现逻辑和取数逻辑在商品展现场景中,都是能够被复用的资产,咱们心愿它们能积淀下来,被独自按需应用;2) 简略,放弃简略,这样容易批改和保护。基于这两点思考,颗粒度的定义如下:
- 取数单元:尽量只封装 1 个内部数据源,同时负责对外部数据源返回的模型进行简化,这部分生成的模型咱们称之为取数模型。
- 展现单元:尽量只封装 1 个展现字段的加工逻辑。
离开的益处是简略且可被组合应用,那么具体如何实现组合应用呢?咱们的思路是通过元数据来形容它们之间的关系,基于元数据由对立的执行框架来关联运行,具体设计下文会开展介绍。通过取数和展现的拆散,元数据的关联和运行时的组合调用,能够放弃逻辑单元的简略,同时又满足复用诉求,这也很好地解决了传统计划中存在的 展现服务的颗粒度问题。
4.2.2 查问模型归一
展现单元的加工后果通过什么样的接口透出呢?接下来,咱们介绍一下查问接口设计的问题。
1)查问接口设计的难点
常见查问接口的设计模式有以下两种:
- 强类型模式:强类型模式指的是查问接口返回的是 POJO 对象,每一个查问后果对应 POJO 中的一个明确的具备特定业务含意的字段。
- 弱类型模式:弱类型模式指的是查问后果以 K - V 或 JSON 模式返回,没有明确的动态字段。
以上两种模式在业界都有广泛应用,且它们都有明确的优缺点。强类型模式对开发者敌对,然而业务是一直迭代的,与此同时,零碎积淀的展现单元会不断丰富,在这样的状况下,接口返回的 DTO 中的字段将会愈来愈多,每次新性能的反对,都要随同着接口查问模型的批改,JAR 版本的降级。而 JAR 的降级波及数据提供方和数据生产两方,存在显著效率问题。另外,能够设想,查问模型的一直迭代,最终将会包含成千盈百个字段,难以保护。
而弱类型模式恰好能够补救这一毛病,然而弱类型模式对于开发者来说十分不敌对,接口查问模型中有哪些查问后果对于开发者来说在开发的过程中齐全没有感觉,然而程序员的本能就是喜爱通过代码去了解逻辑,而非配置和文档。其实,这两种接口设计模式都存在着一个共性问题——短少形象,上面两节,咱们将介绍在接口返回的查问模型设计方面的形象思路及框架能力反对。
2)查问模型归一化设计
回到商品展现场景中,一个展现字段有多种不同的实现,如商品题目的两种不同实现形式:1)商品题目;2)[类目]+ 商品题目。商品题目和这两种展现逻辑的关系实质上是一种形象 - 具体的关系。辨认这个关键点,思路就明了了,咱们的思路是对查问模型做形象。查问模型上都是形象的展现字段,一个展现字段对应多个展现单元,如下图所示:
在实现层面同样基于元数据形容展现字段和展现单元之间的关系,基于以上的设计思路,能够在肯定水平上减缓模型的扩散,然而还不能防止扩大。比方除了价格、库存、销量等每个商品都有的规范属性之外,不同的商品类型个别还会有这个商品特有的属性。比方密室主题拼场商品才有“几人拼”这样的形容属性,这种字段自身形象的意义不大,且放在商品查问模型中作为一个独自的字段会导致模型扩张,针对这类问题,咱们的解决思路是引入扩大属性,扩大属性专门承载这类非标准的字段。通过规范字段 + 扩大属性的形式建设查问模型,可能较好地解决 字段扩散 的问题。
4.2.3 元数据驱动架构
到目前为止,咱们定义了如何合成 业务逻辑单元 以及如何设计 查问模型,并提到用元数据形容它们之间的关系。基于以上定义实现的业务逻辑及模型,都具备很强的复用价值,能够作为业务资产积淀下来。那么,为什么用元数据形容业务性能及模型之间的关系呢?
咱们引入元数据形容次要有两个目标:1)代码逻辑的主动编排,通过元数据形容业务逻辑之间的关联关系,运行时能够主动基于元数据实现逻辑之间的关联执行,从而能够打消大量的人工逻辑编排代码;2)业务性能的可视化,元数据自身形容了业务逻辑所提供的性能,如上面两个示例:
团单根底售价字符串展现,例:30 元。
团单市场价展现字段,例:100 元。
这些元数据上报到零碎中,能够用于展现以后零碎所提供的性能。通过元数据形容组件及组件之间关联关系,通过框架解析元数据主动进行业务组件的调用执行,造成了如下的元数据架构:
整体架构由三个外围局部组成:
- 业务能力:规范的业务逻辑单元,包含取数单元、展现单元和查问模型,这些都是要害的可复用资产。
- 元数据:形容业务性能(如:展现单元、取数单元)以及业务性能之间的关联关系,比方展现单元依赖的数据,展现单元映射的展现字段等。
- 执行引擎:负责生产元数据,并基于元数据对业务逻辑进行调度和执行。
通过以上三个局部有机的组合在一起,造成了一个元数据驱动格调的架构。
5 针对 GraphQL 的优化实际
5.1 应用简化
1)GraphQL 间接应用问题
引入 GraphQL,会引入一些额定的复杂性,比方会波及到 GraphQL 带来的一些概念如:Schema、RuntimeWiring,上面是基于 GraphQL 原生 Java 框架的开发过程:
这些概念对于未接触过 GraphQL 的同学来说,减少了学习和了解的老本,而这些概念和业务畛域通常没有什么关系。而咱们仅仅心愿应用 GraphQL 的按需查问个性,却被 GraphQL 自身连累了,业务开发同学的关注点应该聚焦在业务逻辑自身才对,这个问题如何解决呢?
驰名计算机科学家 David Wheeler 说了一句名言,”All problems in computer science can be solved by another level of indirection”。没有加一层解决不了的问题,实质上是须要有人来对这事负责,因而咱们在原生 GraphQL 之上减少了一层执行引擎层来解决这些问题,指标是屏蔽 GraphQL 的复杂性,让开发人员只须要关注业务逻辑。
2)取数接口标准化
首先要简化数据的接入,原生的 DataFetcher
和DataLoader
都是处在一个比拟高的抽象层次,短少业务语义,而在查问场景,咱们可能演绎出,所有的查问都属于以下三种模式:
- 1 查 1 :依据一个条件查问一个后果。
- 1 查 N :依据一个条件查问多个后果。
- N 查 N :一查一或一查多的批量版本。
由此,咱们对查问接口进行了标准化,业务开发同学基于场景判断是那种,按需抉择应用即可,取数接口标准化设计如下:
业务开发同学按需抉择所须要应用的取数器,通过泛型指定后果类型,1 查 1 和 1 查 N 比较简单,N 查 N 咱们对其定义为批量查问接口,用于满足 ”N+1″ 的场景,其中 batchSize
字段用于指定分片大小,batchKey
用于指定查问 Key,业务开发只须要指定参数,其余的框架会主动解决。除此之外,咱们还束缚了返回后果必须是CompleteFuture
,用于满足聚合查问的全链路异步化。
3)聚合编排自动化
取数接口标准化使得数据源的语义更清晰,开发过程按需抉择即可,简化了业务的开发。然而此时业务开发同学写好 Fetcher
之后,还须要去另一个中央去写 Schema
,而且写完Schema
还要再写 Schema
和Fetcher
的映射关系,业务开发更享受写代码的过程,不太违心写完代码还要去另外一个中央取配置,并且同时保护代码和对应配置也进步了出错的可能性,是否将这些繁杂的步骤移除掉?
Schema
和 RuntimeWiring
实质上是想形容某些信息,如果这些信息换一种形式形容是不是也能够,咱们的优化思路是:在业务开发过程中标记注解,通过注解标注的元数据形容这些信息,其余的事件交给框架来做。解决思路示意图如下:
5.2 性能优化
5.2.1 GraphQL 性能问题
尽管 GraphQL 曾经开源了,然而 Facebook 只开源了相干规范,并没有给出解决方案。GraphQL-Java 框架是由社区奉献的,基于开源的 GraphQL-Java 作为按需查问引擎的计划,咱们发现了 GraphQL 利用方面的一些问题,这些问题有局部是因为应用姿态不当所导致的,也有局部是 GraphQL 自身实现的问题,比方咱们遇到的几个典型的问题:
- 耗 CPU 的查问解析,包含
Schema
的解析和Query
的解析。 - 当查问模型比较复杂特地是存在大列表时候的延时问题。
- 基于反射的模型转换 CPU 耗费问题。
DataLoader
的层级调度问题。
于是,咱们对应用形式和框架做了一些优化与革新,以解决下面列举的问题。本章着重介绍咱们在 GraphQL-Java 方面的优化和革新思路。
5.2.2 GraphQL 编译优化
1)GraphQL 语言原理概述
GraphQL 是一种查询语言,目标是基于直观和灵便的语法构建客户端应用程序,用于形容其数据需要和交互。GraphQL 属于一种畛域特定语言(DSL),而咱们所应用的 GraphQL-Java 客户端在语言编译层面是基于 ANTLR 4 实现的,ANTLR 4 是一种基于 Java 编写的语言定义和辨认工具,ANTLR 是一种元语言(Meta-Language),它们的关系如下:
GraphQL 执行引擎所承受的 Schema
及Query
都是基于 GraphQL 定义的语言所表白的内容,GraphQL 执行引擎不能间接了解 GraphQL,在执行之前必须由 GraphQL 编译器翻译成 GraphQL 执行引擎可了解的文档对象。而 GraphQL 编译器是基于 Java 的,教训表明在大流量场景实时解释的状况下,这部分代码将会成为 CPU 热点,而且还占用响应提早,Schema
或 Query
越简单,性能损耗越显著。
2)Schema 及 Query 编译缓存
Schema
表白的是数据视图和取数模型同构,绝对稳固,个数也不多,在咱们的业务场景一个服务也就一个。因而,咱们的做法是在启动的时候就将基于 Schema
结构的 GraphQL 执行引擎结构好,作为单例缓存下来,对于 Query
来说,每个场景的 Query
有些差别,因而 Query
的解析后果不能作为单例,咱们的做法是实现 PreparsedDocumentProvider
接口,基于 Query
作为 Key 将 Query
编译后果缓存下来。如下图所示:
5.2.3 GraphQL 执行引擎优化
1)GraphQL 执行机制及问题
咱们先一起理解一下 GraphQL-Java 执行引擎的运行机制是怎么样的。假如在执行策略上咱们选取的是AsyncExecutionStrategy
,来看看 GraphQL 执行引擎的执行过程:
以上时序图做了些简化,去除了一些与重点无关的信息,AsyncExecutionStrategy
的 execute
办法是对象执行策略的异步化模式实现,是查问执行的终点,也是根节点查问的入口,AsyncExecutionStrategy
对对象的多个字段的查问逻辑,采取的是循环 + 异步化的实现形式,咱们从 AsyncExecutionStrategy
的execute
办法触发,了解 GraphQL 查问过程如下:
- 调用以后字段所绑定的
DataFetcher
的get
办法,如果字段没有绑定DataFetcher
,则通过默认的PropertyDataFetcher
查问字段,PropertyDataFetcher
的实现是基于反射从源对象中读取查问字段。 - 将从
DataFetcher
查问失去后果包装成CompletableFuture
,如果后果自身是CompletableFuture
,那么不会包装。 -
后果
CompletableFuture
实现之后,调用completeValue
,基于后果类型别离解决。- 如果查问后果是列表类型,那么会对列表类型进行遍历,针对每个元素在递归执行
completeValue
。 - 如果后果类型是对象类型,那么会对对象执行
execute
,又回到了终点,也就是AsyncExecutionStrategy 的 execute
。
- 如果查问后果是列表类型,那么会对列表类型进行遍历,针对每个元素在递归执行
以上是 GraphQL 的执行过程,这个过程有什么问题呢?上面基于图上的标记程序一起看看 GraphQL 在咱们的业务场景中利用和实际所遇到的问题,这些问题不代表在其余场景也是问题,仅供参考:
问题 1 :PropertyDataFetcher
CPU 热点问题,PropertyDataFetcher
在整个查问过程中属于热点代码,而其自身的实现也有一些优化空间,在运行时 PropertyDataFetcher
的执行会成为 CPU 热点。(具体问题可参考 GitHub 上的 commit 和 Conversion:https://github.com/graphql-java/graphql-java/pull/1815)
问题 2 :列表的计算耗时问题,列表计算是循环的,对于查问后果中存在大列表的场景,此时循环会造成整体查问显著的提早。咱们举个具体的例子,假如查问后果中存在一个列表大小是 1000,每个元素的解决是 0.01ms,那么总体耗时就是 10ms,基于 GraphQL 的查机制,这个 10ms 会阻塞整个链路。
2)类型转换优化
通过 GraphQL 查问引擎拿到的 GraphQL 模型,和业务实现的 DataFetcher
返回的取数模型是同构,然而所有字段的类型都会被转换成 GraphQL 外部类型。PropertyDataFetcher
之所以会成为 CPU 热点,问题就在于这个模型转换过程,业务定义的模型到 GraphQL 类型模型转换过程示意图如下图所示:
当查问后果模型中的字段十分多的时候,比方上万个,意味着每次查问有上万次的 PropertyDataFetcher
操作,理论就反映到了 CPU 热点问题上,这个问题咱们的解决思路是放弃原有业务模型不变,将非 PropertyDataFetcher
查问的后果反过来填充到业务模型上。如下示意图所示:
基于这个思路,咱们通过 GraphQL 执行引擎拿到的后果就是业务 Fetcher
返回的对象模型,这样不仅仅解决了因字段反射转换带来的 CPU 热点问题,同时对于业务开发来说减少了敌对性。因为 GraphQL 模型相似 JSON 模型,这种模型是短少业务类型的,业务开发间接应用起来十分麻烦。以上优化在一个场景上试点测试,结果显示该场景的均匀响应工夫缩短 1.457ms,均匀 99 线缩短 5.82ms,均匀 CPU 利用率升高约 12%。
3)列表计算优化
当列表元素比拟多的时候,默认的单线程遍历列表元素计算的形式所带来的提早耗费非常明显,对于响应工夫比拟敏感的场景这个提早优化很有必要。针对这个问题咱们的解决思路是充分利用 CPU 多外围计算的能力,将列表拆分成工作,通过多线程并行执行,实现机制如下:
5.2.4 GraphQL-DataLoader 调度优化
1)DataLoader 基本原理
先简略介绍一下 DataLoader 的基本原理,DataLoader 有两个办法,一个是load
,一个是dispatch
,在解决 N + 1 问题的场景中,DataLoader 是这么用的:
整体分为 2 个阶段,第一个阶段调用 load
,调用 N 次,第二个阶段调用dispatch
,调用dispatch
的时候会真正的执行数据查问,从而达到批量查问 + 分片的成果。
2)DataLoader 调度问题
GraphQL-Java 对 DataLoader 的集成反对的实现在 FieldLevelTrackingApproach
中,FieldLevelTrackingApproach
的实现会存在怎么的问题呢?上面基于一张图表白原生 DataLoader 调度机制所产生的问题:
问题很显著,基于 FieldLevelTrackingApproach
的实现,下一层级的 DataLoader
的dispatch
是须要等到本层级的后果都回来之后才收回。基于这样的实现,查问总耗时的计算公式等于:TOTAL = MAX(Level 1 Latency)+ MAX(Level 2 Latency)+ MAX(Level 3 Latency)+ …,总查问耗时等于每层耗时最大的值加起来,而实际上如果链路编排由业务开发同学本人来写的话,实践上的成果是总耗时等于所有链路最长的那个链路所耗的工夫 ,这个才是正当的。而FieldLevelTrackingApproach
的实现所体现进去的后果是反常识的,至于为什么这么实现,目前咱们了解可能是设计者基于简略和通用方面的思考。
问题在于以上的实现在有些业务场景下是不能承受的,比方咱们的列表场景的响应工夫束缚一共也就不到 100ms,其中几十 ms 是因为这个起因搭进去的。针对这个问题的解决思路,一种形式是对于响应工夫要求特地高的场景独立编排,不采纳 GraphQL;另一种形式是在 GraphQL 层面解决这个问题,放弃架构的统一性。接下来,介绍一下咱们是如何扩大 GraphQL-Java 执行引擎来解决这个问题的。
3)DataLoader 调度优化
针对 DataLoader 调度的性能问题,咱们的解决思路是在最初一次调用某个 DataLoader
的load
之后,立刻调用 dispatch
办法收回查问申请,问题是咱们怎么晓得哪一次的 load 是最初一次 load 呢?这个问题也是解决 DataLoader 调度问题的难点,以下举个例子来解释咱们的解决思路:
假如咱们查问到的模型构造如下:根节点是 Query
下的字段,字段名叫 subjects
,subject
援用的是个列表,subject
下有两个元素,都是 ModelA
的对象实例,ModelA
有两个字段,fieldA
和 fieldB
,subjects[0]
的fieldA
关联是 ModelB
的一个实例,subjects[0]
的 fieldB
关联多个 ModelC
实例。
为了不便了解,咱们定义一些概念,字段、字段实例、字段实例执行完、字段实例值大小、字段实例值对象执行大小、字段实例值对象执行完等等:
- 字段 :具备惟一门路,是动态的,和运行时对象大小没有关系,如:
subjects
和subjects/fieldA
。 - 字段实例 :字段的实例,具备惟一门路,是动静的,跟运行时对象大小有关系,如:
subjects[0]/fieldA
和subjects[1]/fieldA
是字段subjects/fieldA
的实例。 - 字段实例执行完:字段实例关联的对象实例都被 GraphQL 执行完了。
- 字段实例值大小 :字段实例援用对象实例的个数,如以上示例,
subjects[0]/fieldA
字段实例值大小是 1,subjects[0]/fieldB
字段实例值大小是 3。
除了以上定义之外,咱们的业务场景还满足以下条件:
- 只有 1 个根节点,且根节点是列表。
DataLoader
肯定属于某个字段,某个字段下的DataLoader
应该被执行次数等于其下的对象实例个数。
基于以上信息,咱们能够得出以下问题剖析:
- 在执行字段实例的时候,咱们能够晓得以后字段实例的大小,字段实例的大小等于字段关联
DataLoader
在以后实例下须要执行load
的次数,因而在执行load
之后,咱们能够晓得以后对象实例是否是其所在字段实例的最初一个对象。 - 一个对象的实例可能会挂在不同的字段实例下,所以仅当以后对象实例是其所在字段实例的最初一个对象实例的时候,不代表以后对象实例是所有对象实例中的最初一个,当且仅当对象实例所在节点实例是节点的最初一个实例的时候才成立。
- 咱们可从字段实例大小推算字段实例的个数,比方咱们晓得
subjects
的大小是 2,那么就晓得subjects
字段有两个字段实例subjects[0]
和subjects[1]
,也就晓得字段subjects/fieldA
有两个实例,subjects[0]/fieldA
和subjects[1]/fieldA
,因而咱们从根节点能够往下推断出某个字段实例是否执行完。
通过以上剖析,咱们能够得出,一个对象执行完的条件是其所在的字段实例以及其所在的字段所有的父亲字段实例都执行完,且以后执行的对象实例是其所在字段实例的最初一个对象实例的时候。基于这个判断逻辑,咱们的实现计划是在每次调用完 DataFetcher
的时候,判断是否须要发动 dispatch
,如果是则发动。另外,以上机会和条件存在漏发dispatch
的问题,有个非凡状况,当以后对象实例不是最初一个,然而剩下的对象大小都为 0 的时候,那么就永远不会触发以后对象关联的 DataLoader
的load
了,所以在对象大小为 0 的时候,须要额定再判断一次。
依据以上逻辑剖析,咱们实现了 DataLoader
调用链路的最优化,达到实践最优的成果。
6 新架构对研发模式的影响
生产力决定生产关系,元数据驱动信息聚合架构是展现场景搭建的外围生产力,而业务开发模式和过程是生产关系,因而也会随之扭转。上面咱们将会从开发模式和流程两个角度来介绍新架构对研发带来的影响。
6.1 聚焦业务的开发模式
新架构提供了一套基于业务形象出的标准化代码合成束缚。以前开发同学对系统的了解很可能就是“查一查服务,把数据粘在一起”,而当初,研发同学对于业务的了解及代码合成思路将会是统一的。比方展现单元代表的是展现逻辑,取数单元代表的是取数逻辑。同时,很多繁杂且容易出错的逻辑曾经被框架屏蔽掉了,研发同学可能有更多的精力聚焦于业务逻辑自身,比方:业务数据的了解和封装,展现逻辑的了解和编写,以及查问模型的形象和建设。如下示意图所示:
6.2 研发流程降级
新架构不仅仅影响了研发的代码编写,同时也影响着研发流程的改良,基于元数据架构实现的可视化及配置化能力,现有研发流程和之前研发流程相比有了显著的区别,如下图所示:
以前是“一杆子捅到底”的开发模式,每个展现场景的搭建须要经验过从接口的沟通到 API 的开发整个过程,基于新架构之后,零碎主动具备多层复用及可视化、配置化能力。
状况一:这是最好的状况,此时取数性能和展现性能都曾经被积淀下来,研发同学须要做的只是创立查问计划,基于经营平台按需抉择须要的展现单元,拿着查问计划 ID 基于查问接口就能够查到须要的展现信息了,可视化、配置化界面如下示意图所示:
状况二:此时可能没有展现性能,然而通过经营平台查看到,数据源曾经接入过,那么也不难,只须要基于现有的数据源编写一段加工逻辑即可,这段加工逻辑是十分爽的一段纯逻辑的编写,数据源列表如下示意图所示:
状况三:最坏的状况是此时零碎不能满足以后的查问能力,这种状况比拟少见,因为后端服务是比较稳定的,那么也无需惊恐,只须要依照标准规范将数据源接入进来,而后编写加工逻辑片段即可,之后这些能力是能够被继续复用的。
7 总结
商品展现场景的复杂性体现在:场景多、依赖多、逻辑多,以及不同场景之间存在差别。在这样的背景下,如果是业务初期,怎么快怎么来,采纳“烟囱式”个性化建设的形式不用有过多的质疑。然而随着业务的一直倒退,性能的一直迭代,以及场景的规模化趋势,“烟囱式”个性化建设的弊病会缓缓凸显进去,包含代码复杂度高、短少能力积淀等问题。
本文以基于对美团到店商品展现场景所面临的外围矛盾剖析,介绍了:
- 业界不同的 BFF 利用模式,以及不同模式的劣势和毛病。
- 基于 GraphQL BFF 模式改良的元数据驱动的架构方案设计。
- 咱们在 GraphQL 实际过程中遇到的问题及解决思路。
- 新架构对研发模式产生的影响出现。
目前,笔者所在团队负责的外围商品展现场景都已迁入新架构,基于新的研发模式,咱们实现了 50% 以上的展现逻辑复用以及 1 倍以上的效率晋升,心愿本文对大家可能有所帮忙。
8 参考文献
- [1]https://samnewman.io/patterns/architectural/bff/
- [2] https://www.thoughtworks.com/cn/radar/techniques/graphql-for-server-side-resource-aggregation
- [3] 理解电商后盾零碎,看这篇就够了
- [4]框架定义 - 百度百科
- [5] 高效研发 - 闲鱼在数据聚合上的摸索与实际
- [6]《零碎架构 - 简单零碎的产品设计与开发》
9 招聘信息
美团到店综合研发核心长期招聘前端、后端、数据仓库、机器学习 / 数据挖掘算法工程师,坐标上海,欢送感兴趣的同学发送简历至:tech@meituan.com(邮件题目注明:美团到店综合研发核心—上海)。
浏览美团技术团队更多技术文章合集
前端 | 算法 | 后端 | 数据 | 平安 | 运维 | iOS | Android | 测试
| 在公众号菜单栏对话框回复【2020 年货】、【2019 年货】、【2018 年货】、【2017 年货】等关键词,可查看美团技术团队历年技术文章合集。
| 本文系美团技术团队出品,著作权归属美团。欢送出于分享和交换等非商业目标转载或应用本文内容,敬请注明“内容转载自美团技术团队”。本文未经许可,不得进行商业性转载或者应用。任何商用行为,请发送邮件至 tech@meituan.com 申请受权。