如何在 Azure 表存储中使用 partitionkey 加快查询速度

Posted

技术标签:

【中文标题】如何在 Azure 表存储中使用 partitionkey 加快查询速度【英文标题】:how to speed up a query with partitionkey in azure table storage 【发布时间】:2020-03-01 06:05:26 【问题描述】:

我们如何提高此查询的速度?

1-2 minutes 的范围内,我们大约有 100 个消费者在执行以下查询。这些运行中的每一个都代表消耗函数的 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 个结果传输到代码中就会花费大量的网络时间。没关系,之后您仍然要进行过滤。 |始终在查询中进行尽可能多的过滤处理。理想情况下,在获得索引和/或计算视图的结果的行上。 那些“翻译”对象大吗?为什么你不喜欢获取一些参数而不是像整个数据库那样 gettin`? @HirasawaYui 不,他们很小 你应该做更多的过滤,拉5000个结果似乎没有意义。在不知道您的数据的情况下无法判断,但我想说您需要找到一种方法以更有意义的方式对其进行分区或在查询中引入某种过滤 有多少个不同的分区? 【参考方案1】:

您可以考虑 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))
));

因为您要检索大量数据,所以最好并行运行查询。所以,你应该用我基于Stephen Toub Parallel.While写的Parallel.ForEach替换ExecuteQueryAsync方法中的do 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>();

如您所见,方法本身不是异步的(您应该更改它的名称)并且 Parallel.ForEach 与传入异步方法不兼容。这就是我改用 ExecuteQuerySegmented 的原因。但是,为了提高性能并利用异步方法的所有优点,您可以在 Dataflow 或 中将上述 ForEach 循环替换为 ActionBlock 方法>ParallelForEachAsync 来自AsyncEnumerator Nuget package 的扩展方法。

2。执行独立的并行查询然后合并结果是一个不错的选择,即使它的性能提升最多 10%。这使您有时间能够找到最佳性能友好的查询。但是,永远不要忘记在其中包含所有约束,并测试两种方法以了解哪一种更适合您的问题。

3。我不确定这是否是一个好建议,但是请执行此操作并查看结果。如MSDN中所述:

表服务强制执行服务器超时,如下所示:

查询操作:在超时间隔期间,查询可能会执行 最多五秒。如果查询未在 五秒间隔,响应包括继续标记 用于在后续请求中检索剩余项目。见查询 超时和分页了解更多信息。

插入、更新和删除操作:最大超时间隔为 30秒。三十秒也是所有的默认间隔 插入、更新和删除操作。

如果您指定的超时时间小于服务的默认超时时间,则将使用您的超时时间间隔。

所以你可以玩超时并检查是否有任何性能改进。

2021 年 6 月 30 日更新

感谢@WouterVanRanst 仔细研究了上面的sn-p,我决定更新它并使用Parallel.ForEach 方法的另一个重载,使循环单线程并防止TableContinuationToken 上的竞争条件。您可以在 MSDN 上通过示例 here 找到有关分区局部变量的描述。这是ExecuteQueryAsync&lt;T&gt; 方法的新外观:

public static IEnumerable<T> ExecuteQueryAsync<T>(this CloudTable table, TableQuery<T> query) where T : ITableEntity, new()

    TableContinuationToken token = null;
    var items = new List<T>();

    Parallel.ForEach(new InfinitePartitioner(), () =>
    
        return null as TableQuerySegment<T>;
    , (ignored, loopState, segment) =>
    
        segment = table.ExecuteQuerySegmented(query, token) as TableQuerySegment<T>;
        
        token = segment.ContinuationToken;

        if (token == null)
            loopState.Stop();

        return segment;
    ,
    (seg) => items.AddRange(seg)
    );

    return items;

注意: 当然,您可以完善上面的代码或找到一种更好的方法来防止竞争条件,但它很快就会很简单。我很高兴听到您对此的看法。

【讨论】:

您的片段是否与 ContinuationToken 没有竞争条件?即 Parallel.ForEach 将使用相同的 ContinuationToken 进行多次比赛,第一次将更新令牌以引发新的比赛? @WouterVanRanst 是的,它可能容易受到竞争条件的影响。谢谢你提到。查看更新的答案。【参考方案2】:
  var over1000Results = table.ExecuteQueryAsync(treanslationsQuery).Result.Cast<Translation>();
        return over1000Results.Where(x => x.expireAt > DateTime.Now)
                           .Where(x => x.effectiveAt < DateTime.Now);

这是其中一个问题,您正在运行查询,然后使用这些“wheres”从内存中过滤它。将过滤器移到查询运行之前,这应该会有很大帮助。

其次,您必须提供一些从数据库中检索的行数限制

【讨论】:

这并没有什么不同【参考方案3】:

不幸的是,下面的查询引入了全表扫描

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

你应该把它分成两个Partition Key过滤器,分别查询,这样会变成两个分区扫描,效率更高。

【讨论】:

我们看到这可能提高了 10%,但这还不够【参考方案4】:

所以秘诀不仅在于代码,还在于设置 Azure 存储表。

a) 在 Azure 中优化查询的主要选项之一是引入缓存。这将大大缩短您的整体响应时间,从而避免您提到的高峰时段出现瓶颈。

b) 此外,当从 Azure 中查询实体时,最快的方法是同时使用 PartitionKey 和 RowKey。这些是表存储中唯一的索引字段,任何使用这两者的查询都将在几毫秒内返回。因此,请确保您同时使用 PartitionKey 和 RowKey。

在此处查看更多详细信息: https://docs.microsoft.com/en-us/azure/storage/tables/table-storage-design-for-query

希望这会有所帮助。

【讨论】:

【参考方案5】:

注意:这是一般的数据库查询优化建议。

ORM 可能正在做一些愚蠢的事情。在进行优化时,可以降低抽象层。所以我建议用查询语言(SQL?)重写查询,以便更容易看到发生了什么,也更容易优化。

优化查找的关键是排序!与每次查询扫描整个表相比,保持表排序通常要便宜得多!因此,如果可能,请按照查询中使用的键对表进行排序。在大多数数据库解决方案中,这是通过创建索引键来实现的。

如果组合很少,另一种有效的策略是将每个查询作为一个单独的(内存中的临时)表,该表始终是最新的。因此,当插入某些内容时,它也会“插入”到“视图”表中。一些数据库解决方案将此称为“视图”。

更粗暴的策略是创建只读副本来分配负载。

【讨论】:

以上是关于如何在 Azure 表存储中使用 partitionkey 加快查询速度的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Azure Blob 存储中备份和还原 Azure SQL 表,反之亦然

如何在 C# 中获取 Azure 表存储中的所有行?

如何向现有的 Azure 表存储添加新列

如何在 azure 表存储中使用 new TableQuery<T>

如何使用 OData 筛选器筛选 Azure 表存储中的布尔值?

如何将属性排除在 Azure 表存储中?