Slide 1

Slide 1 text

Как ускорить GraphQL API Дмитрий Цепелев, Злые марсиане

Slide 2

Slide 2 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev • an API query language • an execution engine 2

Slide 3

Slide 3 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 3 evilmartians.com

Slide 4

Slide 4 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 4 evilmartians.com

Slide 5

Slide 5 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 5

Slide 6

Slide 6 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 6 DUMP 2019 Saint P Rubyconf 2019 🌍 https:!//youtu.be/xUrLslKdnr8 🌍 https:!//youtu.be/CjOwKbf8L3I?t=9615

Slide 7

Slide 7 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 7 🌍 https:!//evl.ms/blog/graphql-on-rails-1-from-zero-to-the-first-query

Slide 8

Slide 8 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 8 Путь запроса

Slide 9

Slide 9 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 9 Путь запроса

Slide 10

Slide 10 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 10 Путь запроса

Slide 11

Slide 11 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 11 Путь запроса

Slide 12

Slide 12 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 12 Путь запроса

Slide 13

Slide 13 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 13 Путь запроса

Slide 14

Slide 14 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 14 Путь запроса

Slide 15

Slide 15 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 15 Путь запроса

Slide 16

Slide 16 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 16 Путь запроса

Slide 17

Slide 17 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev КАК НАЙТИ УЗКОЕ МЕСТО УСТРАНЯЕМ ЧАСТЫЕ ПРОБЛЕМЫ ЕЩЕ БЫСТРЕЕ?

Slide 18

Slide 18 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 18 Подключаем трейсер class MySchema < GraphQL!::Schema use GraphQL!::Tracing!::NewRelicTracing end

Slide 19

Slide 19 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 19 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 Метрики: graphql_fields_request_count graphql_field_resolve_runtime query_fields_count mutation_fields_count

Slide 20

Slide 20 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 20 🌍 https:!//book.productionreadygraphql.com

Slide 21

Slide 21 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 21 Клиентские заголовки GQL-Client-Name: ios-app GQL-Client-Version: 1.2.4 для поддержки нескольких клиентов проще понять, нужно ли ускорять конкретный запрос

Slide 22

Slide 22 text

Ruby Meetup 13 @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) Rails.logger.info "!#{key} (!#{"%.5f" % t})" end end end end tracer(tracer) end

Slide 23

Slide 23 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 23 Ищем узкое место lex (0.00036) parse (0.00040) validate (0.00142) … execute_field (0.18968) authorized (0.00003) execute_field (0.00002) execute_query (0.19087) execute_query_lazy (0.00005) execute_multiplex (0.19627)

Slide 24

Slide 24 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 24 Ищем медленное поле 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_field" Rails.logger.info "!#{key} (!#{"%.5f" % t}): !#{data[:field].name}" else Rails.logger.info "!#{key} (!#{"%.5f" % t})" if t > 0.0001 end end end end end tracer(tracer) end

Slide 25

Slide 25 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 25 lex (0.00018) parse (0.00020) validate (0.00118) … execute_field (0.16963): field1 execute_field (0.00001): field2 execute_query (0.17047) execute_multiplex (0.17467) Ищем медленное поле

Slide 26

Slide 26 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 26 Директивный профайлинг 🌍 https:!//evl.ms/chronicles/catch-a-batch-making-mayhem-5-times- more-responsive

Slide 27

