表存储的性能与可伸缩性查检表

在开发使用表存储的高性能应用程序方面,Azure 制定了许多经过证实的做法。 此查检表列出了开发人员在优化性能时可以遵循的关键做法。 在设计应用程序时以及在整个流程中,请牢记这些做法。

Azure 存储在容量、事务速率和带宽方面存在可伸缩性与性能目标。 有关 Azure 存储可伸缩性目标的详细信息,请参阅标准存储帐户的可伸缩性和性能目标表存储的可伸缩性和性能目标

清单

本文以查检表的形式组织了在开发表存储应用程序时在性能方面可以遵循的经过证实的做法。

完成 类别 设计注意事项
  可伸缩性目标 是否可将应用程序设计为避免使用的存储帐户数超过最大数目?
  可伸缩性目标 是否要避免接近容量和事务限制?
  可伸缩性目标 是否在接近实体数/秒的可伸缩性目标?
  网络 客户端设备是否具有足够高的带宽和足够低的延迟,以实现所需的性能?
  网络 客户端设备是否具有优质网络链接?
  网络 客户端应用程序是否位于存储帐户所在的同一区域?
  直接客户端访问 是否使用共享访问签名 (SAS) 和跨源资源共享 (CORS) 来实现对 Azure 存储的直接访问?
  批处理 应用程序是否使用实体组事务来批处理更新?
  .NET 配置 是否使用 .NET Core 2.1 或更高版本来实现最佳性能?
  .NET 配置 是否已将客户端配置为使用足够数量的并发连接?
  .NET 配置 对于 .NET 应用程序,是否已将 .NET 配置为使用足够数量的线程?
  并行度 是否能够确保对并行度进行适当的界定,使客户端功能不会过载或接近可伸缩性目标?
  工具 是否使用 Microsoft 提供的最新版客户端库和工具?
  重试 是否对限制错误和超时使用重试策略和指数退避?
  重试 对于不可重试的错误,应用程序是否会避免重试?
  配置 是否使用 JSON 进行表请求?
  配置 是否已关闭 Nagle 算法以改进小型请求的性能?
  表和分区 是否已对数据进行了适当的分区?
  热分区 是否会避免使用仅追加和仅预置模式?
  热分区 插入/更新的内容是否会分布在多个分区中?
  查询范围 是否已将架构设计为允许在大多数情况下使用点查询,尽量少用表查询?
  查询密度 查询是否通常只扫描和返回应用程序会使用的行?
  限制返回的数据 是否使用筛选来避免返回不需要的实体?
  限制返回的数据 是否使用投影来避免返回不需要的属性?
  非规范化 是否已对数据实施非规范化,以此避免在尝试获取数据时无效的查询或多次读取请求?
  插入、更新和删除 是否会对需要进行事务处理或可以同时完成的请求进行批处理,以此减少不必要的重复操作?
  插入、更新和删除 是否会避免仅仅为了确定是否需要调用插入或更新而检索某个实体?
  插入、更新和删除 是否考虑过将各种需要频繁检索的数据作为属性一起存储在单个实体中而非多个实体中?
  插入、更新和删除 对于那些始终需要一起检索并可成批写入的实体(例如时序数据),是否考虑过使用 Blob 而非表?

可伸缩性目标

如果应用程序接近或超过任何可伸缩性目标,则可能会出现事务处理延迟或限制越来越严重的现象。 当 Azure 存储对应用程序进行限制时,该服务将开始返回 503(服务器繁忙)或 500(操作超时)错误代码。 保持在可伸缩性目标限制范围内,以避免这些错误,是增强应用程序性能的重要组成部分。

有关表服务的可伸缩性目标的详细信息,请参阅表存储的可伸缩性和性能目标

最大存储帐户数

