Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Building high-performance GraphQL APIs

Building high-performance GraphQL APIs

One day you decided to give GraphQL a try and implement an API of your new application using GraphQL. You deployed the app to production, but after a while, you realized, that responses are not super fast. How to find out what makes your GraphQL endpoint slow?

We’ll discuss how queries are executed and what makes processing slower. After that, we’ll learn how to measure performance in the GraphQL era and determine the part we should improve. Finally, we’ll discuss possible solutions and some advanced technics to keep your GraphQL app fast!

Dmitry Tsepelev

May 28, 2021
Tweet

More Decks by Dmitry Tsepelev

Other Decks in Programming

Transcript

  1. Building high–performance
    GraphQL APIs
    Dmitry Tsepelev, Evil Martians

    View Slide

  2. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev
    • an API query language


    • an execution engine
    2

    View Slide

  3. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 3

    View Slide

  4. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 4
    🌍 https:
    / /
    youtu.be/CjOwKbf8L3I?t=9615

    View Slide

  5. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 5
    🌍 https:
    / /
    evl.ms/blog/graphql
    -
    on
    -
    rails-1-from
    -
    zero
    -
    to
    -
    the
    -
    f
    i
    rst
    -
    query

    View Slide

  6. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 6
    Query path

    View Slide

  7. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 7
    Query path

    View Slide

  8. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 8
    Query path

    View Slide

  9. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 9
    Query path

    View Slide

  10. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 10
    Query path

    View Slide

  11. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 11
    Query path

    View Slide

  12. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 12
    Query path

    View Slide

  13. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 13
    Query path

    View Slide

  14. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 14
    Query path

    View Slide

  15. Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev
    HOW TO FIND THE BOTTLENECK


    HOW TO FIX COMMON ISSUES


    WANT MORE SPEED?

    View Slide

  16. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 16
    Tracers
    class MySchema < GraphQL
    : :
    Schema


    use GraphQL
    : :
    Tracing
    : :
    NewRelicTracing


    end

    View Slide

  17. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 17
    yabeda
    🌍 https:
    / /
    github.com/yabeda
    -
    rb/yabeda
    -
    graphql
    gem 'yabeda
    -
    graphql'


    gem 'yabeda
    -
    prometheus'


    gem 'yabeda
    -
    rails'


    class YourAppSchema < GraphQL
    : :
    Schema


    use Yabeda
    : :
    GraphQL


    end
    Available metrics:


    graphql_
    fi
    elds_request_count


    graphql_
    fi
    eld_resolve_runtime


    query_
    fi
    elds_count


    mutation_
    fi
    elds_count

    View Slide

  18. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 18
    🌍 https:
    / /
    book.productionreadygraphql.com

    View Slide

  19. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 19
    Client headers
    GQL-Client-Name: ios
    -
    app


    GQL-Client-Version: 1.2.4
    support many client applications


    easier to understand if a concrete query should
    be improved

    View Slide

  20. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 20
    Finding a bottleneck using tracer
    class GraphqlSchema < GraphQL
    : :
    Schema


    tracer = Class.new do


    class
    < <
    self


    def trace(key, data)


    started_at = Time.now


    yield.tap do


    t = (Time.now - started_at).round(5)


    Rails.logger.info "
    # {
    key} (
    # {
    "%.5f" % t})"


    end


    end


    end


    end


    tracer(tracer)


    end

    View Slide

  21. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 21
    lex (0.00036)


    parse (0.00040)


    validate (0.00142)





    execute_f
    i
    eld (0.18968)


    authorized (0.00003)


    execute_f
    i
    eld (0.00002)


    execute_query (0.19087)


    execute_query_lazy (0.00005)


    execute_multiplex (0.19627)
    Finding a bottleneck using tracer

    View Slide

  22. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 22
    class GraphqlSchema < GraphQL
    : :
    Schema


    tracer = Class.new do


    class
    < <
    self


    def trace(key, data)


    started_at = Time.now


    yield.tap do


    t = (Time.now - started_at).round(5)


    if key
    = =
    "execute_f
    i
    eld"


    Rails.logger.info "
    # {
    key} (
    # {
    "%.5f" % t})
    : # {
    data[:f
    i
    eld].name}"


    else


    Rails.logger.info "
    # {
    key} (
    # {
    "%.5f" % t})" if t > 0.0001


    end


    end


    end


    end


    end


    tracer(tracer)


    end
    Looking for a slow
    fi
    eld

    View Slide

  23. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 23
    lex (0.00018)


    parse (0.00020)


    validate (0.00118)





    execute_f
    i
    eld (0.16963)
    :
    f
    i
    eld1


    execute_f
    i
    eld (0.00001)
    :
    f
    i
    eld2


    execute_query (0.17047)


    execute_multiplex (0.17467)
    Looking for a slow
    fi
    eld

    View Slide

  24. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 24
    Directive pro
    fi
    ling
    🌍 https:
    / /
    evl.ms/chronicles/catch
    -
    a
    -
    batch
    -
    making
    -
    mayhem-5-times
    -
    more
    -
    responsive

    View Slide

  25. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 25
    class Pp < GraphQL
    : :
    Schema
    : :
    Directive


    class Prof
    i
    ler < Types
    : :
    BaseEnum


    value :mem


    value :stack


    value :ruby


    end


    locations(GraphQL
    : :
    Schema
    : :
    Directive
    : :
    FIELD)


    argument :type, Prof
    i
    ler, required: true


    class
    < <
    self


    def resolve(_object, arguments, context)


    f
    i
    eld_name = context.query.selected_operation.selections.f
    i
    rst.name


    msg = send(arguments[:type], f
    i
    eld_name) do


    yield


    end


    $stdout.puts "\e[34m[PP]
    # {
    msg.join}\e[0m"


    context.namespace(:interpreter)[:runtime].write_in_response(["pp"], msg)


    end


    def stack(f
    i
    eld)


    require "stackprof"


    report_path = Rails.root.join("tmp/
    # {
    f
    i
    eld}_stackprof.dump")


    StackProf.run(mode: :cpu, raw: true, out: report_path) do


    yield


    end


    end


    end


    end
    Directive pro
    fi
    ling

    View Slide

  26. Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev
    HOW TO FIND THE BOTTLENECK


    HOW TO FIX COMMON ISSUES


    WANT MORE SPEED?

    View Slide

  27. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 27

    View Slide

  28. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 28
    Parser benchmark
    2 f
    i
    elds, 1 nested level:


    query {


    f
    i
    eld1 {


    f
    i
    eld1


    f
    i
    eld2


    }


    f
    i
    eld2 {


    f
    i
    eld1


    f
    i
    eld2


    }


    }
    🌍 https:
    / /
    gist.github.com/DmitryTsepelev/
    36e290cf64b4ec0b18294d0a57fb26ff#f
    i
    le-1_result
    -
    md

    View Slide

  29. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 29
    🚨 Parsing takes a lot of time
    💡 Let's keep queries simple!
    user system total real


    1 f
    i
    elds, 0 nested levels: 0.000153 0.000002 0.000155 ( 0.000152)


    1 f
    i
    elds, 2 nested levels: 0.000188 0.000001 0.000189 ( 0.000187)


    1 f
    i
    elds, 4 nested levels: 0.000252 0.000001 0.000253 ( 0.000240)


    2 f
    i
    elds, 0 nested levels: 0.000134 0.000013 0.000147 ( 0.000132)


    2 f
    i
    elds, 2 nested levels: 0.000420 0.000006 0.000426 ( 0.000411)


    2 f
    i
    elds, 4 nested levels: 0.001695 0.000001 0.001696 ( 0.001694)


    4 f
    i
    elds, 0 nested levels: 0.000154 0.000001 0.000155 ( 0.000153)


    4 f
    i
    elds, 2 nested levels: 0.001744 0.000001 0.001745 ( 0.001744)


    4 f
    i
    elds, 4 nested levels: 0.032188 0.000269 0.032457 ( 0.032581)


    8 f
    i
    elds, 0 nested levels: 0.000316 0.000034 0.000350 ( 0.000352)


    8 f
    i
    elds, 2 nested levels: 0.013464 0.000218 0.013682 ( 0.013917)


    8 f
    i
    elds, 4 nested levels: 0.910159 0.008081 0.918240 ( 0.919507)
    🌍 https:
    / /
    gist.github.com/DmitryTsepelev/
    36e290cf64b4ec0b18294d0a57fb26ff#f
    i
    le-1_result
    -
    md

    View Slide

  30. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 30
    🚨 Response building is slow
    🌍 https:
    / /
    github.com/rmosolgo/graphql
    -
    ruby/issues/3213
    354.91 execute_multiplex


    196.75 execute_multiplex:execute_query


    62.66 execute_multiplex:execute_query:execute_f
    i
    eld


    7.78 execute_multiplex:execute_query:authorized


    152.18 execute_multiplex:execute_query_lazy


    33.86 execute_multiplex:execute_query_lazy:execute_f
    i
    eld


    8.61 execute_multiplex:execute_query_lazy:execute_f
    i
    eld_lazy


    2.24 execute_multiplex:execute_query_lazy:authorized


    5.81 execute_multiplex:analyze_multiplex


    3.47 execute_multiplex:analyze_multiplex:validate


    1.39 execute_multiplex:analyze_multiplex:lex


    0.72 execute_multiplex:analyze_multiplex:parse


    0.01 execute_multiplex:analyze_multiplex:analyze_query
    💡 Let's reduce response size!

    View Slide

  31. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 31
    💡 Update graphql-ruby!
    user system total real


    1.11.4 (no interpreter) 1.069720 0.003178 1.072898 ( 1.076896)


    1.11.4 (with interpreter) 1.045195 0.002486 1.047681 ( 1.050175)


    1.12.4 1.020112 0.001398 1.021510 ( 1.022449)
    Time:
    Memory:
    1.11.4 (no interpreter)


    Total allocated: 98.99 kB (943 objects)


    Total retained: 5.38 kB (29 objects)


    allocated memory by gem


    -----------------------------------


    79.71 kB graphql-1.11.5


    7.67 kB other


    6.05 kB ostruct


    2.70 kB set


    2.49 kB racc


    256.00 B time


    120.00 B forwardable
    1.12.4


    Total allocated: 55.20 kB (689 objects)


    Total retained: 0 B (0 objects)


    allocated memory by gem


    -----------------------------------


    40.76 kB graphql-1.12.4


    8.55 kB other


    5.64 kB ostruct


    256.00 B time

    View Slide

  32. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 32
    🚨 Database requests are slow
    💡 improve requests


    💡
    fi
    nd and
    fi
    x N+1

    View Slide

  33. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 33
    💡 Fighting N+1: request everything!
    class Resolvers
    : :
    FeedResolverPreload < Resolvers
    : :
    BaseResolver


    type [Types
    : :
    Tweet], null: false


    def resolve


    FeedBuilder.for(current_user).includes(:author)


    end


    end

    View Slide

  34. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 34
    💡 Fighting N+1: lookahead
    class Resolvers
    : :
    FeedResolverLookahead < Resolvers
    : :
    BaseResolver


    type [Types
    : :
    Tweet], null: false


    extras [:lookahead]


    def resolve(lookahead:)


    FeedBuilder.for(current_user)


    .merge(relation_with_includes(lookahead))


    end


    private


    def relation_with_includes(lookahead)


    # .selects?(:author) returns true when author f
    i
    eld is requested


    return Tweet.all unless lookahead.selects?(:author)


    Tweet.includes(:author)


    end


    end

    View Slide

  35. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 35
    💡 Fighting N+1: lazy preloading
    gem "ar_lazy_preload"


    class Resolvers
    : :
    FeedResolverLazyPreload < Resolvers
    : :
    BaseResolver


    type [Types
    : :
    Tweet], null: false


    def resolve


    FeedBuilder.for(current_user).lazy_preload(:author)


    end


    end
    🌍 https:
    / /
    github.com/DmitryTsepelev/ar_lazy_preload

    View Slide

  36. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 36
    💡 Fighting N+1: what else?
    🌍 https:
    / /
    evilmartians.com/chronicles/how
    -
    to
    -
    graphql
    -
    with
    -
    ruby
    -
    rails
    -
    active
    -
    record
    -
    and
    -
    no
    -
    n
    -
    plus
    -
    one
    💡 lazy resolvers


    💡 batch loading


    💡 built–in dataloader (>= 1.12.0)

    View Slide

  37. Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev
    HOW TO FIND THE BOTTLENECK


    HOW TO FIX COMMON ISSUES


    WANT MORE SPEED?

    View Slide

  38. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 38
    HTTP caching
    Cache-Control/Expires — time–to–live for data


    Last-Modif
    i
    ed/If-Modif
    i
    ed-Since — moment
    when data was changed


    ETag — unique resource identi
    fi
    er

    View Slide

  39. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 39
    HTTP caching
    😔 POST requests cannot be cached

    View Slide

  40. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 40
    HTTP caching
    Rails.application.routes.draw do


    get "/graphql", to: "graphql#execute"


    post "/graphql", to: "graphql#execute"


    end


    class GraphqlController < ApplicationController


    def execute


    if operation_name
    ! =
    GET_BANNERS
    | |

    stale?(etag: Date.current)


    render json: GraphqlSchema.execute(query)


    end


    end


    end
    🌍 https:
    / /
    guides.rubyonrails.org/caching_with_rails.html#conditional
    -
    get
    -
    support

    View Slide

  41. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 41
    Apollo Persisted Queries
    🌍 https:
    / / w w w
    .apollographql.com/docs/apollo
    -
    server/performance/apq/

    View Slide

  42. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 42
    Persisted Queries
    class GraphqlSchema < GraphQL
    : :
    Schema


    use GraphQL
    : :
    PersistedQueries


    end


    class GraphqlController < ApplicationController


    def execute


    GraphqlSchema.execute(


    params[:query],


    variables: ensure_hash(params[:variables]),


    context: {


    extensions: ensure_hash(params[:extensions])


    },


    operation_name: params[:operationName]


    )


    end


    end
    🌍 https:
    / /
    github.com/DmitryTsepelev/graphql
    -
    ruby
    -
    persisted_queries

    View Slide

  43. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 43
    🚨 Client batching
    client can group multiple GraphQL requests to a
    single HTTP request


    reduces network load


    makes parsing and response building longer

    View Slide

  44. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 44
    💡 Lets skip parsing!
    user system total real


    1 f
    i
    elds, 0 nested levels: 0.000153 0.000002 0.000155 ( 0.000152)


    1 f
    i
    elds, 2 nested levels: 0.000188 0.000001 0.000189 ( 0.000187)


    1 f
    i
    elds, 4 nested levels: 0.000252 0.000001 0.000253 ( 0.000240)


    2 f
    i
    elds, 0 nested levels: 0.000134 0.000013 0.000147 ( 0.000132)


    2 f
    i
    elds, 2 nested levels: 0.000420 0.000006 0.000426 ( 0.000411)


    2 f
    i
    elds, 4 nested levels: 0.001695 0.000001 0.001696 ( 0.001694)


    4 f
    i
    elds, 0 nested levels: 0.000154 0.000001 0.000155 ( 0.000153)


    4 f
    i
    elds, 2 nested levels: 0.001744 0.000001 0.001745 ( 0.001744)


    4 f
    i
    elds, 4 nested levels: 0.032188 0.000269 0.032457 ( 0.032581)


    8 f
    i
    elds, 0 nested levels: 0.000316 0.000034 0.000350 ( 0.000352)


    8 f
    i
    elds, 2 nested levels: 0.013464 0.000218 0.013682 ( 0.013917)


    8 f
    i
    elds, 4 nested levels: 0.910159 0.008081 0.918240 ( 0.919507)
    🌍 https:
    / /
    gist.github.com/DmitryTsepelev/
    36e290cf64b4ec0b18294d0a57fb26ff#f
    i
    le-1_result
    -
    md
    🚨 Parsing takes a lot of time

    View Slide

  45. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 45
    💡 Save AST instead of query text

    View Slide

  46. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 46
    class GraphqlSchema < GraphQL
    : :
    Schema


    use GraphQL
    : :
    PersistedQueries,


    compiled_queries: true


    end
    🌍 https:
    / /
    github.com/DmitryTsepelev/graphql
    -
    ruby
    -
    persisted_queries
    💡 Save AST instead of query text

    View Slide

  47. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 47
    Compiled Queries: benchmark
    Plain schema:


    user system total real


    50 f
    i
    elds (nested) 0.056927 0.000995 0.057922 ( 0.057997)


    100 f
    i
    elds (nested) 0.245235 0.001336 0.246571 ( 0.246727)


    200 f
    i
    elds (nested) 0.974444 0.006531 0.980975 ( 0.981810)


    300 f
    i
    elds (nested) 2.175855 0.012773 2.188628 ( 2.190130)


    Schema with compiled queries:


    user system total real


    50 f
    i
    elds (nested) 0.029933 0.000087 0.030020 ( 0.030040)


    100 f
    i
    elds (nested) 0.133933 0.000502 0.134435 ( 0.134756)


    200 f
    i
    elds (nested) 0.495052 0.003545 0.498597 ( 0.499452)


    300 f
    i
    elds (nested) 1.041463 0.005130 1.046593 ( 1.047137)
    🌍 https:
    / /
    github.com/DmitryTsepelev/graphql
    -
    ruby
    -
    persisted_queries
    2 f
    i
    elds (nested)
    :

    query {


    f
    i
    eld1 {


    f
    i
    eld1


    f
    i
    eld2


    }


    f
    i
    eld2 {


    f
    i
    eld1


    f
    i
    eld2


    }


    }

    View Slide

  48. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 48
    Response caching
    query {


    currentUser {


    displayName


    avatarURL


    }


    }


    query {


    tweets {


    text


    author {


    displayName


    avatarURL


    }


    }


    }
    clients can send different queries


    queries can share some fragments


    let's cache parts of responses!

    View Slide

  49. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 49
    Response caching

    View Slide

  50. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 50
    graphql-cache
    class GraphqSchema < GraphQL
    : :
    Schema


    use GraphQL
    : :
    Cache


    end


    class BaseType < GraphQL
    : :
    Schema
    : :
    Object


    f
    i
    eld_class GraphQL
    : :
    Cache
    : :
    Field


    end


    class PostType < BaseObject


    f
    i
    eld :id, ID, null: false


    f
    i
    eld :title, String, null: false, cache: true


    end
    🚨 Does not work with Interpreter!

    View Slide

  51. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 51
    raw_value
    class BannerType < GraphQL
    : :
    Schema
    : :
    Object


    f
    i
    eld :id, String, null: false


    f
    i
    eld :image_url, String, null: false


    end


    class Query < GraphQL
    : :
    Schema
    : :
    Object


    f
    i
    eld :banners, [BannerType], null: false


    def expansion_raw


    raw_value(


    [


    { id: 1, image_url: "
    . . .
    " },


    { id: 2, image_url: "
    . . .
    " }


    ]


    )


    end


    end
    🌍 https:
    / /
    github.com/rmosolgo/graphql
    -
    ruby/pull/2699

    View Slide

  52. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 52
    graphql-fragment_cache
    class GraphqSchema < GraphQL
    : :
    Schema


    use GraphQL
    : :
    FragmentCache


    end


    class BaseType < GraphQL
    : :
    Schema
    : :
    Object


    include GraphQL
    : :
    FragmentCache
    : :
    Object


    end


    class PostType < BaseObject


    f
    i
    eld :id, ID, null: false


    f
    i
    eld :title, String, null: false, cache_fragment: true


    end
    🌍 https:
    / /
    github.com/DmitryTsepelev/graphql
    -
    ruby
    -
    fragment_cache

    View Slide

  53. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 53
    set up monitoring before any optimizations


    tracer can help to debug a slow query


    in case of slow parsing—simplify queries or try compiled
    queries


    eliminate N+1


    small responses are serialized faster


    HTTP caching is possible


    heavy response fragments can be cached
    To sum up

    View Slide

  54. evl.ms/blog
    @dmitrytsepelev @evilmartians
    evl.ms/telegram
    Thank you!
    RUBYRUSSIA 2019
    Ruby Meetup 13 DmitryTsepelev
    @dmitrytsepelev
    @dmitrytsepelev
    54

    View Slide