你当前正在访问 Microsoft Azure Global Edition 技术文档网站。 如果需要访问由世纪互联运营的 Microsoft Azure 中国技术文档网站,请访问 https://docs.azure.cn

为 Azure 表存储设计可缩放的分区策略

本文讨论在 Azure 表存储中对表进行分区,以及可用于确保高效可伸缩性的策略。

Azure 提供高度可用且高度可缩放的云存储。 Azure 的基础存储系统通过一组服务提供,包括 Azure Blob 存储、Azure 表存储、Azure 队列存储和Azure 文件存储。

Azure 表存储旨在存储结构化数据。 Azure 存储服务支持无限数量的表。 每个表都可以缩放到大规模级别,并提供 TB 的物理存储。 若要充分利用表,必须以最佳方式对数据进行分区。 本文探讨可用于有效地为 Azure 表存储分区数据的策略。

表实体

表实体表示存储在表中的数据单位。 表实体类似于典型关系数据库表中的行。 每个实体都定义一个属性集合。 每个属性都按其名称、值和值的数据类型定义为键/值对。 实体必须将以下三个系统属性定义为属性集合的一部分:

  • PartitionKeyPartitionKey 属性存储标识实体所属分区的字符串值。 如后文所述,分区是表可伸缩性不可或缺的一部分。 具有相同 PartitionKey 值的实体存储在同一分区中。

  • RowKeyRowKey 属性存储唯一标识每个分区中的实体的字符串值。 PartitionKeyRowKey 共同构成实体的主键。

  • TimestampTimestamp 属性提供实体的可跟踪性。 时间戳是一个日期/时间值,用于指示上次修改实体的时间。 时间戳有时称为实体 的版本。 对时间戳的修改将被忽略,因为表服务在所有插入和更新操作期间维护此属性的值。

表主键

Azure 实体的主键由 组合的 PartitionKeyRowKey 属性组成。 这两个属性构成表中的单个聚集索引。 PartitionKeyRowKey 属性最多可存储 1 KiB 的字符串值。 也允许使用空字符串;但是,不允许使用 null 值。

聚集索引按 PartitionKey 升序排序,然后按 RowKey 升序排序。 可以在所有查询响应中观察到排序顺序。 在排序操作期间将使用词汇比较。 字符串值“111”显示在字符串值“2”之前。 在某些情况下,你可能希望排序顺序为数字。 若要按数字和升序排序,必须使用固定长度的零填充字符串。 在前面的示例中,“002”显示在“111”之前。

表分区

分区表示具有相同 PartitionKey 值的实体集合。 始终从一个分区服务器提供分区。 每个分区服务器可以为一个或多个分区提供服务。 分区服务器在一段时间内为一个分区的实体提供服务时,存在一个速率限制。 具体而言,分区的可伸缩性目标为每秒 2000 个实体。 在存储节点上的最小负载期间,此吞吐量可能更高,但当节点变为热或活动状态时,吞吐量会受到限制。

为了更好地说明分区的概念,下图显示了一个表,其中包含用于足部比赛项目注册的一小部分数据。 该图显示了分区的概念视图,其中 PartitionKey 包含三个不同的值:事件的名称与三个距离 (全马拉松、半程马拉松和 10 公里) 。 此示例使用两个分区服务器。 服务器 A 包含半程马拉松和 10 公里距离的注册。 服务器 B 仅包含全马拉松距离。 显示 RowKey 值以提供上下文,但这些值对于此示例没有意义。

显示具有三个分区的表
包含三个分区的表

可伸缩性

因为一个分区始终由一个分区服务器提供服务,一个分区服务器可以为一个或多个分区提供服务,因此,为实体提供服务的效率与服务器的运行状况有关。 遇到高流量的分区的服务器可能无法维持高吞吐量。 例如,在上图中,如果收到“2011 纽约市Marathon__Half”的许多请求,服务器 A 可能会变得太热。 为增大该服务器的吞吐量,存储系统会将分区的负载在其他服务器之间进行平衡。 因此,流量将分布到许多其他服务器。 为了优化流量负载均衡,应使用更多分区,以便 Azure 表存储可以将分区分发到更多分区服务器。

实体组事务

实体组事务是在具有相同 PartitionKey 值的实体上以原子方式实现的一组存储操作。 如果实体组中的任何存储操作失败,则会回滚实体中的所有存储操作。 实体组事务包含不超过 100 个存储操作,大小可能不超过 4 MiB。 实体组事务为 Azure 表存储提供有限形式的原子性、一致性、隔离性和持久性, (ACID) 关系数据库提供的语义。

