Anthony Giniers

@antogyn

@aginiers

GraphQL at Scale

https://slides.com/antogyn/graphql-at-scale

Staff Engineer @ Swan

ou GraphQL dans la vraie vie

GraphQL, c'est quoi ?

GraphQL est un langage de requête pour des données de type graphe

  • Débuté par Facebook en 2012
  • Open-source depuis 2015

Alternative à REST

- Hiérarchique

Une requête GraphQL est une liste hiérarchique de champs

{
  user(id: 123) {
    id
    name
    account {
      IBAN
    }
  }
}
{
  "user": {
    "id": 123,
    "name": "Anthony Giniers",
    "account": {
      "IBAN": "NL93RABO6684756000"
    }
  }
}

→ Les données ont la même forme que la requête

→ Elles sont accessibles en une seule requête

- Fortement typé

type Query {
  user(id: Int): User
}
  • Garanties sur la forme et la nature de la réponse
  • Un graphe = une url
type User {
  id: Int!
  name: String
  account: Account
}
type Account {
  IBAN: String
}
{
  user(id: 123) {
    id
    name
    account {
      IBAN
    }
  }
}

- Centré sur le produit

Les spécifications sont encodées dans le client

{
  user(id: 123) {
    id
    name
    account {
      IBAN
    }
  }
}

- Introspectif

{
  "__schema": {
    "types": [
      {
        "name": "User",
        "fields": [
          {
            "name": "id",
            "type": {
              "name": "Int"
            }
          },
          {
            "name": "name",
            "type": {
              "name": "String"
            }
          },
          {
            "name": "account",
            "type": {
              "name": "Account"
            }
          }
        ]
      }
      ...
    ]
  }
}
type User {
  id: Int
  name: String
  account: Account
}
{
  __schema {
    types {
      name
      fields {
        name
        type {
          name
        }
      }
    }
  }
}

GraphQL après un tuto

GraphQL en prod

GraphQL en prod dans une architecture distribuée

https://principledgraphql.com/

1. One Graph

  • Tout faire en une requête
  • Catalogue central

Première approche

Ce qu'on nous a vendu

GraphQL Gateway: Limitations

Mise à jour systématique de la gateway

REST

Service

GraphQL Gateway

GraphQL

Service

MAJ

Mise à jour systématique de la gateway

Double travail

Complexité des releases

Ownership: qui met quoi à jour ?

GraphQL Gateway: Limitations

User

Service

GraphQL Gateway

Account

Service

GraphQL Gateway: Limitations

Graphe complexe = Gateway complexe

User

Service

GraphQL Gateway

Account

Service

GraphQL Gateway: Limitations

Graphe complexe = Gateway complexe

User

Service

GraphQL Gateway

Account

Service

type User {
  id: String
  name: String
}

GraphQL Gateway: Limitations

Graphe complexe = Gateway complexe

User

Service

GraphQL Gateway

Account

Service

type Account {
  id: String
  balance: Number
  user: User
}
type User {
  id: String
  name: String
}

GraphQL Gateway: Limitations

Graphe complexe = Gateway complexe

User

Service

GraphQL Gateway

Account

Service

type Account {
  id: String
  balance: Number
  user: User
}
type User {
  id: String
  name: String
}

GraphQL Gateway: Limitations

Graphe complexe = Gateway complexe

User

Service

GraphQL Gateway

Account

Service

type Account {
  id: String
  balance: Number
  user: User
}
extend type User {
  accounts: [Account]
}
type User {
  id: String
  name: String
}

GraphQL Gateway: Limitations

Graphe complexe = Gateway complexe

User

Service

GraphQL Gateway

Account

Service

type Account {
  id: String
  balance: Number
  user: User
}
type User {
  id: String
  name: String
}
extend type User {
  accounts: [Account]
}

GraphQL Gateway: Limitations

Graphe complexe = Gateway complexe

User

Service

GraphQL Gateway

Account

Service

type Account {
  id: String
  balance: Number
  user: User
}
extend type User {
  accounts: [Account]
}
type User {
  id: String
  name: String
}

GraphQL Gateway: Limitations

User

Service

GraphQL Gateway

Account

Service

+ GraphQL Stitching

Graphe complexe = Gateway complexe

https://principledgraphql.com/

2. Federated implementation

GraphQL Federated Schema

User Subgraph

Payment Subgraph

Account
Subgraph

Federation Gateway

GraphQL Federated Schema

User Subgraph

Payment Subgraph

Account
Subgraph

No code

Federation Gateway

GraphQL Federated Schema

Federation Gateway

User Subgraph

