65.9K
CodeProject 正在变化。 阅读更多。
Home

尽在 Azure .NET – Azure 表格存储(第二部分)

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2014 年 10 月 1 日

CPOL

14分钟阅读

viewsIcon

21878

在我们《尽在 Azure .NET – Azure 表格存储(第一部分)》中,我们介绍了 Azure 表格存储的详细信息,例如如何使用 NoSQL 数据库,它们与关系数据库的比较,如何设计和创建 Azure 表格,以及所有用于持久化数据的可用操作。在第二部分中,我们将介绍表格存储服务的剩余方面,例如

在我们《尽在 Azure .NET – Azure 表格存储(第一部分)》中,我们介绍了 Azure 表格存储的详细信息,例如如何使用 NoSQL 数据库,它们与关系数据库的比较,如何设计和创建 Azure 表格,以及所有用于持久化数据的可用操作。在第二部分中,我们将介绍表格存储服务的剩余方面,例如

  1. 高效与低效的查询
  2. 糟糕!我们遇到了一个障碍(处理重试策略)
  3. 并发
  4. 安全
  5. 辅助索引(奖励)

查询:高效与低效

现在我们已经设计并构建了 Azure 表格并有效地将数据持久化到其中,下一个重要的问题是如何从中检索数据。由于 Azure 表格的性质和限制,查询数据很容易变得低效。然而,我们并不是等到需要检索数据时才开始考虑查询数据,而是在设计表格时就考虑到了这一点。但是,尽管我们已经采取了步骤以有效地检索数据,但没有任何保障措施可以确保我们不会编写低效的查询。因此,我们不仅将介绍如何编写查询和检索数据,还将探讨编写高效查询所需的组件以及如何避免编写低效查询。 

高效查询

回顾一下,我们的表格没有辅助索引,只有由分区键和主键组成的聚集索引。因此,我们最高效的查询始终会涉及分区键和行键。通过使用特定的分区键和行键检索单个实体将始终获得最佳性能。

按分区键和行键检索单个实体

TableOperation query = TableOperation.Retrieve<footwear>(partitionkey, rowKey); TableResult result = table.Execute(query); Footwear footwear = (Footwear) result.Result;  

按特定分区键检索集合

次高效的查询将是涉及基于特定分区键的集合的查询

IQueryable<Footwear> query = table.CreateQuery<Footwear>().Where(f => f.PartitionKey == partitionKey);
List<Footwear> shoes = query.ToList();

您会注意到这里我们没有使用 *Retrieve* 方法,而是使用了 *CreateQuery*,这将允许您使用 LINQ 构建和执行查询。*CreateQuery* 有一些限制,因为它已针对处理 Azure 表格存储进行了专门优化,但您可以查看所有支持的 LINQ 操作。稍微低效的查询将涉及行键的范围或分区键和/或行键的范围。

IQueryable<Footwear> query =
                table.CreateQuery<Footwear>()
                    .Where(f => f.PartitionKey.Equals(startPartitionKey) || f.PartitionKey.Equals(endPartitionKey));
List<Footwear> shoes = query.ToList();

尽管我们在查询中指定了分区键,但由于这会跨越分区边界,因此根据每个分区中的实体数量以及涉及的分区数量,这都有可能成为一个相当低效的查询。 

低效查询

(短暂广告)

在深入研究什么决定了一个低效查询之前,现在是时候更细粒度地分解查询的处理方式了。我们已经讨论了分区键如何在允许数据可伸缩性方面发挥主要作用,以及它与查询效率的直接关系。让我们更仔细地研究使分区键发挥如此主要作用的根本原因。

这只是 Azure 存储架构的一些高层视图,但您可以找到一份 详细论文 以了解底层细节。Azure 存储由各种数量的存储节点组成,以帮助分散请求的负载。数据分区可能很容易跨越多个存储节点。分区服务器用于处理对特定数据分区的的所有请求,并且可以负责处理一个以上分区的请求。

因此,在重负载下,Azure 可以将数据分区分离到它们自己的独立分区服务器。因此,当您的查询跨越多个数据分区时,一个查询很容易跨越多个边界。这些边界可能包括数据分区本身以及分区服务器。每个边界都会带来性能成本。很快,我们将进一步讨论如何处理分区大小以及跨服务器分区的查询的一些要求,例如使用延续标记。

查询所有内容

