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!

F5c2731f9a4dbfb4af319295a1f0cd28?s=128

Dmitry Tsepelev

May 28, 2021
Tweet

Transcript

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

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

    an execution engine 2
  3. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 3

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

  5. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 5 🌍 https: / / evl.ms/blog/graphql

    - on - rails-1-from - zero - to - the - f i rst - query
  6. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 6 Query path

  7. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 7 Query path

  8. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 8 Query path

  9. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 9 Query path

  10. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 10 Query path

  11. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 11 Query path

  12. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 12 Query path

  13. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 13 Query path

  14. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 14 Query path

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

    HOW TO FIX COMMON ISSUES WANT MORE SPEED?
  16. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 16 Tracers class MySchema < GraphQL

    : : Schema use GraphQL : : Tracing : : NewRelicTracing end
  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
  18. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 18 🌍 https: / / book.productionreadygraphql.com

  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
  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
  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
  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
  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
  24. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 24 Directive pro fi ling 🌍

    https: / / evl.ms/chronicles/catch - a - batch - making - mayhem-5-times - more - responsive
  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
  26. Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev HOW TO FIND THE BOTTLENECK

    HOW TO FIX COMMON ISSUES WANT MORE SPEED?
  27. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 27

  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
  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
  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!
  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
  32. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 32 🚨 Database requests are slow

    💡 improve requests 💡 fi nd and fi x N+1
  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
  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
  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
  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)
  37. Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev HOW TO FIND THE BOTTLENECK

    HOW TO FIX COMMON ISSUES WANT MORE SPEED?
  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
  39. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 39 HTTP caching 😔 POST requests

    cannot be cached
  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
  41. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 41 Apollo Persisted Queries 🌍 https:

    / / w w w .apollographql.com/docs/apollo - server/performance/apq/
  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
  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
  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
  45. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 45 💡 Save AST instead of

    query text
  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
  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 } }
  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!
  49. EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 49 Response caching

  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!
  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
  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
  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
  54. evl.ms/blog @dmitrytsepelev @evilmartians evl.ms/telegram Thank you! RUBYRUSSIA 2019 Ruby Meetup

    13 DmitryTsepelev @dmitrytsepelev @dmitrytsepelev 54