GraphSQLの改良NETデータローダ実行戦略


更新日29/05/2021 : GitHub .
更新30/05/2021 :インターフェイスの種類のサポートを追加しました.

GraphSQLパフォーマンスの改善
任意のGraphSQLの実装を使用する場合は、バックエンド実装の効率的な実行のために、むしろ迅速に収穫する重要な問題はパフォーマンスが悪いです.単純なCRUD質問のために、リゾルバは一般的に多くの問題を持ちません.データと/または複雑な階層のリストを検索し始めると、data loader
効果的に使用されます.これにより、すべてのリゾルバは、データを効率的に取得できるように、バックエンドの実装(通常はデータベースまたはAPI)にリクエストをキューに登録します.

データローダの動作
データローダは解決される実行ノードをキューに入れるために使用されます.そして、それはそれから定義された実行戦略を使用して実行されます.既定の実行戦略(parallel)の問題は、保留中のノードが実行される順序が既存のバッチの一部になる可能性のある他のノードをブロック解除するのに最適ではないということです.

  • はあなたのアドレスを含むすべての顧客を検索します.そして、それは順番に彼らの郊外
  • を含みます
    この質問の一部として
  • も「仕事の郊外」(このシナリオに合うために仮定的に)が含まれていたいが、限られたデータ収集としてそれを考えてください.
  • クエリを実行し、次のようにします.
  • 顧客の検索を待ち行列とする.
  • は、これを取得するまでのデータがないので、このノードは継続する前に実行される.
  • アドレス検索はすべての顧客(バッチアドレス検索REST APIまたはSQLクエリを使用していると思われる)のためにバッチされます、そして、郊外検索はすべての顧客のためにバッチされます.アドレス・アウトラインはアドレス検索によってブロックされるので、それらはバッチされない.
  • アドレス検索と郊外検索を行う.
  • 現在、アドレスが利用可能であるならば、郊外は彼らのために打撃を受けることができます.
  • 郊外検索を再度実行する.
  • ここでの問題は、すべてのアドレスを取得し、再度チェックするということです.問題はより複雑なシナリオで拡大します、そして、1つの質問において、これは23の代わりに45の質問を意味しました.

    デフォルト戦略の変更
    実行戦略選択はIDocumentExecuterの登録されたインスタンスによって行われる.これをカスタマイズする最も簡単な方法はDocumentExecuterから派生したクラスを作成し、SelectExecutionStrategyメソッドをオーバーライドすることです.
    public class OptimisedDocumentExecutor : DocumentExecuter
    {
      protected override IExecutionStrategy SelectExecutionStrategy(ExecutionContext context)
      {
        return context.Operation.OperationType == OperationType.Query
          ? new PrioritisedParallelExecutionStrategy()
          : base.SelectExecutionStrategy(context);
      }
    }
    

    並列実行戦略の変更

    データローダの実行を展開する
    デフォルトParallelExecutionStrategyのカスタマイズは難しいです.これを達成する最良の方法はexisting source codeを取って、それを修正することです.変更する必要がある重要なセクションは、try/catchの最後の部分ExecuteNodeTreeAsyncです.
    //run pending data loaders
    while (pendingDataLoaders.Count > 0)
    {
      var dataLoaderNode = pendingDataLoaders.Dequeue();
      currentTasks.Add(CompleteDataLoaderNodeAsync(context, dataLoaderNode));
      currentNodes.Add(dataLoaderNode);
    }
    
    まず、すべての必須コンテキストを実行する独自のメソッドに移動し、実行ノードのpendingDataLoadersリストを追加します.
    private void ProcessDataLoaders(Queue<ExecutionNode> dataLoaderQueue, ExecutionContext context, List<Task> currentTasks, List<ExecutionNode> currentNodes, List<ExecutionNode> pendingDataLoaders)
    {
      while (dataLoaderQueue.Count > 0)
      {
        var dataLoaderNode = dataLoaderQueue.Dequeue();
        pendingDataLoaders.Remove(dataLoaderNode);
        currentTasks.Add(CompleteDataLoaderNodeAsync(context, dataLoaderNode));
        currentNodes.Add(dataLoaderNode);
      }
    }
    

    ブロッキングデータローダの優先順位付け
    次に、他のデータローダをブロックしているデータローダを常に処理するようにロジックを変更する必要があります.まず、pendingDataLoadersクラスのタイプをList<ExecutionNode>に変更します.また、EnqueueからAddへの次の変更(2回発生)を必要とします
    if (pendingNode.Result is IDataLoaderResult)
    {
      pendingDataLoaders.Enqueue(pendingNode);
    }
    
    それから、上からループ(現在、それがメソッドに移動される現在)を下にある間、オリジナルの「保留中のデータローダ」を取り替えてください
    var pendingLoaderGraphTypes = pendingDataLoaders
        .Select(x => GetGraphTypes(x)?.Name)
        .Where(x => x != null)
        .ToHashSet();
    
    // always process priority data loaders first as they potentially block others that can be batched in
    var priorityDataLoaders = new Queue<ExecutionNode>(pendingDataLoaders
        .Where(x => HasChildOfGraphType(x, context, pendingLoaderGraphTypes))
    );
    
    var remainingDataLoaders = new Queue<ExecutionNode>(pendingDataLoaders
        .Except(priorityDataLoaders)
    );
    
    if (priorityDataLoaders.Count > 0)
    {
        ProcessDataLoaders(priorityDataLoaders, context, currentTasks, currentNodes, pendingDataLoaders);
        continue;
    }
    
    //run pending data loaders
    ProcessDataLoaders(remainingDataLoaders, context, currentTasks, currentNodes, pendingDataLoaders);
    
    これには、次のようなフローがあります.
  • すべての保留中のデータローダに関連するグラフタイプを取得します.
  • 保留中のローダグラフタイプの問い合わせの子を持つすべての保留中のデータローダを取得します.これらは“優先度”データローダになります.
  • 任意の優先順位のローダが識別された場合、最初に抽出されたメソッドを使用して処理し、フローを再起動します.
  • が優先ローダーなしであるならば、どんな残りのデータローダーも処理してください.
    これは、すべての優先ローダが処理され続けていないまで、残りの部分を解決することができます.

  • 実行ノードのグラフ型の解決
    私は、graphqlから以下を導きました.NETソース:
    private static IObjectGraphType[] GetGraphTypes(ExecutionNode executionNode)
    {
      IGraphType? graphType = null;
      switch (executionNode)
      {
        case ValueExecutionNode:
          return Array.Empty<IObjectGraphType>();
        case ObjectExecutionNode objectNode:
          graphType = objectNode.GraphType;
          break;
        case ArrayExecutionNode arrayNode:
          {
            graphType = ((ListGraphType)arrayNode.GraphType).ResolvedType;
            break;
          }
      }
    
      if (graphType is NonNullGraphType nonNullGraphType)
        graphType = nonNullGraphType.ResolvedType;
    
      return graphType switch
      {
        IInterfaceGraphType interfaceGraphType => interfaceGraphType.PossibleTypes.ToArray(),
        IObjectGraphType objectGraphType => new[] { objectGraphType },
        _ => Array.Empty<IObjectGraphType>()
      };
    }
    
    これは、保留中のデータローダが配列かオブジェクトであることを想定します.オブジェクトにはGetObjectGraphTypeを使用してグラフタイプを取得できます.配列型では、項目の型を識別する必要があります.これはリストの解決されたタイプです.

    ノードがサブタイプであるかどうかを判断する
    また、以下の項目についても説明した.NETソース内部
    private bool HasChildOfGraphType(ExecutionNode executionNode, ExecutionContext context, ISet<string> searchGraphTypes)
    {
      var graphTypes = GetGraphTypes(executionNode);
      if (graphTypes.Length == 0)
        return false;
    
      return graphTypes
        .Any(graphType => CollectFieldsFrom(context, graphType, executionNode.Field?.SelectionSet, null)
          .Select(x => new
          {
            Field = x.Value,
            FieldDefinition = GetFieldDefinition(context.Schema, graphType, x.Value)
          })
          .Select(x => BuildExecutionNode(executionNode, x.FieldDefinition.ResolvedType, x.Field, x.FieldDefinition))
          .Any(x => GetGraphTypes(x).Any(t => searchGraphTypes.Contains(t.Name)) || HasChildOfGraphType(x, context, searchGraphTypes))
        );
    }
    
    これは、より新しいバージョンのgraphqlを必要とします.以前のように、executionHelperは集合フィールドとgetFieldDefinitionを提供しました.
    これは、実行コンテキストとノードを受け取り、クエリから選択された設定をASTフィールドに展開し、各フィールドの定義を取得し、実行ノードを構築します.これらの子実行ノードから、グラフの種類を決定することができます.クエリの選択に保留データ型があるかどうかを確認できます.