如果即将达到特定订阅/区域组合允许的最大存储帐户数,你是否会使用多个存储帐户进行分片,以增加流入量、流出量、每秒 I/O 操作次数 (IOPS) 或容量? 对于此方案,Azure 建议在可能的情况下,利用存储帐户的更高限制来减少工作负荷所需的存储帐户数。 若要请求提高存储帐户的限制,请联系 Azure 支持部门

容量和事务目标

如果应用程序正接近单个存储帐户的可伸缩性目标,可考虑采用以下方法之一:

  • 重新考虑导致应用程序接近或超过可伸缩性目标的工作负载。 能否对其进行另外的设计,以便使用较少的带宽、容量或处理事务?
  • 如果应用程序肯定会超出伸缩性目标之一,请创建多个存储帐户并将应用程序数据跨多个这样的存储帐户进行分区。 如果使用这种模式,则在设计应用程序时,必须确保能够在以后添加更多的存储帐户,以便进行负载均衡。 存储帐户本身除了用于数据存储、事务处理或数据传输之外,并无其他开销。
  • 如果应用程序接近带宽目标,请考虑压缩客户端的数据,以减少将数据发送到 Azure 存储所需的带宽。 压缩数据虽然可以节省带宽并提高网络性能,但也可能会对性能带来负面影响。 评估客户端数据压缩和解压缩的额外处理要求对性能造成的影响。 请记住,存储压缩数据可能会使故障排除变得更复杂,因为使用标准工具查看这些数据可能会更困难。
  • 如果应用程序接近可伸缩性目标,请确保对重试使用指数退避。 最好是尝试通过实施本文中所述的建议来避免达到可伸缩性目标。 但是,对重试使用指数退避会导致应用程序无法快速重试,从而导致限制问题恶化。 有关详细信息,请参阅标题为超时和服务器繁忙错误的部分。

数据操作的目标

Azure 存储会在存储帐户流量增加时进行负载均衡,但如果流量突然增加,则可能无法立即获得此吞吐量。 在激增期间会出现限制和/或超时现象,因为 Azure 存储会自动对表进行负载均衡。 让流量缓慢增加通常会有更好的效果,因为系统有时间进行适当的负载均衡。

实体数/秒(存储帐户)

对于单个帐户来说,访问表时的可伸缩性限制高达每秒 20,000 个实体(每个实体 1 KB)。 一般情况下,每个插入、更新、删除或扫描的实体都会计入此目标的计数。 因此,包含 100 个实体的批量插入计为 100 个实体。 一个查询扫描了 1000 个实体但只返回 5 个,则会将其计为 1000 个实体。

实体数/秒(分区)

在单个分区中,访问表时的可伸缩性目标为每秒 2,000 个实体(每个实体 1 KB),使用前面部分所述的相同计数方法。

网络

物理网络对应用程序的约束可能会严重影响性能。 以下部分描述了用户可能会遇到的某些限制。

客户端网络功能

如以下各部分所述,网络链接的带宽和质量在应用程序性能方面发挥着重要作用。

吞吐量

通常情况下,对带宽来说,问题在于客户端的功能。 较大的 Azure 实例的 NIC 具有较大的容量,因此如果需要提高单个计算机的网络限制,则应考虑使用较大的实例或更多 VM。 如果从本地应用程序访问 Azure 存储,可应用相同的规则:了解客户端设备的网络功能以及与 Azure 存储位置的网络连接情况,然后根据需要对其进行改进,或者将应用程序设计为可在这种网络功能下工作。

请注意,因错误和数据包丢失而导致的网络状况会降低有效吞吐量,使用任何网络都是这样。 WireShark 或 NetMon 可用于诊断此问题。

位置

在任何分布式环境中,将客户端放置在服务器附近可提供最佳性能。 要以最低的延迟访问 Azure 存储,则最好是将客户端放置在同一 Azure 区域内。 例如,如果 Azure Web 应用使用 Azure 存储,请将二者放在同一个区域(例如中国东部或中国北部)。 将资源放到一起可降低延迟和成本,因为在同一个区域使用带宽是免费的。

