4. 服务端限流策略:基于QPS的限流、基于并发数的限流、基于CPU/内存负载的自适应限流
服务端限流是保护后端服务免受过载冲击的第一道防线。gRPC 服务端限流策略主要分为三类:基于 QPS(吞吐量)的静态限流、基于并发数(Inflight)的静态限流,以及基于 CPU/内存负载的自适应限流。每种策略的适用场景、实现原理和故障表现各不相同,SRE 需要根据业务特征和资源模型进行选择与组合。
4.1 三种限流策略的核心对比
| 策略类型 | 限流维度 | 典型实现 | 优势 | 风险 |
|---|---|---|---|---|
| 基于 QPS | 请求速率(req/s) | 令牌桶、漏桶 | 实现简单,控制入口流量平滑 | 无法感知后端处理能力变化;突发请求可能堆积 |
| 基于并发数 | 正在处理的请求数 | 信号量、最大 Inflight 限制 | 直接保护线程池/协程池,防止资源耗尽 | 对长尾请求不敏感;需要合理设置并发上限 |
| 自适应限流 | 系统负载(CPU/内存) | TCP BBR 启发、CPU 阈值反馈 | 动态调整,自动适应机器规格和流量变化 | 实现复杂;负载采样延迟可能导致限流滞后 |
SRE 实践建议:生产环境中通常采用“并发数 + 自适应”的组合策略。并发数作为硬上限防止资源耗尽,自适应限流作为动态调节器,在 CPU 或内存接近瓶颈时提前降级。
4.2 基于 QPS 的限流:令牌桶实现与配置
基于 QPS 的限流是最直观的策略。gRPC 服务端通常使用 令牌桶算法:以固定速率向桶中放入令牌,每个请求消耗一个令牌;桶满则丢弃令牌,桶空则拒绝请求。
// 基于令牌桶的 QPS 限流中间件示例(Go)
import (
"golang.org/x/time/rate"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func RateLimitInterceptor(limiter *rate.Limiter) grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
if !limiter.Allow() {
return nil, status.Errorf(codes.ResourceExhausted, "rate limit exceeded: %v", info.FullMethod)
}
return handler(ctx, req)
}
}
// 初始化:每秒 1000 个请求,桶容量 2000(允许短时突发)
limiter := rate.NewLimiter(rate.Limit(1000), 2000)
常见故障:QPS 限流无法区分“健康请求”和“慢请求”。当后端处理能力下降时,即使 QPS 未达到阈值,大量慢请求仍会堆积导致超时雪崩。此时应优先使用并发数限流。
4.3 基于并发数的限流:信号量与 Inflight 控制
基于并发数的限流关注的是 正在处理中的请求数量(Inflight)。它直接限制同时进入业务逻辑的请求数,从而保护线程池、数据库连接池或协程资源。
// 基于信号量的并发数限流中间件示例(Go)
import (
"golang.org/x/sync/semaphore"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func ConcurrencyLimitInterceptor(sem *semaphore.Weighted) grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
if err := sem.Acquire(ctx, 1); err != nil {
return nil, status.Errorf(codes.ResourceExhausted, "concurrency limit: %v", info.FullMethod)
}
defer sem.Release(1)
return handler(ctx, req)
}
}
// 初始化:最大并发数 100
sem := semaphore.NewWeighted(100)
并发数上限的设定需要结合服务延迟分布:
- 低延迟服务(P99 < 10ms):并发数可设为 CPU 核心数 × 2~4。
- 中延迟服务(P99 100ms~500ms):并发数需考虑 IO 等待,通常设为 CPU 核心数 × 8~16。
- 高延迟服务(P99 > 1s):并发数应严格控制,避免请求堆积导致内存溢出。
故障排查要点:当客户端收到
RESOURCE_EXHAUSTED 错误时,服务端日志应记录当前 Inflight 计数和上限。如果 Inflight 长期接近上限但 CPU 利用率较低,说明并发数设置过小;如果 Inflight 远低于上限但 CPU 已满载,说明需要引入自适应限流。
4.4 基于 CPU/内存负载的自适应限流
自适应限流的核心思想是:根据系统实时负载动态调整允许的请求量。当 CPU 或内存使用率超过阈值时,自动降低限流窗口;当负载回落时,逐步恢复吞吐。
下图展示了自适应限流的闭环控制流程:
4.5 自适应限流的实现要点
生产级自适应限流通常采用 滑动窗口 + 指数加权移动平均(EWMA) 来平滑负载信号,避免毛刺导致限流抖动。
// 自适应限流核心逻辑伪代码
type AdaptiveLimiter struct {
maxConcurrency int64
currentLoad float64 // 当前 CPU 使用率(0.0 ~ 1.0)
cpuThreshold float64 // CPU 阈值,默认 0.8
memoryThreshold float64 // 内存阈值,默认 0.85
cooldownPeriod time.Duration // 冷却期,默认 30s
lastAdjustTime time.Time
}
func (l *AdaptiveLimiter) shouldDrop() bool {
// 每 100ms 采样一次 CPU 和内存
cpu := getCPUUsage()
mem := getMemoryUsage()
l.currentLoad = 0.7*l.currentLoad + 0.3*math.Max(cpu, mem) // EWMA 平滑
if l.currentLoad > l.cpuThreshold && time.Since(l.lastAdjustTime) > l.cooldownPeriod {
// 降级:并发上限减少 20%
l.maxConcurrency = int64(float64(l.maxConcurrency) * 0.8)
l.lastAdjustTime = time.Now()
return true
}
if l.currentLoad < l.cpuThreshold*0.7 && time.Since(l.lastAdjustTime) > l.cooldownPeriod {
// 恢复:并发上限增加 10%
l.maxConcurrency = int64(float64(l.maxConcurrency) * 1.1)
l.lastAdjustTime = time.Now()
}
return false
}
自适应限流的陷阱:
- 采样延迟:CPU 使用率采样存在 100ms~500ms 延迟,高突发流量可能先打满 CPU 才触发限流。建议配合并发数硬上限使用。
- 内存泄漏误判:如果服务存在内存泄漏,自适应限流会持续降级直到拒绝所有请求。需要设置最小并发数保护(如不低于 10)。
- 多容器争抢:在 Kubernetes 环境中,CPU 使用率是容器级别的,但宿主机争抢可能导致 cgroup 限流。建议同时监控容器 CPU Throttle 指标。
4.6 三种策略的故障排查对照表
| 故障现象 | 可能原因 | 排查方向 | 推荐策略调整 |
|---|---|---|---|
客户端大量 RESOURCE_EXHAUSTED |
QPS 或并发数上限设置过低 | 查看服务端 Inflight 计数和 CPU 利用率 | 若 CPU 空闲,增大并发上限;若 CPU 满载,启用自适应限流 |
| CPU 未满载但请求超时 | 并发数过高导致请求排队 | 检查 gRPC 等待队列长度和请求延迟分布 | 降低并发数上限,引入 QPS 限流平滑流量 |
| CPU 满载但 Inflight 很低 | 请求处理逻辑 CPU 密集,并发数限制过于保守 | 分析火焰图,确认 CPU 热点 | 保持并发数不变,启用自适应限流动态调节 |
| 限流频繁抖动 | 负载采样毛刺或阈值设置过紧 | 检查 EWMA 平滑系数和冷却期配置 | 增大冷却期(如 60s),降低降级幅度(如 10%) |
SRE 最佳实践:在 gRPC 服务端同时部署三种限流策略,形成“三层防护”:
第一层:QPS 限流(令牌桶)—— 防止突发流量冲垮网关。
第二层:并发数限流(信号量)—— 保护业务线程池不被慢请求耗尽。
第三层:自适应限流(CPU/内存反馈)—— 动态调整前两层参数,适应机器规格和运行时变化。
第一层:QPS 限流(令牌桶)—— 防止突发流量冲垮网关。
第二层:并发数限流(信号量)—— 保护业务线程池不被慢请求耗尽。
第三层:自适应限流(CPU/内存反馈)—— 动态调整前两层参数,适应机器规格和运行时变化。
三种策略并非互斥,而是互补。SRE 需要根据服务的延迟特征、资源模型和故障历史,为每个 gRPC 方法配置差异化的限流参数,并通过监控面板持续验证限流效果。