Slide 27 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 27 Директивный профайлинг class Pp < GraphQL!::Schema!::Directive class Profiler < Types!::BaseEnum value :mem value :stack value :ruby end locations(GraphQL!::Schema!::Directive!::FIELD) argument :type, Profiler, required: true class !<< self def resolve(_object, arguments, context) field_name = context.query.selected_operation.selections.first.name msg = send(arguments[:type], field_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(field) require "stackprof" report_path = Rails.root.join("tmp/!#{field}_stackprof.dump") StackProf.run(mode: :cpu, raw: true, out: report_path) do yield end end end end

Slide 28

Slide 28 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev КАК НАЙТИ УЗКОЕ МЕСТО УСТРАНЯЕМ ЧАСТЫЕ ПРОБЛЕМЫ ЕЩЕ БЫСТРЕЕ?

Slide 29

Slide 29 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 29

Slide 30

Slide 30 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 30 Бенчмарк парсера 2 fields, 1 nested level: query { field1 { field1 field2 } field2 { field1 field2 } } 🌍 https:!//gist.github.com/DmitryTsepelev/ 36e290cf64b4ec0b18294d0a57fb26ff#file-1_result-md

Slide 31

Slide 31 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 31 🚨 Парсинг занимает много времени 💡 Нужно сделать запросы проще! user system total real 1 fields, 0 nested levels: 0.000153 0.000002 0.000155 ( 0.000152) 1 fields, 2 nested levels: 0.000188 0.000001 0.000189 ( 0.000187) 1 fields, 4 nested levels: 0.000252 0.000001 0.000253 ( 0.000240) 2 fields, 0 nested levels: 0.000134 0.000013 0.000147 ( 0.000132) 2 fields, 2 nested levels: 0.000420 0.000006 0.000426 ( 0.000411) 2 fields, 4 nested levels: 0.001695 0.000001 0.001696 ( 0.001694) 4 fields, 0 nested levels: 0.000154 0.000001 0.000155 ( 0.000153) 4 fields, 2 nested levels: 0.001744 0.000001 0.001745 ( 0.001744) 4 fields, 4 nested levels: 0.032188 0.000269 0.032457 ( 0.032581) 8 fields, 0 nested levels: 0.000316 0.000034 0.000350 ( 0.000352) 8 fields, 2 nested levels: 0.013464 0.000218 0.013682 ( 0.013917) 8 fields, 4 nested levels: 0.910159 0.008081 0.918240 ( 0.919507) 🌍 https:!//gist.github.com/DmitryTsepelev/ 36e290cf64b4ec0b18294d0a57fb26ff#file-1_result-md

Slide 32

Slide 32 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 32 🚨 Ответ долго формируется 🌍 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_field 7.78 execute_multiplex:execute_query:authorized 152.18 execute_multiplex:execute_query_lazy 33.86 execute_multiplex:execute_query_lazy:execute_field 8.61 execute_multiplex:execute_query_lazy:execute_field_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 💡 Нужно сделать ответ меньше!

Slide 33

Slide 33 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 33 💡 Обновляйте 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) Время: Память: 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 34

Slide 34 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 34 🚨 Много времени в БД 💡 ускорить запросы 💡 найти и убрать N+1

Slide 35

Slide 35 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 35 💡 Боремся с N+1: запрашиваем все! class Resolvers!::FeedResolverPreload < Resolvers!::BaseResolver type [Types!::Tweet], null: false def resolve FeedBuilder.for(current_user).includes(:author) end end

Slide 36

Slide 36 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 36 💡 Боремся с 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 field is requested return Tweet.all unless lookahead.selects?(:author) Tweet.includes(:author) end end

Slide 37

Slide 37 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 37 💡 Боремся с N+1: ленивая загрузка 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 38

Slide 38 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 38 💡 Боремся с N+1: что еще? 🌍 https:!//evilmartians.com/chronicles/how-to-graphql-with-ruby- rails-active-record-and-no-n-plus-one 💡 ленивые резолверы 💡 batch loading 💡 встроенный dataloader! (с 1.12.0)

Slide 39

Slide 39 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev КАК НАЙТИ УЗКОЕ МЕСТО УСТРАНЯЕМ ЧАСТЫЕ ПРОБЛЕМЫ ЕЩЕ БЫСТРЕЕ?

Slide 40

Slide 40 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 40 🚨 Клиентский батчинг клиент собирает несколько запросов в один сокращает нагрузку на сеть увеличивает время парсинга и подготовки ответа

Slide 41

Slide 41 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 41 HTTP кэширование Cache-Control/Expires — время жизни данных Last-Modified/If-Modified-Since — время модификации данных ETag — идентификатор ресурса

Slide 42

Slide 42 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 42 HTTP кэширование 😔 POST запросы не кэшируются

Slide 43

Slide 43 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 43 HTTP кэширование 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 44

Slide 44 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 44 Apollo Persisted Queries 🌍 https:!//!!www.apollographql.com/docs/apollo-server/performance/apq/

Slide 45

Slide 45 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 45 Persisted Queries

Slide 46

Slide 46 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 46 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 47

Slide 47 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 47 ⚠ GET + mutation = CSRF class GraphqlSchema < GraphQL!::Schema use GraphQL!::PersistedQueries, verify_http_method: true end GET /graphql?query=mutation+%7B+sendMoney+%7D Авторизованный пользователь может перейти по ссылке и выполнить нежелательную операцию!

Slide 48

