在现在的公司使用GraphQL有一段时间了。

现公司从创立之后的很长一段时间内是纯PHP的技术栈,前端、后端都在PHP代码中糅合在一起。新功能越加越多,页面越来越复杂之后,那些混在在PHP代码中的HTML代码越来越不可维护,于是终于有公司里的程序员看不下去,开始了技术革命,将PHP代码抽象成一个个微服务提供API,前端则采用Node+React,解放了前端工程师的生产力,使得新界面的开发越来越顺利,前端程序员也越发不用关心后端的实现了。

故事说到这里听起来皆大欢喜,然而时间长了,新的问题出现了——我们的微服务需要的调整越来越多。PM永远想要尝试新的点子,我们的新需求仍然是更加“花里胡哨”的页面,原先一个页面调用的微服务,在新的需求下需要新的数据,于是和原来比,要做的工作反而多了:在纯PHP的框架下,PHP后端代码和HTML前端代码都在同一个文件中,新需求也可能需要改一个(套)文件;然而在新的架构下,我们即需要调整微服务(PHP文件),又需要去调整前端代码(JS文件),还需要更改两者之间的协议(Apache Thrift),并且还需要严格的遵守Release的顺序和向前兼容的问题。

GraphQL就是在这样的背景下被引入到我们的技术栈之中,关于GraphQL的介绍网上有很多博文,在这里就不展开描述,个人觉得对于我们的产品开发中最有利的两点:

1. 降低了后端API的调整频度。所谓的新“需求”,有很多时候其实就是将数据转移,比如将本来在A页面展示的数据挪到B页面,或者将A和B页面合并成一个页面,抑或是A页面拆成B和C两个页面。在GraphQL引入之前,这样的展示层面的增删改都必将导致后端API的变化,但在GraphQL引入之后,前端程序员只需在Node端调整查询语句,就可以自己定制出自己需要的API。

2. 增加了前端的灵活性和可调试性。前端可以根据需求,理论上可以将整个数据库的数据在一个页面上实现任意的组合,并且由于有graphiql等强大的工具,可以边实现新的页面,边调整自己的查询语言,在出现问题时也可以通过直接执行查询语句来看是否后端返回的数据有问题。

比如有一款社交网络的应用,我们后端有一个getUserByUserId的API,可以查询一个用户的信息(ID,用户名,朋友们的ID),如果我们要做一个页面来显示一个用户的三度好友树,如果不使用GraphQL的解决方案,需要创建一个新的API,在API中先通过getUserByUserId去查询一个用户的信息和所有好友ID,再通过getUserByUserId去获得每个朋友的信息和好友ID,如此循环最后返回。

而如果使用GraphQL的解决方案,我们只需要定义用户和API的Schema:

type User {userId: IntuserName: Stringfriends: [User]
}
extend type Query {    getUserByUserId(userId: Int): User //根据用户Id查询单个用户    getUsersByUserIds(userIds: [Int]): [User] //根据多个用户Id查询多个用户
}

而对应的Resolver逻辑为

export default {Query: {getUserByUserId: async (root, args, context) => await context.service.getUserByUserId(args.userId).then(response => response.user),getUsersByUserIds: async (root, args, context) => await context.service.getUsersByUserIds(args.userIds).then(response => response.users),}User: {userId: (root) => root.userId,userName: (root) => root.userName,friends: (root, args, context) => await context.service.getUsersByUserIds(root.friendIds).then(response => response.users), } }

而getUserByUserId的返回格式为以下的格式,getUserByUserIds的话则是以下格式的列表形式

context.service.getUserByUserId(10001){"userId": 10001,"userName": "Sample User Name","friendIds": [ 10002, 10003, 10004 ]
}

这里我们提供了两个API,一个是单数形式getUserByUserId,一个是复数形式getUsersByUsersId,实际实现中单数形式的API可以坍缩成复数形式API只有一个参数的调用,所以可以继续简化其实现。为什么不只创建单数形式的API呢?这在之后的实战问题中会描述。

