Slide 1

Slide 1 text

Building high–performance GraphQL APIs Dmitry Tsepelev, Evil Martians

Slide 2

Slide 2 text

EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev β€’ an API query language β€’ an execution engine 2

Slide 3

Slide 3 text

EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 3

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 6 Query path

Slide 7

Slide 7 text

EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 7 Query path

Slide 8

Slide 8 text

EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 8 Query path

Slide 9

Slide 9 text

EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 9 Query path

Slide 10

Slide 10 text

EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 10 Query path

Slide 11

Slide 11 text

EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 11 Query path

Slide 12

Slide 12 text

EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 12 Query path

Slide 13

Slide 13 text

EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 13 Query path

Slide 14

Slide 14 text

EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 14 Query path

Slide 15

Slide 15 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev HOW TO FIND THE BOTTLENECK HOW TO FIX COMMON ISSUES WANT MORE SPEED?

Slide 16

Slide 16 text

EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 16 Tracers class MySchema < GraphQL : : Schema use GraphQL : : Tracing : : NewRelicTracing end

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev HOW TO FIND THE BOTTLENECK HOW TO FIX COMMON ISSUES WANT MORE SPEED?

Slide 27

Slide 27 text

EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 27

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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!

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 32 🚨 Database requests are slow πŸ’‘ improve requests πŸ’‘ fi nd and fi x N+1

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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)

Slide 37

Slide 37 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev HOW TO FIND THE BOTTLENECK HOW TO FIX COMMON ISSUES WANT MORE SPEED?

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 39 HTTP caching πŸ˜” POST requests cannot be cached

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 45 πŸ’‘ Save AST instead of query text

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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 } }

Slide 48

Slide 48 text

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!

Slide 49

Slide 49 text

EuRuKo 2021 @dmitrytsepelev @dmitrytsepelev 49 Response caching

Slide 50

Slide 50 text

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!

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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