Slide 48 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 48 🚨 Парсинг занимает много времени 💡 Нужно убрать парсинг! user system total real 1 fields, 0 nested levels: 0.000153 0.000002 0.000155 ( 0.000152) 1 fields, 2 nested levels: 0.000188 0.000001 0.000189 ( 0.000187) 1 fields, 4 nested levels: 0.000252 0.000001 0.000253 ( 0.000240) 2 fields, 0 nested levels: 0.000134 0.000013 0.000147 ( 0.000132) 2 fields, 2 nested levels: 0.000420 0.000006 0.000426 ( 0.000411) 2 fields, 4 nested levels: 0.001695 0.000001 0.001696 ( 0.001694) 4 fields, 0 nested levels: 0.000154 0.000001 0.000155 ( 0.000153) 4 fields, 2 nested levels: 0.001744 0.000001 0.001745 ( 0.001744) 4 fields, 4 nested levels: 0.032188 0.000269 0.032457 ( 0.032581) 8 fields, 0 nested levels: 0.000316 0.000034 0.000350 ( 0.000352) 8 fields, 2 nested levels: 0.013464 0.000218 0.013682 ( 0.013917) 8 fields, 4 nested levels: 0.910159 0.008081 0.918240 ( 0.919507) 🌍 https:!//gist.github.com/DmitryTsepelev/ 36e290cf64b4ec0b18294d0a57fb26ff#file-1_result-md

Slide 49

Slide 49 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 49 💡 Cохраняем AST вместо текста запроса

Slide 50

Slide 50 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 50 💡 Cохраняем AST вместо текста запроса class GraphqlSchema < GraphQL!::Schema use GraphQL!::PersistedQueries, compiled_queries: true end 🌍 https:!//github.com/DmitryTsepelev/graphql-ruby-persisted_queries

Slide 51

Slide 51 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 51 Compiled Queries: бенчмарк Plain schema: user system total real 50 fields (nested) 0.056927 0.000995 0.057922 ( 0.057997) 100 fields (nested) 0.245235 0.001336 0.246571 ( 0.246727) 200 fields (nested) 0.974444 0.006531 0.980975 ( 0.981810) 300 fields (nested) 2.175855 0.012773 2.188628 ( 2.190130) Schema with compiled queries: user system total real 50 fields (nested) 0.029933 0.000087 0.030020 ( 0.030040) 100 fields (nested) 0.133933 0.000502 0.134435 ( 0.134756) 200 fields (nested) 0.495052 0.003545 0.498597 ( 0.499452) 300 fields (nested) 1.041463 0.005130 1.046593 ( 1.047137) 🌍 https:!//github.com/DmitryTsepelev/graphql-ruby-persisted_queries 2 fields (nested): query { field1 { field1 field2 } field2 { field1 field2 } }

Slide 52

Slide 52 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 52 Кэширование ответов query { currentUser { displayName avatarURL } } query { tweets { text author { displayName avatarURL } } } клиенты могут присылать разные запросы части ответов могут повторяться можно кэшировать части ответов!

Slide 53

Slide 53 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 53 Кэширование ответов

Slide 54

Slide 54 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 54 graphql-cache class GraphqSchema < GraphQL!::Schema use GraphQL!::Cache end class BaseType < GraphQL!::Schema!::Object field_class GraphQL!::Cache!::Field end class PostType < BaseObject field :id, ID, null: false field :title, String, null: false, cache: true end 🚨 Не работает с Interpreter!

Slide 55

Slide 55 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 55 raw_value class BannerType < GraphQL!::Schema!::Object field :id, String, null: false field :image_url, String, null: false end class Query < GraphQL!::Schema!::Object field :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 56

Slide 56 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 56 graphql-fragment_cache class GraphqSchema < GraphQL!::Schema use GraphQL!::FragmentCache end class BaseType < GraphQL!::Schema!::Object include GraphQL!::FragmentCache!::Object end class PostType < BaseObject field :id, ID, null: false field :title, String, null: false, cache_fragment: true end 🌍 https:!//github.com/DmitryTsepelev/graphql-ruby-fragment_cache

Slide 57

Slide 57 text

Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 57 настраивайте мониторинг до оптимизаций конкретный запрос можно отладить с помощью трейсера для ускорения парсинга — упрощайте запросы или попробуйте compiled queries устраняйте N+1 короткие ответы быстрее сериализуются HTTP кэширование реализуемо тяжелые части ответа можно кэшировать Выводы

Slide 58

Slide 58 text

evl.ms/blog @dmitrytsepelev @evilmartians evl.ms/telegram Спасибо! RUBYRUSSIA 2019 Ruby Meetup 13 DmitryTsepelev @dmitrytsepelev @dmitrytsepelev 58