暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

房多多前后端系统架构演进之Duo-GraphQL

房多多技术 2019-10-09
661

场景:Scrum团队需求评审会议上,产品经理正在进行业务故事描述和产品需求讲解,前端开发小强打开Duo-GraphQL,边听边写gql(GraphQL查询语言)。当产品经理讲解完,小强说接口已经准备好了,数据不多不少,不需要调试,不需要Mock,不需要一步步从测试到预发布再到生产...


架构演进背景

      房多多技术经过8年的发展,从最初的PHP单体服务,到现在由几百个微服务紧密协同;从原来单一的网站发展到现在APP、M站/H5、Web、小程序....多终端,多场景,纵横A(Agent)、B(Business)、C(Customer)三大端产品矩阵的开放性平台,以及众多中后台运营产品体系。发展过程中,公司技术架构经过了几次大的演化。

      第一次从单体PHP服务,开始按功能拆分,技术选型是Nginx + Thrift,基本上是靠人工配置路由。这个阶段有了微服务和前后端分离的雏形,但只能叫做分布式服务化阶段,在服务划分和层次上还很粗旷,服务划分的粒度、层次和边界还很不清晰,调用略显混乱。这个阶段解决了产品和系统的扩展性问题和团队组织灵活性问题。但随着服务数量的增加,蜘蛛网样的调用链,让后端人员很痛苦;前端也很痛苦,强依赖了Thrift Client包,耦合非常强,各种兼容问题,包体积还很大。

       

       经过1~2年的实践后,团队对微服务有了更深的理解,于是进行了第二次的框架演化。本次演化出来的,主要对各模块进一步细化、抽取,清晰了各微服务的边界和职责。在业务服务与前端之间添加了接入层,以RESTful API方式提供服务(不再强依赖thrift),而底层的微服务主要以Dubbo进行治理。这一阶段,更有层次感了,职责更加清晰,技术开始有了积淀。运维也朝着DevOps方向发展,持续集成和持续发布、虚拟化、服务治理等应运而生,请求路由也通过内部DNS,简化了很多,效率也不断提升。

      随着迭代的进行,版本的叠加,无论是接入层还是底层业务层,又开始变得臃肿起来。互联网产品的特点就是前端产品和业务持续变化和迭代,每次变化都从前端界面到后端N个服务,牵一发而动全身。每个版本迭代都需要兼顾各端,还得兼容各APP版本。例如,经过近两年的迭代/版本的叠加,接入层服务里面的一个楼盘列表接口,返回的数据字段越来越多!   (楼盘列表页的响应体文档,一共75个字段。很多时候,列表上只需要10个以内字段!)

       随着工程技术的积累,工程师们在千丝万缕业务间不断抽象、聚合、沉淀,数据中台、技术中台一个个发展了出来,业务领域服务已经渐渐清晰成型。运维层面,房多多迈进了容器时代,容器化和Mesh网格的使用,大大提高了服务的可用性和稳定性,运维成本也大幅下降。

       前端技术在近几年飞速发展,Node.js日趋成熟,房多多前端团队也开始由JavaWeb向Node.js现代前端技术栈发展。架构分层中BFF(Backend For Frontend)的职责越来越明确,其理念很简单,就是前端团队最清楚自己需要哪些数据,实现什么样的UI和交互,就让前端开发人员自己去实现。如下图所示,我们让最懂UI和交互的前端全栈团队去面对高速多变的UI和交互,让后端工程师去针对行业业务进行领域服务沉淀,使其趋于稳定和中台化。   

(房多多第三版系统架构设计原则图)

       架构理念的进一步清晰,同时也无形中给前端团队更大的压力,一下提高了技术门槛,全栈呀!对此,从2019年开始,房多多开始有意识构建我们的大前端团队,我们将在后续文章中介绍我们的最新架构理念和团队组织迭代。

       在BFF层,我们也进行了几种探索。其一就是由前端人员自己调用(用Node.js或Java)业务领域服务接口,获取数据;其二,使用GraphQL,通过技术手段替代人工,实现数据的获取和组织,本文下半部分重点介绍这种通用型GraphQL的实践。


GraphQL介绍

2012年,GraphQL由Facebook内部开发,2015年公开公布。2018年11月7日,Facebook将GraphQL项目转移到新成立的GraphQL基金会。Facebook开源的是一个协议(SDL,Type System Definition Language),并没有开源它的实现。现在看到的各种语言的实现,都是GraphQL的信徒们开源出来的。