这样,我们如果要实现上面所描述的三度好友页面,只需要定义两个API——getUserByUserId和getUsersByUserIds和下面的一条GraphQL查询语句

query getUserByUserId ($userId: Int) {user: getUserByUserId(userId: $userId) {userIduserNamefriends { //朋友userIduserNamefriends { //朋友的朋友userIduserName}}}
}

这个查询会返回给我们这样的结果

{"userId": 10001,"userName": "Sample User Name","friends": [{"userId": 10002,"userName": "Sample User Name 2","friends": [{"userId": 10003,"userName": "Sample User Name 3"}, {"userId": 10004,"userName": "Sample User Name 4"},....]},{"userId": 10005,"userName": "Sample User Name 5","friends": [{"userId": 10006,"userName": "Sample User Name 6"}, {"userId": 10007,"userName": "Sample User Name 7"},....]},....]
}

这样我们只用了一个API(单复数共用一个实现的话)就组合出了这样的一个复合API,如果将来想要实现四度好友,五度好友,则可以在以上面的查询基础上继续嵌套,仍旧不需要增加后台的API代码。

这个示例只是最简单的示例,理论上如果你的服务的所有实体数据之间都有联结关系,那么只需要你实现每个实体数据根据ID的自查询API和实体之间的联结查询API,那么用GraphQL就可以将所有的实体连接成一张图(Graph),你可以通过GraphQL查询语句来构建这张图中的任何子图。

-------------------------- 我是和谐的分割线 --------------------------

正如每颗硬币都有正反面,在实际使用GraphQL的时候我们也遇到了很多问题,特别是性能上的问题。拿以上这个三度好友的GraphQL查询来举例,它有哪些问题呢?

1. 过度查询(Overfetching)

在一个页面中我们可能只会用一个实体的某几个属性,那么我们在后端的查询最好只需要选取需要的字段。而我们在实现GraphQL和后端服务的桥接时,不论GraphQL的查询语句请求了几个字段,后端服务永远会查询实体的所有字段并返回,而GraphQL的引擎则会根据查询语句只提取需要的字段作为返回结果。但是在这个过程中,不必要的字段占用了数据库的传输以及前后端网络传输的带宽。

比如上面的例子,如果我们的页面只要求获得用户ID并不要求返回用户名,那么我们的query可以改成以下的模式

query getUserByUserId ($userId: Int) {user: getUserByUserId(userId: $userId) {userIdfriends { userIdfriends {userId}}}
}

表面上来看我们确实没有去查询userName,但实际上由于我们的API会返回所有的userId, userName, friendIds,所以这个查询和前面那个例子的查询开销上是一样的。

解决方案:针对整个问题,我们在GraphQL的Resolver层面做了一些改造,在查询被执行的时候从GraphQL引擎获得当前的查询语句请求的字段,并将字段作为隐藏参数传递给后端服务,后端服务根据传进来的字段进行数据库查询的优化。解决方案的伪代码如下。

export default {User: {userId: (root) => root.userId,userName: (root) => root.userName,friends: (root, args, context, info) => await context.service.getUsersByUserIds(root.friendIds, info.fields).then(response => response.users), // 传入GraphQL查询中的field}
}

而服务器端的API返回值也随着调用传参也变化

context.service.getUserByUserId(10001,["userId"]){"userId": 10001
}

2. 重复查询(Repeated Query)

一个较为复杂的页面中可能一个实体在页面的不同位置都有展现,比如上面那个查询,用户的一度好友们的二度好友们,很有可能互相之间也是好友,那么我们的两层嵌套查询中,有部分的查询实际上是可以避免的。

解决方案:这一点暂时没有很完美的解决方案,我们目前可以做到的是在上层Query中已经查询到的数据,如果下层Query也要查询,那么通过缓存的方式,使的下层的Query不去访问API,但是如果本身是不同的Query,暂时没有办法做跨请求的缓存。缓存实现的伪代码如下。

export default {User: {userId: (root) => root.userId,userName: (root) => root.userName,friends: (root, args, context, info) => {let cachedUsers = root.friendIds.map(id => context.cache.users[id]).filter(x => !!x); //找出所有缓存的用户let idsToFetch = root.friendIds.filter(id => !context.cache.users[id]); //取得未缓存的用户IDreturn context.service.getUsersByUserIds(idsToFetch, info.fields).then(response => { //查询未缓存的用户信息for(let user in response.users) {context.cache.users[user.id]  = user;//将结果存储到缓存中}return response.users.concat(cachedUsers)://合并缓存结果和返回结果});}}
}

3. N+1查询 (N+1 Query)

N+1查询是GraphQL使用中最可能也是最经常遇到的性能问题,当出现查询嵌套并且在内部嵌套的数据是列表类型时最容易出现这样的性能问题。还是以上面的查询为例,如果系统中每个用户平均有10个好友,那么以上的三度好友查询一共进行了多少次后端API的调用?答案是1 + 1 + 10 = 12次, 为什么是12次呢?

1. 第一次调用getUserByUserId, 获得了目标用户的ID和用户名信息以及平均10个朋友的ID

2. 第二次调用getUsersByUserIds,获得了10个目标的用户民信息已经他们10*10个朋友的ID

3. 对于2中获得的10批朋友ID,我们需要分别调用10次getUsersByUserIds,去获得者100个朋友的用户名信息

第二次调用获得了N批朋友ID,每批朋友信息的查询带来了N次的额外查询,所以我们将这种Pattern称为N+1查询。这里我们可以看出为什么我们一开始在定义API的时候一定要定义复数形式的API,这样一开始我们就考虑到了会有批(Batch)查询的的需求,否则的话如果只有单查询的接口,我们则需要1 + 10 + 10 * 10 = 111次API查询。但是12次查询也是非常大的消耗,并且收到前段和后端通信的并发限制,这最后的10次通信可能需要分批进行,那么最终会导致服务器端的返回速度收到了极大的限制。

解决方案:N+1 Query的问题没有一个非常好的解决方案,我们目前的做法是在GraphQL的Resolver逻辑中插入了自己的逻辑,当我们遇到这种多层嵌套查询的时候,在第N层去尝试等待其他的resolver,拿上面的例子,我们的第二次调用后,获得了10批朋友的ID,那么在第三层的结构进行resolve逻辑的时候,我们会收集所有需要调用getUsersByUserIds的参数,将其合并成一次调用,这次调用返回的Promise在全局共享,同时在运行时将每个resolver的逻辑替换成合并后调用的结果中找出自己需要的结果并返回。为了更好的帮助大家理解,可以参照下图。

GraphQL为前后端的API提供了一种便利的解决方案,但同时收到自身设计的限制,会有各种各样的问题需要针对具体的应用场景去优化,在使用之前不妨先问问自己:我到底需不需要GraphQL。

转载于:https://www.cnblogs.com/chaosyang/p/graphql-practice-and-resolve-performance-issue.html

GraphQL实战经验和性能问题的解决方案相关推荐

  1. 客户端性能优化实战经验分享

    合理的架构设计,对客户端后期优化至关重要 暴风影音播放器一直因为"慢",而引发用户诸多抱怨.新发布的暴风影音5在启动速度上较暴风影音3提升了3倍.暴风影音播放研发总监黄森堂以暴风影 ...

  2. Pinterest 谈实战经验:如何在两年内实现零到数百亿的月访问

    Pinterest 谈实战经验:如何在两年内实现零到数百亿的月访问 发表于2013-04-17 17:20| 5639次阅读| 来源High Scalability| 46 条评论| 作者Todd H ...

  3. MongoDB实战经验分享

    2019独角兽企业重金招聘Python工程师标准>>> 转自: http://www.cnblogs.com/ymind/archive/2012/04/25/2470551.htm ...

  4. 资深大数据/AI专家:大数据知识图谱-实战经验总结

    作为数据科学家,我想把行业最新知识图谱总结并分享给技术专家们,让大数据知识真正转化为互联网生产力!大数据与人工智能.云计算.物联网.区块链等技术日益融合,成为全球最热的战略性技术,给大数据从业者带来了 ...

  5. 2018 年将打响 AI 战争,7 条实战经验帮你战胜恐惧

    来源:36氪 概要:不管是对科技巨头还是对创业公司,人工智能可以说是移动互联网时代以来最大的一个机遇.但是,人工智能同时也是让大多数人感到非常困惑的一项新技术,对它的发展现状和未来都非常迷惑. 不管是 ...

  6. 【线上直播】深度学习简介与落地实战经验分享

    分享嘉宾: 嘉宾简介: 郑泽宇,知衣科技联合创始人兼CEO,美国Carnegie Mellon University(CMU)硕士,畅销书<TensorFlow:实战Google深度学习框架&g ...

  7. (转)MySQL数据库的优化-运维架构师必会高薪技能,笔者近六年来一线城市工作实战经验...

    标签:服务器 数据库 老男孩 高薪技能 一线城市 原创作品,允许转载,转载时请务必以超链接形式标明文章 原始出处 .作者信息和本声明.否则将追究法律责任.http://liangweilinux.bl ...

  8. button 样式_缩减 SCSS 50%样式代码的 14 条实战经验

    原标题:缩减 SCSS 50%样式代码的 14 条实战经验 作者:feishi123 前言 Sass是CSS3语言的扩展,它能帮你更省事地写出更好的样式表,使你摆脱重复劳动,使工作更有创造性.因为你能 ...

  9. RabbitMQ实战经验分享

    RabbitMQ实战经验分享 原文:RabbitMQ实战经验分享 前言 最近在忙一个高考项目,看着系统顺利完成了这次高考,终于可以松口气了.看到那些即将参加高考的学生,也想起当年高三的自己. 下面分享 ...

最新文章

  1. jupyter notebook 内核好像挂掉了
  2. php server script name,$_SERVER[SCRIPT_NAME]变量可值注入恶意代码
  3. 为什么需要非线性激活函数
  4. 触发键盘_雷蛇这款光轴机械键盘开箱评测,光速触发,颜值爆表
  5. SpringBoot项目中,获取配置文件信息
  6. linux如何全局搜索目录,Linux 全目录全文搜索
  7. Cookie、Session 和 Token区别
  8. EBS R12.1安装中文补丁包BUG:FAILED: file XLIFFLoader.class on worker [X]
  9. Spring Boot Serverless 实战系列“架构篇”首发 | 光速入门函数计算
  10. 语音情感识别--RNN
  11. 无线电波在介质中的传播速度计算公式和印刷电路板(PCB)的特性阻抗与特性阻抗控制
  12. Docker安装CentOS容器并使用SSH工具远程连接
  13. Activiti工作流引擎
  14. JavaScript 全栈工程师培训教程
  15. 求方差FPGA的实现方法
  16. PulseAudio安装流程
  17. 从游戏AI到自动驾驶,一文看懂强化学习的概念及应用
  18. 怎样能用计算机打出表白数字,数字表白大全 怎么用数字表白
  19. 快播将关闭QVOD服务器 宅男,你心碎了吗?
  20. 论文笔记(一)《Intriguing properties of neural networks》

热门文章

  1. springboot 2.0版本自定义ReidsCacheManager的改变
  2. 惠普在安全领域发力 收购大数据加密企业
  3. Python的if判断与while循环
  4. 算法(Algorithms)第4版 练习 2.1.24
  5. 十分钟-Nginx入门到上线
  6. Java设计模式—工厂方法模式抽象工厂模式
  7. pure-ftpd搭建教程
  8. Win10,Win7,WinServer2012,WinServer2008内存最大支持
  9. 2015年5月移动游戏Benchmark
  10. 8天学通MongoDB——第四天 索引操作