如果客户端应用程序要访问 Azure 存储但不是托管在 Azure 中(例如移动设备应用或本地企业服务),则将存储帐户放在靠近这些客户端的区域可降低延迟。 如果客户端广泛分布在各地,请考虑在每个区域使用一个存储帐户。 如果应用程序存储的数据是特定于各个用户的,不需要在存储帐户之间复制数据,则此方法更容易实施。

SAS 和 CORS

假设你需要授权用户 Web 浏览器或手机应用中运行的代码(例如 JavaScript)访问 Azure 存储中的数据。 一种方法是构建充当代理的服务应用程序。 用户的设备将对服务进行身份验证,而后者又可授权访问 Azure 存储资源。 这样,就可以避免在不安全的设备上公开存储帐户密钥。 但是,此方法会明显增大服务应用程序的开销,因为在用户设备与 Azure 存储之间传输的所有数据必须通过服务应用程序。

使用共享访问签名 (SAS) 即可避免将服务应用程序用作 Azure 存储的代理。 使用 SAS 可让用户设备通过受限访问令牌直接对 Azure 存储发出请求。 例如,如果用户想要将照片上传到应用程序,则服务应用程序可以生成 SAS 并将其发送到用户的设备。 SAS 令牌可按指定的时间间隔授予写入 Azure 存储资源的权限,该时间过后,SAS 令牌将会过期。 有关 SAS 的详细信息,请参阅使用共享访问签名 (SAS) 授予对 Azure 存储资源的有限访问权限

通常,Web 浏览器不允许某个域上的网站所托管的页面中的 JavaScript 对另一个域执行某些操作(例如写入操作)。 此策略称为同源策略,可防止一个页面上的恶意脚本获取另一网页上的数据的访问权限。 但是,在云中构建解决方案时,同源策略可能会成为一种限制。 跨源资源共享 (CORS) 是一种浏览器功能,它使目标域能够与信任源自源域的请求的浏览器通信。

例如,假设 Azure 中运行的某个 Web 应用程序对 Azure 存储帐户发出了某个资源请求。 该 Web 应用程序是源域,存储帐户是目标域。 可为任何 Azure 存储服务配置 CORS,以便与从 Azure 存储信任的源域发出请求的 Web 浏览器通信。 有关 CORS 的详细信息,请参阅 Azure 存储的跨源资源共享 (CORS) 支持

SAS 和 CORS 都有助于避免 Web 应用程序上出现不必要的负载。

批处理事务

表服务支持对位于同一个表中的属于同一分区组的实体执行事务批处理。 有关详细信息,请参阅执行实体组事务

.NET 配置

如果使用的是 .NET Framework,则本部分列出的数种快速配置设置可以用于显著提高性能。 如果使用其他语言,则需查看类似的概念是否适用于所选择的语言。

使用 .NET Core

使用 .NET Core 2.1 或更高版本开发 Azure 存储应用程序,以利用性能增强功能。 建议尽量使用 .NET Core 3.x。

有关 .NET Core 的性能改进的详细信息,请参阅以下博客文章:

提高默认连接限制

在 .NET 中,以下代码可将默认的连接限制(通常情况下,在客户端环境中为 2,在服务器环境中为 10)提高到 100。 通常情况下,应将值大致设置为应用程序使用的线程数。

ServicePointManager.DefaultConnectionLimit = 100; //(Or More)  

在打开任何连接前设置连接限制。

对于其他编程语言,请参阅该语言的文档以确定如何设置连接限制。

有关详细信息,请参阅博客文章 Web 服务:并发连接

增大最小线程数

如果结合异步任务使用同步调用,可能需要增大线程池中的线程数:

ThreadPool.SetMinThreads(100,100); //(Determine the right number for your application)  

有关详细信息,请参阅 ThreadPool.SetMinThreads 方法。

不受限制的并行度

