← 中文资源

简体中文版本

Governor 配额限流中间件

用 governor 的配额限流器:每分钟固定配额加突发容量,被限请求拿到精确的等待时长换算 Retry-After 响应。

阅读英文原文

示例说明

governor 实现 GCRA 算法——配额与突发容量的组合比固定窗口更平滑:允许短时突发后按均速收敛,窗口边界不会出现双倍放行。check 的拒绝信息不是简单布尔:负余额携带精确的等待时长,直接换算成 Retry-After 头,合规客户端可以精确退避。

技术要点

  • GCRA 算法平滑无窗口突变
  • 拒绝携带精确等待时长
  • Arc 共享让限流器跨任务复用

代码解读

ApiGate 包装非键控限流器:Quota::per_minute 声明配额,Arc 让 clone 的门卫共享同一状态。check 的 Ok 表示放行、Err 的 wait_time_from_now 返回需等待的时长;handle_request 把它映射为 429 加秒数提示。八连发的演示里前五个放行、后三个被限并拿到退避时间。

使用建议

按调用方分键用 keyed 变体防单租户独占。分布式口径需换 Redis 支持的实现。限流响应务必带标准头让客户端自适应。

代码示例

use governor::{
    clock::DefaultClock, middleware::NoOpMiddleware, state::direct::NotKeyed,
    state::InMemoryState, Quota, RateLimiter,
};
use std::num::NonZeroU32;
use std::sync::Arc;
use std::time::Duration;

type SharedLimiter = RateLimiter<
    NotKeyed,
    InMemoryState,
    DefaultClock,
    NoOpMiddleware,
>;

#[derive(Clone)]
struct ApiGate {
    limiter: Arc<SharedLimiter>,
}

impl ApiGate {
    fn new(per_minute: u32) -> Self {
        let quota = Quota::per_minute(NonZeroU32::new(per_minute).expect("positive"));
        Self {
            limiter: Arc::new(RateLimiter::direct(quota)),
        }
    }

    fn check(&self) -> Result<(), Duration> {
        match self.limiter.check() {
            Ok(_) => Ok(()),
            Err(negative) => Err(negative.wait_time_from_now()),
        }
    }
}

async fn handle_request(
    gate: &ApiGate,
    route: &str,
) -> Result<String, (u16, String)> {
    if let Err(wait) = gate.check() {
        let retry_after = wait.as_secs().max(1);
        return Err((
            429,
            format!("rate limited, retry after {retry_after}s"),
        ));
    }
    Ok(format!("{route} served"))
}

#[tokio::main]
async fn main() {
    let gate = ApiGate::new(5);

    let mut results = Vec::new();
    for i in 0..8 {
        let outcome = handle_request(&gate, "/search").await;
        let rendered = match outcome {
            Ok(body) => body,
            Err((status, message)) => format!("HTTP {status}: {message}"),
        };
        results.push(format!("req {i}: {rendered}"));
    }

    for line in results {
        println!("{line}");
    }
}