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

速率限制模式

控制应用程序向服务发送请求的速率,以使应用程序保持在服务的 限制 和总体容量范围内。 此方法有助于避免或尽量减少限流错误,并更准确地预测吞吐量。

速率限制适用于许多方案,但对于大规模重复的自动化任务(如批处理)尤其有用。

上下文和问题

针对受限制的服务执行大量操作可能会导致流量增加并降低吞吐量,因为需要跟踪被拒绝的请求,然后重试操作。 随着操作次数的增加,节流限制可能需要多次重发数据,从而对性能造成更大的影响。

例如,考虑以下将数据写入 Azure Cosmos DB 时存在问题的出错重试流程:

  1. 应用程序需要将 10,000 条记录引入 Azure Cosmos DB。 每个记录需要 10 个请求单位(RU)才能引入,因此完成作业需要总共 100,000 个 RU。

  2. Azure Cosmos DB 实例具有 20,000 个 RU 的预配容量。

  3. 将所有 10,000 条记录发送到 Azure Cosmos DB。2,000 条记录写入成功,8,000 条记录被拒绝。

  4. 将剩余的 8,000 条记录发送到 Azure Cosmos DB。2,000 条记录写入成功,6,000 条记录被拒绝。

  5. 将剩余的 6,000 条记录发送到 Azure Cosmos DB。2,000 条记录写入成功,4,000 条记录被拒绝。

  6. 将剩余的 4,000 条记录发送到 Azure Cosmos DB。2,000 条记录写入成功,2,000 条记录被拒绝。

  7. 将剩余的 2,000 条记录发送到 Azure Cosmos DB。 所有记录写入成功。

引入作业成功完成,但仅在将 30,000 条记录发送到Azure Cosmos DB之后。 整个数据集仅包含 10,000 条记录。

此示例中需要考虑其他因素:

  • 大量的错误也可能导致额外的工作记录这些错误并处理生成的日志数据。 上述方法处理 20,000 个错误,记录这些错误可能会产生处理、内存或存储资源成本。

  • 由于你不了解数据引入服务的限流限制,因此无法预估数据处理需要多长时间。 速率限制允许计算引入过程所需的时间。

解决方案

速率限制可以减少流量,并可能会通过减少在给定时间段内发送到服务的记录数量来提高吞吐量。

服务可以根据一段时间内不同的指标对请求进行限制,例如:

  • 操作数量(例如,每秒 20 个请求)。
  • 数据量(例如,每分钟 2 GiB)。
  • 相对操作成本(例如,每秒 20,000 RU)。

无论用于限制的指标如何,速率限制实现都将涉及控制在特定时间段内发送到服务的操作的数量和/或大小。 速率限制可优化服务的使用,而不会超过其限制容量。

如果 API 可以比受限制引入服务更快地处理请求,则必须管理使用该服务的速度。 仅将限制视为数据速率不匹配,并在服务恢复前缓冲引入请求,会带来风险。 如果应用程序在此方案中停止响应,则任何缓冲数据都可能会丢失。

