{* YBB Header v4.0 — Alpine.js + 自定义 CSS *}

引言

在高并发系统中,限流(Rate Limiting)是保护核心服务不被流量冲垮的第一道防线。无论是秒杀活动、突发新闻还是爬虫攻击,流量往往以远超系统处理能力的速率涌来。合理的限流策略不仅关乎服务稳定性,更直接影响用户体验和成本控制。

本文将从经典限流算法入手,逐步深入到分布式场景下的工程实现,最后探讨自适应限流的最新进展。

一、经典单机限流算法

1.1 计数器算法(Counter)

计数器是最简单的限流方案:在一个固定时间窗口内累加请求数,超过阈值则拒绝。

class CounterRateLimiter:
    def __init__(self, limit, window_size):
        self.limit = limit
        self.window_size = window_size  # 秒
        self.count = 0
        self.start_time = time.time()

    def allow(self):
        now = time.time()
        if now - self.start_time > self.window_size:
            self.count = 0
            self.start_time = now
        if self.count < self.limit:
            self.count += 1
            return True
        return False

缺陷:窗口边界问题——如果大量请求恰好集中在两个窗口的交界处,实际通过的请求量可能是阈值的 2 倍。

1.2 滑动窗口算法(Sliding Window)

滑动窗口将时间轴切分为多个小窗口,随着时间推移,窗口逐渐滑动,从而解决边界突变问题。

class SlidingWindowLimiter:
    def __init__(self, limit, window_size, split_num=10):
        self.limit = limit
        self.window_size = window_size
        self.split_num = split_num
        self.unit_size = window_size / split_num
        self.counters = [0] * split_num

    def allow(self):
        now = time.time()
        idx = int((now % self.window_size) / self.unit_size)
        # 清零过期窗口
        old_idx = int(((now - self.window_size) % self.window_size) / self.unit_size)
        if idx != old_idx:
            self.counters[idx] = 0
        total = sum(self.counters)
        if total < self.limit:
            self.counters[idx] += 1
            return True
        return False

滑动窗口在精度和内存之间取得了平衡,但sum(self.counters)的遍历操作在大分片数时成为性能瓶颈。

1.3 令牌桶算法(Token Bucket)

令牌桶以固定速率向桶中添加令牌,每个请求到达时消耗一个令牌。桶满时令牌丢弃,桶空时请求被限流。

type TokenBucket struct {
    capacity   int64         // 桶容量
    tokens     float64       // 当前令牌数
    rate       float64       // 令牌添加速率(个/秒)
    lastTime   time.Time     // 上次更新时间
    mu         sync.Mutex
}

func (tb *TokenBucket) Allow() bool {
    tb.mu.Lock()
    defer tb.mu.Unlock()
    now := time.Now()
    elapsed := now.Sub(tb.lastTime).Seconds()
    tb.tokens = math.Min(tb.capacity, tb.tokens + elapsed*tb.rate)
    tb.lastTime = now
    if tb.tokens >= 1.0 {
        tb.tokens--
        return true
    }
    return false
}

令牌桶的关键优势在于:允许突发流量(桶满时),同时持续控制平均速率。适合需要应对短时尖峰但又要控制整体 QPS 的场景。

1.4 漏桶算法(Leaky Bucket)

与令牌桶不同,漏桶以恒定速率"漏出"请求。即使瞬间涌入大量请求,系统也只以固定间隔处理,输出流量是平滑的。

令牌桶 vs 漏桶:令牌桶限制的是"平均速率"(允许突发),漏桶限制的是"输出速率"(强制匀速)。网关层需要突发能力用令牌桶,消息队列需要平滑输出用漏桶。

二、分布式限流的工程挑战

单机限流在分布式场景下会遇到三个核心问题:

  1. 状态一致性:多个节点各自计数,全局阈值难以准确控制
  2. 负载不均:Round-Robin 流量可能使某些节点提前触发限流,而其他节点仍有余量
  3. 时钟不同步:滑动窗口依赖时间戳,节点时钟漂移导致窗口错位

2.1 Redis + Lua 集中计数

最直接的方案:将所有节点的计数集中到 Redis。使用 Lua 脚本保证原子性。

-- KEYS[1]: 限流key, ARGV[1]: 阈值, ARGV[2]: 窗口大小
local current = redis.call('GET', KEYS[1])
if current and tonumber(current) >= tonumber(ARGV[1]) then
    return 0
end
redis.call('INCR', KEYS[1])
if not current then
    redis.call('EXPIRE', KEYS[1], ARGV[2])
end
return 1

