Scaling to 1 million active GraphQL subscriptions (live queries)

Hasura is a GraphQL engine on Postgres that provides instant GraphQL APIs with authorization. Read more at hasura.ioand on github.com/hasura/graphql-engine.

Hasura allows 'live queries' for clients (over GraphQL subscriptions). For example, a food ordering app can use a live query to show the live-status of an order for a particular user.

This document describes Hasura's architecture which lets you scale to handle a million active live queries.

TL;DR:

The setup: Each client (a web/mobile app) subscribes to data or a live-result with an auth token. The data is in Postgres. 1 million rows are updated in Postgres every second (ensuring a new result pushed per client). Hasura is the GraphQL API provider (with authorization).

The test: How many concurrent live subscriptions (clients) can Hasura handle? Does Hasura scale vertically and/or horizontally?

single instance configuration No. of active live queries CPU load average
1xCPU, 2GB RAM 5000 60%
2xCPU, 4GB RAM 10000 73%
4xCPU, 8GB RAM 20000 90%

At 1 million live queries, Postgres is under about 28% load with peak number of connections being around 850.

Notes on configuration:

  • AWS RDS postgres, Fargate, ELB were used with their default configurations and without any tuning.
  • RDS Postgres: 16xCPU, 64GB RAM, Postgres 11
  • Hasura running on Fargate (4xCPU, 8GB RAM per instance) with default configurations

GraphQL and subscriptions

GraphQL makes it easy for app developers to query for precisely the data they want from their API.

For example, let’s say we’re building a food delivery app. Here’s what the schema might look like on Postgres:

For an app screen showing a “order status” for the current user for their order, a GraphQL query would fetch the latest order status and the location of the agent delivering the order.

Underneath it, this query is sent as a string to a webserver that parses it, applies authorization rules and makes appropriate calls to things like databases to fetch the data for the app. It sends this data, in the exact shape that was requested, as a JSON.

Enter live-queries: Live queries is the idea of being able to subscribe to the latest result of a particular query. As the underlying data changes, the server should push the latest result to the client.

On the surface this is a perfect fit with GraphQL because GraphQL clients support subscriptions that take care of dealing with messy websocket connections. Converting a query to a live query might look as simple as replacing the word query with subscription for the client. That is, if the GraphQL server can implement it.

Implementing GraphQL live-queries

Implementing live-queries is painful. Once you have a database query that has all the authorization rules, it might be possible to incrementally compute the result of the query as events happen. But this is practically challenging to do at the web-service layer. For databases like Postgres, it is equivalent to the hard problem of keeping a materialized view up to date as underlying tables change. An alternative approach is to refetch all the data for a particular query (with the appropriate authorization rules for the specific client). This is the approach we currently take.

Secondly, building a webserver to handle websockets in a scalable way is also sometimes a little hairy, but certain frameworks and languages do make the concurrent programming required a little more tractable.

Refetching results for a GraphQL query

To understand why refetching a GraphQL query is hard, let’s look at how a GraphQL query is typically processed:

The authorization + data fetching logic has to run for each “node” in the GraphQL query. This is scary, because even a slightly large query fetching a collection could bring down the database quite easily. The N+1 query problem, also common with badly implemented ORMs, is bad for your database and makes it hard to optimally query Postgres. Data loader type patterns can alleviate the problem, but will still query the underlying Postgres database multiple times (reduces to as many nodes in the GraphQL query from as many items in the response).

For live queries, this problem becomes worse, because each client’s query will translate into an independent refetch. Even though the queries are the “same”, since the authorization rules create different session variables, independent fetches are required for each client.

Hasura approach

Is it possible to do better? What if declarative mapping from the data models to the GraphQL schema could be used to create a single SQL query to the database? This would avoid multiple hits to the database, whether there are a large number of items in the response or the number of nodes in the GraphQL query are large.

Idea #1: “Compile” a GraphQL query to a single SQL query

Part of Hasura is a transpiler that uses the metadata of mapping information for the data models to the GraphQL schema to “compile” GraphQL queries to the SQL queries to fetch data from the database.

GraphQL query → GraphQL AST → SQL AST → SQL

This gets rid of the N+1 query problem and allows the database to optimise data-fetching now that it can see the entire query.

But this in itself isn't enough as resolvers also enforce authorization rules by only fetching the data that is allowed. We will therefore need to embed these authorization rules into the generated SQL.

Idea #2: Make authorization declarative

Authorization when it comes to accessing data is essentially a constraint that depends on the values of data (or rows) being fetched combined with application-user specific “session variables” that are provided dynamically. For example, in the most trivial case, a row might container a user_id that denotes the data ownership. Or documents that are viewable by a user might be represented in a related table, document_viewers. In other scenarios the session variable itself might contain the data ownership information pertinent to a row, for eg, an account manager has access to any account [1,2,3…] where that information is not present in the current database but present in the session variable (probably provided by some other data system).

To model this, we implemented an authorization layer similar to Postgres RLS at the API layer to provide a declarative framework to configure access control. If you’re familiar with RLS, the analogy is that the “current session” variables in a SQL query are now HTTP “session-variables” coming from cookies or JWTs or HTTP headers.

Incidentally, because we had started the engineering work behind Hasura many years ago, we ended up implementing Postgres RLS features at the application layer before it landed in Postgres. We even had the same bug in our equivalent of the insert returning clause that Postgres RLS fixed 