实体组事务提高了吞吐量,因为它们减少了必须提交到 Azure 表存储的单个存储操作的数量。 实体组交易还提供经济效益。 无论实体组事务包含多少个存储操作,都会按单个存储操作计费。 由于实体组事务中的所有存储操作都会影响具有相同 PartitionKey 值的实体,因此需要使用实体组事务可以驱动 PartitionKey 值的选择。

范围分区

如果对实体使用唯一 的 PartitionKey 值,则每个实体都属于其自己的分区。 如果使用的唯一值增加或减少值,Azure 可能会创建范围分区。 范围分区对具有顺序唯一 PartitionKey 值的实体进行分组,以提高范围查询的性能。 如果没有范围分区,范围查询必须跨越分区边界或服务器边界,这可能会降低查询性能。 考虑使用下表的应用程序,该表具有 PartitionKey 的递增序列值:

PartitionKey RowKey
"0001" -
"0002" -
"0003" -
"0004" -
"0005" -
"0006" -

Azure 可能会将前三个实体分组到一个范围分区中。 如果将范围查询应用于使用 PartitionKey 作为条件的表,并请求从“0001”到“0003”的实体,则查询可能会有效地执行,因为实体是从单个分区服务器提供的。 无法保证何时以及如何创建范围分区。

如果插入具有增加或减小 PartitionKey 值的实体,表存在范围分区可能会影响插入操作的性能。 插入具有增加 PartitionKey 值的实体称为仅追加模式。 插入具有递减值的实体称为仅追加前模式。 请考虑不要使用这些类型的模式,因为插入请求的总体吞吐量受单个分区服务器的限制。 这是因为,如果存在范围分区,则第一个和最后一个 (区域) 分区分别包含最小和最大的 PartitionKey 值。 因此,插入一个新实体(其 PartitionKey 值按顺序较低或更高)将面向其中一个最终分区。 下图显示了基于上一个示例的一组可能的范围分区。 如果插入了一组“0007”、“0008”和“0009”实体,则会将这些实体分配给最后一个 (橙色) 分区。

显示一组范围分区AZU_CH03_Figure2的关系图
一组范围分区

请务必注意,如果插入操作使用更分散的 PartitionKey 值,则不会对性能产生负面影响。

分析数据

与关系数据库中可用于管理索引的表不同,Azure 表存储中的表只能有一个索引。 Azure 表存储中的索引始终由 PartitionKeyRowKey 属性组成。

在 Azure 表中,无法通过添加更多索引或在推出现有表后更改表来优化表的性能。设计表时必须分析数据。 为了获得最佳可伸缩性以及查询和插入效率,需要考虑的最重要方面是 PartitionKeyRowKey 值。 本文重点介绍如何选择 PartitionKey ,因为它与表的分区方式直接相关。

分区大小

分区大小指的是一个分区包含的实体数。 正如我们在 可伸缩性中讨论的那样,拥有更多分区意味着获得更好的负载均衡。 PartitionKey 值的粒度会影响分区的大小。 在最粗糙的级别,如果将单个值用作 PartitionKey,则所有实体都位于非常大的单个分区中。 在最佳粒度级别, PartitionKey 可以包含每个实体的唯一值。 结果是每个实体都有一个分区。 下表显示了粒度范围的优缺点:

PartitionKey 粒度 分区大小 优点 缺点
单值 少量实体 批处理事务可用于任何实体。

所有实体都是本地实体,并从同一个存储节点提供服务。
单值 大量实体 实体组事务可用于任何实体。 有关实体组事务的限制的详细信息,请参阅 执行实体组事务 缩放是有限的。

吞吐量受限于单个服务器的性能。
多个值 多个分区

分区大小取决于实体分布。
某些实体可以进行批处理事务。

可以进行动态分区。

(没有延续标记) ,则可能会进行单请求查询。

可以在更多分区服务器之间进行负载均衡。
跨分区的实体分布非常不均匀可能会限制更大和更活跃的分区的性能。
唯一值 许多小分区 该表具有高度可伸缩性。

范围分区可以提高跨分区范围查询的性能。
涉及范围的查询可能需要访问多个服务器。

批处理事务是不可能的。

仅追加模式或仅追加前模式可能会影响插入吞吐量。

该表显示了 PartitionKey 值对缩放的影响。 最佳做法是优先使用较小的分区,因为它们提供更好的负载均衡。 在某些情况下,较大的分区可能适用,但不一定是不利的。 例如,如果应用程序不需要可伸缩性,则可能适合使用单个大型分区。

确定查询

