限流器为什么是后端系统的第一道防线
高并发场景下,后端服务承受的流量往往超出设计容量。限流器在流量入口做准入控制,防止过载导致服务雪崩。单机限流用令牌桶或滑动窗口即可实现,分布式限流要解决的问题是:多个服务实例共享同一限流配额,保证总QPS不超过设定阈值。分布式限流器的核心挑战在于精度和性能的平衡——共享计数意味着网络开销,本地计数意味着配额不均。
令牌桶限流的Go实现
令牌桶算法是工业界应用最广的限流方案。核心参数:桶容量(burst)、令牌生成速率(rate)。请求到来时取走令牌,桶空则拒绝。允许短时突发,长期平均速率受控。Go语言time/rate包提供了开箱即用的令牌桶实现:
package main
import (
"context"
"net/http"
"golang.org/x/time/rate"
)
type RateLimiter struct {
limiter *rate.Limiter
}
func NewRateLimiter(rps int, burst int) *RateLimiter {
return &RateLimiter{
limiter: rate.NewLimiter(rate.Limit(rps), burst),
}
}
func (rl *RateLimiter) Allow(ctx context.Context) bool {
return rl.limiter.Allow()
}
func (rl *RateLimiter) Wait(ctx context.Context) error {
return rl.limiter.Wait(ctx)
}
func handler(w http.ResponseWriter, r *http.Request) {
limiter := NewRateLimiter(100, 200)
if !limiter.Allow(r.Context()) {
http.Error(w, "rate limit exceeded", http.StatusTooManyRequests)
return
}
// 正常处理请求
}
Allow()立即返回是否允许,Wait()阻塞等待直到获取令牌。API网关场景用Allow()快速拒绝超限请求,后台任务场景用Wait()平滑排队。
分布式限流:Redis滑动窗口方案
多实例共享限流配额,需要集中式计数器。Redis是实现分布式限流的首选存储。滑动窗口限流比固定窗口更平滑,避免窗口边界处的突发通过。基于Redis ZSET实现的滑动窗口限流:
package distlimiter
import (
"context"
"time"
"github.com/redis/go-redis/v9"
)
type SlidingWindowLimiter struct {
client *redis.Client
key string
limit int64
window time.Duration
}
func NewSlidingWindowLimiter(client *redis.Client, key string, limit int64, window time.Duration) *SlidingWindowLimiter {
return &SlidingWindowLimiter{client: client, key: key, limit: limit, window: window}
}
func (l *SlidingWindowLimiter) Allow(ctx context.Context) (bool, error) {
now := time.Now()
script := redis.NewScript(`
local key = KEYS[1]
local now = tonumber(ARGV[1])
local window_ms = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])
redis.call('ZREMRANGEBYSCORE', key, '-inf', now - window_ms)
local count = redis.call('ZCARD', key)
if count < limit then
redis.call('ZADD', key, now, now .. ':' .. math.random(1e6))
redis.call('PEXPIRE', key, window_ms)
return 1
end
return 0
`)
result, err := script.Run(ctx, l.client,
[]string{l.key},
now.UnixMilli(), l.window.Milliseconds(), l.limit,
).Int64()
return result == 1, err
}
Lua脚本在Redis端原子执行,避免竞态条件。每次请求记录时间戳到ZSET,清理窗口外数据后计数判断。key按API路径+租户ID组合,实现按接口按租户细粒度限流。QPS万级以内性能稳定,P99延迟在2ms以内。
自适应限流:根据系统负载动态调整阈值
固定阈值限流的问题在于:阈值设高了保护不足,设低了浪费容量。自适应限流根据系统实时指标动态调整限流阈值。Google的BBR算法思路:当系统排队延迟上升时降低准入,延迟下降时增加准入。
// 自适应限流器核心逻辑
type AdaptiveLimiter struct {
maxQPS int64
minQPS int64
currentQPS int64
cpuThreshold float64
rttThreshold time.Duration
}
func (l *AdaptiveLimiter) adjustLimit(cpuUsage float64, avgRTT time.Duration) {
if cpuUsage > l.cpuThreshold || avgRTT > l.rttThreshold {
l.currentQPS = max(l.minQPS, int64(float64(l.currentQPS)*0.8))
} else if cpuUsage < l.cpuThreshold*0.7 && avgRTT < l.rttThreshold/2 {
l.currentQPS = min(l.maxQPS, int64(float64(l.currentQPS)*1.05))
}
}
自适应限流的关键是指标采集精度。CPU利用率、请求排队延迟、错误率三个指标构成决策依据。采集窗口5秒,调整间隔10秒,避免抖动。上线前压测确定maxQPS和rttThreshold,minQPS保证最低吞吐不被限死。自适应限流配合固定限流做兜底——自适应算法异常时回退到固定阈值,确保系统始终有保护。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/go-yu-yan-fen-bu-shi-xian-liu-qi-she-ji-yu-zi-shi-ying-xian/