这种方案简单但有单点性能瓶颈——当 QPS 达到数万时,Redis 的网络开销和单线程模型会限制整体吞吐。

2.2 Redis-CINATOR:更高效的滑动窗口

利用 Redis Sorted Set(ZSET)实现精确滑动窗口限流。

-- ZSET: score=timestamp_micro, member=unique_request_id
local key = KEYS[1]
local now = tonumber(ARGV[1])
local window_start = now - tonumber(ARGV[2]) * 1000
local limit = tonumber(ARGV[3])

-- 清除过期记录
redis.call('ZREMRANGEBYSCORE', key, 0, window_start)
-- 统计当前窗口内请求数
local current = redis.call('ZCARD', key)

if current >= limit then
    return 0
end
-- 添加当前请求记录
redis.call('ZADD', key, now, ARGV[4])
redis.call('EXPIRE', key, ARGV[2])
return 1

优点是精度极高(可精确到微秒级),缺点是存储开销随 QPS 线性增长。

2.3 分层限流:本地 + 全局

工业界常用的折中方案:每个节点先做本地限流(快速拒绝明显超额的请求),再周期性同步到 Redis 进行全局校验。

  • 本地限流阈值 = 全局阈值 / 节点数 × 安全系数(如 1.2)
  • 每 100ms 向 Redis 上报本节点计数,合并后判断全局是否超限
  • 全局超限时,按节点当前负载比例,按比例降低各节点本地限流阈值

这种分层策略将 Redis 交互频率降低 10-100 倍,在保证全局精度的同时支撑高吞吐。

三、自适应限流(Adaptive Rate Limiting)

传统静态阈值无法应对系统负载的动态变化。自适应限流根据系统健康指标(RT、错误率、CPU 利用率)动态调整限流阈值。

3.1 BBR 启发的限流策略

借鉴 TCP BBR 拥塞控制思想:系统持续探测最大可持续吞吐量,当 RT 陡增或错误率上升时主动降低阈值。


// 采样周期内测量
deliveryRate = successRequests / sampleInterval
RTprop = min(RT over measurement window)
BtlBw = deliveryRate

// 当 RT 超过基线的 1.5 倍时,阈值 = BtlBw * RTprop
// 保守比例: inflight = BtlBw * RTprop * 0.75

3.2 Netflix Concurrency Limits

Netflix 开源的自适应限流库,核心思路是追踪服务器当前的并发请求数(inflight),当 newly measured RT 比历史估计值升高时,降低并发上限。

其梯度下降策略:new_limit = current_limit × (1 ± gradient),其中 gradient 由 RT 变化率决定。

3.3 Sentinel 的匀速排队策略

阿里 Sentinel 提供的"匀速通过"模式(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER),将请求以固定时间间隔排队放行,本质上是一种漏桶的工程实现。对于突发流量,超过阈值的请求会按预期等待时间排队,超出最大排队时间的请求被拒绝。

四、工程实践要点

4.1 限流粒度设计

  • 接口级:对每个 API 端点独立限流
  • 用户/租户级:按用户 ID 或 API Key 区分配额
  • 全局级:保护底层资源(数据库连接、第三方 API)
  • 优先级:核心接口(支付、登录)高配额,非核心接口(推荐、搜索)低配额

4.2 限流响应策略

  • 直接拒绝:返回 429 Too Many Requests + Retry-After
  • 排队等待:请求进入队列,定时放行(保证公平但增加 RT)
  • 降级响应:返回缓存数据或简化版本的响应
  • 熔断:当限流持续触发时,触发熔断层彻底阻断流量

4.3 监控与告警

  • 实时监控:限流触发次数、被拒绝请求比例、排队等待时间分布
  • 告警阈值:当限流触发率 > 5% 时通知 oncall
  • 联动降级:限流率超阈值时自动触发下游降级预案

五、总结

算法突发容忍精度内存开销适用场景
计数器高(边界突变)低O(1)粗粒度限制
滑动窗口中中O(N)通用场景
令牌桶高(桶容量内)高O(1)网关/API限流
漏桶无高O(1)消息队列/流量整形
自适应限流动态调整高O(1)复杂生产环境

没有银弹。单机场景直接用 Guava RateLimiter 或自实现令牌桶;分布式场景优先考虑 Redis 分层限流;对稳定性有极高要求的生产系统,建议采用 Sentinel、Envoy 等成熟框架并开启自适应模式。

限流的本质是在"拒绝一部分请求"和"保护整个系统"之间找平衡点。工程上常常需要在精确度和性能之间做取舍,关键是理解每种算法的行为边界,在合适的场景选择合适的工具。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部