查询从表中检索数据。 分析 Azure 表存储中表的数据时,请务必考虑应用程序将使用哪些查询。 如果应用程序有多个查询,你可能需要确定其优先级,尽管你的决策可能是主观的。 在许多情况下,主查询与其他查询是可区分的。 就性能来说,查询可以分为不同的类别。 由于表只有一个索引,因此查询性能通常与 PartitionKeyRowKey 属性相关。 下表显示了不同类型的查询及其性能评级:

查询类型 PartitionKey 匹配 RowKey 匹配 性能分级
行范围扫描 Exact 部分 最好使用较小的分区。

对于非常大的分区,则错误。
分区范围扫描 部分 部分 适合接触少量分区服务器。

更糟的是,更多的服务器被接触。
全表扫描 部分、无 部分、无 更糟的是扫描分区的子集。

扫描所有分区时最差。

注意

此表定义了相对性的性能评级。 分区的数量和大小最终可能决定查询的执行方式。 例如,与对具有几个小分区的表进行完整表扫描相比,对具有许多大分区的表的分区范围扫描可能表现不佳。

上表中列出的查询类型根据性能评级显示从要使用的最佳查询类型到最差类型的进度。 点查询是要使用的最佳查询类型,因为它们完全使用表的聚集索引。 以下点查询使用脚赛注册表中的数据:

http://<account>.windows.core.net/registrations(PartitionKey=”2011 New York City Marathon__Full”,RowKey=”1234__John__M__55”)  
  

如果应用程序使用多个查询,则并非所有查询都是点查询。 就性能而言,范围查询在点查询之后。 有两种类型的范围查询:行范围扫描和分区范围扫描。 行范围扫描指定单个分区。 由于操作发生在单个分区服务器上,因此行范围扫描通常比分区范围扫描更高效。 但是,行范围扫描性能的一个关键因素是查询的选择性程度。 查询选择性是指必须遍历多少行才能找到匹配行。 在行范围扫描期间,选择性越高的查询越高效。

若要评估查询的优先级,请考虑每个查询的频率和响应时间要求。 频繁执行的查询的优先级可能更高。 但是,一个重要但很少使用的查询可能具有较低的延迟要求,这可能会在优先级列表中排名较高。

选择 PartitionKey 值

任何表设计的核心是其可伸缩性、用于访问它的查询以及存储操作要求。 所选 的 PartitionKey 值决定了表的分区方式以及可以使用的查询类型。 存储操作(尤其是插入)也可能会影响你选择的 PartitionKey 值。 PartitionKey 值的范围可以从单个值到唯一值。 还可以使用多个值创建它们。 可以使用实体属性来形成 PartitionKey 值。 或者,应用程序可以计算该值。 以下部分讨论重要注意事项。

实体组事务

开发人员应首先考虑应用程序是否将使用实体组事务 (批量更新) 。 实体组事务要求实体具有相同的 PartitionKey 值。 此外,由于批处理更新适用于整个组, 因此 PartitionKey 值的选择可能会受到限制。 例如,对现金交易进行维护的银行应用程序必须以原子方式将现金交易插入到表中。 现金交易代表借方和信贷方,必须净为零。 此要求意味着不能将帐号用作 PartitionKey 值的任何一部分,因为事务的每一方都使用不同的帐号。 相反,事务 ID 可能是更好的选择。

分区

分区数和大小会影响加载的表的可伸缩性。 它们还受 PartitionKey 值的粒度控制。 根据分区大小确定 PartitionKey 可能很困难,尤其是在值分布难以预测的情况下。 一个好的经验法则是使用多个较小的分区。 许多表分区使 Azure 表存储能够更轻松地管理从中提供分区的存储节点。

PartitionKey 选择唯一或更精细的值会导致分区更小但更多。 这通常是有利的,因为系统可以对多个分区进行负载均衡,以跨多个分区分配负载。 不过,你应当考虑采用许多分区对跨分区范围查询的影响。 这些类型的查询必须访问多个分区才能满足查询要求。 分区可能分布在多个分区服务器中。 如果某个查询跨过了服务器边界,则必须返回继续标记。 继续标记指定下一个 PartitionKeyRowKey 值,以检索查询的下一组数据。 换句话说,继续标记表示至少一个对服务的请求,这可能会降低查询的整体性能。

查询选择性是可能会影响查询性能的另一个因素。 查询选择性是一个表示必须为每个分区遍历多少行的度量。 查询的选择性越高,查询返回所需行的效率就越高。 范围查询的总体性能可能取决于必须接触的分区服务器数或查询的选择性。 在将数据插入表中时,还应避免使用仅追加模式或仅追加前模式。 如果使用这些模式,尽管创建了小分区和多个分区,但可能会限制插入操作的吞吐量。 范围分区中讨论了仅追加模式和仅追加前模式。

