首页
/ LiteLLM Dynamic Rate Limiter v3:饱和度感知的优先级限流机制全面解析

LiteLLM Dynamic Rate Limiter v3:饱和度感知的优先级限流机制全面解析

2026-09-07 09:16:38作者:范靓好Udolf

本技术指南以 LiteLLM Proxy 中动态限流器的 v3 实现(_PROXY_DynamicRateLimitHandlerV3)为核心,系统讲解"饱和度感知 + 优先级分配"的限流设计思路、Generous/Strict 双模式行为、完整配置方法与源码级实现原理。读完本文,你将掌握如何在 LiteLLM Proxy 上为不同业务等级(如 premium/standard)预留模型吞吐能力,并理解为何该方案能同时兼顾"低负载时的资源利用率"与"高负载下的公平性"。

本文依据的关联文档为 README.dynamic_rate_limiter_v3.md,并对应当前仓库 dynamic_rate_limiter_v3.py 的实际源码与 test_dynamic_rate_limiter_v3.py 中的测试用例进行交叉印证。

一、背景:传统限流的两个痛点

在 Proxy 网关中,模型组的 RPM(每分钟请求数)与 TPM(每分钟 Token 数)是稀缺资源。传统做法通常要么"一刀切"地对所有用户施加相同上限(公平但低效),要么仅做模型级总量限流(高效但无差别,高价值用户可能被低价值流量挤掉)。

v3 动态限流器要同时解决这两个问题:

  • 资源效率:系统处于低负载时,允许某个优先级"借用"其它优先级未被使用的剩余容量,不让模型容量空转。
  • 公平性:系统接近饱和时,严格按预设的优先级权重为每个等级分配独立的保留额度,保证高优先级用户在高压下依然得到其应得份额。

v3 区别于早期版本(_PROXY_DynamicRateLimitHandler,见 dynamic_rate_limiter.py)的关键在于引入了模型饱和度(saturation) 作为实时切换限流策略的依据,并完全复用 v3 限流器(parallel_request_limiter_v3.py)基于 Redis + Lua 的原子计数基础设施,天然支持多实例部署。

二、核心机制:饱和度感知的双模式策略

2.1 两条核心行为规则

限流器维护一个"当前用量 / 模型容量"的饱和度比值,并以此划分两种模式:

  • 饱和度 < 80%(Generous 宽松模式):优先考虑资源利用率,允许优先级借用剩余容量,按先到先得直到模型总容量耗尽。
  • 饱和度 ≥ 80%(Strict 严格模式):优先保证公平性,按归一化后的优先级权重执行各自的保留额度。

2.2 完整请求处理流程

原文档给出了完整的请求流程示意,整理如下:

Incoming Request
   │
   ▼
1. Check Model Saturation
   - 查询 v3 limiter 的 Redis 计数器
   - 计算:current_usage / capacity
   - 返回值区间:0.0(空闲)~ 1.0+(超饱和)
   │
   ▼
   Saturation < 80% ────────────── Saturation >= 80%
        │                                │
        ▼                                ▼
  Generous Mode                   Strict Mode
  - 只执行模型级总量限制           - 先归一化优先级权重(若合计 > 1.0)
  - 不做优先级限制                 - 为每个优先级创建独立描述符
  - 允许借用(borrowing)          - 强制执行各优先级保留额度
  - 先到先得直到容量占满           - 单独追踪模型级用量供后续饱和度判断
        │                                │
        └───────────────┬────────────────┘
                        ▼
                 v3 Limiter Check(原子检查 + 计数)
                    │              │
                    ▼              ▼
                 OVER_LIMIT      OK
                    │              │
                    ▼              ▼
              返回 429 错误     放行请求

关于阈值"80%",需要特别注意:文档示例与 types/utils.pyPriorityReservationSettings 的实际默认值存在差异——源码中 saturation_threshold实际默认值为 0.50(50%),文档示例中的 0.80 是可选的自定义取值。这意味着若用户不显式配置,v3 会在饱和度达到 50% 时便进入严格模式。配置时请以源码默认值为基准,切勿假设阈值恒为 80%。

三、配置指南

v3 动态限流器的行为由两处全局配置驱动:litellm.priority_reservation(每级优先级各保留多少容量)与 litellm.priority_reservation_settings(模式切换与兜底策略)。

3.1 优先级容量预留:priority_reservation

在代理配置中,通过全局字典声明各优先级等级占模型容量的权重:

litellm.priority_reservation = {
    "premium": 0.75,    # 75% of capacity
    "standard": 0.25    # 25% of capacity
}

值既可以是普通浮点百分比,也可以是精确容量值(PriorityReservationDict 类型)。从 rate_limiter_utils.pyconvert_priority_to_percent() 的实现可看到三种合法取值格式:

