1. 限流基础:什么是限流、为什么需要限流、常见限流算法
1.1 什么是限流
限流(Rate Limiting)是一种通过控制单位时间内允许通过的请求数量,来保护系统不被突发流量冲垮的防护手段。它本质上是对系统入口或关键路径施加一个“流量阀门”,确保后端资源(CPU、内存、数据库连接、下游服务)始终工作在安全水位以下。
核心思想:与其让系统在过载时崩溃,不如在入口处主动拒绝一部分请求,保障大部分请求仍能获得正常响应。
1.2 为什么需要限流
在微服务架构和高并发场景下,限流是系统稳定性的第一道防线。以下四个典型场景直接驱动了限流组件的引入:
| 场景 | 风险描述 | 限流作用 |
|---|---|---|
| 突发流量冲击 | 秒杀、热点事件、爬虫导致流量瞬间飙升 10 倍以上 | 削峰填谷,防止数据库/缓存被打爆 |
| 下游服务脆弱 | 依赖的第三方 API 或 RPC 服务有 QPS 上限 | 主动降速,避免触发下游熔断 |
| 资源成本控制 | 云资源按量计费,无限制扩容成本不可控 | 将流量控制在预算范围内 |
| 公平性与隔离 | 某个恶意租户或异常模块占用全部连接池 | 按租户/接口独立限流,实现资源隔离 |
1.3 常见限流算法概述
业界主流限流算法有三种:计数器、漏桶、令牌桶。它们在实现复杂度、流量整形效果和突发处理能力上各有侧重。
1.3.1 计数器算法(固定窗口)
计数器是最直观的限流方式:将时间划分为固定窗口(如 1 秒),每个窗口内维护一个计数器,请求到达时计数器加 1,超过阈值则拒绝。窗口结束时重置计数器。
// 伪代码:固定窗口计数器
var counter int64
var lastResetTime int64 = now()
func TryAcquire(limit int64) bool {
if now() - lastResetTime >= 1_000_000_000 { // 1秒窗口
counter = 0
lastResetTime = now()
}
if counter >= limit {
return false
}
atomic.AddInt64(&counter, 1)
return true
}
边界突刺问题:固定窗口在窗口切换瞬间可能出现两倍阈值的流量通过。例如窗口 0.9s 时计数器满,1.0s 时重置,0.9~1.1s 之间实际通过了 2 倍阈值。滑动窗口算法可缓解此问题,但精度与存储成本成正比。
1.3.2 漏桶算法(Leaky Bucket)
漏桶将请求比作水滴,桶以固定速率“漏水”(处理请求),桶容量有限。当桶满时,新请求被丢弃。漏桶强制输出速率恒定,适合需要严格平滑流量的场景。
// 伪代码:漏桶算法
type LeakyBucket struct {
rate float64 // 每秒处理速率
capacity float64 // 桶容量
water float64 // 当前水量
lastLeakMs int64 // 上次漏水时间
}
func (lb *LeakyBucket) TryAcquire() bool {
nowMs := now()
elapsed := float64(nowMs - lb.lastLeakMs) / 1000.0
lb.water = max(0, lb.water - elapsed * lb.rate) // 先漏水
lb.lastLeakMs = nowMs
if lb.water < lb.capacity {
lb.water += 1.0
return true
}
return false
}
1.3.3 令牌桶算法(Token Bucket)
令牌桶以固定速率向桶中放入令牌,请求到达时需获取一个令牌才能通过。桶有最大容量,允许一定程度的突发流量(桶中积攒的令牌可被瞬间取走)。这是 Go 标准库 golang.org/x/time/rate 采用的算法,也是本课程压测的核心对象。
// 伪代码:令牌桶算法
type TokenBucket struct {
rate float64 // 令牌放入速率
capacity float64 // 桶容量
tokens float64 // 当前令牌数
lastFillMs int64 // 上次填充时间
}
func (tb *TokenBucket) TryAcquire() bool {
nowMs := now()
elapsed := float64(nowMs - tb.lastFillMs) / 1000.0
tb.tokens = min(tb.capacity, tb.tokens + elapsed * tb.rate) // 先填充令牌
tb.lastFillMs = nowMs
if tb.tokens >= 1.0 {
tb.tokens -= 1.0
return true
}
return false
}
1.3.4 三种算法对比
| 维度 | 计数器(固定窗口) | 漏桶 | 令牌桶 |
|---|---|---|---|
| 输出速率 | 不保证平滑 | 严格恒定 | 平均恒定,允许突发 |
| 突发处理 | 窗口内可突发 | 不支持突发 | 支持突发(桶容量内) |
| 实现复杂度 | 低 | 中 | 中 |
| 内存开销 | 低(1 个计数器) | 低(2 个变量) | 低(2 个变量) |
| 适用场景 | 简单限流,可接受边界突刺 | 流量整形,数据库写入限流 | 通用限流,API 网关,微服务 |
1.4 三种算法核心流程对比
下图从“请求到达 → 判断是否通过”的视角,直观展示三种算法的决策路径差异: