在分布式系统架构中,限流就像城市交通的信号灯控制系统。当某个服务的请求量突然激增(比如双十一秒杀活动),如果没有合理的流量控制,整个系统就会像早晚高峰没有红绿灯的十字路口一样陷入瘫痪。
我经历过最严重的一次事故是某次营销活动期间,由于未做限流保护,一个核心接口在30秒内收到了平时10倍的请求量,直接导致数据库连接池耗尽,整个交易系统雪崩。从那次教训后,我深入研究了各种限流算法,并在多个网关系统中进行了实践验证。
计数器算法是最直观的实现方式,就像餐厅门口的取号机。我们定义一个时间窗口(比如1分钟),在这个窗口内:
java复制// 简单计数器实现示例
public class CounterLimiter {
private long timeWindow = 60_000; // 1分钟
private int threshold = 100; // 限流阈值
private AtomicLong counter = new AtomicLong(0);
private long windowStart = System.currentTimeMillis();
public boolean tryAcquire() {
long now = System.currentTimeMillis();
if (now - windowStart > timeWindow) {
counter.set(0);
windowStart = now;
}
return counter.incrementAndGet() <= threshold;
}
}
注意:简单计数器存在临界问题。比如限制100次/分钟,如果在59秒时突然涌入100个请求,下一秒重置后又来100个请求,实际上2秒内处理了200个请求。
滑动窗口算法改进了固定窗口的缺陷,就像把一分钟细分为6个10秒的小格子:
python复制class SlidingWindowLimiter:
def __init__(self, window_size=60, threshold=100):
self.window_size = window_size # 秒
self.threshold = threshold
self.windows = [0] * window_size
self.current_time = int(time.time())
def try_acquire(self):
now = int(time.time())
# 计算需要滑动的窗口数
slide_num = now - self.current_time
if slide_num >= self.window_size:
# 全部重置
self.windows = [0] * self.window_size
else:
# 滑动过期窗口
for i in range(slide_num):
self.windows.pop(0)
self.windows.append(0)
self.current_time = now
# 统计当前窗口总数
total = sum(self.windows)
if total >= self.threshold:
return False
self.windows[-1] += 1
return True
漏桶算法模拟了一个底部有固定流出速率的桶:
go复制type LeakyBucket struct {
capacity int64 // 桶容量
rate float64 // 漏水速率(请求/秒)
water int64 // 当前水量
lastLeak time.Time // 上次漏水时间
mutex sync.Mutex
}
func (lb *LeakyBucket) Allow() bool {
lb.mutex.Lock()
defer lb.mutex.Unlock()
now := time.Now()
// 计算上次漏水到现在的漏水量
elapsed := now.Sub(lb.lastLeak).Seconds()
leakAmount := int64(elapsed * lb.rate)
if leakAmount > 0 {
lb.water -= leakAmount
if lb.water < 0 {
lb.water = 0
}
lb.lastLeak = now
}
if lb.water >= lb.capacity {
return false
}
lb.water++
return true
}
令牌桶算法与漏桶相反:
javascript复制class TokenBucket {
constructor(capacity, fillRate) {
this.capacity = capacity; // 桶容量
this.fillRate = fillRate; // 令牌添加速率(个/秒)
this.tokens = capacity; // 当前令牌数
this.lastFill = Date.now();// 上次添加时间
}
canConsume(tokens = 1) {
// 计算时间差并补充令牌
const now = Date.now();
const elapsed = (now - this.lastFill) / 1000;
this.lastFill = now;
this.tokens = Math.min(
this.capacity,
this.tokens + elapsed * this.fillRate
);
if (this.tokens >= tokens) {
this.tokens -= tokens;
return true;
}
return false;
}
}
Nginx内置了漏桶算法实现的限流模块:
nginx复制http {
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
server {
location /api/ {
limit_req zone=api_limit burst=20 nodelay;
proxy_pass http://backend;
}
}
}
rate=10r/s:每秒10个请求的基本速率burst=20:允许突发20个请求nodelay:不延迟处理突发请求,直接拒绝超限部分Spring Cloud Gateway整合Redis实现分布式限流:
java复制@Bean
public KeyResolver apiKeyResolver() {
return exchange -> Mono.just(
exchange.getRequest().getPath().value()
);
}
@Bean
public RedisRateLimiter redisRateLimiter() {
return new RedisRateLimiter(
10, // 每秒令牌补充速率
20, // 桶容量
1 // 每次请求消耗令牌数
);
}
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("api_route", r -> r.path("/api/**")
.filters(f -> f.requestRateLimiter(c -> {
c.setRateLimiter(redisRateLimiter());
c.setKeyResolver(apiKeyResolver());
}))
.uri("lb://backend-service"))
.build();
}
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| Redis + Lua | 集中式计数 | 实现简单,性能较好 | Redis单点问题 |
| 令牌桶算法 | 预生成令牌 | 允许突发流量 | 实现较复杂 |
| 滑动窗口算法 | 分片统计 | 精度高,无临界问题 | 内存占用较大 |
| Sentinel | 自适应限流 | 支持熔断降级 | 学习成本较高 |
| 本地限流+全局配额 | 两级限流 | 减轻中心节点压力 | 配额分配需要协调 |
在实际项目中,我们通常需要组合多种限流维度:
yaml复制# 限流规则配置示例
rules:
- resource: /api/order
strategy: sliding_window
threshold: 1000/60s # 每分钟1000次
params:
windowSize: 60 # 60秒窗口
splitNum: 6 # 分成6个子窗口
- resource: /api/payment
strategy: token_bucket
threshold: 500/1s # 每秒500次
params:
capacity: 1000 # 桶容量1000
burst: 200 # 允许突发200
- resource: USER_${userId}
strategy: counter
threshold: 30/10s # 每用户10秒30次
我们开发了一套动态限流系统,关键特性包括:
java复制// 动态限流规则示例
public class DynamicRule {
private double baseThreshold; // 基础阈值
private double maxThreshold; // 最大阈值
private List<AdjustmentRule> adjustmentRules;
public double getCurrentThreshold() {
double adjustment = 0;
for (AdjustmentRule rule : adjustmentRules) {
adjustment += rule.calculateAdjustment();
}
return Math.min(
baseThreshold + adjustment,
maxThreshold
);
}
}
// 基于CPU使用率的调整规则
public class CpuAdjustmentRule implements AdjustmentRule {
private double cpuThreshold = 0.7;
private double step = 0.1;
@Override
public double calculateAdjustment() {
double cpuUsage = getCpuUsage();
if (cpuUsage > cpuThreshold) {
return -step * (cpuUsage - cpuThreshold) * 10;
}
return 0;
}
}
当请求被限流时,我们通常有以下处理方式:
java复制@RestControllerAdvice
public class RateLimitHandler {
@ResponseStatus(HttpStatus.TOO_MANY_REQUESTS)
@ExceptionHandler(RateLimiterException.class)
public ErrorResponse handleRateLimit(RateLimiterException e) {
return new ErrorResponse(429, "Too many requests");
}
}
python复制class RequestQueue:
def __init__(self, max_concurrent):
self.semaphore = threading.Semaphore(max_concurrent)
def process_request(self, request):
if not self.semaphore.acquire(timeout=1):
raise RateLimitExceeded()
try:
return handle_request(request)
finally:
self.semaphore.release()
javascript复制app.use((req, res, next) => {
if (rateLimiter.isLimited(req)) {
// 返回缓存数据
if (cache.has(req.path)) {
return res.json(cache.get(req.path));
}
// 或执行简化流程
return simplifiedHandler(req, res);
}
next();
});
我们在Prometheus中监控以下指标:
| 指标名称 | 类型 | 说明 |
|---|---|---|
| rate_limit_requests_total | Counter | 总请求数(按资源分类) |
| rate_limit_rejected_total | Counter | 被拒绝的请求数 |
| rate_limit_queue_time_seconds | Histogram | 排队等待时间 |
| rate_limit_threshold | Gauge | 当前限流阈值(动态调整值) |
| system_cpu_usage | Gauge | 系统CPU使用率 |
Grafana监控面板配置示例:
json复制{
"panels": [{
"title": "限流统计",
"type": "graph",
"targets": [{
"expr": "sum(rate(rate_limit_requests_total[1m])) by (resource)",
"legendFormat": "{{resource}}"
}]
},{
"title": "拒绝率",
"type": "singlestat",
"targets": [{
"expr": "sum(rate(rate_limit_rejected_total[1m])) / sum(rate(rate_limit_requests_total[1m]))",
"format": "percent"
}]
}]
}
java复制// 优化前的同步代码
public synchronized boolean tryAcquire() {
// ...
}
// 优化后使用LongAdder
private LongAdder counter = new LongAdder();
public boolean tryAcquire() {
if (counter.sum() >= threshold) {
return false;
}
counter.increment();
return true;
}
go复制type TickUpdater struct {
ticker *time.Ticker
now int64 // atomic
}
func (u *TickUpdater) Start() {
go func() {
for range u.ticker.C {
atomic.StoreInt64(&u.now, time.Now().UnixNano())
}
}()
}
func getNow() int64 {
return atomic.LoadInt64(&updater.now)
}
python复制# 避免在限流判断中创建临时对象
class Window:
__slots__ = ['count'] # 固定属性,减少内存占用
def __init__(self):
self.count = 0
问题1:限流不生效
问题2:突发流量导致误限
问题3:分布式环境下计数不准
问题4:限流导致业务异常
我们尝试使用LSTM预测流量趋势:
python复制class TrafficPredictor:
def __init__(self):
self.model = Sequential([
LSTM(64, input_shape=(60, 1)), # 输入60个历史点
Dense(1)
])
self.model.compile(loss='mse', optimizer='adam')
def predict_next_minute(self, history):
# history: 过去60分钟的数据点
x = np.array(history[-60:]).reshape(1, 60, 1)
return self.model.predict(x)[0][0]
在Istio中的限流配置示例:
yaml复制apiVersion: config.istio.io/v1alpha2
kind: handler
metadata:
name: redis-quota
spec:
compiledAdapter: redisquota
params:
redisServerUrl: "redis-service:6379"
connectionPoolSize: 10
quotas:
- name: api-calls
maxAmount: 1000
validDuration: 1s
bucketDuration: 0.5s
rateLimitAlgorithm: ROLLING_WINDOW
---
apiVersion: config.istio.io/v1alpha2
kind: rule
metadata:
name: quota
spec:
actions:
- handler: redis-quota
instances:
- requestcount.quota
使用Chaos Mesh测试限流系统的健壮性:
yaml复制apiVersion: chaos-mesh.org/v1alpha1
kind: NetworkChaos
metadata:
name: network-latency
spec:
action: delay
mode: one
selector:
namespaces:
- ratelimit-service
delay:
latency: "500ms"
correlation: "100"
jitter: "100ms"
duration: "10m"
在网关系统中实施限流就像给高速公路安装智能交通系统,需要根据不同的路段特点(业务场景)配置不同的限速策略(限流算法)。经过多个项目的实践验证,我总结了几个关键经验:
不要追求完美精度:限流本身是一种保护手段,允许5-10%的误差可以大幅降低实现复杂度
监控比算法更重要:再好的算法也需要配合完善的监控,我们花了30%的时间在算法实现,70%的时间在监控告警建设
留足缓冲空间:生产环境的阈值应该比压测结果低20-30%,给系统波动留出余量
定期演练:通过混沌工程主动触发限流,验证系统行为是否符合预期