格式 写法 语义
百分比 0.9{"type": "percent", "value": 0.9} 占模型容量的 90%
RPM 绝对值 {"type": "rpm", "value": 900} 每分钟保留 900 个请求(900 / 模型 rpm
TPM 绝对值 {"type": "tpm", "value": 900000} 每分钟保留 900000 Token(900000 / 模型 tpm

注意 rpm/tpm 绝对值换算依赖模型组的 rpm/tpm 配置;同时,模型组必须先在 Router 中声明 rpm/tpm 上限,限流描述符才有生成基础(详见下文 5.3 节)。

此外,源码在 _get_priority_weight() 中对企业许可证做了显式检查:若 os.getenv("LITELLM_LICENSE") 未设置,会记录一条 PREMIUM FEATURE 错误日志并退回默认权重。这意味着按优先级保留 tpm/rpm 属于企业版(premium)能力,需要在 .env 中配置有效的 LITELLM_LICENSE 才能生效。

3.2 饱和度行为设置:priority_reservation_settings

from litellm.types.utils import PriorityReservationSettings

litellm.priority_reservation_settings = PriorityReservationSettings(
    default_priority=0.5,           # 无显式优先级的用户的默认权重
    saturation_threshold=0.80,      # 进入严格模式的饱和度阈值
    tracking_multiplier=10          # 严格模式下模型级追踪计数的放大倍数
)

对照源码字段(litellm/types/utils.py),各配置项的实际默认值与语义如下:

  • default_priority:文档示例默认 0.5源码实际默认 0.25。语义为:没有携带任何优先级元数据的 key/team 所使用的兜底权重。这些 key 并不会各自独享一份额度,而是全部共享同一个 default_pool
  • saturation_threshold:文档示例默认 0.80源码实际默认 0.50。取值范围 0.0 ~ 1.0,是宽松/严格模式的分界点。
  • tracking_multiplier:文档中的 10 表示严格模式下"仅追踪、不拦截"的模型级计数所采用的放大倍数;当前实现中该类通过 _create_model_tracking_descriptor(..., high_limit_multiplier=...) 参数来放大限制值。需要指出的是,当前 dynamic_rate_limiter_v3.py 内调用该函数时传入的是 high_limit_multiplier=1,即模型级容量以 100% 上限被始终强制执行(用于防止过度订阅),请以源码现状为准。
  • saturation_check_cache_ttl:文档未列出但源码中真实存在的字段,默认 60。控制读取饱和度值时本地缓存(DualCache 的 local cache)的 TTL,用于削峰 Redis 读取压力。对应测试用例见 test_saturation_check_cache_ttl_configuration

3.3 为请求分配优先级:user / team 元数据

优先级是挂在调用方身份上的属性。_get_priority_from_user_api_key_dict()(源码位于 dynamic_rate_limiter_v3.py)的解析顺序为:先读 team_metadata 中的 priority,再回退到 key 的 metadata

user_api_key_dict.metadata = {"priority": "premium"}          # 按 key 维度
user_api_key_dict.team_metadata = {"priority": "premium"}     # 按团队维度(优先)

测试 test_async_log_success_event_uses_team_priority_from_auth_metadata 专门验证了"团队优先级优先于 key 优先级、并贯穿到成功回调的 Token 计数"这一链路。若两处都未设置,则落入 default_priority 对应的共享默认池。

四、优先级权重归一化:防止过度分配

当各优先级权重之和超过 1.0 时,v3 会自动归一化,保证总分配永远不会超过模型容量:

输入:  {key_a: 0.60, key_b: 0.80} = 合计 1.40
输出:  {key_a: 0.43, key_b: 0.57} = 合计 1.00

归一化的工程实现在 _normalize_priority_weights()dynamic_rate_limiter_v3.py):

  1. 先将 priority_reservation 中的每个值(浮点、rpm、tpm)统一换算为百分比;
  2. 计算 total_weight
  3. 若总和大于 1.0,按 v / total_weight 等比压缩。

测试 test_fake_calls_case_4_over_allocated_with_normalization 覆盖了"过度分配时自动归一化"场景,验证归一化后的保留额度确实按比例生效。

五、源码级实现纵深

核心类 _PROXY_DynamicRateLimitHandlerV3dynamic_rate_limiter_v3.py)继承自 CustomLogger,通过 Proxy 的 pre-call / post-call / log-success 三类钩子接入请求生命周期。类文档字符串总结了它的五个关键设计点:

  1. 模型容量始终按 100% 强制执行(防止过度分配);
  2. 优先级用量从第一个请求起就持续追踪(保证计费与饱和度量准确);
  3. 优先级限制仅在饱和度 ≥ 阈值时才强制执行
  4. 三阶段检查杜绝计数器的"部分自增"
  5. 复用 v3 限流器的 Redis 追踪机制(多实例安全)。

