Как ускорить запрос с помощью ключа раздела в хранилище таблиц Azure


10

Как мы можем увеличить скорость этого запроса?

У нас есть около 100 потребителей в течение 1-2 minutesвыполнения следующего запроса. Каждый из этих прогонов представляет собой 1 прогон функции потребления.

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

Этот запрос даст около 5000 результатов.

Полный код:

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

Во время этих исполнений, когда есть 100 потребителей, как вы видите, запросы будут кластеризоваться и сформировать пики:

введите описание изображения здесь

Во время этих всплесков запросы часто занимают более 1 минуты:

введите описание изображения здесь

Как мы можем увеличить скорость этого запроса?


5000 результатов, похоже, недостаточно фильтруют в запросе. Простая передача 5000 результатов в код обойдется в тонну сетевого времени. Не берите в голову, что Вы все еще собираетесь делать фильтрацию впоследствии. | Всегда делайте как можно больше обработки в запросе. В идеале для строк, которые получили индекс и / или являются результатом вычисленного представления.
Кристофер

Эти объекты "перевода" большие? Почему вы не хотите получать некоторые параметры вместо того, чтобы получать как целые БД?
Хирасава Юи

@HirasawaYui нет, они маленькие
я - '' '''--------- '' '' '' '' '' ''

Вы должны сделать больше фильтрации, получение 5000 результатов кажется бессмысленным. Невозможно сказать, не зная ваших данных, но я бы сказал, что вам нужно найти способ разделить их более осмысленно или ввести какую-то фильтрацию в запросе
4c74356b41

Сколько существует разных разделов?
Питер Бонс

Ответы:


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

Вот одна из проблем: вы запускаете запрос, а затем фильтруете его из памяти, используя эти «источники». Переместите фильтры до выполнения запроса, что должно помочь.

Во-вторых, вы должны указать некоторое количество строк для извлечения из базы данных.


это не имело значения
l --''''''--------- '' '' '' '' '' ''

3

Есть 3 вещи, которые вы можете рассмотреть:

1 . Прежде всего, избавьтесь от своих Whereпредложений, которые вы выполняете для результата запроса. Лучше включать в запрос как можно больше предложений (даже лучше, если у вас есть какие-либо индексы в таблицах, включая их). Сейчас вы можете изменить свой запрос, как показано ниже:

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

Поскольку у вас есть большой объем данных для извлечения, лучше выполнять ваши запросы параллельно. Итак, вы должны заменить do whileцикл внутри ExecuteQueryAsyncметода на « Parallel.ForEachЯ написал» на основе Stephen Toub Parallel.While ; Таким образом, это сократит время выполнения запроса. Это хороший выбор, потому что вы можете удалить его Resultпри вызове этого метода, но у него есть небольшое ограничение, о котором я расскажу после этой части кода:

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

И тогда вы можете вызвать это в вашем Getметоде:

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

Как видите, метод itselft не является асинхронным (вы должны изменить его имя) и Parallel.ForEachне совместим с передачей асинхронного метода. Вот почему я использовал ExecuteQuerySegmentedвместо этого. Но, чтобы сделать его более производительным и использовать все преимущества асинхронного метода, вы можете заменить вышеуказанный ForEachцикл на ActionBlockметод в потоке данных или ParallelForEachAsyncметод расширения из пакета Nuget AsyncEnumerator .

2. Это хороший выбор для выполнения независимых параллельных запросов, а затем объединения результатов, даже если его повышение производительности составляет не более 10 процентов. Это дает вам время, чтобы найти наиболее удобный для пользователя запрос. Но никогда не забывайте включать в него все свои ограничения и тестируйте оба способа, чтобы узнать, какой из них лучше подходит вашей проблеме.

3 . Я не уверен, что это хорошее предложение или нет, но сделайте это и посмотрите результаты. Как описано в MSDN :

Служба таблиц обеспечивает тайм-ауты сервера следующим образом:

  • Операции запроса. В течение интервала времени ожидания запрос может выполняться не более пяти секунд. Если запрос не завершается в течение пятисекундного интервала, ответ включает маркеры продолжения для извлечения оставшихся элементов в последующем запросе. См. Тайм-аут запроса и разбиение на страницы для получения дополнительной информации.

  • Операции вставки, обновления и удаления: максимальный интервал ожидания составляет 30 секунд. Тридцать секунд также являются интервалом по умолчанию для всех операций вставки, обновления и удаления.

Если вы укажете тайм-аут, который меньше, чем тайм-аут сервиса по умолчанию, будет использоваться ваш интервал тайм-аута.

Таким образом, вы можете поиграть с таймаутом и проверить, есть ли улучшения производительности.


2

К сожалению, приведенный ниже запрос вводит полное сканирование таблицы :

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

Вы должны разделить его на два фильтра ключей разделов и запросить их по отдельности, что станет двумя проверками разделов и будет работать более эффективно.


с этим мы, возможно, увидели улучшение на 10%, но этого недостаточно
я -

1

Так что секрет кроется не только в коде, но и в настройке таблиц хранения Azure.

а) Одним из важных способов оптимизации ваших запросов в Azure является внедрение кэширования. Это значительно сократит общее время отклика и, таким образом, позволит избежать узких мест в течение часа пик, который вы упомянули.

б) Кроме того, при запросе сущностей из Azure самый быстрый способ сделать это - и с PartitionKey, и с RowKey. Это единственные проиндексированные поля в Table Storage, и любой запрос, который использует оба из них, будет возвращен в течение нескольких миллисекунд. Поэтому убедитесь, что вы используете и PartitionKey & RowKey.

Подробнее смотрите здесь: https://docs.microsoft.com/en-us/azure/storage/tables/table-storage-design-for-query

Надеюсь это поможет.


-1

примечание: это общий совет по оптимизации запросов к БД.

Возможно, что ORM делает что-то глупое. При выполнении оптимизаций можно перейти на уровень абстракции. Поэтому я предлагаю переписать запрос на языке запросов (SQL?), Чтобы было легче увидеть, что происходит, а также легче оптимизировать.

Ключ к оптимизации поиска - сортировка! Хранение отсортированной таблицы обычно намного дешевле, чем сканирование всей таблицы при каждом запросе! Поэтому, если возможно, сохраняйте таблицу отсортированной по ключу, используемому в запросе. В большинстве решений для баз данных это достигается путем создания ключа индекса.

Другая стратегия, которая хорошо работает, если комбинаций немного, состоит в том, чтобы каждый запрос был отдельной (временной в памяти) таблицей, которая всегда актуальна. Поэтому, когда что-то вставляется, оно также «вставляется» в таблицы «представления». Некоторые решения для баз данных называют это «представлениями».

Более грубой стратегией является создание реплик только для чтения для распределения нагрузки.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.