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

Как ускорить GraphQL API

Как ускорить GraphQL API

Разбираемся с мониторингом, учимся находить узкое место и разбираем способы эти узкие места обходить.

Dmitry Tsepelev

March 11, 2021
Tweet

More Decks by Dmitry Tsepelev

Other Decks in Programming

Transcript

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

  2. Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev • an API query language

    • an execution engine 2
  3. Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 3 evilmartians.com

  4. Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 4 evilmartians.com

  5. Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 5

  6. Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 6 DUMP 2019 Saint P

    Rubyconf 2019 🌍 https:!//youtu.be/xUrLslKdnr8 🌍 https:!//youtu.be/CjOwKbf8L3I?t=9615
  7. Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 7 🌍 https:!//evl.ms/blog/graphql-on-rails-1-from-zero-to-the-first-query

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

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

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

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

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

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

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

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

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

  17. Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev КАК НАЙТИ УЗКОЕ МЕСТО УСТРАНЯЕМ

    ЧАСТЫЕ ПРОБЛЕМЫ ЕЩЕ БЫСТРЕЕ?
  18. Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 18 Подключаем трейсер class MySchema

    < GraphQL!::Schema use GraphQL!::Tracing!::NewRelicTracing end
  19. 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
  20. Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 20 🌍 https:!//book.productionreadygraphql.com

  21. Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 21 Клиентские заголовки GQL-Client-Name: ios-app

    GQL-Client-Version: 1.2.4 для поддержки нескольких клиентов проще понять, нужно ли ускорять конкретный запрос
  22. 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
  23. 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)
  24. 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
  25. 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) Ищем медленное поле
  26. Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 26 Директивный профайлинг 🌍 https:!//evl.ms/chronicles/catch-a-batch-making-mayhem-5-times-

    more-responsive
  27. 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
  28. Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev КАК НАЙТИ УЗКОЕ МЕСТО УСТРАНЯЕМ

    ЧАСТЫЕ ПРОБЛЕМЫ ЕЩЕ БЫСТРЕЕ?
  29. Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 29

  30. 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
  31. 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
  32. 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 💡 Нужно сделать ответ меньше!
  33. 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
  34. Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 34 🚨 Много времени в

    БД 💡 ускорить запросы 💡 найти и убрать N+1
  35. 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
  36. 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
  37. 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
  38. 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)
  39. Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev КАК НАЙТИ УЗКОЕ МЕСТО УСТРАНЯЕМ

    ЧАСТЫЕ ПРОБЛЕМЫ ЕЩЕ БЫСТРЕЕ?
  40. Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 40 🚨 Клиентский батчинг клиент

    собирает несколько запросов в один сокращает нагрузку на сеть увеличивает время парсинга и подготовки ответа
  41. Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 41 HTTP кэширование Cache-Control/Expires —

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

    запросы не кэшируются
  43. 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
  44. Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 44 Apollo Persisted Queries 🌍

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

  46. 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
  47. 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 Авторизованный пользователь может перейти по ссылке и выполнить нежелательную операцию!
  48. 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
  49. Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 49 💡 Cохраняем AST вместо

    текста запроса
  50. 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
  51. 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 } }
  52. Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 52 Кэширование ответов query {

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

  54. 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!
  55. 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
  56. 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
  57. Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 57 настраивайте мониторинг до оптимизаций

    конкретный запрос можно отладить с помощью трейсера для ускорения парсинга — упрощайте запросы или попробуйте compiled queries устраняйте N+1 короткие ответы быстрее сериализуются HTTP кэширование реализуемо тяжелые части ответа можно кэшировать Выводы
  58. evl.ms/blog @dmitrytsepelev @evilmartians evl.ms/telegram Спасибо! RUBYRUSSIA 2019 Ruby Meetup 13

    DmitryTsepelev @dmitrytsepelev @dmitrytsepelev 58