5.1 关键方法一览

方法 职责
async_pre_call_hook() 主入口,路由到宽松/严格模式(源码
_check_model_saturation() 只读查询 Redis 计数器,计算饱和度(源码
_check_rate_limits() 三阶段检查:先只读校验、再按饱和度决策、最后原子自增(源码
_handle_generous_mode() / _handle_strict_mode() 两种模式对应的描述符组装与执行逻辑
_normalize_priority_weights() 处理权重过度分配
_create_priority_based_descriptors() 构建按优先级拆分的限流描述符
async_post_call_success_hook() / async_log_success_event() 成功回调中回写请求/Token 计数与响应头

5.2 饱和度检测:只读不递增

_check_model_saturation() 的实现要点:

  • 复用 v3 limiter 的 create_rate_limit_keys() 生成与限流完全一致的 Redis 计数键({key:value}:rate_limit_type),键格式完全对齐;
  • 分别查询 RPM(键后缀 requests)与 TPM(键后缀 tokens)两个计数器,返回两者中更高的饱和度值;
  • 纯读操作,绝不递增计数器,因此饱和度检查本身不会污染用量统计;
  • 读取走 saturation_check_cache_ttl 控制 TTL 的 DualCache(见 _get_saturation_value_from_cache(),本地读 Redis);
  • 容错策略为 Fail-open:任一异常都会记录错误并返回 0.0(按"未饱和"处理),避免限流器自身的故障阻断线上请求。

5.3 两种模式的描述符差异

Generous 模式(饱和度 < 阈值):仅构建一个模型级描述符(model_saturation_check),只校验模型总容量,任何优先级都能使用剩余容量;对应的"低负载不设限、先到先得"语义由 test_fake_calls_case_1_no_rate_limiting_at_capacity 验证。

Strict 模式(饱和度 ≥ 阈值):在模型级描述符之外,再构建按优先级拆分的描述符(priority_model)。_create_priority_based_descriptors()_get_priority_allocation() 的关键逻辑是:

  • 显式优先级(在 priority_reservation 中有登记):拿到自己的归一化权重与专属 pool key "{model}:{priority}",独占一份 model_rpm × weight / model_tpm × weight 的保留额度;
  • 无显式优先级的 key:全部共享 "{model}:default_pool",共同瓜分 default_priority 那份额度——这意味着低优先级流量内部是"拼池子"关系,容量有盈余时可彼此借用(由 test_fake_calls_case_3_spillover_capacity_default_keys 覆盖);
  • 若模型组未配置 rpm/tpm,则不会生成任何优先级描述符,限流退化为仅模型级。

5.4 三阶段原子检查:杜绝"幻影计数"

_check_rate_limits() 采用注释中明确描述的 THREE-PHASE 流程:

  • Phase 1(只读):一次性把"模型级 + 当前优先级"的全部描述符、限额检查完毕,不做任何自增;
  • Phase 2(决策):依据 saturation >= saturation_threshold 决定哪些描述符进入"强制执行集"——model_saturation_check 永远强制,priority_model 仅在饱和时加入;
  • Phase 3(原子自增):调用 v3 limiter 的 atomic_check_and_increment_by_n(),由 Redis Lua 脚本保证多描述符的"检查 + 自增 + 到期清理"在进程外原子完成(asyncio.Lock + 内存实现作为单进程回退)。

这套流程同时修复了两类经典 bug:

  1. 模型计数器先自增、随后优先级检查失败 → 请求被拦却已消耗容量;
  2. 优先级计数器未强制却自增 → 指标失真。

只要返回码为 OVER_LIMIT,任何计数器都不会被修改(all-or-nothing)。低饱和度下优先级描述符虽然不参与强制,仍通过 v3 的 should_rate_limit()read_only=False)以"只计数不拦截"的方式追踪,保证一旦切换为严格模式时用量数据是完整的。

5.5 从请求到 Token 的全链路计数

限流并不只发生在请求进入时:

  • async_pre_call_hook:每次请求原子 +1 请求计数;
  • async_post_call_success_hook:委托 v3 limiter 写入标准 RateLimit 响应头,并追加 x-litellm-priority(取值为优先级名或 default)与 x-litellm-rate-limiter-version: v3
  • async_log_success_event:从 standard_logging_object.metadata.user_api_key_auth_metadata 中还原优先级,按 rate_limit_type(input/output/total)从响应 usage 中取对应 Token 数,通过 Redis pipeline(async_increment_tokens_with_ttl_preservation,TTL=window)分别递增 model_saturation_checkpriority_model 两个 Token 键——TPM 饱和度与优先级 TPM 消耗由此保持实时准确。

一个值得注意的工程细节:日志打印优先级时会先与安全白名单 {"low", "medium", "high", "default"} 比对,白名单之外的优先级一律以 REDACTED 记录,避免把客户自定义的敏感等级名泄漏进日志。

5.6 拒绝响应与限流窗口

当原子检查返回 OVER_LIMIT,方法抛出 ProxyRateLimitError(其 HTTP 状态即 429)。根据命中的描述符,错误详情与响应头有所区分:

命中的描述符 错误语义 关键响应头
model_saturation_check 模型总容量已达上限(与优先级无关) retry-after(=window)、rate_limit_typex-litellm-priority
priority_model 该优先级保留额度耗尽 额外附加 x-litellm-saturation(如 83.00%

所有响应头中的 retry-afterv3_limiter.window_size,其默认值为环境变量 LITELLM_RATE_LIMIT_WINDOW_SIZE默认 60 秒,见 parallel_request_limiter_v3.py)。另外,v3 的原子响应包含 OVER_LIMIT 状态,但对未知 descriptor_key 存在一套 fail-closed 兜底:宁可拒绝请求也不让超限流量静默穿透到模型。

5.7 一个直观的算例

类文档给出了理解两种模式的最简算例。假设某模型组上限为 100 RPM、某优先级保留 60% 容量、阈值为 80%:

  • 饱和度 < 80%:该优先级最多可用满 100 RPM——模型级上限是唯一约束,剩余容量可被任意优先级借走;
  • 饱和度 ≥ 80%:该优先级被限制在 60 RPM——模型级与优先级两个上限同时生效,先到先得让位于公平分配。

六、测试覆盖:五类场景逐一验证

test_dynamic_rate_limiter_v3.py(共 1800+ 行)完整覆盖了文档列出的全部五类场景,且测试命名与文档条目一一对应:

文档场景 对应测试
1. 容量未满时不限流 test_fake_calls_case_1_no_rate_limiting_at_capacity
2. 饱和期间的优先级队列行为 test_fake_calls_case_2_priority_queue_during_saturation
3. 默认 key 的溢出(借用)容量 test_fake_calls_case_3_spillover_capacity_default_keys
4. 过度分配时的归一化 test_fake_calls_case_4_over_allocated_with_normalization
5. 默认优先级取值处理 test_fake_calls_case_5_default_value_priority_reservation

此外还有一组值得关注的补充测试:

  • test_priority_weight_allocation:用 {"high": 0.9, "low": 0.1} 验证"高优先级拿 90% TPM(900/1000)、低优先级拿 10%(100/1000)",而非错误的五五平分——测试文件开头即注明这是 v3 相对旧版的核心修复点;
  • test_concurrent_priority_requests / test_100_concurrent_priority_requests / test_concurrent_pre_call_hooks_stress:并发压力与竞态验证;
  • test_default_priority_shared_pool:验证所有默认优先级 key 共享同一额度池;
  • test_tpm_only_model_enforces_priority_and_model_capacity:仅配置 TPM 的模型同样正确执行优先级与容量约束;
  • test_priority_429_includes_model_name_and_configured_limits:429 错误体包含模型名、TPM/RPM、剩余额度等排障信息。

测试通过注入 TimeController 伪时钟推进 time.time()、以 Router(model_list=[...]) 声明 rpm/tpm、用 mock 的 AsyncMock 替代 Redis 依赖,可以在不启动真实 Redis 的前提下跑通上述场景。

七、适用前提与使用注意

综合文档与源码,落地 v3 动态限流器时建议确认以下几点:

  1. 模型组必须声明 rpm/tpm:饱和度计算、保留额度换算均以 ModelGroupInforpm/tpm 为分母。模型组无此信息时,优先级描述符不会生成,功能退化为空操作。
  2. 依赖 v3 限流器的 Redis 追踪:原子 Lua 脚本与多实例一致读写在真实部署中依赖 Redis;其"读 Redis、跳过本地缓存"的饱和度查询对 saturation_check_cache_ttl 做了缓存折衷,可根据 Redis 负载调整。
  3. 企业许可证:为优先级保留 rpm/tpm 是 premium 特性,需要在环境变量中配置 LITELLM_LICENSE,否则将记录错误日志并回退到默认权重。
  4. 显式区分"文档默认值"与"代码默认值"default_prioritysaturation_threshold 在本文所依据的文档示例(0.5 / 0.80)与当前源码(0.25 / 0.50)中不同,配置前请以本仓库实际版本与 types/utils.py 中字段定义为准。
  5. 限流窗口:所有计数键的过期时间与 retry-after 均基于 LITELLM_RATE_LIMIT_WINDOW_SIZE(默认 60s),多实例共享 Redis 时保证窗口语义一致。
登录后查看全文
热门项目推荐
相关项目推荐