许多服务使用节流来控制资源消耗,对应用程序访问它们的速率施加限制。速率限制模式帮助你避免节流错误并准确预测吞吐量,特别是对于大规模重复性自动化任务(如批处理)。
情境与问题
对受节流服务执行大量操作可能导致流量增加和效率降低。你需要追踪被拒绝的请求并重试操作,可能需要多次传递才能完成工作。
考虑这个将数据导入数据库的示例:
- 你的应用程序需要导入 10,000 条记录。每条记录需要 10 个请求单位(RUs),总共需要 100,000 RUs。
- 你的数据库实例有 20,000 RUs 的预配容量。
- 你发送所有 10,000 条记录。2,000 条成功,8,000 条被拒绝。
- 你重试 8,000 条记录。2,000 条成功,6,000 条被拒绝。
- 你重试 6,000 条记录。2,000 条成功,4,000 条被拒绝。
- 你重试 4,000 条记录。2,000 条成功,2,000 条被拒绝。
- 你重试 2,000 条记录。全部成功。
工作完成了,但只是在发送了 30,000 条记录之后——是实际数据集大小的三倍。
这种天真方法的额外问题:
- 错误处理开销:20,000 个错误需要记录和处理,消耗内存和存储空间。
- 无法预测的完成时间:不知道节流限制,你无法估计处理需要多长时间。
解决方案
速率限制通过控制在一段时间内发送到服务的记录数量来减少流量并提升吞吐量。
服务基于不同指标进行节流:
- 操作数量(例如,每秒 20 个请求)
- 数据量(例如,每分钟 2 GiB)
- 操作的相对成本(例如,每秒 20,000 RUs)
你的速率限制实现必须控制发送到服务的操作,在不超过容量的情况下优化使用。
使用持久化消息系统
当你的 API 可以比受节流服务允许的速度更快地处理请求时,你需要管理导入速度。简单地缓冲请求是有风险的——如果你的应用程序崩溃,你会失去缓冲的数据。
相反,将记录发送到可以处理你完整导入速率的持久化消息系统。使用作业处理器以受节流服务限制内的受控速率读取记录。
持久化消息选项包括:
- 消息队列(例如,RabbitMQ、ActiveMQ)
- 事件流平台(例如,Apache Kafka)
- 云端队列服务
(高速率)"] --> B["持久化
消息队列"] B --> C["作业处理器 1"] B --> D["作业处理器 2"] B --> E["作业处理器 3"] C --> F["受节流服务
(有限速率)"] D --> F E --> F style A fill:#e1f5ff style B fill:#fff4e1 style F fill:#ffe1e1
细粒度时间间隔
服务通常基于可理解的时间跨度(每秒或每分钟)进行节流,但计算机处理速度要快得多。与其每秒批量释放一次,不如更频繁地发送较小的数量以:
- 保持资源消耗(内存、CPU、网络)均匀流动
- 防止突发请求造成的瓶颈
例如,如果服务允许每秒 100 个操作,每 200 毫秒释放 20 个操作:
管理多个不协调的进程
当多个进程共享受节流服务时,逻辑分区服务的容量并使用分布式互斥系统来管理这些分区的锁定。
示例:
如果受节流系统允许每秒 500 个请求:
- 创建 20 个分区,每个价值每秒 25 个请求
- 需要 100 个请求的进程请求四个分区
- 系统授予两个分区 10 秒
- 进程速率限制为每秒 50 个请求,在 2 秒内完成,然后释放锁定
实现方法:
使用 blob 存储为每个逻辑分区创建一个小文件。应用程序在短时间内(例如,15 秒)获得这些文件的独占租约。对于授予的每个租约,应用程序可以使用该分区的容量。
25 请求/秒"] L2["租约 2
25 请求/秒"] L3["租约 3
25 请求/秒"] end space:3 block:service:3 S["受节流服务
总共 500 请求/秒"] end P1 --> L1 P2 --> L2 P3 --> L3 L1 --> S L2 --> S L3 --> S style processes fill:#e1f5ff style leases fill:#fff4e1 style service fill:#ffe1e1
为了减少延迟,为每个进程分配少量独占容量。进程只在超过其保留容量时才寻求共享容量租约。
租约管理的替代技术包括 Zookeeper、Consul、etcd 和 Redis/Redsync。
问题与考量
💡 关键考量
处理节流错误:速率限制减少错误但不会消除它们。你的应用程序仍必须处理任何发生的节流错误。
多个工作流:如果你的应用程序有多个工作流访问相同的受节流服务(例如,批量加载和查询),将所有工作流整合到你的速率限制策略中,或为每个工作流保留单独的容量池。
多应用程序使用:当多个应用程序使用相同的受节流服务时,增加的节流错误可能表示竞争。考虑暂时降低吞吐量,直到其他应用程序的使用量降低。
!!!
何时使用此模式
使用此模式以:
- 减少来自受节流限制服务的节流错误
- 与天真的错误重试方法相比减少流量
- 仅在有容量处理记录时才将记录出列,从而减少内存消耗
- 提高批处理完成时间的可预测性
示例架构
考虑一个应用程序,用户向 API 提交各种类型的记录。每种记录类型都有一个独特的作业处理器,执行验证、丰富化和数据库插入。
所有组件(API、作业处理器)都是独立扩展的独立进程,不直接通信。
(类型 A 记录)"] API --> QB["队列 B
(类型 B 记录)"] QA --> JPA["作业处理器 A"] QB --> JPB["作业处理器 B"] JPA --> LS["租约存储
(Blob 0-9)"] JPB --> LS JPA --> DB["数据库
(1000 请求/秒限制)"] JPB --> DB style API fill:#e1f5ff style QA fill:#fff4e1 style QB fill:#fff4e1 style LS fill:#f0e1ff style DB fill:#ffe1e1
工作流程:
- 用户向 API 提交 10,000 条类型 A 记录
- API 将记录加入队列 A
- 用户向 API 提交 5,000 条类型 B 记录
- API 将记录加入队列 B
- 作业处理器 A 尝试租用 blob 2
- 作业处理器 B 尝试租用 blob 2
- 作业处理器 A 失败;作业处理器 B 获得 15 秒的租约(100 请求/秒容量)
- 作业处理器 B 将 100 条记录出列并写入
- 1 秒后,两个处理器都尝试额外的租约
- 作业处理器 A 获得 blob 6(100 请求/秒);作业处理器 B 获得 blob 3(现在总共 200 请求/秒)
- 处理器继续竞争租约并以其授予的速率处理记录
- 当租约到期时(15 秒后),处理器相应地降低其请求速率
相关模式
节流:速率限制通常是为了响应受节流服务而实现的。
重试:当请求导致节流错误时,在适当的间隔后重试。
基于队列的负载均衡:与速率限制类似但更广泛。主要差异:
- 速率限制不一定需要队列,但需要持久化消息
- 速率限制引入分布式互斥在分区上,允许管理与相同受节流服务通信的多个不协调进程的容量
- 基于队列的负载均衡适用于服务之间的任何性能不匹配;速率限制专门针对受节流服务