1. GraphQL的优点

  GraphQL有以下好处:(直接抄自中文版官方的网站详见 https://graphql.cn)

  • 请求你所要的数据不多不少

  • 获取多个资源只用一个请求

  • 描述所有的可能类型系统

  • 快步前进强大的开发者工具

  • API 演进无需划分版本

  • 使用你现有的数据和代码


2. GraphQL的缺点

    GraphQL这种查询语言所带来的灵活性和丰富性的同时也增加了复杂性。也是因为这些“缺点”和生态还不成熟,导致了它的普及度不高,但是大部分的“缺点”是可以通过技术手段解决的。

    • Schema和Resolvergraphql-java实现DataFetcher维护成本比较高。图(GraphQL术语:Data Graph)需要精心设计,需要更长远的规划,往往跟现有的服务或数据源不搭,导致了从旧系统切换成GraphQL时难度较大或者图未设计好,用起来比较混乱;

    • GraphQL搭建之初,是不知道客户端会怎样使用,完全依赖GraphQL引擎根据Schema去获取数据,很容易造成N+1或循环请求的问题,增加了数据库的压力,接口性能也是个不小的挑战;

    • 独特的查询语法也需要额外的学习。另外在IDE上的智能提示、语法高亮支持还不到位,这也降低了前端人员的使用积极性;

    • 还有其它种种原因(网上有很多不用GraphQL的N种理由的文章)


    3. 谁在使用GraphQL


    4. GraphQL的实现现状

    GraphQL还很新。目前比较成熟的是JavaScript生态圈的Apollo GraphQL,提供了从服务、监控、客户端等一整套体系,监控等一些高级功能是收费的,基础功能已开源免费。还有各种语言的实现,没有实地使用过,不敢做定论。在单体服务里面基本没太大问题,但要适应当前主流的分布式、微服务化开发场景,还有点难度。目前房多多大前端团队正在其他项目中尝试Apollo GraphQL。

    graphql-java是GraphQL规范的Java实现,绝大部分java系的GraphQL项目都是基于此项目展开。像graphql-java-tools可以非常快速地将一个RESTful/RPC服务通过几个注释转换为GraphQL服务,有的项目可以直接将某个库(Elasticsearch / MongoDB / MySQL...)暴露成GraphQL接口服务。这些快餐式的实现虽然可以快速上线一个GraphQL服务,但也留下不少坑:

    • 单体服务。所有的Schema(有些可以自动生成)都在一个工程里面,多个团队协同会比较复杂,扩展性不好,后期也比较难维护;
    • 无领域或模块的概念,所有的图都是平铺的,对于前端人员来说要找一个需要的图会变得越来越难;
    • 没有根本性解决问题。通过RESTful API转换成GraphQL服务的无法提供良好的注释(文档)也没有解决传统接口遇到的问题,只是换汤不换药;通过数据库Schema自动生成GraphQL的,没有分层,也不利于扩展,后续的数据库变更会是个很大的问题。

     

    Duo-GraphQL的诞生

    GraphQL的优点太过突出了,特别适合于C端类面向数据展示型的产品。在它出现的那一刻起,我们就关注着它的发展,如何把它引进来,如何与房多多现有的开发、测试、发布流程融合,如何最大限度复用现有的基础设施和开发资源,是我们需要解决的首要问题。最重要的是,GraphQL带来的效率提升可以解放我们大量的开发人力。

    按理说,选用比较成熟的Apollo GraphQL,把BFF层交由纯前端同学负责会是比较“正确”的选择。但回到现实,房多多过去BFF层一直由后台(Java)开发人员开发和维护,刚刚组建的大前端团队需要时间逐步掌握和擅长服务端开发(在本文发表时,我们的大前端团队已经有了长足的进步,现在以Node.js建立起的BFF模块已经不少了)。

    恰巧,我们还在维护着另一个开源项目:Duo-Doc,利用javadoc技术,在项目构建期通过解析代码和注释,抽取接口信息,生成文档并实现了多环境的接口调用功能。接口信息是现成的,这不就是GraphQL里的Schema需要的吗?字段与接口的关系也是现成的,这就是GraphQL里的Resolver!

    于是,一个大胆的想法出现了:利用Duo-Doc的接口信息和数据绑定关系,将多个“传统”微服务组织起来,共同实现一个大图。   

    为了与公司现有的工作模式、开发流程融合,新的框架需要完全支持GraphQL协议之外,还需要支持以下特性:

      • 支持领域,各领域服务独立开发、维护、部署,共同提供一张大图。更清晰的划分,前端人员可更快速找到需要的资源,各团队只需要关注各自领域,而不用担心与其它团队的接口、命名冲突;

      • 各GraphQL Provider服务间边界要非常清晰,弱耦合。尽量当前公司的团队组织、微服务划分不变;

      • 开发、调试期间可以不依赖GraphQL环境,以普通RESTful服务的方式开发。各团队可独立开发、测试、部署;

      • 各领域服务单独发版,服务需要平滑切换;

      • Schema、Resolver必须自动生成绑定,不能依靠人工维护;

      • 数据必须是经过处理的,不能是太原子,否则前端处理难度太大(Elasticsearch/MongoDB这类文档型数据库非常适合GraphQL);

      • 一些旧接口希望可以直接接进去(有些旧接口短期内不容易迁入,以JsonScalar响应的方式接入,也可以快速兼容旧服务)。


      我们的目标

      通过技术手段,让BFF层从繁琐而枯燥的取数据、拼数据工作中解放出来,做更重要的事。

       


      Duo-GraphQL架构

      1. GraphQL Provider(数据供应端)

      GraphQL Provider为Duo-GraphQL引擎提供某个领域的图和数据接口。GraphQL Provider本身是一个标准的RESTful服务,开发人员可脱离GraphQL引擎独立开发、调试。这一层面对接口提供的服务定义了两类响应体,分别称为标准视图与非标准视图(我们自己定的概念,非GraphQL规范)。

      • 标准视图

      标准视图是各领域对象的抽象,比如楼盘、小区、经纪人、二手房、订单等。可以理解为数据库中的一个实体表。

                   

      标准视图通过@SchemaProvider标注,注解中也声明了一组全局的外联名称,并实现byId和byIds(可选)查询。在任一视图内,只要出现了此名称的字段,就会自动关联上此实体。视图的字段可以是个普通的属性(比如名称、地址等),也可以是绑定外部服务接口(比如用户是否关注过,比如浏览人数等)。

                   

       

      • 非标准视图

      非标准视图只是RESTful接口的普通响应体。它不是一个领域对象,比如推荐列表、热卖楼盘、驻场经纪人等,可以理解为数据库里的关联表。暴露这些关联表时,不需要冗余名称、地址等字段,而是通过一堆的id/ids,交由Duo-GraphQL去自动关联上对应的实体,也就可以获取到相关的任何数据。这也是GraphQL比传统APIs更轻便的原因:不需要去组装数据。

       

      2. GraphQL Engine(引擎)

      Duo-GraphQL引擎分为两部分:        

      • graphql-java,GraphQL的Java实现,未做任何变更,主要负责GraphQL查询解析、查询执行、数据组装等基本的GraphQL功能
      • Duo-GraphQL,此模块主要负责:
        • 各GraphQL Provider注册及发现

        • GraphQL Schema生成和DataFetcher数据接口绑定。将各分布式的GraphQL Provider接口信息转换成Schema、进行字段关联等组织成一张大图,并负责数据的调用;

        • gql优化编排与调用。在graphql-java的基础上进行请求合并、缓存优化、异步编排,并完成数据接口的调用;

        • 自定义扩展。扩展了GraphQL类型、指令、监控器、提供Inner Provider等;

        • 监控、Schema选择(支持多个Schema)及其它优化。


      3. 接入层

              目前各开发端Node.js对GraphQL查询语法支持相对友好,Android / iOS希望通过传统的RESTful接口进行调用。所以我们在GraphQL引擎之上,部署了一层接入层。接入层负责存储GraphQL查询语句,并将参数以RESTful接口参数的方式暴露出去,每个暴露的接口,都会有自己唯一的命名。因此,使用GraphQL作为引擎写接口的过程将会是:用户定义好一个接口名称 --> 写查询语句 --> 保存&发布 --> 前端就可以使用了。至于数据调用、优化、合并查询等事,全都由Duo-GraphQL引擎完成!


      4. 前端(调用端

              前端可通过HTTP请求访问Duo-GraphQL,目前Duo-GraphQL实现完全遵循GraphQL规范,并支持apollo-client消息规范(比如Apollo对Subscrption消息做了些封装)。


      Duo-GraphQL实现细节 

       

      1. GraphQL Provider项目构建

          GraphQL Provider在项目构建时,通过Duo-Doc提供的功能(以Maven插件的方式,整合进CI),将接口信息(接口签名、响应体、请求参数等)抽取出来,在本地classes目录生成api.json文件。这一步是关键,GraphQL Schema和Resolver数据绑定需要这些信息(如果GraphQL Provider接入Duo-Doc,则不需生成api.json)。


      2. Duo-GraphQL启动

           Duo-GraphQL启动时,会执行以下动作:

        • 订阅了注册中心某个节点数据变更(目前实现了ZooKeeper和Redis两种注册中心)。如果有GraphQL Provider重新发布时,会自动重新加载;

        • 读取此节点下现有的GraphQL Provider信息(服务名称、版本号、服务地址等),并从Redis或Duo-Doc中获取到对应的接口信息;

        • 通过这些信息构建Schema,绑定数据接口。


        3. GraphQL Provider启动

             GraphQL Provider启动时,会将当前服务的版本、名称等信息以HTTP方式提交给Duo-GraphQL,由引擎代为提交更新到注册中心(GraphQL Provider不需要引入ZooKeeper这么重的依赖)。如果使用的是Redis实现的注册中心,则会将接口数据写入Redis中。

        当注册中心有变更 --> Duo-GraphQL收到变更 --> Duo-GraphQL获取变更信息 --> 重新构建GraphQL Schema、绑定接口,完成GraphQL服务的平滑过度。 

        4. 优化策略

             GraphQL最大的问题可能就是性能问题了,我们对性能做了以下优化:

          • 并发请求。这个是graphql-java里内置的功能,但需要在写DataFetcher时使用CompletionStage作为返回值,实现异步并发;

          • 查询预编译。每个GraphQL查询语句都比较大,graphql-java底层使用antlr4解析虽然很快,但成本还是比较高的。同一类页面的请求,除了几个变量,绝大部分是一样的。前端配合查询脚本参数化,后台编译结果缓存,可以减少这一步骤的开销;

          • 合并请求。在列表里面再绑定数据接口,就会触发循环调用的问题,我们做了合并请求的策略,让多次循环调用的接口合并起来一次调用,返回来再拆分到列表各元素里;

          • N+1优化。对于像拉取我朋友列表里的朋友这样的需求,正常请求会发生N+1的严重性能问题,这个问题使用了graphql-java提供的策略来规避;

          • 提供了Inner Provider,将一些常用的视图以内置Provider的方式提供,比如行政区域、地铁等,Inner Provider可以将数据加载进内存里提供服务;

          • http2。GraphQL Provider都是以RESTful API的方式提供,每次请求都会重新发起连接(如果不考虑session keep alive的话),使用http2可以大幅提升服务性能。因为所有的调用都是在内网完成的,使用的是内部域名或IP,没有SSL证书,可以利用OkHttp的H2C协议(规划中,尚未实现)。


          5. Subscription实现

               目前订阅使用的比较少,不过我们对订阅功能做了支持。主要依赖Redis的PUB/SUB实现,原理如下。GraphQL Provider的Redis注册中心就是使用此方案实现的。      

          Duo-GraphQL性能

                  Duo-GraphQL已经在房多多C端大量使用,发稿截止近7天的服务SLA:100%,响应95线:20.3ms99.9线:65.3ms

               

          再看看我们这个接口都取了些什么数据:

                 

          Duo-GraphQL性能已经很接近有经验的工程师写的传统数据接口性能,甚至更优!接口工程师不需要懂太多的并发编程知识,不需要自己做合并请求,不需要再拼数据,最重要的是它不会形成代码负债!


          结束语

          GraphQL是把双刃剑,用得好,它可以极大提升开发效率,减少技术负债;用得不好,它也会是一个炸弹,把项目弄得一团乱。在我们实践GraphQL的过程中,经历了很多困惑,踩了很多坑,但最终,我们走通,捋顺了这条路。总结了上面一系列的经验和一些实现思路。


          源于社区,回归社区,我们已经将Duo-GraphQL开源到github,欢迎star++,欢迎fork,欢迎一起探讨:

          https://github.com/fangdd-open/duo-graphql


          另一个提供接口文档接口的开源项目Duo-Doc:

          https://github.com/fangdd-open/duo-doc

          文章转载自房多多技术,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

          评论