如何限流?

解读

“限流”在国内后端面试里几乎必问,它不仅是高并发系统的“保险丝”,也是 Rust 岗位区分“只写业务”与“懂系统”的关键分水岭。
面试官想听的不是“用 Redis 限流”一句话,而是:

  1. 你能否在 无 GC、零成本抽象 的 Rust 里把限流算法落地;
  2. 是否理解 并发原语 + 内存模型 才能保证“计数”不出 race;
  3. 能否给出 Cargo 生态 里的现成方案,并指出其源码级优缺点;
  4. 是否考虑 分布式场景 下 Rust 微服务如何与 Sidecar 协同。
    回答时先给“本地单进程”方案,再递进“多实例 + Redis”方案,最后补“熔断降级”联动,就能拿到高分。

知识点

  1. 速率限制模型:固定窗口、滑动窗口、漏桶、令牌桶。
  2. Rust 并发安全:Arc<Mutex<T>>、AtomicU64、crossbeam::channel、parking_lot::Mutex 性能差异。
  3. 异步限流:tokio::time::interval + tokio::sync::Semaphore 做令牌桶。
  4. 无锁算法:dashmap + atomic 实现滑动窗口,避免全局锁。
  5. Cargo 生态
    • governor:基于 GCRA 的令牌桶,no_std 友好,支持异步。
    • ratelimit:简化版漏桶,单线程使用。
    • redis-cell 模块:Rust 通过 redis-rs 调用,解决 多实例漂移
  6. 观测性:prometheus 的 counter/histogram 如何与限流埋点对接。
  7. Service Mesh 集成:Envoy 的 local_rate_limit 与 Rust 服务 共享 PID namespace 时的流量一致性。

答案

“我按‘本地→分布式→熔断’三层给您展开。
第一层,单进程异步令牌桶
用 tokio 1.35 + governor 0.6。

  1. 构造 governor::RateLimiter::direct(Quota::per_second(NonZeroU32::new(1000).unwrap()))
  2. 在 Tower Service 层写一层 RateLimit<S>,内部调用 rate_limiter.check()失败立即返回 429,latency 几乎零增加;
  3. 为了 跨任务共享,把 RateLimiter 包进 Arc<dashmap::DashMap<ClientIp, RateLimiter>>,用 atomic 保证 无锁分片
  4. 压测 8 核 2.8 GHz,QPS 28 万,P99 延迟 0.18 ms,zero-allocation hot path

第二层,多实例统一窗口
把令牌桶的 剩余令牌数 放到 Redis Cell 模块,用 CL.THROTTLE user123 15 30 60 1 命令,Rust 端用 redis-rs 0.24 的 多路复用连接池,超时设 5 ms;若 Redis 超时,退化到本地令牌桶,保证 fail-open 不击垮自身。

第三层,熔断联动
限流触发后,把事件写进 tokio mpsc,异步线程更新熔断器状态(Hystake 实现),下游直接 短路返回 503,减少 无效重试带来的 dog-pile。

整个实现 单 crate 发布,cargo feature 开关控制是否启用 redis 后端,编译期决定零依赖。”

拓展思考

  1. 无锁滑动窗口:如果业务突发 10 k QPS,固定窗口 1 s 边界会 双倍突刺。可以用 AtomicBucketRing(分 60 槽,每 100 ms 一槽)+ crossbeam::epoch 保证无锁回收,内存仅 480 字节/键
  2. eBPF 加速:在 k8s 场景 下,用 aya-rs 把限流逻辑下沉到 cgroup skb hook,Rust 用户态只负责 动态改配额内核态直接丢包,单机可扛 1 000 万 CPS。
  3. WebAssembly 边缘限流:把 governor 编译到 wasm32-wasi,在 Cloudflare Worker 里做边缘限流,冷启动 < 1 ms,与中心 Redis 方案互补。