Envoy/HTTP 本地限流

用令牌桶限制速率并保留可控突发

Envoy 本地限流按单调时钟补充令牌,用原子更新协调同一桶的并发扣减。桶容量决定可积累的突发量。

v1.33.0通信与流量核验 2026-10-11
相关概念

工程问题#

代理既要限制持续请求速率,也要允许空闲后的一小段突发。多个 worker 共享本地配额时,还必须避免各自检查成功却重复消费同一份令牌。

实际实现#

HTTP 过滤器调用 LocalRateLimiterImpl::requestAllowed 检查描述符桶和默认桶。在启用 no_timer_based_rate_limit_token_bucket 的路径上,AtomicTokenBucket 将 tokens_per_fill / fill_interval 换算为每秒速率。AtomicTokenBucketImpl 把余额编码为一个时间值:当前时间与该值的差乘以速率得到令牌数,上限为桶容量;消费后用 CAS 推进这个时间值,冲突则重试。

处理流程#

  1. 按请求描述符找到要检查的桶;默认桶是否额外扣减由配置决定
  2. 按单调时间差计算现有令牌,并以 max_tokens 限制积累量
  3. 令牌不足则返回不允许;足够时用 CAS 提交扣减,冲突后重新检查余额
  4. HTTP 过滤器按 enforced 配置选择继续放行或返回本地限流响应

源码片段#

固定版本 · 原文摘录
source/extensions/filters/common/local_ratelimit/local_ratelimit_impl.ccv1.33.0 · L313–L326
查看原文件
313  // See if the request is forbidden by the default token bucket.314  if (matched_results.empty() || always_consume_default_token_bucket_) {315    if (const bool result = default_token_bucket_->consume(share_factor); !result) {316      // If the request is forbidden by the default token bucket, return the result and the317      // default token bucket.318      return {false, makeOptRefFromPtr<TokenBucketContext>(default_token_bucket_.get())};319    }320321    // If the request is allowed then return the result the token bucket. The descriptor322    // token bucket will be selected as priority if it exists.323    return {true, makeOptRef<TokenBucketContext>(matched_results.empty()324                                                     ? *default_token_bucket_325                                                     : matched_results[0].token_bucket.get())};326  };
LocalRateLimiterImpl::requestAllowed

请求检查直接调用默认桶的 consume。这里只展示默认桶分支;此前代码先处理匹配的描述符桶,失败会提前返回。

source/extensions/filters/common/local_ratelimit/local_ratelimit_impl.ccv1.33.0 · L159–L170
查看原文件
159AtomicTokenBucket::AtomicTokenBucket(uint64_t max_tokens, uint64_t tokens_per_fill,160                                     std::chrono::milliseconds fill_interval,161                                     TimeSource& time_source)162    : token_bucket_(max_tokens, time_source,163                    // Calculate the fill rate in tokens per second.164                    tokens_per_fill / std::chrono::duration<double>(fill_interval).count()) {}165166bool AtomicTokenBucket::consume(double factor, uint64_t to_consume) {167  ASSERT(!(factor <= 0.0 || factor > 1.0));168  auto cb = [tokens = to_consume / factor](double total) { return total < tokens ? 0.0 : tokens; };169  return token_bucket_.consume(cb) != 0.0;170}
AtomicTokenBucket::AtomicTokenBucket / consume

把补充间隔换算为每秒速率;消费回调只有在 total 足够时才扣减。share_factor 会调整实际消费量,不能忽略本地集群配额配置。

source/common/common/token_bucket_impl.hv1.33.0 · L56–L66
查看原文件
56  template <class GetConsumedTokens> double consume(const GetConsumedTokens& cb) {57    const double time_now = timeNowInSeconds();5859    double time_old = time_in_seconds_.load(std::memory_order_relaxed);60    double time_new{};61    double consumed{};62    do {63      const double total_tokens = std::min(max_tokens_, (time_now - time_old) * fill_rate_);64      if (consumed = cb(total_tokens); consumed == 0) {65        return 0;66      }
AtomicTokenBucketImpl::consume — 余额检查

用单调时钟与保存的时间值计算余额,以 max_tokens 封顶。余额不足时回调返回 0,本次检查不更新原子状态。

source/common/common/token_bucket_impl.hv1.33.0 · L74–L81
查看原文件
74      // Move the time_in_seconds_ forward by the number of tokens consumed.75      const double total_tokens_new = total_tokens - consumed;76      time_new = time_now - (total_tokens_new / fill_rate_);77    } while (78        !time_in_seconds_.compare_exchange_weak(time_old, time_new, std::memory_order_relaxed));7980    return consumed;81  }
AtomicTokenBucketImpl::consume — CAS 提交

扣减后的余额重新编码为时间值,通过 compare_exchange_weak 提交;失败会更新 time_old 并回到前面的 do 循环重新计算。memory_order_relaxed 只同步这份配额,不负责发布其他对象状态。

四段分别展示请求配额检查、单位换算、余额计算和原子提交。最后两段来自同一个模板函数的不同连续区间,未拼接为一段源码。模板允许调用方定义消费回调,本文的 HTTP 本地限流回调只在余额足够时返回正数。

收益与取舍#

工程分析

带来的收益

  • 不需要为每个请求保存时间戳,单桶状态与请求数量无关
  • 容量和补充速率分别控制突发量与长期速率
  • 同一桶的原子扣减避免多个线程共同消费同一份余额

付出的代价

  • 单次无冲突补充与扣减为常数工作;CAS 竞争重试、描述符匹配和多桶排序另计,不能保证整次检查严格 O(1)
  • 共享范围取决于本地过滤器配置;它不是跨 Envoy 实例的强一致全局配额
  • 多个描述符桶逐个消费,后续桶拒绝不会回滚此前扣减;因此一次失败请求也可能消耗部分桶的额度

可比较的选择

nginx 漏桶限流nginx 通过随时间衰减的 excess 记录超额量,并可在 burst 范围内延迟请求;Envoy 这里检查令牌并决定是否允许,不采用同样的定时延迟放行机制。两者都可以控制突发,不能简单分成“令牌桶允许突发、漏桶禁止突发”。

外部全局限流服务可协调多个代理实例的共享额度,但需要远程调用、服务可用性和失败策略;本地桶常用于每个实例的保护。

版本与适用范围#

固定 Envoy v1.33.0。本文分析运行时开关 envoy.reloadable_features.no_timer_based_rate_limit_token_bucket 启用后的原子桶;同文件仍保留按定时器补充的 TimerTokenBucket,不能将两条路径混为一种实现。令牌不足是否最终拒绝还受过滤器启用比例、enforced 和路由配置影响。

参考来源#

源码、官方文档与原始论文
01
envoy · Filter::decodeHeaders / Filter::requestAllowed

source/extensions/filters/http/local_ratelimit/local_ratelimit.cc · Filter::decodeHeaders / Filter::requestAllowed

源码v1.33.0
02
envoy · AtomicTokenBucket / LocalRateLimiterImpl::requestAllowed

source/extensions/filters/common/local_ratelimit/local_ratelimit_impl.cc · AtomicTokenBucket / LocalRateLimiterImpl::requestAllowed

源码v1.33.0
03
envoy · AtomicTokenBucketImpl::consume

source/common/common/token_bucket_impl.h · AtomicTokenBucketImpl::consume

源码v1.33.0
04
envoy · timeNowInSeconds

source/common/common/token_bucket_impl.cc · timeNowInSeconds

源码v1.33.0

相关实现与比较#

1 个相关案例
8 个匹配案例