羽ura 高性能GraphQL到SQL Server的体系结构

哈Ha! 我向您介绍了文章“高性能GraphQL到SQL引擎的体系结构”的翻译。

这是一篇文章的翻译,内容涉及Hasura的内部结构,优化和体系结构解决方案的引入-高性能的轻量级GraphQL服务器,充当Web应用程序和PostgreSQL数据库之间的一层。

它允许您基于现有数据库生成GraphQL模式或创建一个新数据库。 它支持基于Postgres触发器的盒装GraphQL订阅,动态访问控制,自动生成联接,解决了N + 1个请求(分批处理)的问题,等等。


您可以在PostgreSQL中使用外键约束在单个查询中检索层次结构数据。 例如,您可以执行此查询以获取专辑及其对应的曲目(如果在指向“专辑”表的“曲目”表中创建了外键)

{ album (where: {year: {_eq: 2018}}) { title tracks { id title } } } 

您可能已经猜到了,您可以请求任何深度的数据。 该API与访问控制相结合,使Web应用程序可以从PostgreSQL查询数据,而无需编写自己的后端。 它旨在尽快执行查询,具有高带宽,同时节省服务器上的处理器时间和内存消耗。 我们将讨论使我们实现这一目标的体系结构解决方案。

请求生命周期


发送给Hasura的请求经历以下阶段:

  1. 接收会话 :请求进入网关,网关检查密钥(如果有)并添加各种标头,例如标识符和用户角色。
  2. 解析请求 :Hasura接收请求,解析标头以获取有关用户的信息,并基于请求正文创建GraphQL AST。
  3. 请求验证 :检查请求在语义上是否正确,然后应用与用户角色相对应的访问权限。
  4. 查询执行 :查询被转换为SQL并发送给Postgres。
  5. 响应生成 :处理SQL查询的结果并将其发送到客户端( 如果需要网关可以使用gzip )。

目标


要求大致如下:

  1. HTTP堆栈应增加最小的开销,并能够处理许多并发请求以实现高吞吐量。
  2. 通过GraphQL查询快速生成SQL。
  3. 生成的SQL查询对于Postgres应该是有效的。
  4. SQL查询的结果应从Postgres有效地传回。

GraphQL查询处理


有几种方法可获取GraphQL查询所需的数据:

常规解析器


执行GraphQL查询通常涉及为每个字段调用解析器。
在示例请求中,我们获得了2018年发行的专辑,然后为每张专辑请求与之对应的曲目-N + 1请求的经典问题。 查询数量随着查询深度的增加而呈指数增长。

Postgres发出的请求将是:

 SELECT id,title FROM album WHERE year = 2018; 

此请求将把所有专辑退还给我们。 假设请求返回的专辑数量等于N。那么对于每个专辑,我们将执行以下请求:

 SELECT id,title FROM tracks WHERE album_id = <album-id> 

总共,您获得N + 1个查询以获取所有必需的数据。

批处理请求


诸如数据加载器之类的工具旨在使用批处理解决N + 1个请求的问题。 对嵌入式数据进行SQL查询的次数不再取决于初始样本的大小,因为 现在,它会影响GraphQL查询中的节点数。 在这种情况下,需要2个对Postgres的请求才能获得所需的数据:

我们得到相册:

 SELECT id,title FROM album WHERE year = 2018 

我们获得了上一个请求中收到的专辑的曲目:

 SELECT id, title FROM tracks WHERE album_id IN {the list of album ids} 

总共收到2个查询。 我们避免在每个专辑的曲目上执行SQL查询;相反,我们使用WHERE运算符一次在一个查询中获取所有必需的曲目。

加入


Dataloader旨在与不同的数据源一起使用,并且不允许利用特定数据源的功能。 在我们的案例中,Postgres是唯一的数据源,并且像所有关系数据库一样,它提供了使用JOIN运算符通过单个查询从多个表收集数据的功能。 我们可以确定GraphQL查询所需的所有表,并使用JOIN生成单个SQL查询以获取所有数据。 事实证明,使用单个SQL查询可以获取任何GraphQL查询所需的数据。 该数据在发送到客户端之前先进行转换。

这样的要求:

 SELECT album.id as album_id, album.title as album_title, track.id as track_id, track.title as track_title FROM album LEFT OUTER JOIN track ON (album.id = track.album_id) WHERE album.year = 2018 

