-
GraphQL
在微服务架构中的实践架构
目录
GraphQL
是什么?
.....
..................................................
...........................................
1
GraphQL
在微服务架构中的使用
.
............................................ .....................
2
GraphQL
在实践过程中遇到的棘手问题
.......................................
............
3
合理的
GraphQL
微服务架构的设计
.
..............................................
...........
4
来源:
C
DA
数据分析
1
一、
GraphQL
是什么?
简单对象访问协议
(
SOAP
)
从今天来
看已经是一门非常古老的
Web
服
务
技术了,
虽然很多服务仍然在使用遵循
SOAP
的接口,
但是到今天
REST
风格
的面向资源的
API
接口已经非常深入人心,也非常的成熟;但是这篇文章
要介
绍的主角其实是另一门更加复杂、完备的查询语言
GraphQL
。
作为
Facebook
在
2015
年推出的查询语言,
GraphQL
能够对
API
中
的数据提供一套易于理解的完整描述,
使得客户端能够更
加准确的获得它需要的
数据,目前包括
Facebook
、
Twitter
、
GitHub
在内的很多公司都已经在生产
环境使用
GraphQL
提供
API
;其实无论
我们是否决定生产环境中使用
p>
GraphQL
,它确实是一门值得学习
的技术。
二、
GraphQL
在微服务架构中的使用
类型系统
GraphQL
的强大表达能力主要还是来自于它完备的类型系统,与
REST
不同,它将整个
Web
服务中的全部资源看成一个有连接的图,而不是一个个
资源孤岛,在访问任何资源时都可以通过资源之间的连接访问其它的资源。
来源:
CDA
数据分析
2
如上图所示,当我们访问
User
资源时,就可以通过
GraphQL
中的连接
访问当前
User
的
Repo
和
Issue
等资源,我们不再需要通过多个
REST
的
接口分别获取这些资源,
只需要通过如下所示的查询就能一次性拿到全部的结
果:
{
user {
id
email
username
repos(first: 10) {
id
url
name
issues(first: 20) {
id
author
来源:
CDA
数据分析
3
title }
}
}
}
GraphQL
这种方式能够将原有
RESTful
风格时的多次请求聚合成一次请
求,
不仅能够减少多次请求带来的延迟,
还能够降低
服务器压力,
加快前端的渲
染速度。它的类型系统也非常丰富,
除了标量、枚举、列表和对象等类型之外,
还支持接口和联合类型等高级特性。
为了能够更好的表示非空和空字段,
GraphQL
也引入了
Non-Null
等标
识代表非空的类型,例如
String!
表示非空的字符串。
schema {
query: Query
mutation: Mutation
}
来源:
CDA
数据分析
4
Schema
中绝大多数的类型都是普通的对象类型,
但是每一个
Schema
中
都有两个特殊类型:
query
和
mutation
,它们是
GraphQL
中所有查询的入
口,
在使用时所有查询接口都是
query
的子字段,所有改变服务器资源的请求
都应该属于
mutation
类型。
集中式
vs
分散式
GraphQL
以图的形式将整个
Web
服务中的资源展示出来,
其实我们可以
理解为它
将整个
Web
服务以
“
SQL
”
的方式展示给前端和客户端,服务端
的资源最终都被聚合到一张
完整的图上,这样客户端可以按照其需求自行调用,
类似添加字段的需求其实就不再需要
后端多次修改了。
与
RESTful
不同,
每一个的
GraphQL
服务其实对外只提供了一个用于调
用内部接口的端点,所有的请求都访问这个暴露出来的端点。
来源:
C
DA
数据分析
5
GraphQL
实际上将多个
HTTP
请求聚合成了一个请求,它只是将多个
RESTful
请求的资源变成了一个从根资源
Post
访问其他资源的
Comment
和
Author
的图,多个请求变成了一个请求的不同字段,从
原有的分散式请求
变成了集中式的请求,这种方式非常适合单体服务直接对外提供
GraphQL
服
务,
能够在数据源和展示层建立一个非常清晰的分离,
同时也
能够通过一些强大
的工具,例如
GraphiQL
直接提供可视化的文档;但是在业务复杂性
指数提升
的今天,
微服务架构成为了解决某些问题时必不可少的
解决方案,
所以如何在微
服务架构中使用
GraphQL
提高前后端之间的沟通效率并降低开发成
本成为了
一个值得考虑的问题。
Relay
标准
如果说
RESTful
其实是客户端与服务端在
HTTP
协议通信时定义的固定
标准,那么
Relay
其实也是我们在使用
GraphQL
可以遵循的一套规范。
这种标准的出现能够让不同的工程
师开发出较为相似的通信接口,
在一些场
景下,
例如标识对象和分页这种常见的需求,
引入设计良好的标准能够降低开发
人员之间的沟通成本。
Relay
标准其实为三个与
API
有关的最常见的问题制定了一些规范:
来源:
CDA
数据分析
6
提供能够重新获取对象的机制;
提供对如何对连接进行分页的描述;
标准化
mutation
请求,使它们变得更加可预测;
通过
将上述的三个问题规范化,
能够极大地增加前后端对于接口制定和对接
< br>时的工作效率。
对象标识符
Node
是
Relay
标准中定义的一个接口,所有遵循
Node
接口的类型都
应该包含一个
id
字段:
interface Node {
id: ID!
}
type Faction : Node {
id: ID!
name: String
ships: ShipConnection
}
来源:
CDA
数据分析
7
type Ship : Node {
id: ID!
name: String
}
Faction
和
Ship
两个类型都拥有标识符
id
字段,
我们可以通过该标识符<
/p>
重新从服务端取回对应的对象,
Node
接口和字段在默认情况下会假定整个服
务中的所有资源的
id
都是不同的,
但是很多时候我们
都会将类型和
id
绑定到
一起,
组合后才能一个类型特定的
ID
;
为了保证
id
的不透明性,
返回的
id
往
往都是
Base64
编码的字符串,
GraphQL
服务器接收到对应
id
时进行解码就
可以得到相关的信息。
连接与分页
在一个常见的数据库中,一对多关系是非常常见的,一个
User
可以同时
拥有多个
Post
以及多个
Comment
,这些资源的数量在理论上不是有穷的,
没有办法在同一个请求全部返回,所以要对这部分资源进行分页。
query {
viewer {
name
email
来源:
CDA
数据分析
8
posts(first: 1) {
edge {
cursor
node {
title }
}
}
}
}
Relay
通过抽象出的
『连接模型』
为一对多的关系提供了分片
和分页的支持,
在
Relay
看来,当我们获取某一个
User
对应的多个
Post
时,其实是得到了
一个
PostConnection
,也就是一个连接:
{
来源:
CDA
数据分析
9
}
]
}
}
}
在一个
PostConnection
中会存在多个
PostEdge
对象,其中的
cursor
就是我们用来做分页的字段,
所有的
cursor
其实都是
Base64
编码的字符串,
这能够提醒调用方
cursor
是一个不透明的指针,
拿到当前
cursor
后就可以将
它作为
after
参数传到下一个查询中:
query {
viewer {
name
email
posts(first: 1, after:
edge
{
cursor
来源:
CDA
p>
数据分析
10
node {
title }
}
}
}
}
当我们想要知道当前页是否是
最后一页时,其实只需要使用每一个连接中的
PageInfo
对象,其中包含了很多与分页相关的信息,
一个连接对象中一般都有
以下的结构和字段,例如:
Edge<
/p>
、
PageInfo
以及游标和节点等。
PostConnection
├──
PostEdge
│
├──
cursor
│
└──
Post
└──
PageInfo
├──
hasNextPage
├──
hasPreviousPage
来源:
CDA
数据分析
11
├──
startCursor
└──
endCursor
Relay
使用了非常多的功能在连接周围构建抽象,
让我们能够更加方便地管
理客户端中的游标,
整个连接相关的规范其实特别复杂,
可以阅读
Relay Cursor
Connections
Specification
了解更多与连接和游标有关的设计。
可变请求
每一个
Web
服务都可以看做一个大型的复杂状态机,这个状态机对外提
供两种不同的接口,
p>
一种接口是查询接口,
它能够查询状态机的当前状态,
而另
一种接口是可以改变服务器状态的可变操作,例如
POST
、
DELETE
等请求。
p>
按照约定,所有的可变请求都应该以动词开头并且它们的输入都以
Input
结尾,与之相对应的,所有的输出都以
Payload
结尾:
input IntroduceShipInput {
factionId: ID!
shipName:
String!
来源:
CDA
数据分
析
12
clientMutationId: String!
}
type IntroduceShipPayload
{
faction: Faction
ship:
Ship
clientMutationId: String!
}
除此之外,可变请求还可以通过传入
clientMutationId
保证请求的幂等
性。
小结
Facebook
的
Relay
标准其实是一个在
GraphQL
上对于常见领域问题
的约定,
通过这种
约定我们能够减少工程师的沟通成本和项目的维护成本并在多
人协作时保证服务对外提供
接口的统一。
三、
GraphQL<
/p>
在实践过程中遇到的棘手问题
N +
1
问题
在传统的后端服务中,
N + 1
查
询的问题就非常明显,由于数据库中一对
多的关系非常常见,再加上目前大多服务都使用
ORM
取代了数据层,所以在
p>
来源:
CDA
数据分析
13
很多时候相关问题都不会暴露出来,
只有真正出现性能问题或者慢查询时才会发
现。
SELECT * FROM users LIMIT 3;
SELECT * FROM posts WHERE user_id = 1;
SELECT * FROM posts WHERE user_id = 2;
SELECT * FROM posts WHERE user_id = 3;
SELECT * FROM users LIMIT 3;
SELECT * FROM posts WHERE user_id IN
(1, 2, 3);
GraphQL
作为一种更灵活的
API
服务提供方式,相比于传统的
Web
服
务更容易出现上述问题,
类似的问题
在出现时也可能更加严重,
所以我们更需要
避免
N + 1
问题的发生。
数据库层面的
N + 1
查询我们可以通过减少
SQL
查询的次数来解决,一
般我们会将多个
=
查询转换成
IN
查询;
但是
GraphQL
中的
N + 1
问题就
有些复杂了,
p>
尤其是当资源需要通过
RPC
请求从其他微服务中获取时,
更不能
通过简单的
改变
SQL
查询来解决。
来源:
CDA
数据分析
14
在处理
N + 1
问题之前,我们要真正了解如何解决这一类问题的
核心逻
辑,
也就是将多次查询变成一次查询,
< br>将多次操作变成一次操作,
这样能够减少
由于多次请求增
加的额外开销
——
网络延迟、
请求解析等;
GraphQL
使用了
DataLoader
从业务层面解决了
N + 1
p>
问题,其核心逻辑就是整个多个请求,
通过批量请求的方式解决问题
。
微服务架构
微服务架构在当下已经成为了遇到业务异常复杂、
团队人数增加以及高并发
等需求或者问题时会使用的常见解决方案,当微服务架构遇到
GraphQL
时就
会出现很多理论
上的碰撞,会出现非常多的使用方法和解决方案。
来源:
CDA
数据分析
15
在这一节中,我们将介绍在微服务架构中使用
GraphQL
会遇到哪些常见
的问
题,对于这些问题有哪些解决方案需要权衡,同时也会分析
GraphQL
的
设计理念在融入微
服务架构中应该注意什么。
当我们在微服务架构中融入
GraphQL
的标准时,会遇到三个核心问题,
这些问题其实主要是从单体服务迁移到微服务架构这种分布式系统时引入的一
系列技术难点,
这些技术难点以及选择之间的折衷是在微服务中实践
< br>
GraphQL
的关键。
Schema
设计
GraphQL
独特的
Schema
设计其实为整个服务的架构带来了非常多的变<
/p>
数,
如何设计以及暴露对外的接口决定了我们内部应该如何实现用
户的认证与鉴
权以及路由层的设计。
从总体来看,微服务架构暴露的
GraphQL
接口应该只有两种;一种接口
是分散式的,每一个微服务对外暴露不同的端点,分别对外界提供服务。
p>
在这种情况下,
流量的路由是根据用户请求的不同服务进行分发的,
也就是我们
会有以下的一些
GraphQL API
服务:
<
/p>
来源:
CDA
数据分析
< br>
16
/posts/api/graphql
/comments/api/graphql
/subscriptions/api/graphql
我
们可以看到当前博客服务总共由内容、
评论以及订阅三个不同的服务来提
供,在这时其实并没有充分利用
GraphQL <
/p>
服务的好处,当客户端或前端同时
需要多个服务的资源时,需要分
别请求不同服务上的资源,并不能通过一次
HTTP
请求满足全部的需求。
另一种方式其
实提供了一种集中式的接口,
所有的微服务对外共同暴露一个
端
点,
在这时流量的路由就不是根据请求的
URL
了,
而是根据请求中不同的字
段进行路由。
这种路由的方式并不能够通过传统的
nginx
来做,因为在
nginx
看来整
个请求其实只有一个
URL
以及一些参数,
我们只有解析
请求参数中的查询才能
知道客户端到底访问了哪些资源。
p>
来源:
CDA
数据分析
17
/api/graphql
请求的
解析其实是对一颗树的解析,
这部分解析其实是包含业务逻辑的,
在
这里我们需要知道的是,这种
Schema
设计下的请求是按照
field
进行路由
的,
GraphQL
其实帮助我们完成了解析查询树的过程,我们只需要对相应字段
实现特
定的
Resolver
处理返回的逻辑就可以了。
然而在多个微服务提供
Schema
时,
我们需要通过一种机制将多个服务的
Schema
整合起来,
这种整合
Schema
的思路最重要的就是需要解决服务之间
的重复资源和冲突字段问题,如果多个服务需要同时提供同一个类型的基础资
源,例如:
User
可以从多种资源间接访问到。
{
post(id: 1) {
user {
id
email }
id
title
content }
作
为微服务的开发者或者提供方来讲,不同的微服务之间的关系是平等的,
我们需要一个更
高级别或者更面向业务的服务对提供整合
Schema
p>
的功能,
确
保服务之间的字段与资源类型不
会发生冲突。
< br>来源:
CDA
数据分析
18
前缀
如何
解决冲突资源从目前来看有两种不同的方式,
一种是为多个服务提供的
< br>资源添加命名空间,一般来说就是前缀,在合并
Schema
时,通过添加前缀能
够
避免不同服务出现重复字段造成冲突的可能。
SourceGraph
在实践
GraphQL
时其实就使用了这种增加前缀的方式,
这
种方式的实现成本比较低,能够快速解决微服务中
< br>
Schema
冲突的问题,读者
可以阅读
GraphQL at massive scale: GraphQL as
the glue in a
microservice architecture
一文了解这种做法的实现细节;这种增加前缀解决
冲突的方式优
点就是开发成本非常低,
但是它将多个服务的资源看做孤岛,
没
有
办法将多个不同服务中的资源关系串联起来,这对于中心化设计的
GraphQL
来说其实会造成一定体验上的丢失。
来源:
CDA
数据分析
19
粘合
除了增加前缀这种在工程上开发
成本非常低的方法之外,
GraphQL
官方提
供了一种名为
Schema Stitching
的方案,能够将不同服务的
GraphQL
Schema
粘合
起来并对外暴露统一的接口,这种方式能够将多个服务中的不同
资源粘合起来,能够充分
利用
GraphQL
的优势。
为了打通不同服务之间资源的壁垒、建立合理并且完善的
GraphQL API
,
我们其实需
要付出一些额外的工作,
也就是在上层完成对公共资源的处理;
当对
整个
Schema
进行合并时,如果遇到公共资源,就会选用特定的
Resolver
进
行解析,这些解析器的逻辑是在
Schema Stitching
时指定的。
const
linkTypeDefs = `
extend type User {
chirps: [Chirp]
}
`;
来源:
CDA
< br>数据分析
20
-
-
-
-
-
-
-
-
-
上一篇:开关电源测试规范
下一篇:雅思基础词汇-3500