到目前为止,我们所有的查询都要求分区键、行键或两者兼而有之。但分区键和行键通常不是表格实体上仅有的两个属性。一旦您开始编写利用实体属性的查询,您就进入了低效查询的领域。

IQueryable<Footwear> query = table
                 .CreateQuery<Footwear>()
                 .Where(f => f.Gender == "Male" && (f.Size > 4 && f.Size < 7));
IEnumerable<Footwear> shoes = query.ToList();

<footwear><footwear><footwear>如上所示,当未指定分区键或行键时,您可以保证会执行全表扫描。此外,由于未指定分区键,查询将被发送到每个分区服务器。一个升级是包含一个分区键,这将把查询减少到发送到特定分区服务器并仅产生分区扫描。

在每次查询中包含分区键可以极大地提高性能。对于重负载下的数据,这一点尤其明显。

延续标记

延续标记或多或少是查询的书签。它们允许查询从中断的地方继续。在 Azure 表格存储中,有许多场景要求您使用延续标记才能获取查询的所有可用结果,例如:

  • 查询已超出最多返回 1000 个实体的结果
  • 查询执行时间已超过 5 秒
  • 查询已跨越分区边界

由于这些限制,您可以看出分区大小如何轻松影响性能。过大的分区已被证明会直接影响可伸缩性,而过小则会轻易影响性能。一个返回 5 个实体,每个实体都有自己的分区的查询,可能会迫使您处理 4 个延续标记才能完全获取所有结果。那么,我们如何处理延续标记呢?

好消息是,如果您不想这样做,存储客户端 SDK 会为您处理延续标记。但是,在某些情况下,您需要对查询采取更细粒度的方法,例如,当您想确保不拉取过多的实体,或者您可能想实现分页时。下面是使用 *延续标记* 的示例

TableQuery<Footwear> query = table.CreateQuery<Footwear>().Where(f => f.PartitionKey == partitionKey).AsTableQuery();
TableContinuationToken token = null;
List<Footwear> shoes = new List<Footwear>();
do
{
     TableQuerySegment<Footwear> queryResult = query.ExecuteSegmented(token);
     token = queryResult.ContinuationToken;
     shoes.AddRange(queryResult.Results);
} while (token != null);
续订应基于延续标记是否为 Null,而不是基于是否返回了任何结果。这是因为发送到分区服务器的查询可能没有任何结果,但由于它跨越了分区服务器,因此它会自动返回一个没有查询结果的延续标记。此外,始终建议编写处理标记的查询,以确保您不会无意中拉取过多的数据结果。

糟糕!我们遇到了一个障碍(处理重试策略)

无论是互联网仓鼠睡着了还是发生了其他故障,您可以肯定会遇到操作尝试失败的情况。幸运的是,我们可以定义一个 *重试策略*,该策略为失败的操作提供了一个在必要时重复的机制。这是我们在第一篇“尽在 Azure”关于 Blob 存储 中没有涵盖的主题,但它适用于所有存储服务。有一些开箱即用的策略,并且还可以创建自定义重试策略。3 种可用策略是

  • Linear
  • 指数

我们可以设置 *CloudTableClient* 上的 *DefaultRequestOptions*,它允许我们指定重试策略等设置。除了在下面的示例中设置重试策略外,我们还设置了 *MaximumExecutionTime*,仅用于说明目的。重试策略允许您指定重试之间的 *Delta Backoff* 时间以及最大尝试次数: 

TableClient.DefaultRequestOptions = new TableRequestOptions
{
     RetryPolicy = new ExponentialRetry(TimeSpan.FromSeconds(10), 5),
     MaximumExecutionTime = TimeSpan.FromSeconds(10)
};

指数重试策略将强制重试之间的间隔呈指数增长,这样上述示例将在 5 秒后触发第一次重试,然后在下一次重试之间间隔 10 秒,然后是 20 秒,依此类推,直到达到最大尝试次数。这被定义为第一个构造函数的参数 *deltaBackoff*。此外,我们可以指定最大重试次数。*线性* 重试的工作方式是,重试之间的间隔时间基于指定的 *deltaBackoff* 时间保持不变。*无* 是一种策略,但仅当 CloudTableClient 检查时,它会为 *ShouldRetry()* 方法返回 *False*。

那么我们如何知道重试何时发生?