将返回给我们这样的数据:

 album_id, album_title, track_id, track_title 1, Album1, 1, track1 1, Album1, 2, track2 2, Album2, NULL, NULL 

然后将其转换为JSON并发送给客户端:

 [ { "title" : "Album1", "tracks": [ {"id" : 1, "title": "track1"}, {"id" : 2, "title": "track2"} ] }, { "title" : "Album2", "tracks" : [] } ] 

优化响应生成


我们发现查询处理中的大部分时间都花在了将SQL查询结果转换为JSON的功能上。

在尝试以各种方式优化此功能后,我们决定将其转移到Postgres。 Postgres 9.4( 在Hasura的第一个版本发布时发布 )添加了JSON聚合功能,可帮助我们完成工作。 经过优化之后,SQL查询开始看起来像这样:

 SELECT json_agg(r.*) FROM ( SELECT album.title as title, json_agg(track.*) as tracks FROM album LEFT OUTER JOIN track ON (album.id = track.album_id) WHERE album.year = 2018 GROUP BY album.id ) r 

该查询的结果将只有一列和一行,并且该值将被发送到客户端,而无需任何进一步的转换。 根据我们的测试,这种方法比Haskell转换函数快3到6倍。

准备好的陈述


取决于查询的嵌套级别和使用条件,生成的SQL查询可能非常大且复杂。 通常,Web应用程序具有一组查询,这些查询使用不同的参数重复执行。 例如,上一个查询需要在2017年而不是2018年执行。预准备语句最适合于重复的复杂SQL查询(其中仅更改参数)的情况。

假设此查询是第一次执行:

 { album (where: {year: {_eq: 2018}}) { title tracks { id title } } } 

我们为SQL查询创建一个准备好的语句,而不是执行它:

 PREPARE prep_1 AS SELECT json_agg(r.*) FROM ( SELECT album.title as title, json_agg(track.*) as tracks FROM album LEFT OUTER JOIN track ON (album.id = track.album_id) WHERE album.year = $1 GROUP BY album. 

之后,我们立即执行它:

 EXECUTE prep_1('2018'); 

当您需要执行2017年的GraphQL查询时,我们只需使用不同的参数调用相同的预处理语句即可:

 EXECUTE prep_1('2017'); 

根据GraphQL查询的复杂程度,这将使速度提高大约10-20%。

哈斯克尔


Haskell运作良好的原因有几个:


最后


上面提到的所有优化都带来了非常明显的性能优势:



实际上,与直接调用PostgreSQL相比,低内存消耗和微不足道的延迟使大多数情况下,可以通过调用GraphQL API来替换后端中的ORM。

基准测试:

测试台:

  1. 配备8GB RAM和i7的笔记本电脑
  2. 在同一台计算机上运行的Postgres
  3. wrk用作比较工具,针对各种类型的请求,我们尝试“最大化” RPS
  4. Hasura GraphQL引擎的一个实例
  5. 连接池大小:50
  6. 数据集


请求1:tracks_media_some

 query tracks_media_some { tracks (where: {composer: {_eq: "Kurt Cobain"}}){ id name album { id title } media_type { name } }} 

  • 每秒请求数:1375请求/秒
  • 延迟:17.5ms
  • CPU:〜30%
  • 内存:〜30MB(Hasura)+ 90MB(Postgres)

请求2:tracks_media_all

 query tracks_media_all { tracks { id name media_type { name } }} 

  • 每秒请求数:410 req / s
  • 延迟:59ms
  • CPU:〜100%
  • 内存:〜30MB(Hasura)+ 130MB(Postgres)

要求3:album_tracks_genre_some

 query albums_tracks_genre_some { albums (where: {artist_id: {_eq: 127}}) { id title tracks { id name genre { name } } }} 

  • 每秒请求数:1029个请求/秒
  • 延迟时间:24ms
  • CPU:〜30%
  • 内存:〜30MB(Hasura)+ 90MB(Postgres)

要求4:album_tracks_genre_all

 query albums_tracks_genre_all { albums { id title tracks { id name genre { name } } } 

  • 每秒请求数:328请求/秒
  • 延迟:73毫秒
  • CPU:100%
  • 内存:〜30MB(Hasura)+ 130MB(Postgres)

Source: https://habr.com/ru/post/zh-CN428133/


All Articles