$30 off During Our Annual Pro Sale. View Details »

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. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 5 🌍 https: / / evl.ms/blog/graphql

    - on - rails-1-from - zero - to - the - f i rst - query
  2. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 16 Tracers class MySchema < GraphQL

    : : Schema use GraphQL : : Tracing : : NewRelicTracing end
  3. 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
  4. 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
  5. 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
  6. 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
  7. 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
  8. 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
  9. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 24 Directive pro fi ling 🌍

    https: / / evl.ms/chronicles/catch - a - batch - making - mayhem-5-times - more - responsive
  10. 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
  11. 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
  12. 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
  13. 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!
  14. 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
  15. 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
  16. 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
  17. 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
  18. 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)
  19. 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
  20. 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
  21. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 41 Apollo Persisted Queries 🌍 https:

    / / w w w .apollographql.com/docs/apollo - server/performance/apq/
  22. 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
  23. 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
  24. 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
  25. 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
  26. 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 } }
  27. 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!
  28. 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!
  29. 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
  30. 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
  31. 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