Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

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

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

Dmitry Tsepelev

March 11, 2021
Tweet

More Decks by Dmitry Tsepelev

Other Decks in Programming

Transcript

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  5. Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 5

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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)

    View Slide

  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

    View Slide

  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)
    Ищем медленное поле

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

  29. Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 29

    View Slide

  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

    View Slide

  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

    View Slide

  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
    💡 Нужно сделать ответ меньше!

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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)

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

  45. Ruby Meetup 13 @dmitrytsepelev @dmitrytsepelev 45
    Persisted Queries

    View Slide

  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

    View Slide

  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
    Авторизованный пользователь может перейти
    по ссылке и выполнить нежелательную
    операцию!

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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!

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide