Save 37% off PRO during our Black Friday Sale! »

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

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

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

F5c2731f9a4dbfb4af319295a1f0cd28?s=128

Dmitry Tsepelev

March 11, 2021
Tweet

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