web-dev-qa-db-fra.com

comment accélérer une requête avec partitionkey dans le stockage de table Azure

Comment pouvons-nous augmenter la vitesse de cette requête?

Nous avons environ 100 consommateurs s dans l'intervalle de 1-2 minutes exécution de la requête suivante. Chacune de ces exécutions représente 1 exécution d'une fonction de consommation.

        TableQuery<T> treanslationsQuery = new TableQuery<T>()
         .Where(
          TableQuery.CombineFilters(
            TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, sourceDestinationPartitionKey)
           , TableOperators.Or,
            TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, anySourceDestinationPartitionKey)
          )
         );

Cette requête donnera environ 5000 résultats.

Code complet:

    public static async Task<IEnumerable<T>> ExecuteQueryAsync<T>(this CloudTable table, TableQuery<T> query) where T : ITableEntity, new()
    {
        var items = new List<T>();
        TableContinuationToken token = null;

        do
        {
            TableQuerySegment<T> seg = await table.ExecuteQuerySegmentedAsync(query, token);
            token = seg.ContinuationToken;
            items.AddRange(seg);
        } while (token != null);

        return items;
    }

    public static IEnumerable<Translation> Get<T>(string sourceParty, string destinationParty, string wildcardSourceParty, string tableName) where T : ITableEntity, new()
    {
        var acc = CloudStorageAccount.Parse(Environment.GetEnvironmentVariable("conn"));
        var tableClient = acc.CreateCloudTableClient();
        var table = tableClient.GetTableReference(Environment.GetEnvironmentVariable("TableCache"));
        var sourceDestinationPartitionKey = $"{sourceParty.ToLowerTrim()}-{destinationParty.ToLowerTrim()}";
        var anySourceDestinationPartitionKey = $"{wildcardSourceParty}-{destinationParty.ToLowerTrim()}";

        TableQuery<T> treanslationsQuery = new TableQuery<T>()
         .Where(
          TableQuery.CombineFilters(
            TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, sourceDestinationPartitionKey)
           , TableOperators.Or,
            TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, anySourceDestinationPartitionKey)
          )
         );

        var over1000Results = table.ExecuteQueryAsync(treanslationsQuery).Result.Cast<Translation>();
        return over1000Results.Where(x => x.expireAt > DateTime.Now)
                           .Where(x => x.effectiveAt < DateTime.Now);
    }

Au cours de ces exécutions, lorsqu'il y a 100 consommateurs, comme vous pouvez le voir, les demandes se regroupent et forment des pics:

enter image description here

Lors de ces pics, les demandes prennent souvent plus d'une minute:

enter image description here

Comment pouvons-nous augmenter la vitesse de cette requête?

Vous pouvez considérer 3 choses:

1 . Tout d'abord, supprimez vos clauses Where que vous effectuez sur le résultat de la requête. Il est préférable d'inclure autant que possible des clauses dans la requête (encore mieux si vous avez des index sur vos tables, incluez-les également). Pour l'instant, vous pouvez modifier votre requête comme ci-dessous:

var translationsQuery = new TableQuery<T>()
.Where(TableQuery.CombineFilters(
TableQuery.CombineFilters(
    TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, sourceDestinationPartitionKey),
    TableOperators.Or,
    TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, anySourceDestinationPartitionKey)
    ),
TableOperators.And,
TableQuery.CombineFilters(
    TableQuery.GenerateFilterConditionForDate("affectiveAt", QueryComparisons.LessThan, DateTime.Now),
    TableOperators.And,
    TableQuery.GenerateFilterConditionForDate("expireAt", QueryComparisons.GreaterThan, DateTime.Now))
));

Parce que vous avez une grande quantité de données à récupérer, il est préférable d'exécuter vos requêtes en parallèle. Donc, vous devez remplacer do while boucle à l'intérieur ExecuteQueryAsync méthode avec Parallel.ForEach J'ai écrit sur la base de Stephen Toub Parallel.While ; De cette façon, cela réduira le temps d'exécution des requêtes. C'est un bon choix car vous pouvez supprimer Result lorsque vous effectuez un appel sur cette méthode, mais il y a une petite limitation que je vais en parler après cette partie du code:

public static IEnumerable<T> ExecuteQueryAsync<T>(this CloudTable table, TableQuery<T> query) where T : ITableEntity, new()
{
    var items = new List<T>();
    TableContinuationToken token = null;

    Parallel.ForEach(new InfinitePartitioner(), (ignored, loopState) =>
    {
        TableQuerySegment<T> seg = table.ExecuteQuerySegmented(query, token);
        token = seg.ContinuationToken;
        items.AddRange(seg);

        if (token == null) // It's better to change this constraint by looking at https://www.vivien-chevallier.com/Articles/executing-an-async-query-with-Azure-table-storage-and-retrieve-all-the-results-in-a-single-operation
            loopState.Stop();
    });

    return items;
}

Et puis vous pouvez l'appeler dans votre méthode Get:

return table.ExecuteQueryAsync(translationsQuery).Cast<Translation>();

Comme vous pouvez le voir, la méthode itselft n'est pas asynchrone (vous devez changer son nom) et Parallel.ForEach n'est pas compatible avec le passage d'une méthode asynchrone. C'est pourquoi j'ai plutôt utilisé ExecuteQuerySegmented. Mais, pour le rendre plus performant et utiliser tous les avantages de la méthode asynchrone, vous pouvez remplacer la boucle ci-dessus ForEach par ActionBlock méthode dans Dataflow ou ParallelForEachAsync méthode d'extension de package AsyncEnumerator Nuget .

2 . C'est un bon choix pour exécuter des requêtes parallèles indépendantes puis fusionner les résultats, même si son amélioration des performances est d'au plus 10%. Cela vous donne le temps de trouver la meilleure requête adaptée aux performances. Mais, n'oubliez jamais d'y inclure toutes vos contraintes et testez les deux façons de savoir laquelle convient le mieux à votre problème.

3 . Je ne suis pas sûr que ce soit une bonne suggestion ou non, mais faites-le et voyez les résultats. Comme décrit dans MSDN :

Le service de table applique les délais d'expiration du serveur comme suit:

  • Opérations de requête: pendant l'intervalle de temporisation, une requête peut s'exécuter pendant un maximum de cinq secondes. Si la requête ne se termine pas dans l'intervalle de cinq secondes, la réponse comprend des jetons de continuation pour récupérer les éléments restants lors d'une demande ultérieure. Voir Délai de requête et pagination pour plus d'informations.

  • Opérations d'insertion, de mise à jour et de suppression: l'intervalle de temporisation maximum est de 30 secondes. Trente secondes est également l'intervalle par défaut pour toutes les opérations d'insertion, de mise à jour et de suppression.

Si vous spécifiez un délai inférieur au délai par défaut du service, votre intervalle de délai sera utilisé.

Vous pouvez donc jouer avec timeout et vérifier s'il y a des améliorations de performances.

3
MasLoo
  var over1000Results = table.ExecuteQueryAsync(treanslationsQuery).Result.Cast<Translation>();
        return over1000Results.Where(x => x.expireAt > DateTime.Now)
                           .Where(x => x.effectiveAt < DateTime.Now);

Voici l'un des problèmes: vous exécutez la requête, puis vous la filtrez à partir de la mémoire en utilisant ces "où". Déplacez les filtres avant l'exécution de la requête, ce qui devrait être très utile.

Deuxièmement, vous devez fournir une certaine limite de lignes à extraire de la base de données

3
Miranda

Malheureusement, la requête ci-dessous introduit un analyse complète de la table:

    TableQuery<T> treanslationsQuery = new TableQuery<T>()
     .Where(
      TableQuery.CombineFilters(
        TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, sourceDestinationPartitionKey)
       , TableOperators.Or,
        TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, anySourceDestinationPartitionKey)
      )
     );

Vous devez le diviser en deux filtres de clé de partition et les interroger séparément, qui deviendront deux analyses de partition et fonctionneront plus efficacement.

2
Zhaoxing Lu

Le secret réside donc non seulement dans le code, mais également dans la configuration de vos tables de stockage Azure.

a) L'une des options importantes pour optimiser vos requêtes dans Azure est d'introduire la mise en cache. Cela réduira considérablement vos temps de réponse globaux et évitera ainsi un goulot d'étranglement pendant l'heure de pointe que vous avez mentionnée.

b) En outre, lors de l'interrogation d'entités à partir d'Azure, le moyen le plus rapide de le faire est à la fois avec PartitionKey et RowKey. Ce sont les seuls champs indexés dans le stockage de table et toute requête qui utilise les deux sera retournée en quelques millisecondes. Assurez-vous donc d'utiliser à la fois PartitionKey et RowKey.

Voir plus de détails ici: https://docs.Microsoft.com/en-us/Azure/storage/tables/table-storage-design-for-query

J'espère que cela t'aides.

1
S.S.Prabhu