Payment Subgraph

Account
Subgraph

Respectent une spec

GraphQL Federated Schema

Federation Gateway

User Subgraph

Payment Subgraph

Account
Subgraph

Respectent une spec

Sont "composables"

GraphQL Federated Schema

type Account {
  id: String
  balance: Number
  user: User
}
type User {
  id: String
  name: String
}

User Subgraph

Account
Subgraph

GraphQL Federated Schema

type Account @key(fields: "id") {
  id: String
  balance: Number
  user: User
}
type User @key(fields: "id") {
  id: String
  name: String
}

User Subgraph

Account
Subgraph

GraphQL Federated Schema

Federation Gateway

query {
  user {
    id
    account {
      balance
    }
  }
}
QueryPlan {
  Sequence {
    Fetch(service: "User") {
      {
        user {
		  id
        }
      }
    },
    Flatten(path: "user") {
      Fetch(service: "Account") {
        {
          ... on User {
            id
          }
        } =>
        {
          ... on User {
            account {
              balance
            }
          }
        }
      },
    },
  },
}
  • https://www.apollographql.com/graphos
    The industry standard platform for GraphQL federation
  • https://the-guild.dev/graphql/mesh

    Federated architecture for any API service

  • https://wundergraph.com/

    The Open-Source GraphQL Federation Solution

  • https://grafbase.com/

    The GraphQL Federation platform

  • ...

https://principledgraphql.com/

3. Track the Schema in a Registry

GraphQL Gateway

User Subgraph

Account
Subgraph

type Account {
  user: User
}
type User {
  id: String
}

User ?

GraphQL Gateway

User.id ?

User Subgraph

Account
Subgraph

type User {
  id: Int
}
type User {
  id: String
}

User Subgraph

Payment Subgraph

Account
Subgraph

GraphQL Registry

GraphQL Gateway

Composition

User Subgraph

Payment Subgraph

Account
Subgraph

Git
GraphQL Registry

GraphQL Gateway

https://principledgraphql.com/

4. Abstract, Demand-Oriented Schema

  • Séparer le schéma de son implémentation

User Subgraph

type User {
  first_name: String
}

Account
Subgraph

type Account {
  legalRepresentative: User
}
query {
  account {
    legalRepresentative {
      first_name
    }
  }
}

https://principledgraphql.com/

5. Use an Agile Approach to Schema Development

  • Evolutions incrémentales
{
  user {
    id
    name
  }
}
type User {
  id: Int
  name: String @deprecated
  identity: Identity
}

type Identity {
  name: String
}
{
  user {
    id
    identity {
      name
    }
  }
}
type User {
  id: Int
  name: String
}

Breaking changes

https://principledgraphql.com/

6. Iteratively Improve Performance

  • Se focaliser sur ce qui est vraiment utilisé en production

Federation Gateway

query {
  user {
    id
    account {
      balance
    }
  }
}
QueryPlan {
  Sequence {
    Fetch(service: "User") {
      {
        user {
		  id
        }
      }
    },
    Flatten(path: "user") {
      Fetch(service: "Account") {
        {
          ... on User {
            id
          }
        } =>
        {
          ... on User {
            account {
              balance
            }
          }
        }
      },
    },
  },
}

Query planner

Apollo Gateway => Apollo Router

Réécrit en Rust

Mesh => Conductor

Cosmo (Wundergraph)

Réécrit en Rust

Ecrit en Go

Query plan caching

N+1 Query Problem

type Query {
  accounts: [Account]
}

type Account {
  lastPayment: Payment
}

type Payment {
  amount: Int!
}
query {             
  accounts {        # N accounts => 1 requête
    lastPayment {   # 1 payment  => 1 + N requêtes
      amount
    }
  }
}

N+1 Query Problem

type Query {
  accounts: [Account]
}

type Account {
  lastPayment: Payment
}

type Payment {
  amount: Int!
}
query {             
  accounts {        # N accounts => 1 requête
    lastPayment {   # 1 payment  => 1 + N requêtes
      amount
    }
  }
}

N+1 Query Problem

type Query {
  accounts: [Account]
}

type Account {
  lastPayment: Payment
}

type Payment {
  amount: Int!
}
query {             
  accounts {        # N accounts => 1 requête
    lastPayment {   # 1 payment  => 1 + N requêtes
      amount
    }
  }
}

N+1 Query Problem

type Query {
  accounts: [Account]
}

type Account {
  lastPayment: Payment
}

type Payment {
  amount: Int!
}
query {             
  accounts {        # N accounts => 1 requête
    lastPayment {   # 1 payment  => 1 + N requêtes
      amount
    }
  }
}

