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


10

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

Nous avons environ 100 consommateurs dans le délai d' 1-2 minutesexé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:

entrez la description de l'image ici

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

entrez la description de l'image ici

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


5000 résultats semblent que vous ne filtrez pas assez dans la requête. Le simple transfert de 5000 résultats vers le code coûtera une tonne de temps réseau. Peu importe que vous fassiez encore du filtrage par la suite. | Faites toujours autant de filtrage d'un traitement dans la requête. Idéalement sur les lignes qui ont un index et / ou sont le résultat d'une vue calculée.
Christopher

Ces objets "Traduction" sont-ils gros? Pourquoi n'aimez-vous pas obtenir certains des paramètres au lieu de gettin` comme l'ensemble de la base de données?
Hirasawa Yui

@HirasawaYui non, ils sont petits
l --''''''----------------- '' '' '' '' '' '

vous devriez faire plus de filtrage, tirer 5000 résultats semble vide de sens. il est impossible de le dire sans connaître vos données, mais je dirais que vous devez trouver un moyen de les partitionner de manière plus significative ou d'introduire une sorte de filtrage dans la requête
4c74356b41

Combien de partitions différentes existe-t-il?
Peter Bons

Réponses:


3
  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


cela
n'a

3

Il y a 3 choses que vous pouvez considérer:

1 . Tout d'abord, supprimez vos Whereclauses 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 la méthode do whileloop inside ExecuteQueryAsyncpar celle que Parallel.ForEachj'ai écrite en fonction 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 le supprimer Resultlorsque vous appelez sur cette méthode, mais il y a une petite limitation que je vais en parler après cette partie de 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 Getméthode:

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.ForEachn'est pas compatible avec le passage d'une méthode asynchrone. C'est pourquoi je l'ai utilisé à la ExecuteQuerySegmentedplace. Mais, pour le rendre plus performant et utiliser tous les avantages de la méthode asynchrone, vous pouvez remplacer la ForEachboucle ci-dessus par la ActionBlockméthode Dataflow ou la ParallelForEachAsyncméthode d'extension du 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 inclut 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 le timeout et vérifier s'il y a des améliorations de performances.


2

Malheureusement, la requête ci-dessous introduit une 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, ce qui deviendra deux analyses de partition et fonctionnera plus efficacement.


nous avons peut-être vu une amélioration de 10% avec cela, mais ce n'est pas suffisant
l --''''''----------------- '' '' '' '' '' ''

1

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

Remarque: Il s'agit d'un conseil général d'optimisation de requête DB.

Il est possible que l'ORM fasse quelque chose de stupide. Lorsque vous effectuez des optimisations, vous pouvez quitter une couche d'abstraction. Je suggère donc de réécrire la requête dans le langage de requête (SQL?) Pour le rendre plus facile à voir ce qui se passe, et aussi plus facile à optimiser.

La clé pour optimiser les recherches est le tri! Garder une table triée est généralement beaucoup moins cher que de scanner la table entière à chaque requête! Donc, si possible, conservez la table triée par la clé utilisée dans la requête. Dans la plupart des solutions de base de données, ceci est réalisé en créant une clé d'index.

Une autre stratégie qui fonctionne bien s'il y a peu de combinaisons consiste à avoir chaque requête dans une table distincte (temporaire en mémoire) qui est toujours à jour. Ainsi, lorsqu'un élément est inséré, il est également "inséré" dans les tables "view". Certaines solutions de base de données appellent cela des "vues".

Une stratégie plus brute consiste à créer des répliques en lecture seule pour répartir la charge.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.