虽然并行度有助于提高性能,但在使用不受限制的并行度时应保持谨慎,因为这意味着对线程数或并行请求数没有限制。 请务必限制上传或下载数据、访问同一存储帐户中的多个分区以及访问同一分区中的多个项的并行请求。 如果并行度不受限制,应用程序则可能会超出客户端设备的承受程度或超出存储帐户的可伸缩性目标,导致延迟和限制时间增长。

客户端库和工具

为获得最佳性能,请始终使用 Microsoft 提供的最新客户端库和工具。 Azure 存储客户端库适用于各种语言。 Azure 存储还支持 PowerShell 和 Azure CLI。 Microsoft 正在积极开发这些客户端库和工具,并注重其性能,使用最新服务版本对其进行更新,确保这些工具可以在内部协调好许多经过证实的做法。

处理服务错误

当服务无法处理请求时,Azure 存储会返回错误。 了解 Azure 存储在特定情况下可能返回的错误将有助于优化性能。

超时和服务器繁忙错误

如果应用程序即将达到可伸缩性限制,Azure 存储可能会对其进行限制。 在某些情况下,Azure 存储可能会出于某种暂时性的状况而无法处理请求。 对于这两种情况,服务可能返回 503(服务器繁忙)或 500(超时)错误。 如果服务正在对数据分区进行重新均衡以提高吞吐量,则也可能会发生这些错误。 通常,客户端应用程序应重试导致上述某种错误的操作。 但是,如果 Azure 存储因为应用程序即将超出可伸缩性目标而限制应用程序,或者其他某种原因导致服务无法为请求提供服务,则过于频繁的重试可能会使问题变得更糟。 建议使用指数退避重试策略,客户端库默认采用此行为。 例如,应用程序可能会在 2 秒后、4 秒后、10 秒后,以及 30 秒后进行重试,最后彻底放弃重试。 这样,应用程序可明显减少其在服务中施加的负载,而不会使得导致出现限制的行为恶化。

连接错误可以立即重试,因为它不是限制造成的,而且应该是暂时性的。

不可重试的错误

客户端库将处理重试,同时能够识别哪些错误可重试,哪些不可重试。 但是,如果直接调用 Azure 存储 REST API,则不应重试某些错误。 例如,400(错误的请求)错误表示客户端应用程序发送了一个无法处理的请求(因为该请求未采用预期的格式)。 每次重新发送此请求都会导致相同的响应,因此没有必要重试。 如果直接调用 Azure 存储 REST API,请注意潜在错误以及是否应重试这些错误。

有关 Azure 存储错误代码的详细信息,请参阅状态和错误代码

配置

本部分列出了多个快速配置设置,可以使用这些设置显著提高表服务的性能:

使用 JSON

从存储服务 2013-08-15 版开始,表服务就支持使用 JSON 而非基于 XML 的 AtomPub 格式来传输表数据。 使用 JSON 最多可以减少 75% 的有效负载大小,并可以显著提高应用程序的性能。

有关详细信息,请参阅文章 Azure 表:JSON 简介表服务操作的有效负载格式

禁用 Nagle

Nagle 的算法已跨 TCP/IP 网络进行了广泛的实施,是一种改进网络性能的方法。 不过,该方法并非适用于所有情况(例如高度交互式的环境)。 Nagle 的算法会对 Azure 表服务请求的性能造成负面影响,因此应尽量将其禁用。

架构

数据的呈现和查询方式是影响表服务性能的单个最大因素。 虽然每个应用程序都不同,但本部分仍概要列出了一些通用的经过验证的做法,这些做法适用于:

  • 表设计
  • 高效的查询
  • 高效的数据更新

表和分区

