Azure开发了许多经过验证的做法,用于开发具有表存储的高性能应用程序。 此查检表列出了开发人员在优化性能时可以遵循的关键做法。 在设计应用程序时以及在整个流程中,请牢记这些做法。
Azure 存储在容量、事务速率和带宽方面存在可伸缩性与性能目标。 有关Azure 存储可伸缩性目标的详细信息,请参阅标准存储帐户的可伸缩性和性能目标以及表存储的性能目标。
清单
本文将经过验证的性能做法整理到开发表存储应用程序时可以遵循的清单中。
| Done | Category | 设计注意事项 |
|---|---|---|
| 可伸缩性目标 | 是否可将应用程序设计为避免使用的存储帐户数超过最大数目? | |
| 可伸缩性目标 | 您是否在避免接近容量上限和事务限制? | |
| 可伸缩性目标 | 您是否快要达到每秒实体数的可扩展性目标? | |
| 网络 | 客户端设备是否具有足够高的带宽和足够低的延迟,以实现所需的性能? | |
| 网络 | 客户端设备是否具有优质网络链接? | |
| 网络 | 客户端应用程序是否位于存储帐户所在的同一区域? | |
| 直接客户端访问 | 是否使用共享访问签名 (SAS) 和跨源资源共享 (CORS) 来实现对 Azure 存储的直接访问? | |
| 批处理 | 应用程序是否使用实体组事务对更新进行批处理? | |
| .NET 配置 | 对于.NET框架应用程序,是否已将客户端配置为使用足够数量的并发连接? | |
| .NET 配置 | 对于 .NET Framework 应用程序,是否已将.NET配置为使用足够数量的线程? | |
| 并行度 | 是否能够确保对并行度进行适当的界定,使客户端功能不会过载或接近可伸缩性目标? | |
| Tools | 是否使用 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),使用上一节中所述的相同计数。
网络
应用程序的物理网络约束可能对性能产生重大影响。 以下部分介绍了用户可能会遇到的一些限制。
客户端网络功能
如以下各部分所述,网络链接的带宽和质量在应用程序性能方面发挥着重要作用。
Throughput
通常情况下,对带宽来说,问题在于客户端的功能。 较大的 Azure 实例的 NIC 具有较大的容量,因此如果需要提高单个计算机的网络限制,则应考虑使用较大的实例或更多 VM。 如果从本地应用程序访问 Azure 存储,可应用相同的规则:了解客户端设备的网络功能以及与 Azure 存储位置的网络连接情况,然后根据需要对其进行改进,或者将应用程序设计为可在这种网络功能下工作。
链接质量
请注意,因错误和数据包丢失而导致的网络状况会降低有效吞吐量,使用任何网络都是这样。 WireShark 或 NetMon 可用于诊断此问题。
Location
在任何分布式环境中,将客户端放置在服务器附近可提供最佳性能。 要以最低的延迟访问 Azure 存储,则最好是将客户端放置在同一 Azure 区域内。 例如,如果你有一个使用Azure 存储的 Azure Web 应用,请在单个区域(例如中国东部 2 或中国北部 2)中找到它们。 将资源放到一起可降低延迟和成本,因为在同一个区域使用带宽是免费的。
如果客户端应用程序将访问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,以向 Web 浏览器表明,来自源域的请求受到 Azure 存储的信任。 有关 CORS 的详细信息,请参阅 Azure 存储的跨源资源共享 (CORS) 支持。
SAS 和 CORS 都有助于避免 Web 应用程序上出现不必要的负载。
批处理事务
表服务支持对位于同一表中且属于同一分区组的实体执行批处理事务。 有关详细信息,请参阅 执行实体组事务。
.NET 配置
对于使用 .NET Framework 的项目,本部分列出的某些快速配置设置可以用于显著提高性能。 如果使用 .NET 之外的语言,则需查看类似的概念是否适用于所选择的语言。
提高默认连接限制
注释
本部分适用于使用 .NET Framework 的项目,因为连接池由 ServicePointManager 类控制。 .NET Core 在连接池管理方面推出了重大更改,使连接池在 HttpClient 级别进行,默认情况下池大小不受限制。 这意味着 HTTP 连接会自动缩放以满足工作负载。 建议尽可能使用最新版本的 .NET,以利用增强的性能。
对于使用 .NET Framework 的项目,可以使用以下代码将默认连接限制(通常在客户端环境中为 2 或服务器环境中的 10)增加到 100。 通常情况下,应将值大致设置为应用程序使用的线程数。 在打开任何连接前设置连接限制。
ServicePointManager.DefaultConnectionLimit = 100; //(Or More)
若要详细了解 .NET Framework 中的连接池限制,请参阅 .NET Framework 连接池限制和新 Azure SDK for .NET。
对于其他编程语言,请参阅文档以确定如何设置连接限制。
增大最小线程数
如果将同步调用与异步任务一起使用,可能需要增加线程池中的线程数:
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 Tables:JSON 简介 和 表服务操作的有效负载格式。
禁用 Nagle
Nagle 的算法在 TCP/IP 网络中广泛实现,作为提高网络性能的手段。 但是,在所有情况下(如高度交互的环境)都不是最佳选择。 Nagle 算法对Azure表服务的请求性能产生负面影响,应尽可能禁用该算法。
Schema
如何表示和查询数据是影响表服务性能的最大单一因素。 虽然每个应用程序都不同,但本部分概述了一些与以下相关的一般经过验证的做法:
- 表设计
- 高效的查询
- 高效的数据更新
表和分区
表被划分为分区。 存储在分区中的每个实体共享相同的分区键,并具有唯一的行键,用于在该分区中标识它。 分区提供优势,但也引入了可伸缩性限制。
- 优点:可以在包含最多 100 个单独存储操作的单个原子批处理事务中更新同一分区中的实体(总大小限制为 4 MB)。 假设要检索的实体数量相同,那么查询单个分区中的数据也会比查询跨分区的数据更高效(不过,请继续阅读以了解有关查询表中数据的更多建议)。
- 可伸缩性限制:无法对存储在单个分区中的实体的访问进行负载均衡,因为分区支持原子批处理事务。 因此,单个表分区的可伸缩性目标低于整个表服务的可伸缩性目标。
由于表和分区的这些特征,应采用以下设计原则:
- 找到客户端应用程序经常在同一分区中的同一逻辑工作单元中更新或查询的数据。 例如,如果应用程序正在聚合写入或执行原子批处理操作,则查找同一分区中的数据。 此外,单个分区中的数据比跨分区的数据更有效地在单个查询中查询。
- 在单独的分区中找到客户端应用程序不在同一逻辑工作单元(即单个查询或批处理更新)中插入、更新或查询的数据。 请记住,单个表中的分区键数没有限制,因此,拥有数百万个分区键并不会影响性能。 例如,如果应用程序是具有用户登录的热门网站,则使用用户 ID 作为分区键可能是一个不错的选择。
热分区
热分区是接收帐户流量比例不成比例的分区,并且无法进行负载均衡,因为它是单个分区。 通常,热分区是使用以下两种方式之一创建的:
仅追加模式和仅前置追加模式
“仅追加”模式是指:发往某个给定分区键的所有(或几乎所有)流量都会随着当前时间的变化而增加或减少。 例如,假设应用程序使用当前日期作为日志数据的分区键。 此设计会导致所有插入操作都进入表中的最后一个分区,并且系统无法正确进行负载均衡。 如果发往该分区的流量超过分区级扩展性目标,就会导致限流。 最好确保将流量发送到多个分区,以便跨表对请求进行负载均衡。
高访问量数据
如果你的分区方案导致某一个分区中的数据使用频率远高于其他分区,那么当该分区接近单个分区的可扩展性目标时,你也可能会看到限流。 最好确保分区方案不会生成接近可伸缩性目标的单个分区。
Querying
本节介绍了查询表服务的最佳实践。
查询范围
可通过多种方式指定要查询的实体范围。 以下列表描述了查询范围的每个选项。
- 点查询:- 通过指定要检索实体的分区键和行键,点查询可恰好检索到一个实体。 这些查询效率高,应尽可能使用它们。
- 分区查询: 分区查询是一个查询,用于检索共享公共分区键的一组数据。 通常,除了分区键之外,查询还指定一系列行键值或某些实体属性的值范围。 这些查询的效率低于点查询,应谨慎使用。
- 表查询: 表查询是一个查询,用于检索一组不共享公共分区键的实体。 这些查询效率不高,应尽可能避免这些查询。
通常,请避免扫描(查询大于单个实体),但如果必须扫描,请尝试组织数据,以便扫描检索所需的数据,而无需扫描或返回不需要的大量实体。
查询密度
查询效率的另一个关键因素是返回的实体数与扫描以查找返回集的实体数相比。 如果应用程序对表执行带有属性值筛选条件的查询,而只有 1% 的数据具有该属性值,则查询每返回 1 个实体,就会扫描 100 个实体。 前面讨论的表可伸缩性目标都与扫描的实体数(而不是返回的实体数)相关:低查询密度很容易导致表服务限制应用程序,因为它必须扫描这么多实体来检索要查找的实体。 有关如何避免限制速率的更多信息,请参阅题为“非规范化”的章节。
限制返回的数据量
如果知道查询返回客户端应用程序中不需要的实体,请考虑使用筛选器来减小返回集的大小。 虽然未返回到客户端的实体仍计入可伸缩性限制,但应用程序性能会因为网络有效负载大小减少和客户端应用程序必须处理的实体数减少而提高。 请记住,可伸缩性目标与扫描的实体数相关,因此筛选掉多个实体的查询仍可能导致限制,即使返回的实体很少。 有关提高查询效率的详细信息,请参阅标题为 “查询密度”的部分。
如果客户端应用程序只需要表中实体的有限属性集,则可以使用投影来限制返回的数据集的大小。 与筛选一样,投影有助于减少网络负载和客户端处理。
反规范化
不同于使用关系型数据库时的情况,高效查询表数据的最佳实践往往需要对数据进行反规范化。 也就是说,在多个实体中复制同一份数据(每个可能用于查找该数据的键各对应一个实体),以尽量减少查询为找到客户端所需数据而必须扫描的实体数量,而不是为了找到应用程序所需的数据不得不扫描大量实体。 例如,在电子商务网站中,你可能希望通过客户 ID(给我此客户的订单)和日期(在日期给我订单)来查找订单。 在表存储中,最好将实体(或其引用)存储两次——一次按表名、PK 和 RK 存储,以便按客户 ID 查找;另一次则为了便于按日期查找。
插入、更新和删除
本节介绍了修改存储在表服务中的实体的经过验证的做法。
批处理
批处理事务称为Azure 存储中的实体组事务。 实体组事务中的所有操作都必须位于单个表中的单个分区上。 如果可能,请使用实体组事务以批处理方式执行插入、更新和删除。 使用实体组事务可减少从客户端应用程序到服务器的往返次数,减少可计费事务数(实体组事务计数为单个事务用于计费目的,最多可包含 100 个存储操作),并启用原子更新(实体组事务中的所有操作都成功或全部失败)。 延迟较高的环境(如移动设备)受益于使用实体组事务。
Upsert
尽可能使用表 Upsert 操作。 有两种类型的 Upsert,这两种类型比传统的 插入 和 更新 操作更高效:
- InsertOrMerge:如果要上传实体属性的子集,但不确定实体是否已存在,请使用此操作。 如果实体存在,则此调用将更新 Upsert 操作中包含的属性,并保留所有现有属性,如果实体不存在,则会插入新实体。 这类似于在查询中使用投影,即只需上传正在更改的属性。
- InsertOrReplace:如果要上传全新的实体,但不确定该实体是否已存在,请使用此操作。 如果知道新上传的实体完全正确,因为它完全覆盖了旧实体,请使用此操作。 例如,你希望更新存储用户的当前位置的实体,而不管应用程序是否以前为用户存储了位置数据;新位置实体已完成,不需要任何先前实体的任何信息。
将数据系列存储在单个实体中
有时,应用程序存储一系列数据,它经常需要一次检索所有数据:例如,应用程序可能会跟踪一段时间内的 CPU 使用率,以便绘制过去 24 小时内数据的滚动图表。 一种方法是每小时有一个表实体,每个实体表示特定小时,并存储该小时的 CPU 使用率。 若要绘制此数据,应用程序需要检索保存最近 24 小时内数据的实体。
或者,应用程序可以将每小时的 CPU 使用率存储为单个实体的单独属性:若要每小时更新一次,应用程序可以使用单个 InsertOrMerge Upsert 调用更新最近一小时的值。 若要绘制数据,应用程序只需检索单个实体而不是 24 个实体,以便进行高效的查询。 有关查询效率的详细信息,请参阅标题为 “查询范围”的部分。
在 Blob 中存储结构化数据
如果你要执行批量插入,然后一并检索一系列实体,请考虑使用 Blob 存储而不是表存储。 一个很好的示例是日志文件。 您可以将几分钟的日志批量插入,然后每次检索几分钟的日志。 在这种情况下,如果使用 Blob 而不是表,则性能会更好,因为可以显著减少写入或读取的对象数,并可能减少需要发出的请求数。