como acelerar uma consulta com partitionkey no armazenamento de tabelas do azure

10

Como aumentamos a velocidade dessa consulta?

Temos aproximadamente 100 consumidores no período de 1-2 minutesexecução da consulta a seguir. Cada uma dessas execuções representa 1 execução de uma função de consumo.

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

Esta consulta produzirá aproximadamente 5000 resultados.

Código completo:

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

Durante essas execuções, quando houver 100 consumidores, como você pode ver, as solicitações serão agrupadas e formarão picos:

insira a descrição da imagem aqui

Durante esses picos, as solicitações geralmente levam mais de um minuto:

insira a descrição da imagem aqui

Como aumentamos a velocidade dessa consulta?

eu--''''''---------''''''''''''
fonte
5.000 resultados parecem que você não está filtrando o suficiente na consulta. A transferência de 5.000 resultados para o código custará uma tonelada de tempo de rede. Não importa que você ainda faça a filtragem posteriormente. | Sempre faça o mesmo processo de filtragem na consulta. Idealmente, nas linhas que obtiveram um índice e / ou são o resultado de uma exibição computada.
Christopher
Esses objetos de "tradução" são grandes? Por que você não gosta de obter alguns parâmetros em vez de ficar como o banco de dados inteiro?
Hirasawa Yui
@HirasawaYui não, eles são pequenos
l
você deve fazer mais filtragem, obter 5000 resultados parece sem sentido. é impossível dizer sem saber seus dados, mas eu diria que você precisa descobrir uma maneira de particionar-lo de uma forma mais significativa ou introduzir algum tipo de filtragem na consulta
4c74356b41
Quantas partições diferentes existem?
Peter Bons

Respostas:

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

Aqui está um dos problemas: você está executando a consulta e, em seguida, filtrando-a da memória usando esses "onde". Mova os filtros para antes da consulta ser executada, o que deve ajudar bastante.

Segundo, você deve fornecer algum limite de linhas para recuperar do banco de dados

Miranda
fonte
isto não fez a diferença
l
3

Há três coisas que você pode considerar:

1 . Primeiro, livre-se das Wherecláusulas que você executa no resultado da consulta. É melhor incluir cláusulas na consulta o máximo possível (ainda melhor se você tiver algum índice em suas tabelas, inclua-as também). Por enquanto, você pode alterar sua consulta como abaixo:

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))
));

Como você tem uma grande quantidade de dados para recuperar, é melhor executar suas consultas em paralelo. Então, você deve substituir o do whileloop dentro do ExecuteQueryAsyncmétodo pelo Parallel.ForEachque escrevi com base em Stephen Toub Parallel.While ; Dessa forma, reduzirá o tempo de execução da consulta. Essa é uma boa opção, pois você pode remover Resultquando fizer uma chamada nesse método, mas há uma pequena limitação que falarei sobre isso após esta parte do código:

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

E então você pode chamá-lo no seu Getmétodo:

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

Como você pode ver, o método em si não é assíncrono (você deve alterar o nome) e Parallel.ForEachnão é compatível com a passagem de um método assíncrono. É por isso que eu usei ExecuteQuerySegmented. Mas, para torná-lo mais eficiente e usar todos os benefícios do método assíncrono, você pode substituir o ForEachloop acima pelo ActionBlockmétodo Dataflow ou ParallelForEachAsyncmétodo de extensão do pacote AsyncEnumerator Nuget .

2. É uma boa opção para executar consultas paralelas independentes e mesclar os resultados, mesmo que sua melhoria de desempenho seja no máximo 10%. Isso lhe dá tempo para encontrar a melhor consulta amigável ao desempenho. Mas nunca se esqueça de incluir todas as suas restrições e teste as duas maneiras de saber qual deles melhor se adapta ao seu problema.

3 . Não tenho certeza se é uma boa sugestão ou não, mas faça-o e veja os resultados. Conforme descrito no MSDN :

O serviço Tabela impõe tempos limite do servidor da seguinte maneira:

  • Operações de consulta: durante o intervalo de tempo limite, uma consulta pode ser executada por até cinco segundos, no máximo. Se a consulta não for concluída dentro do intervalo de cinco segundos, a resposta incluirá tokens de continuação para recuperar itens restantes em uma solicitação subsequente. Consulte Tempo limite da consulta e paginação para obter mais informações.

  • Operações de inserção, atualização e exclusão: O intervalo de tempo limite máximo é de 30 segundos. Trinta segundos também é o intervalo padrão para todas as operações de inserção, atualização e exclusão.

Se você especificar um tempo limite inferior ao tempo limite padrão do serviço, seu intervalo de tempo limite será usado.

Assim, você pode jogar com o tempo limite e verificar se há alguma melhoria no desempenho.

MasLoo
fonte
2

Infelizmente, a consulta abaixo apresenta uma verificação completa da tabela :

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

Você deve dividi-lo em dois filtros de chave de partição e consultá-los separadamente, que se tornarão duas varreduras de partição e terão um desempenho mais eficiente.

Zhaoxing Lu
fonte
vimos talvez uma melhoria de 10% com isso, mas não é suficiente
l
1

Portanto, o segredo não está apenas no código, mas também na configuração de suas tabelas de armazenamento do Azure.

a) Uma das opções importantes para otimizar suas consultas no Azure é introduzir o cache. Isso reduzirá drasticamente seus tempos de resposta gerais e, assim, evitará gargalos durante o horário de pico que você mencionou.

b) Além disso, ao consultar entidades fora do Azure, a maneira mais rápida possível de fazer isso é com o PartitionKey e o RowKey. Esses são os únicos campos indexados no armazenamento de tabelas e qualquer consulta que utilize os dois será retornada em questão de alguns milissegundos. Portanto, verifique se você usa o PartitionKey e o RowKey.

Veja mais detalhes aqui: https://docs.microsoft.com/en-us/azure/storage/tables/table-storage-design-for-query

Espero que isto ajude.

SSPrabhu
fonte
-1

Nota: Este é um conselho geral de otimização de consulta ao banco de dados.

É possível que o ORM esteja fazendo algo estúpido. Ao fazer otimizações, não há problema em renunciar a uma camada de abstração. Portanto, sugiro reescrever a consulta na linguagem de consulta (SQL?) Para facilitar a visualização do que está acontecendo e também a otimização.

A chave para otimizar pesquisas é a classificação! Manter uma tabela classificada geralmente é muito mais barata em comparação com a verificação de toda a tabela em todas as consultas! Portanto, se possível, mantenha a tabela classificada pela chave usada na consulta. Na maioria das soluções de banco de dados, isso é alcançado através da criação de uma chave de índice.

Outra estratégia que funciona bem se houver poucas combinações é ter cada consulta como uma tabela separada (temporária na memória) que esteja sempre atualizada. Portanto, quando algo é inserido, ele também é "inserido" nas tabelas "visualizar". Algumas soluções de banco de dados chamam isso de "visualizações".

Uma estratégia mais bruta é criar réplicas somente leitura para distribuir a carga.

user5886944
fonte