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
- exemple:
-
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 !
GraphQL at Scale
By antogyn
GraphQL at Scale
GraphQL at scale
- 74