表划分为分区。 存储在分区中的每个实体共享相同的分区键,并具有唯一的行键,用于在该分区中标识自己。 分区具有好处,但也带来了可伸缩性限制。

  • 好处:可以在同一个分区中更新单个事务、原子事务和批处理事务的实体,每种事务最多包含 100 个单独的存储操作(总大小限制为 4 MB)。 此外,假定需要检索相同数量的实体,则在单个分区中查询数据要比跨多个分区查询数据更高效(不过,如果需要查询表数据,则请继续阅读以获取更进一步的建议)。
  • 可伸缩性限制:对存储在单个分区中的实体的访问不能进行负载均衡,因为分区支持原子批处理事务。 因此,总体说来单个表分区的可伸缩性目标低于表服务的相应目标。

考虑到表和分区的这些特点,应该采用以下设计原则:

  • 将客户端应用程序频繁更新或查询的数据放到同一分区中的同一工作逻辑单元。 例如,如果应用程序要聚合写入或者你要执行原子批处理操作,请将数据放到同一分区。 此外,与跨分区的数据相比,可以更高效地对单个分区中的数据进行查询。
  • 将客户端应用程序不会插入、更新或查询的数据放到不同分区中的同一工作逻辑单元(即,单个查询或批量更新)。 请记住,单个表中的分区键没有数量限制,因此即使设置数百万个分区键也不是问题,也不会影响性能。 例如,如果应用程序是一个需要用户登录的热门网站,不妨使用用户 ID 作为分区键。

热分区

热分区是指这样一种分区,即收到了某个帐户的过多流量,但又无法对其进行负载均衡,因为该分区为单个分区。 一般情况下,热分区的创建有以下两种模式:

“仅追加”和“仅预置”模式

“仅追加”模式是指流向某个给定分区键的所有(或几乎所有)流量都会按当前时间增加或减少。 例如,假设应用程序使用当前日期作为日志数据的分区键。 此设计会导致所有插入内容进入表中的最后一个分区,并且系统无法正确地进行负载均衡。 如果进入该分区的流量超出分区级的可伸缩性目标,则会导致限制。 最好是确保将流量发送到多个分区,以便对跨表请求进行负载均衡。

高流量数据

如果分区方案导致单个分区的数据较其他分区的数据使用更为频繁,则也可能会看到限制现象,因为该分区达到了单个分区的可伸缩性目标。 最好是确保分区方案不会导致单个分区接近可伸缩性目标。

查询

本部分介绍有关查询表服务的经过证实的做法。

查询范围

有多种方式可指定需要查询的实体的范围。 以下列表描述了查询范围的每个选项。

  • 点查询:点查询会同时指定要检索的实体的分区键和行键,因此可确切地检索到一个实体。 此类查询非常高效,应尽可能使用。
  • 分区查询: 分区查询用于检索共享分区键的一组数据。 通常情况下,该查询会指定一系列行键值或者一系列用于某些实体属性和分区键的值。 这些查询效率不如点查询,应谨慎使用。
  • 表查询: 表查询用于检索没有共享分区键的一组实体。 此类查询效率不高,应尽可能避免使用。

通常情况下,应避免进行扫描(大于单个实体的查询),但如果必须要进行扫描,则应尝试对数据进行组织,使扫描仅检索所需数据,避免扫描或返回大量不需要的实体。

查询密度

影响查询效率的另一关键因素是返回的实体数与查找返回的集合时扫描过的实体数的比率。 如果应用程序在执行表查询时使用了某个属性值的筛选器,而该属性值仅供 1% 的数据共享,则该查询需要扫描 100 个实体才会返回 1 个实体。 前面讨论的表可伸缩性目标均与所扫描的实体数相关,与返回的实体数无关:查询密度低很容易导致表服务限制应用程序,因为表服务在检索要查找的实体时需要扫描的实体过多。 有关如何避免限制的详细信息,请参阅标题为反规范化的部分。

限制返回的数据量

如果知道某个查询将要返回的实体并不是客户端应用程序所需要的,则应考虑使用筛选器来减少返回的集合的大小。 虽然没有返回到客户端的实体仍会计入可伸缩性限制,但应用程序的性能会提高,因为网络负载大小会下降,同时客户端应用程序必须处理的实体数会下降。 请记住,可伸缩性目标与扫描的实体数相关,因此查询在筛选掉许多实体后仍可能导致限制,即使返回很少的实体。 有关提高查询效率的详细信息,请参阅标题为查询密度的部分。