一个常见的问题是,如何知道操作是否失败以及是否尝试了重试?有几种选择。一种是使用 Microsoft 的 企业瞬时故障应用程序块,可以通过 Nuget 轻松访问,并连接到其事件处理程序以及在发生瞬时故障时了解情况的内置能力。

RetryPolicy<StorageTransientErrorDetectionStrategy> retryPolicy = new RetryPolicy<StorageTransientErrorDetectionStrategy>(new Incremental(5, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(3)));

retryPolicy.Retrying += (obj, eventArgs) => System.Console.WriteLine("Retrying");
TableClient.DefaultRequestOptions = new TableRequestOptions
{
     RetryPolicy = new NoRetry(),
};

TableOperation query = TableOperation.Retrieve<Footwear>(partitionKey, rowKey);
TableResult result = retrypolicy.ExecuteAction(() => table.Execute(query));
Footwear shoe = (Footwear) result.Result;

这种方法的问题在于代码本身的可读性,因为所有执行都由 *RetryPolicy* 处理。好消息是,我们可以创建一个自定义策略,通过实现 *IRetryPolicy* 接口来允许您处理重试的发生方式或额外的逻辑(如日志记录)。

当前的重试策略不重试 HTTP 状态码 4xx、306、501、505。因此,如果您编写自己的自定义重试策略,您也会想确保处理这些。

并发

在上一个关于 Blob 存储的文章中,我概述了不同的并发控制机制:乐观并发和悲观并发。如果您还没有机会阅读或不熟悉这些不同的控件,请 翻阅 并阅读后再继续。Azure 表格的一个很好的优点是它们默认使用乐观并发作为机制。此外,并发是实体级别的,而不是表级别的。这是 Blob 存储并发性与 Azure 表格存储之间的区别,Blob 存储并发性是在 blob 和容器级别提供的。

*ETag* 是 Azure 表格用于执行乐观并发的属性。检索实体时,会提供 *ETag* 属性。当您将更新持久化到实体时,*ETag* 是一个必须设置的必需属性。服务将进行比较,以验证它是否与表格中当前实体的 *ETag* 匹配,以便更新成功。如果不匹配,将返回 HTTP 状态码 412。

但是,如果您需要强制更新,可以将通配符“*”分配给 *ETag* 属性来强制更新。关于表格存储服务,并非所有请求都需要 *ETag*。存储 SDK 反映了 Azure 存储 API 的要求,在尝试执行 *Replace* 或 *Merge* 表操作时,如果未提供 *ETag* 或不是通配符“*”,客户端会在提交前发出警告。但是,在尝试 *InsertOrMerge* 或 *InsertOrReplace* 时,不需要 *ETag*,因为服务不需要它。

Azure 提供了一篇关于其存储服务的 并发管理 的有用文章,包括一些关于为您的表格要求悲观并发机制的建议。

如果您正在实现任何类型的公共 API,请谨慎使用 InsertOrReplace 和 InsertOrMerge 操作,因为您将默认为“最后写入者获胜”场景,这可能是不理想的。

安全

共享访问签名

Microsoft Azure Cloud Security Controls在 Azure 存储服务的安全性方面,所有存储服务都使用一种安全控制。该安全控制称为共享访问签名 (SAS)。共享访问签名提供了一种方式,让您可以指定消费者对特定存储资源的哪些权限。

为了方便那些没有阅读 上一篇关于 Blob 存储的文章 中所有详细信息的读者回顾一下,共享访问签名是一个 HMAC SHA-256 哈希,由一组查询字符串参数组成,这些参数指定了特定资源、授权访问的过期时间以及授予的权限等详细信息,仅举几例。

共享访问签名有两种形式:临时或可撤销。共享访问签名处于核心地位的是共享访问策略。该策略定义了共享访问签名的权限和过期日期等参数。正是共享访问策略使得临时共享访问签名和可撤销共享访问签名有所区别。

临时共享访问签名

临时共享访问签名是通过使用主存储访问密钥在哈希算法中生成的。因此,撤销临时共享访问签名的唯一方法是撤销用于生成哈希的存储访问密钥。这最终意味着任何其他使用存储帐户访问密钥的内容也需要更新。

要了解如何使用它,下面的示例演示了一个外部客户端可以从您的 API 请求共享访问签名的场景。此 SAS 然后可用于获取对已授予其访问权限的表格存储实体的访问权限。

