SpringBoot, утечка производительности GraphQL - PullRequest
2 голосов
/ 19 июня 2020

Дано приложение SpringBoot , на котором размещен сервер graphQL . Он использует множество DataLoaders, и они, кажется, работают нормально, но я заметил очень большую утечку производительности, связанную с ними (или промежуточную), которую я не могу четко сузить, но измерить.

Проблема

  1. Некоторые (UI) -клиенты вызывают запрос API-интерфейса graphQL из службы, который инициирует выборку x элементов (x > 10_000
  2. GraphQLQueryResolver вызывается внутри службы SpringBoot, которая извлекает x элементов.
  3. Вызываются функции получателей полей, которые возвращают CompletionStage<T> из загруженных DataLoader<K,T>
  4. То, что занимает очень много времени
  5. DataLoader<K,T> класс реализации вызывается с пакетными ключами и возвращает результаты.

Пример журнала выглядит следующим образом :

2020-06-19T18:25:14.196Z [http-nio-80-exec-10] ~ Shopping ~ INFO  ~ It took |> PT0.095S <| for 'orders query with filter:[OrderFilter(col=createdAt, operator=BETWEEN, value=2020-01-05T00:00:00Z AND 2020-05-31T00:00:00Z)]'
2020-06-19T18:25:18.686Z [DefaultDispatcher-worker-6] ~ Shopping ~ INFO  ~ It took |> PT0.001S <| for 'orderKpiDataLoader' (PT0.000000095S on average for #10476 executions)
2020-06-19T18:25:23.229Z [DefaultDispatcher-worker-19] ~ Shopping ~ INFO  ~ Start 'priceForOrderReferences'
2020-06-19T18:25:24.840Z [DefaultDispatcher-worker-41] ~ Shopping ~ WARN  ~ It took |> PT1.613S <| for 'orderDepositDataLoader' (PT0.00015397S on average for #10476 executions)

Пояснение к регистру журнала:

  1. 18: 25: 14.196 : вызывается "запрос заказов с фильтром" и возвращается в 95 мс # 10476 элементов
  2. + 4.49 S : все его поля «orderKpi» возвращаются из них. r DataLoader. На их создание и возврат ушло 1 мс.
  3. + 4.54 S : поле «priceForOrderReferences» из «orderKpi» загружается через собственный DataLoader. Для их создания и возврата потребовалось 1.613S.

Решение вопросов

  • Что происходит во время между вызовами DataLoaders? (те + 4.49S и + 4.54S)
  • Как я могу его уменьшить?

То, что я сделал, и мои предположения

  • Я пытался сделать DataLoaders singleletons, потому что я думал, что создание объекта занимает так много времени - проблема не в этом.
  • Я попытался ввести ручное пакетирование, переопределив размер пакета - это немного сократило время, но увеличило общее время выполнения (различные размеры пакетов работают по-разному для разных размеров элементов)
  • Поскольку пакетирование сократило время (очень немного), я предполагаю, что утечка производительности происходит из-за "сбора" ключей элементов для создания списка и передачи их в Загрузчик данных. Я также вручную измерил среднее время утечки производительности для каждого элемента, и оно всегда составляет ~ 1 мс на элемент. Я не нашел ни одной части кода, как они собираются или как переопределить его самостоятельно.

Новая информация:

  • Я экспериментировал с разными библиотеками загрузчика данных, например:
<dependency>
    <groupId>com.graphql-java-kickstart</groupId>
    <artifactId>graphql-kickstart-spring-boot-starter-tools</artifactId>
    <version>7.0.1</version>
</dependency>

и

<dependency>
    <groupId>com.graphql-java-kickstart</groupId>
    <artifactId>graphql-java-tools</artifactId>
    <version>6.0.2</version>
</dependency>

, а также отключенное кеширование - все тот же результат. Я помещаю больше журналов (конечно, осторожно, чтобы они сами не замедляли производительность), и моя новая подсказка заключается в том, что проблема заключается в самой библиотеке graphql, а не в DataLoader. В одном случае я удалил DataLoader для поля - и потребовалось столько же времени, пока он не был вызван!

Stats

Spring

<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.7.RELEASE</version>

Мои зависимости graphQL

<dependency>
    <groupId>com.graphql-java-kickstart</groupId>
    <artifactId>graphql-spring-boot-starter</artifactId>
    <version>7.0.1</version>
</dependency>
<dependency>
    <groupId>com.apollographql.federation</groupId>
    <artifactId>federation-graphql-java-support</artifactId>
    <version>0.4.1</version>
</dependency>
<dependency>
    <groupId>com.graphql-java-kickstart</groupId>
    <artifactId>playground-spring-boot-starter</artifactId>
    <version>7.0.1</version>
</dependency>
<dependency>
    <groupId>com.graphql-java</groupId>
    <artifactId>graphql-java-extended-scalars</artifactId>
    <version>1.0.1</version>
</dependency>
<dependency>
    <groupId>com.graphql-java</groupId>
    <artifactId>java-dataloader</artifactId>
    <version>2.2.3</version>
</dependency>

DataLoader

Все DataLoader реализуют эту абстрактную схему:

import com.smark.shopping.utils.FunctionPerformance
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.future.future
import org.dataloader.DataLoader

private val dataLoaderSingleScopeIO = CoroutineScope(Dispatchers.IO)

interface DataLoaderEntryInterface<T, R> {
    val key: String
    val dataLoader: DataLoader<T, R>
}

abstract class DataLoaderEntryAbstract<T, R>(
    override val key: String,
    private val loader: suspend (List<T>) -> List<R>
) : DataLoaderEntryInterface<T, R> {

    private val performance = FunctionPerformance()

    override val dataLoader: DataLoader<T, R>
        get() = DataLoader.newDataLoader { ids ->
            dataLoaderSingleScopeIO.future {
                performance.executeMeasuringSuspend(key, ids.size) { loader(ids) }
            }
        }
}

@Component
class DataLoaderOrderDeposit(private val orderTotalPriceService: OrderTotalPriceService) : DataLoaderEntryAbstract<Int, Int>(
    key = ORDER_DEPOSIT_DATA_LOADER,
    loader = { orderReferences -> orderTotalPriceService.priceForOrderReferences(orderReferences).map { it.deposit } }
)

Resolver

@Component
class OrderResolver : GraphQLResolver<ShopOrder> {

     fun kpi(shopOrder: ShopOrder, dfe: DataFetchingEnvironment): CompletionStage<OrderKpi> =
        DataLoaderFuture<OrderKpi>(dfe, ORDER_KPI_DATA_LOADER).loadBy(shopOrder)
}

@Component
class OrderKpiResolver : GraphQLResolver<OrderKpi> {

    fun deposit(orderKpi: OrderKpi, dfe: DataFetchingEnvironment): CompletionStage<Int> =
        dfe.getDataLoader<Int, Int>(ORDER_DEPOSIT_DATA_LOADER).load(orderKpi.orderReference)
}

Context Builder

@Component
class CustomGraphQLContextBuilder(
    private val dataLoadersSummelsarium: DataLoadersSummelsarium
) : GraphQLServletContextBuilder {

    override fun build(req: HttpServletRequest, response: HttpServletResponse): GraphQLContext =
        DefaultGraphQLServletContext.createServletContext(buildDataLoaderRegistry(), null)
                .with(req)
                .with(response)
                .build()

    override fun build(session: Session, request: HandshakeRequest): GraphQLContext =
        DefaultGraphQLWebSocketContext.createWebSocketContext(buildDataLoaderRegistry(), null)
                .with(session)
                .with(request)
                .build()

    override fun build(): GraphQLContext = DefaultGraphQLContext(buildDataLoaderRegistry(), null)

    private fun buildDataLoaderRegistry(): DataLoaderRegistry =
        DataLoaderRegistry().apply {
            dataLoadersSummelsarium.dataLoaders().forEach { register(it.key, it.dataLoader) }
        }
}

1 Ответ

0 голосов
/ 25 июня 2020

Мое решение:

Что происходит во время между вызовами DataLoaders? (те + 4.49S и + 4.54S)

Я все еще не уверен, что именно замедляет его, но, похоже, это проблема graphql- java зависимость. При выполнении аналогичного запроса в реализации graphql java -script временные задержки составляли около ~ 60 мс.

Как их уменьшить?

  1. Я нашел хорошую статью о производительности graphQL, в которой я послушался совета загружать больше данных заранее, на верхнем уровне. Таким образом, также возможно создавать собственные классы, которые содержат больше данных и могут уменьшить количество избыточных путей к репозиториям на более низких уровнях.
  2. Чтобы избежать времени ожидания загрузчиками данных для «сбора» ключей перед выполнением функции, вы можете выбрать определенные пути к упреждающему и самим запускать загрузчики - на верхних уровнях.
  3. Поиграйте с размерами пакетов различных загрузчиков данных

Если вы упростите ответ, это может быть: «Если хочешь быть быстрым, не go глубоко».

Я все еще открыт для других решений.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...