如果客户端应用程序只需表中实体提供的一组有限的属性,则可以使用投影来限制所返回数据集的大小。 就像使用筛选一样,投影有助于减少网络负载和客户端处理。

非规范化

与使用关系数据库不同,根据经过验证的做法,若要提高表数据的查询效率,需对数据进行非规范化。 也就是说,需要将相同的数据复制到多个实体中(一个实体对应一个用于查找数据的键)以尽量降低查询在查找客户端所需数据时必须扫描的实体数,这样就不必扫描大量实体来查找应用程序需要的数据。 例如,在电子商务网站中,可能希望通过两种方式查找订单:按客户 ID(供此客户的订单)和按日期(提供某个日期的订单)。 在表存储中,最好是将实体(或者对实体的引用)存储两次 – 一次使用表名称、PK 和 RK 进行存储,以按客户 ID 快速查找,另一次则通过日期来加快查找速度。

插入、更新和删除

本部分介绍的经过验证的做法用于修改存储在表服务中的实体。

批处理

批处理事务在 Azure 存储中称为实体组事务。 实体组事务中的所有操作都必须位于单个表的单个分区中。 在可能的情况下,请使用实体组事务来批量执行插入、更新和删除操作。 使用实体组事务可减少客户端应用程序与服务器之间的往返操作次数、减少需要收费的事务数(一个实体组事务计为一个收费事务,最多可能包含 100 个存储操作),以及启用原子更新(实体组事务中的所有操作都成功或都失败)。 高延迟性的环境(例如移动设备)可以充分利用实体组事务。

Upsert

尽可能使用表的“Upsert”操作。 有两种类型的“Upsert”,两种都可能比传统的“插入”和“更新”操作更高效:

  • InsertOrMerge:若要上传实体的一部分属性,但不确定实体是否已存在,请使用此操作。 如果实体存在,则该调用会更新包含在“Upsert”操作中的属性,保留所有现有的属性不变,而如果实体不存在,则会插入新的实体。 这类似于在查询中使用投影,因为只需上传在更改的属性。
  • InsertOrReplace:若要上传全新实体,但不确定实体是否已存在,请使用此操作。 仅当知道这个刚上传的实体完全正确时才使用此操作,因为该实体会完全覆盖旧实体。 例如,需要更新用于存储用户当前位置的实体,而不管应用程序以前是否存储过该用户的位置数据;新位置实体是完整的,不需要任何旧实体提供的任何信息。

将数据系列存储在单个实体中

有时候,应用程序会存储一系列需要频繁进行一次性检索的数据:例如,应用程序可能会跟踪一段时间内的 CPU 使用情况,以便绘制过去 24 小时内数据的滚动图表。 一种方法是每小时构建一个表实体,每个实体代表一个具体的小时,并存储该小时的 CPU 使用情况。 为了针对该数据绘图,应用程序需要检索保留过去 24 小时内数据的实体。

此外,也可以让应用程序将每小时的 CPU 使用情况存储为单个实体的独立属性:更新每个小时的时候,应用程序可以使用单个“InsertOrMerge Upsert”调用来更新最近的一个小时的值。 针对数据进行绘图时,应用程序只需检索 1 个实体而非 24 个,这样的查询非常高效。 有关查询效率的详细信息,请参阅标题为查询范围的部分。

在 Blob 中存储结构化数据

如果需要在执行批量插入后再统一检索实体的范围,请考虑使用 Blob,而不要使用表。 日志文件就是一个很好的例子。 可以批处理几分钟的日志,插入这些日志,然后一次性检索好几分钟的日志。 在这种情况下,使用 Blob 要比使用表的性能更好,因为这可以大幅减少要写入或读取的对象数,并可能会减少需要发出的请求数。

后续步骤