Solutions: spécifiques à chaque language/platforme

  • package graphql/dataloader pour Node.js 
  • package java-dataloader pour Java
  • solution intégrée à Caliban pour Scala
  • ...

 

Mais est-ce vraiment un problème ?

type Query {
  accounts: [Account]
}

type Account {
  lastPayment: Payment
}

type Payment {
  amount: Int!
}
  • /accounts
  • /accounts/:id/payments/last

... et on a N+1 requêtes

En REST:

query {             
  accounts {        # N accounts => 1 requête
    lastPayment {   # 1 payment  => 1 + N requêtes
      amount
    }
  }
}
type User {
  id: String
  name: String
  accounts: [Account]
}

Pas plus que la requête

Implémentation naïve: systématiquement récupérer les accounts d'un User

query {
  user {
    id
    name
  }
}
query {
  user {
    id
    name
    firstName
    language
    birthDate
    accounts {
      id
      name
      iban
      balance
      virtualIbans
      memberships {
        permissions
        user {
          id
          name
          firstName
        }
      }
      cards {
        id
      }
      transactions {
        id
        amount {
          value
          currency
        }
        reference
        type
        debtor {
          id
          name
          firstName
        }
        creditor {
          id
          name
          firstName
          accountNumber
        }
      }
    }
  }
}

Federation Gateway

query {
  user {
    id
    name
    firstName
    language
    birthDate
    accounts {
      id
      name
      iban
      balance
      virtualIbans
      memberships {
        permissions
        ...

Automatic Persisted Queries

1

query

Federation Gateway

SHA256(query)

Automatic Persisted Queries

2

query

Automatic Persisted Queries

Federation Gateway

query {
  user {
    id
    name
    firstName
    language
    birthDate
    accounts {
      id
      name
      iban
      balance
      virtualIbans
      memberships {
        permissions
        ...

1

register

Automatic Persisted Queries

Federation Gateway

2

ok

id

Automatic Persisted Queries

Federation Gateway

3

query

id

https://principledgraphql.com/

7. Use GraphQL Metadata to Empower Developers

  • Mettre le plus d'info possible dans le graphe
    • exemple: @deprecated
  • Avoir des outils qui profitent le plus possible des informations du graphe

https://principledgraphql.com/

8. Access and Demand Control

  • Access = Authentication / Authorization
  • Demand = Rate Limiting

Authentication

En amont, dans une autre API Gateway

Dans la GraphQL Federation Gateway

Dans chaque subgraph

Authorization

type Query {
  users: [User] @requiresScopes(scopes: [["read:users"]])
}

type Mutation {
  updateUser(input: UpdateUserInput!): User @authenticated
}

Au niveau du schéma (+ Federation Gateway)

Au niveau des subgraphs

Demand control

type Query {
  user: User
}

type User {
  id: String
  friends(limit: Int!): [User]
}
query {
  user {
    friends(limit: 100) {
      friends(limit: 100) {
        friends(limit: 100) {
          friends(limit: 100) {
            friends(limit: 100) {
              ...
            }
          }
        }
      }
    }
  }
}
query {
  user {
    friends(limit: 100) {
      friends(limit: 100) {
        friends(limit: 100) {
          friends(limit: 100) {
            friends(limit: 100) {
              ...
            }
          }
        }
      }
    }
  }
}

Level 1 : Limiteur de profondeur

😭

query {
  user {
    friends(limit: 100) {
      friends(limit: 100) {
        friends(limit: 100) {
          id
        }
      }
    }
    friends(limit: 100) {
      friends(limit: 100) {
        friends(limit: 100) {
          id
        }
      }
    }
    friends(limit: 100) {
      friends(limit: 100) {
        friends(limit: 100) {
          id
        }
      }
    }
    friends(limit: 100) {
      friends(limit: 100) {
        friends(limit: 100) {
          id
        }
      }
    }
    ...
  }
}

Level 2 : Limiteur de complexité

type Query {
  user: User @cost(complexity: 5)
}

type User {
  id: String
  friends(limit: Int!): [User] @cost(complexity: 3, multipliers: ["limit"])
}

Level 2 : Limiteur de complexité

type Query {
  user: User @cost(complexity: 5)
}

type User {
  id: String
  friends(limit: Int!): [User] @cost(complexity: 3, multipliers: ["limit"])
}
query {
  user {                         # 5 
    friends(limit: 10) {         # 35 = 5 + 3*10
      friends(limit: 10) {       # 335 = 5 + 3*10 + 3*10*10
        friends(limit: 10) {     # 3335 = 5 + 3*10 + 3*10*10 + 3*10*10*10
          id
        }
      }
    }
    ...
  }
}

Level 2 : Limiteur de complexité

type Query {
  user: User @cost(complexity: 5)
}

type User {
  id: String
  friends(limit: Int!): [User] @cost(complexity: 3, multipliers: ["limit"])
}
query {
  user {                         # 5 
    friends(limit: 10) {         # 35 = 5 + 3*10
      friends(limit: 10) {       # 335 = 5 + 3*10 + 3*10*10
        friends(limit: 10) {     # 3335 = 5 + 3*10 + 3*10*10 + 3*10*10*10
          id
        }
      }
    }
    ...
  }
}

Level 2 : Limiteur de complexité

type Query {
  user: User @cost(complexity: 5)
}

type User {
  id: String
  friends(limit: Int!): [User] @cost(complexity: 3, multipliers: ["limit"])
}
query {
  user {                         # 5 
    friends(limit: 10) {         # 35 = 5 + 3*10
      friends(limit: 10) {       # 335 = 5 + 3*10 + 3*10*10
        friends(limit: 10) {     # 3335 = 5 + 3*10 + 3*10*10 + 3*10*10*10
          id
        }
      }
    }
    ...
  }
}

Level 2 : Limiteur de complexité

type Query {
  user: User @cost(complexity: 5)
}

type User {
  id: String
  friends(limit: Int!): [User] @cost(complexity: 3, multipliers: ["limit"])
}
query {
  user {                         # 5 
    friends(limit: 10) {         # 35 = 5 + 3*10
      friends(limit: 10) {       # 335 = 5 + 3*10 + 3*10*10
        friends(limit: 10) {     # 3335 = 5 + 3*10 + 3*10*10 + 3*10*10*10
          id
        }
      }
    }
    ...
  }
}

Level 2 : Limiteur de complexité

type Query {
  user: User @cost(complexity: 5)
}

type User {
  id: String
  friends(limit: Int!): [User] @cost(complexity: 3, multipliers: ["limit"])
}
query {
  user {                         # 5 
    friends(limit: 10) {         # 35 = 5 + 3*10
      friends(limit: 10) {       # 335 = 5 + 3*10 + 3*10*10
        friends(limit: 10) {     # 3335 = 5 + 3*10 + 3*10*10 + 3*10*10*10
          id
        }
      }
    }
    ...
  }
}

Level 2 : Limiteur de complexité

type Query {
  user: User @cost(complexity: 5)
}

type User {
  id: String
  friends(limit: Int!): [User] @cost(complexity: 3, multipliers: ["limit"])
}
query {
  user {                         # 5 
    friends(limit: 10) {         # 35 = 5 + 3*10
      friends(limit: 10) {       # 335 = 5 + 3*10 + 3*10*10
        friends(limit: 10) {     # 3335 = 5 + 3*10 + 3*10*10 + 3*10*10*10
          id
        }
      }
    }
    ...
  }
}

😭

query {
  user {
    friends(limit: 15) {
      friends(limit: 10) {
        id
      }
    }
  }
}

... en boucle

Level 3 : Complexity + Rate limiter classique

X req/s

  • Ne tient pas compte de la complexité

Level 4 : Limiteur de complexité par unité de temps

X coût de complexité/min

  • Nativement supporté dans aucune implem de GraphQL Gateway
  • Difficile à comprendre pour les clients

https://principledgraphql.com/

9. Structured Logging

  • Il y a énormément d'informations utiles sur chaque opération
    • qui a fait la requête
    • quels champs ont été demandés
    • ses performances
    • ...
  • Ces informations doivent être centralisées et exploitables

Metrics

95% du travail en une métrique :

temps de réponse + error / resolver

Lier les métriques aux traces

Tracing

Rien à faire sur la Federation Gateway

Bien avoir la propagation sur les subgraphs

Graphql Gateway

Subgraph 1

Subgraph 2

Logs

Lier les logs aux traces

Facultatif: 1 log / resolver

https://principledgraphql.com/

10. Separate the GraphQL Layer from the Service Layer

  • Tout ce qui est relatif au graphe doit être factorisé
    • Federation
    • Access & Demand control
    • Caching
    • ...

Key takeaways

  • Federation architecture
  • Schema registry
  • Utiliser une implémentation récente de la Gateway
  • Utiliser le graphe au maximum
  • Itérer avec les consommateurs de l'API
  • Attention aux problèmes de perf récurrents
  • Demand control à gérer dès le départ

🚀

Merci !