查询

了解将使用的查询有助于确定哪些属性对于 PartitionKey 值很重要。 在查询中使用的属性是 PartitionKey 值的候选项。 下表提供了有关如何确定 PartitionKey 值的一般准则:

如果实体…… 操作
具有一个键属性 将其用作 PartitionKey
具有两个键属性 使用一个作为 PartitionKey ,另一个用作 RowKey
具有两个以上键属性 使用串联值的复合键。

如果有多个同样占主导地位的查询,可以使用所需的不同 RowKey 值多次插入信息。 应用程序将管理辅助 (或第三) 行等。 可以使用这种类型的模式来满足查询的性能要求。 以下示例使用足部比赛注册示例中的数据。 它有两个主要查询:

  • 按选手编号查询
  • 按年龄查询

若要为两个主流查询提供服务,请将两个行作为一个实体组事务插入。 下表显示了此方案的 PartitionKeyRowKey 属性。 RowKey 值为 bib 和 age 提供前缀,以便应用程序可以区分这两个值。

PartitionKey RowKey
2011 New York City Marathon__Full BIB:01234__John__M__55
2011 New York City Marathon__Full AGE:055__1234__John__M

在此示例中,实体组事务是可能的,因为 PartitionKey 值相同。 组事务提供插入操作的原子性。 尽管可以将此模式与不同的 PartitionKey 值一起使用,但我们建议使用相同的值来获得此优势。 否则,可能需要编写额外的逻辑,以确保使用不同 PartitionKey 值的原子事务。

存储操作

Azure 表存储中的表可能不仅遇到来自查询的负载。 它们还可能会遇到来自存储操作(如插入、更新和删除)的负载。 请考虑对表执行哪种类型的存储操作,以及以何种速率执行。 如果不经常执行这些操作,则可能无需担心这些操作。 但是,对于频繁的操作(如在短时间内执行多个插入),必须考虑如何将这些操作作为所选 PartitionKey 值的结果。 重要示例包括仅追加模式和仅追加前模式。 范围分区中讨论了仅追加模式和仅追加前模式。

使用仅追加或仅追加前模式时,对后续插入使用 PartitionKey 的唯一升序或降序值。 如果将此模式与频繁的插入操作相结合,则表将无法以出色的可伸缩性为插入操作提供服务。 表的可伸缩性受到影响,因为 Azure 无法将操作请求负载均衡到其他分区服务器。 在这种情况下,可能需要考虑使用随机值,例如 GUID 值。 然后,分区大小可以保持较小,并在存储操作期间保持负载均衡。

表分区压力测试

PartitionKey 值很复杂或需要与其他 PartitionKey 映射进行比较时,可能需要测试表的性能。 测试应当观察分区在峰值负载下的性能表现。

执行压力测试

  1. 创建测试表。
  2. 加载包含数据的测试表,使其包含具有要面向的 PartitionKey 值的实体。
  3. 使用应用程序模拟表的峰值负载。 使用步骤 2 中的 PartitionKey 值将单个分区作为目标。 此步骤对于每个应用程序都是不同的,但模拟应包括所有必需的查询和存储操作。 可能需要调整应用程序,使其面向单个分区。
  4. 观察表上的 GET 或 PUT 操作的吞吐量。

若要观察吞吐量,请将实际值与单个服务器上单个分区的指定限制进行比较。 分区限制为每秒 2000 个实体。 如果分区的吞吐量超过每秒 2000 个实体,则服务器可能在生产设置中运行得太热。 在这种情况下, PartitionKey 值可能太粗糙,因此分区不足或分区太大。 可能需要修改 PartitionKey 值,以便将分区分布到更多服务器中。

负载均衡

当分区变得太热时,会在分区层进行负载均衡。 当分区太热时,分区(特别是分区服务器)的运行超出了其目标可伸缩性。 对于 Azure 存储,每个分区的可伸缩性目标为每秒 2000 个实体。 负载均衡也会在分布式文件系统 (DFS) 层进行。

DFS 层的负载均衡处理 I/O 负载,不在本文讨论范围内。 超出可伸缩性目标后,分区层的负载均衡不会立即发生。 相反,系统会等待几分钟,然后开始负载均衡过程。 这可以确保分区确实已变得热门。 不需要使用触发负载均衡的生成负载来设置分区,因为系统会自动执行该任务。

如果表具有特定负载,则系统可能能够根据实际负载均衡分区,这会导致分区分布明显不同。 请考虑编写处理超时和服务器繁忙错误的代码,而不是启动分区。 当系统进行负载均衡时,将返回错误。 通过使用重试策略处理这些错误,应用程序可以更好地处理峰值负载。 下面一节中更详细地讨论了重试策略。