一篇来自hasura graphql-engine 百万级别live query 的实践的更多相关文章

  1. epoll的内部实现 & 百万级别句柄监听 & lt和et模式非常好的解释

    epoll是Linux高效网络的基础,比如event poll(例如nodejs),是使用libev,而libev的底层就是epoll(只不过不同的平台可能用epoll,可能用kqueue). epo ...

  2. 百万级别数据Excel导出优化

    前提 这篇文章不是标题党,下文会通过一个仿真例子分析如何优化百万级别数据Excel导出. 笔者负责维护的一个数据查询和数据导出服务是一个相对远古的单点应用,在上一次云迁移之后扩展为双节点部署,但是发现 ...

  3. Facebook兆级别图片存储及每秒百万级别图片查询原理

    前言 Facebook(后面简称fb)是世界最大的社交平台,需要存储的数据时刻都在不断剧增(占比最大为图片,每天存储约20亿张,大概是微信的三倍). 那么问题来了,fb是如何存储兆级别的图片?并且又是 ...

  4. 使用表类型(Table Type-SqlServer)实现百万级别的数据一次性毫秒级别插入

    使用表类型(Table Type)实现百万级别的数据一次性插入 思路 1 创建表类型(TaBleType)         2 创建添加存储过程         3 使用C#语言构建一个DataTab ...

  5. JAVA使用POI如何导出百万级别数据(转)

    https://blog.csdn.net/happyljw/article/details/52809244   用过POI的人都知道,在POI以前的版本中并不支持大数据量的处理,如果数据量过多还会 ...

  6. Hasura GraphQL schema 生成是如何工作的

    不像大部分的graphql 引擎,使用标准的graphql 规范的处理模型,Hasura graphql 不存在resolver 的概念(实际上是有的,只是转换为了sql语法) 以下是Hasura g ...

  7. 通过torodb && hasura graphql 让mongodb 快速支持graphql api

    torodb 可以方便的将mongo 数据实时同步到pg,hasura graphql 可以方便的将pg 数据暴露为graphql api,集成在一起真的很方便 环境准备 docker-compose ...

  8. hasura graphql server 集成gatsby

    hasura graphql server 社区基于gatsby-source-graphql 开发了gatsby-postgres-graphql 插件, 可以快速的开发丰富的网站 基本使用 安装h ...

  9. hasura graphql server event trigger 试用

    hasura graphql server 是一个很不错的graphql 引擎,当前版本已经支持event triiger 了 使用此功能我们可以方便的集成webhook功能,实现灵活,稳定,快捷的消 ...

随机推荐

  1. golang --- time包常用函数以及基础的类型转换

    1.[]byte转为string: package main import ( "fmt" ) func main() { data := [4]byte{0x31, 0x32, ...

  2. SQLite介绍和使用

    SQLite特点: (1)轻量级,跨平台的关系型数据库,所以支持视图,事务,触发器等. (2)零配置-无需安装和管理配置,存储在单一磁盘文件中的完整的数据库 (3)数据库文件可共享,支持多种开发语言. ...

  3. rabbitmq监控之消息确认ack

    rabbitmq springboot ack 监控 一.测试环境 二.启动测试 一直以来,学习rabbitmq都是跟着各种各样的教程.博客.视频和文档,撸起袖子就是干!!!最后,也成功了. 当然,成 ...

  4. 30个关于Shell脚本的经典案例(下)

    本文目录 21.从FTP服务器下载文件 22.连续输入5个100以内的数字,统计和.最小和最大 23.将结果分别赋值给变量 24.批量修改文件名 25.统计当前目录中以.html结尾的文件总大 26. ...

  5. PHP防止SQL注入攻击和XSS攻击

    代码如下: /** * 防SQL注入和XSS攻击 * @param $arr */ function SafeFilter (&$arr) { $ra=Array('/([\x00-\x08, ...

  6. 【TBarCode SDK教程】TBarCode SDK 如何在 Microsoft Office 中工作?

    使用条形码软件组件 TBarCode SDK,你可以在 Microsoft Office 中快速且简便地创建各种条形码.都不需要任何编程的技巧,只需要点击几次鼠标就可以将TBarCode SDK集成到 ...

  7. SpringBoot2.x项目初始化

    1. 项目初始化说明 使用SpringBoot生成器 修改application.properties为application.yml 启动运行SpringBoot项目 2. 初始化项目 Spring ...

  8. MySQL Replication--复制基本工作原理

    复制工作原理(1) master将改变记录到二进制日志(binary log)中(这些记录叫做二进制日志事件,binary log events):(2) slave将master的binary lo ...

  9. 云计算与大数据实验:Hbase shell操作用户表

    [实验目的] 1)了解hbase服务 2)学会hbase shell命令操作用户表 [实验原理] HBase是一个分布式的.面向列的开源数据库,它利用Hadoop HDFS作为其文件存储系统,利用Ha ...

  10. FreeBSD设置开机同步时间

    没有设置开机同步时间的话,重启之后时间不对. 如果装机时没正确设置时区,先设置时区:# tzsetup 用date命令手工设置时间一方面不方便,另一方面也依赖于本地管理员的时钟的正确性,那么网络上不同 ...