为避免这种风险,请考虑将记录发送到一个可以处理完全引入速率的持久消息传递系统。 (Azure 事件中心等服务每秒可以处理数百万个操作。然后,可以使用一个或多个作业处理器以受限制服务限制范围内的受控速率从消息系统读取记录。 将记录提交到消息系统后,就可以仅让在给定时间间隔内能够处理的记录出队,从而节省内部内存。

Azure 提供多种可与此模式配合使用的持久消息传递服务,包括:

显示持久消息传送流的示意图。三个作业处理器调用受限制的服务。

发送记录时,用于释放记录的时间段可能比服务实施限制的时间段粒度更细。 系统通常会根据你可以轻松理解和便于使用的时间跨度来设置限流限制。 但是,对于运行服务的计算机,与处理信息的速度相比,这些时间范围可能很长。 例如,系统可能按秒或按分钟进行限流,但代码处理通常是在纳秒或毫秒量级上进行的。

尽管这不是必需的,但通常建议更频繁地发送少量记录以提高吞吐量。 因此,与其尝试每秒一次或每分钟一次将记录攒批后再发布,不如采用比这更细粒度的方式,这样可以让资源消耗(内存、CPU 和网络)以更均匀的速率变化。 此方法可防止因请求突然突发而导致的潜在瓶颈。 例如,如果某项服务允许每秒进行 100 次操作,那么速率限制器的实现可能会通过每隔 200 毫秒释放 20 次操作来平衡请求,如下图所示。

显示受速率限制的流量随时间变化的图表。

此外,有时多个未协调的进程需要共享一个受限制服务。 若要在此方案中实现速率限制,可以在逻辑上对服务的容量进行分区,然后使用分布式互斥系统来管理这些分区的排他锁。 然后,每当未协调的进程需要容量时,可以竞争这些分区上的锁。 对于进程持有锁的每个分区,都会为其分配一定的容量。

例如,如果受限制系统允许每秒处理 500 个请求,则你可以创建 20 个分区,每个分区每秒可以处理 25 个请求。 如果某个进程需要发出 100 个请求,它可以向分布式互斥系统请求四个分区。 系统可能会分配两个分区,持续 10 秒。 然后,该进程将速率限制为每秒 50 个请求,在两秒内完成任务,然后释放锁。

实现此模式的一种方法是使用Azure 存储。 在这种情况下,你将为容器中的每个逻辑分区创建一个 0 字节 Blob。 然后,应用程序可以直接针对这些 Blob 获取独占租约并短时间(例如 15 秒)占有它。 应用程序每获得一个租约,就可以使用相应分区的容量。 然后,应用程序需要跟踪租约时间,以便在时间到期时,应用程序可以停止使用授予的容量。 实现此模式时,通常需要每个进程在需要容量时尝试租用随机分区。

为了进一步降低延迟,可为每个进程分配少量的独占容量。 然后,只有在需要超出其预留容量时,进程才会寻求获取共享容量的租约。

此图显示多个进程如何竞争 Azure Blob 存储 中 Blob 分区的独占租约。

作为Azure 存储的替代方法,还可以通过使用 ZooKeeper、etcdRedis/Redsync 等技术来实现此类租约管理系统。

问题和注意事项

在决定如何实现此模式时,请考虑以下几点:

  • 尽管速率限制模式可以减少限制错误的数量,但应用程序仍需要正确处理可能发生的任何限制错误。

  • 确保重试与速率限制相协调。 盲目或过于激进的重试可能会增加负载并引发重试风暴,因此应传递回压信号(例如,HTTP 429 以及 Retry-After),并将重试次数限制在较少范围内,同时在每次尝试之间加入较短的随机延迟。

  • 如果应用程序有多个工作流访问同一受限制的服务,则需要将所有工作流集成到速率限制策略中。 例如,你可能支持将记录批量加载到数据库中,但同时支持查询该数据库中的记录。 可以通过确保所有工作流都通过相同的速率限制机制来管理容量。 或者,可为每个工作流保留不同的容量池。

  • 受限的服务可能被多个应用程序使用。 在某些情况下,可以协调该用法(如本文前面所示)。 如果您开始看到超出预期数量的限流错误,这种增加可能表明访问某项服务的应用程序之间存在争用。 在这种情况下,可能需要考虑暂时降低速率限制机制施加的吞吐量,直到其他应用程序的使用量减少。

何时使用此模式

在以下情况下使用此模式:

  • 你需要减少受速率限制的服务引发的限制错误。

  • 与简单的出错即重试方法相比,你会希望尽可能减少网络流量。

  • 仅当有足够的容量处理记录时,才需要通过取消排队记录来减少内存消耗。

在以下情况下,此模式可能不适用:

  • 该操作需要立即同步完成,延迟非常低,不能容忍排队或延迟处理。

  • 主要瓶颈不是请求速率,而是并发或资源争用(例如 CPU 饱和或长时间运行的正在进行的工作)。 在这些情况下,缩放或并发控制更合适。

工作负载设计

评估如何在工作负荷的设计中使用速率限制模式来解决Azure Well-Architected框架支柱中涵盖的目标和原则。 下表提供有关此模式如何支持每个支柱目标的指南。

支柱 此模式如何支持支柱目标
可靠性 设计决策有助于工作负荷在发生故障后 复原 ,并确保它在发生故障后 恢复到 正常运行状态。 当服务希望避免过度使用时,此策略通过确认和遵守与服务通信的限制和成本来保护客户端。

- RE:07 自我保护

如果此模式在某个支柱中引入权衡取舍,请将它们与其他支柱的目标进行对比。

Example

以下示例应用程序允许用户向 API 提交各种类型的记录。 每个记录类型都有一个执行以下步骤的唯一作业处理器:

  1. 验证
  2. 扩充
  3. 将记录插入数据库

应用程序的所有组件(API、作业处理器 A 和作业处理器 B)都是可以独立缩放的单独进程。 这些进程不会直接相互通信。

此图显示了写入受限制数据库的分区租约存储的多队列多处理器流。

此图显示两个用户通过共享 API 提交记录。 每种记录类型都会被路由到单独的队列,由专用的作业处理器进行处理,并以由 Azure 存储 中的 Blob 分区租约控制的速率写入受限流的数据库。 两个用户分别向 API 组件发送一批消息。 一个用户提交 10,000 条消息,另一个用户提交 5,000 条消息。 API 将 10,000 条消息路由到队列 A,将 5,000 条消息路由到队列 B。队列 A 连接到作业处理器 A,队列 B 连接到作业处理器 B。作业处理器 A 以每秒 300 条记录的速度写入数据库。 作业处理器 B 以每秒 500 条记录写入数据库。 这两个作业处理器都连接到标记为“每秒限制 800 条记录”的数据库组件。 在两个作业处理器下面,Azure 存储包含标记为 0 到 7 的 8 个 blob 分区。 有一条说明指出,每个分区相当于每秒 100 条记录。 多个箭头将作业处理器连接到 Blob 分区。 绿色箭头从作业处理器 A 指向分区 0、4 和 6,这表示处理器保留这三个分区的租约。 这些租约造就了其每秒 300 条记录的吞吐率。 绿色箭头从作业处理器 B 指向分区 1、2、3、5 和 7。 这些箭头表示该处理器持有五个分区的租约。 这些租约是其达到每秒 500 条记录速率的原因。 失败的租约尝试显示为红色箭头,指向已由其他处理器持有的分区。 这些箭头表示争用解决。 该图显示两个处理器之间所有保留分区的总和等于每秒 800 条记录,这与数据库限制相匹配。

在此示例中,每个 Blob 租约表示允许的数据库吞吐量的固定共享。 处理器只能以其当前持有的所有租约的合计速率进行出队和写入。 随着时间推移,处理器会获得或失去租约,其允许的写入速率也会随之变化,从而将数据库总流量控制在配置的限制以内,同时仍能让所有排队的工作继续推进。

此图中整合了以下工作流:

  1. 用户向 API 提交 10,000 条 A 类型记录。
  2. API 将这 10,000 条记录加入队列 A。
  3. 用户向 API 提交 5,000 条 B 类型记录。
  4. API 会将这 5,000 条记录排入队列 B 中。
  5. 作业处理器 A 发现队列 A 中有记录,并尝试获取 Blob 2 的独占租约。
  6. 作业处理器 B 发现队列 B 中有记录,并尝试获取 Blob 2 的独占租约。
  7. 作业处理器 A 无法获取租约。
  8. 作业处理器 B 获得 Blob 2 的租约,为期 15 秒。 它现在可以将对数据库的请求速率限制为每秒 100 个。
  9. 作业处理器 B 从队列 B 中取出 100 条记录并将其写入。
  10. 一秒钟过去。
  11. 作业处理器 A 发现队列 A 中有更多记录,并尝试获取 Blob 6 的独占租约。
  12. 作业处理器 B 发现队列 B 中有更多记录,并尝试获取 blob 3 的独占租约。
  13. 作业处理器 A 获取 Blob 6 的租约,租期为 15 秒。 它现在可以将对数据库的请求速率限制为每秒 100 个。
  14. 作业处理器 B 获得 Blob 3 的租约,租期为 15 秒。 它现在可以将对数据库的请求速率限制为每秒 200 个。 (它还持有 Blob 2 的租约。)
  15. 作业处理器 A 从队列 A 中取出 100 条记录,并将其写入。
  16. 作业处理器 B 从队列 B 中取出 200 条记录,并将这些记录写入。
  17. 一秒钟过去。
  18. 作业处理器 A 发现队列 A 中有更多记录,并尝试获取 blob 0 的独占租约。
  19. 作业处理器 B 发现队列 B 中的记录更多,并尝试获取 Blob 1 的独占租约。
  20. 作业处理器 A 获得了 blob 0 的 15 秒租约。 它现在可以将对数据库的请求速率限制为每秒 200 个。 (它还持有 Blob 6 的租约。)
  21. 作业处理器 B 获得了 Blob 1 的 15 秒租约。 它现在可以将对数据库的请求速率限制为每秒 300 个。 (它还占有 Blob 2 和 3 的租约。)
  22. 作业处理器 A 从队列 A 中取出 200 条记录,并将其写入。
  23. 作业处理器 B 从队列 B 中取出 300 条记录并将其写入。
  24. 等等。

15 秒后,其中一个或两个作业仍未完成。 随着租约过期,处理器还应减少出队和写入的请求数量。

此模式的实现以不同的编程语言提供:

  • Go 实现可在 GitHub 中找到。
  • Java 实现可在 GitHub 中找到。

后续步骤

实现此模式时,以下指南可能也相关:

实现此模式时,以下模式和指南也可能相关:

  • Throttling. 速率限制模式通常是为了响应受限制的服务而实现的。

  • Retry. 当对受限流的服务发出的请求导致限流错误时,通常适合在适当的时间间隔后重试这些请求。

  • 基于队列的负载均衡 类似于速率限制模式,但在几个关键方面有所不同:

    • 速率限制不一定需要使用队列来管理负载,但它确实需要利用持久消息传递服务。 例如,速率限制模式可以使用 Apache Kafka 或事件中心等服务。

    • 速率限制模式引入了分区上的分布式相互排斥系统的概念,这使你可以管理与同一限制服务通信的多个未协调进程的容量。

    • 每当服务之间出现性能不匹配或想要提高复原能力时,Queue-Based 负载调配模式都适用。 因此,与速率限制相比,这是一种更广泛的模式,它更具体地涉及高效访问受限制的服务。