//generate Shared Access Policy
SharedAccessTablePolicy policy = new SharedAccessTablePolicy
{
    Permissions = SharedAccessTablePermissions.Add | SharedAccessTablePermissions.Update |
    SharedAccessTablePermissions.Query,
    SharedAccessExpiryTime = DateTime.UtcNow.AddHours(1),
};

//generate Shared Access Signature using the policy
string sas = table.GetSharedAccessSignature(
              policy,
              null,
              "Athletics",
              null,
              "Running",
              null
);

然后,客户端可以使用此共享访问签名最终创建一个 *CloudTableClient*,该客户端将根据共享访问签名设置的限制进行操作。

StorageCredentials creds = new StorageCredentials(sas);

//endpoint created in this fashion to make clear what it represents
string endpoint = string.Format("http://{0}.table.core.windows.net", accountName);

CloudTableClient tableClient = new CloudTableClient(new Uri(endpoint), creds);
CloudTable table = tableClient.GetTableReference("footwear");

从这里开始,我们可以创建和执行与权限一致的 *TableOperations*。当尝试操作超出共享访问签名授予的权限范围时,服务将像找不到实体一样运行。当尝试查询实体并且服务在 *TableResult* 中返回 null 和 HTTP 状态码 404 时,这一点很明显。同时,当尝试执行任何更新或插入操作而没有权限时,将导致抛出带有 HTTP 状态码 404 的 *StorageException*。

托管共享访问签名

如前所述,共享访问签名的核心是策略。但在托管共享访问签名的情况下,策略是在存储帐户上生成和存储的。因此,当我们生成托管共享访问签名时,我们会指定已存在的存储访问策略的标识符。这使我们能够通过简单地撤销用于生成共享访问签名的存储访问策略来撤销共享访问签名。

TablePermissions permissions = new TablePermissions();
permissions.SharedAccessPolicies.Add("tablepolicy1", new SharedAccessTablePolicy
{
    SharedAccessExpiryTime = DateTime.UtcNow.AddDays(2),
    Permissions = SharedAccessTablePermissions.Add | SharedAccessTablePermissions.Query
 });

table.SetPermissions(permissions);

从这里开始,与往常一样,唯一的区别是在创建共享访问签名时指定存储访问策略。

//generate Shared Access Signature using the policy identifier
string sas = table.GetSharedAccessSignature(
     null,
     "tablepolicy1",
     "Athletics",
     null,
     "Running",
     null
);

最后,正如我们在最初的临时示例中所见,已收到共享访问签名的客户端可以通过使用 SAS 间接创建其 *CloudTableClient*,并执行任何符合指定存储访问策略的表操作。

StorageCredentials creds = new StorageCredentials(sas);

//endpoint created in this fashion to make clear what it represents
string endpoint = string.Format("http://{0}.table.core.windows.net", accountName);

CloudTableClient tableClient = new CloudTableClient(new Uri(endpoint), creds);
CloudTable table = tableClient.GetTableReference("footwear");
 尽管只有 5 个存储访问策略的限制,但当需要撤销共享访问签名时,使用它们将为您带来好处。它集中了您的分布式权限,并使管理更加轻松。 

辅助索引(奖励)

我知道你在想,“Azure 的表格存储不支持辅助索引!” 这是正确的。但我认为,对于一个奖励部分,我想提一下,那些使用不支持辅助索引的 NoSQL 数据库的用户并没有完全放弃找到一种方法来优化不基于主键的查询。

我们在《第一部分》的“去规范化”部分已经看了一种方法,当时我们讨论了复制具有不同行键的实体。这提供了辅助索引的好处,但它也带来了沉重的维护开销,并且根据数据的大小,对整体数据库大小有显着影响。但是,如果您有兴趣研究其他生成辅助索引的方法,您可以在 此处 阅读相关内容。

结论

我们在第二部分关于 Azure 表格存储的内容中涵盖了很多内容,包括编写高效查询和如何避免低效查询、使用重试策略、并发和安全性。您可以清楚地看到有很多内容需要学习。但是,.NET 存储客户端 SDK 的一个优点是它使得入门相对容易。如果您直接跳到了第二部分,请查看 第一部分 以获取故事的上半部分以及如何开始使用 Microsoft 的 Azure 表格存储服务。 

一些有用的参考资料

  1. 支持的查询运算符
  2. 重试策略
  3. 企业库 6 故障处理
  4. Azure 表格存储中的并发
  5. 二级索引
© . All rights reserved.