Cómo acelerar una consulta con la partición en el almacenamiento de la tabla azul


10

¿Cómo aumentamos la velocidad de esta consulta?

Tenemos aproximadamente 100 consumidores dentro del lapso de 1-2 minutesejecución de la siguiente consulta. Cada una de estas ejecuciones representa 1 ejecución de una función 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 arrojará 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 estas ejecuciones, cuando hay 100 consumidores, como puede ver, las solicitudes se agruparán y formarán picos:

ingrese la descripción de la imagen aquí

Durante estos picos, las solicitudes suelen tardar más de 1 minuto:

ingrese la descripción de la imagen aquí

¿Cómo aumentamos la velocidad de esta consulta?


Parece que 5000 resultados no están filtrando lo suficiente en la consulta. Solo transferir 5000 resultados al código costará una tonelada de tiempo de red. No importa que sigas filtrando después. El | Siempre haga tanto filerting un procesamiento en la consulta. Idealmente en las filas que obtuvieron un índice y / o son el resultado de una vista calculada.
Christopher el

¿Son grandes esos objetos de "Traducción"? ¿Por qué no te gusta obtener algunos de los parámetros en lugar de obtener como todo el db?
Hirasawa Yui, el

@HirasawaYui no, son pequeños
l --''''''--------- '' '' '' '' '' ''

debería filtrar más, obtener 5000 resultados parece no tener sentido. es imposible saberlo sin conocer sus datos, pero diría que necesitaría encontrar una manera de particionarlo de una manera más significativa o introducir algún tipo de filtro en la consulta
4c74356b41

¿Cuántas particiones diferentes hay?
Peter Bons

Respuestas:


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

Este es uno de los problemas: está ejecutando la consulta y luego la está filtrando de la memoria utilizando estos "wheres". Mueva los filtros antes de que se ejecute la consulta, lo que debería ayudar mucho.

En segundo lugar, debe proporcionar un límite de filas para recuperar de la base de datos


esto no hizo la diferencia
l --''''''--------- '' '' '' '' '' '

3

Hay 3 cosas que puede considerar:

1 . En primer lugar, elimine las Wherecláusulas que realiza en el resultado de la consulta. Es mejor incluir cláusulas en la consulta tanto como sea posible (incluso mejor si tiene algún índice en sus tablas, inclúyalos también). Por ahora, puede cambiar su consulta de la siguiente manera:

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

Debido a que tiene una gran cantidad de datos para recuperar, es mejor ejecutar sus consultas en paralelo. Por lo tanto, debe reemplazar el método do whileloop inside ExecuteQueryAsynccon el Parallel.ForEachque escribí basado en Stephen Toub Parallel . De esta manera, reducirá el tiempo de ejecución de la consulta. Esta es una buena opción porque puede eliminarla Resultcuando realiza una llamada a este método, pero tiene una pequeña limitación de la que hablaré después de esta parte del 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;
}

Y luego puedes llamarlo en tu Getmétodo:

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

Como puede ver, el método itselft no es asíncrono (debe cambiar su nombre) y Parallel.ForEachno es compatible con pasar un método asíncrono. Es por eso que he usado en su ExecuteQuerySegmentedlugar. Pero, para hacerlo más eficiente y usar todos los beneficios del método asincrónico, puede reemplazar el ForEachbucle anterior con el ActionBlockmétodo en Flujo de datos o ParallelForEachAsyncmétodo de extensión del paquete Nuget de AsyncEnumerator .

2. Es una buena opción ejecutar consultas paralelas independientes y luego combinar los resultados, incluso si su mejora del rendimiento es como máximo del 10 por ciento. Esto le da tiempo para poder encontrar la mejor consulta amigable de rendimiento. Pero, nunca olvide incluir todas sus restricciones, y pruebe ambas formas para saber cuál se adapta mejor a su problema.

3 . No estoy seguro de si es una buena sugerencia o no, pero hazlo y mira los resultados. Como se describe en MSDN :

El servicio de mesa impone tiempos de espera del servidor de la siguiente manera:

  • Operaciones de consulta: durante el intervalo de tiempo de espera, una consulta puede ejecutarse durante un máximo de cinco segundos. Si la consulta no se completa dentro del intervalo de cinco segundos, la respuesta incluye tokens de continuación para recuperar los elementos restantes en una solicitud posterior. Consulte Tiempo de espera de consulta y paginación para obtener más información.

  • Operaciones de inserción, actualización y eliminación: el intervalo de tiempo de espera máximo es de 30 segundos. Treinta segundos también es el intervalo predeterminado para todas las operaciones de inserción, actualización y eliminación.

Si especifica un tiempo de espera inferior al tiempo de espera predeterminado del servicio, se utilizará su intervalo de tiempo de espera.

Para que pueda jugar con tiempo de espera y comprobar si hay mejoras en el rendimiento.


2

Desafortunadamente, la consulta a continuación presenta un escaneo completo de la tabla :

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

Debe dividirlo en dos filtros de Clave de partición y consultarlos por separado, lo que se convertirá en dos escaneos de partición y funcionará de manera más eficiente.


vimos quizás una mejora del 10% con esto, pero no es suficiente
l --''''''--------- '' '' '' '' '' ''

1

Por lo tanto, el secreto no solo está en el código sino también en la configuración de las tablas de almacenamiento de Azure.

a) Una de las opciones destacadas para optimizar sus consultas en Azure es introducir el almacenamiento en caché. Esto reducirá drásticamente sus tiempos de respuesta generales y, por lo tanto, evitará el cuello de botella durante la hora pico que ha mencionado.

b) Además, cuando se consultan entidades fuera de Azure, la forma más rápida de hacerlo es con PartitionKey y RowKey. Estos son los únicos campos indexados en Table Storage y cualquier consulta que utilice ambos se devolverá en cuestión de unos pocos milisegundos. Así que asegúrese de usar PartitionKey y RowKey.

Ver más detalles aquí: https://docs.microsoft.com/en-us/azure/storage/tables/table-storage-design-for-query

Espero que esto ayude.


-1

nota: Este es un consejo general de optimización de consultas de base de datos.

Es posible que el ORM esté haciendo algo estúpido. Al hacer optimizaciones, está bien bajar una capa de abstracción. Por lo tanto, sugiero reescribir la consulta en el lenguaje de consulta (SQL?) Para que sea más fácil ver lo que está sucediendo y también más fácil de optimizar.

¡La clave para optimizar las búsquedas es la clasificación! ¡Mantener una tabla ordenada suele ser mucho más barato en comparación con escanear la tabla completa en cada consulta! Entonces, si es posible, mantenga la tabla ordenada por la clave utilizada en la consulta. En la mayoría de las soluciones de bases de datos, esto se logra creando una clave de índice.

Otra estrategia que funciona bien si hay pocas combinaciones es tener cada consulta como una tabla separada (temporal en la memoria) que siempre esté actualizada. Entonces, cuando se inserta algo, también se "inserta" en las tablas de "vista". Algunas soluciones de bases de datos llaman a esto "vistas".

Una estrategia más bruta es crear réplicas de solo lectura para distribuir la carga.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.