发生负载均衡时,分区会脱机几秒钟。 在脱机期间,系统会将分区重新分配给不同的分区服务器。 请务必注意,分区服务器不会存储数据。 相反,分区服务器通过 DFS 层为实体提供服务。 由于数据不存储在分区层,因此将分区移动到不同的服务器是一个快速的过程。 这种灵活性极大地限制了应用程序可能会遇到的停机时间(如果有)。

重试策略

应用程序必须处理存储操作失败,以帮助确保不会丢失任何数据更新。 某些失败不需要重试策略。 例如,返回“401 未授权”错误的更新不会受益于重试操作,因为应用程序状态很可能在解决 401 错误的重试之间不会更改。 但是,服务器繁忙或超时等错误与 Azure 的负载均衡功能有关,这些功能可提供表可伸缩性。 当为实体提供服务的存储节点变得热时,Azure 会通过将分区移到其他节点来平衡负载。 在此期间,分区可能不可访问,从而导致服务器繁忙或超时错误。 最终,分区将重新启用并恢复更新。

重试策略适用于服务器繁忙或超时错误。 在大多数情况下,可以从重试逻辑中排除 400 级错误和大约 500 级错误。 可以排除的错误包括 501 未实现和不支持 505 HTTP 版本。 然后,可以针对最多 500 级错误(例如服务器忙 (503) 和超时 (504) )实施重试策略。

可以从应用程序的三种常见重试策略中进行选择:

  • 不重试:不进行重试尝试。
  • 修复了退避:该操作重试 N 次,具有常量退避值。
  • 指数退避:使用指数退避值重试操作 N 次。

“不重试”策略是用来处理操作故障的一种简单(和逃避性的)方式。 但是,“不重试”策略不是很有用。 不强制进行任何重试尝试会对发生失败操作后未正确存储的数据带来明显的风险。 更好的策略是使用固定退避策略。 提供重试具有相同回退持续时间的操作的能力。

但是,该策略并未针对处理高度可缩放的表进行优化。 如果许多线程或进程等待相同的持续时间,则可能发生冲突。 建议的重试策略是使用指数退避的策略,其中每次重试尝试的时间都长于上次尝试。 它类似于计算机网络(如以太网)中使用的冲突避免算法。 指数回退使用一个随机因子,使生成的间隔具有更多差异。 然后,回退值将受最小限制和最大限制的约束。 可以使用以下公式通过指数算法来计算下一个回退值:

y = Rand(0.8z, 1.2z)(2x-1

y = Min(zmin + y, zmax

其中:

z = 以毫秒为单位的默认回退值

zmin = 以毫秒为单位的默认最小回退值

zmax = 以毫秒为单位的默认最大回退值

x = 重试次数

y = 以毫秒为单位的回退值

Rand (随机) 函数中使用的 0.8 和 1.2 乘数在原始值的 ±20% 内生成默认退避的随机方差。 ±20% 范围对于大多数重试策略都是可接受的,并且可以防止进一步的冲突。 可以使用以下代码实现公式:

int retries = 1;  
  
// Initialize variables with default values  
var defaultBackoff = TimeSpan.FromSeconds(30);  
var backoffMin = TimeSpan.FromSeconds(3);  
var backoffMax = TimeSpan.FromSeconds(90);  
  
var random = new Random();  
  
double backoff = random.Next(  
    (int)(0.8D * defaultBackoff.TotalMilliseconds),   
    (int)(1.2D * defaultBackoff.TotalMilliseconds));  
backoff *= (Math.Pow(2, retries) - 1);  
backoff = Math.Min(  
    backoffMin.TotalMilliseconds + backoff,   
    backoffMax.TotalMilliseconds);  
  

摘要

Azure 表存储中的应用程序可以存储大量数据,因为表存储跨多个存储节点管理和重新分配分区。 你可以使用数据分区来控制表的可伸缩性。 在定义表架构时提前计划,以确保实现高效的分区策略。 具体而言,请在选择 PartitionKey 值之前分析应用程序的要求、数据和查询。 当系统响应流量时,每个分区可能会重新分配给不同的存储节点。 使用分区压力测试确保表具有正确的 PartitionKey 值。 此测试可帮助你确定分区何时太热,并帮助你进行必要的分区调整。

若要确保应用程序处理间歇性错误并保留数据,请使用重试策略和退避。 Azure 存储客户端库使用的默认重试策略具有指数退避,可避免冲突并最大化应用程序的吞吐量。