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 或内存使用率超过阈值时,自动降低限流窗口;当负载回落时,逐步恢复吞吐。

下图展示了自适应限流的闭环控制流程:

自适应限流闭环控制流程 请求进入 gRPC 服务端 负载采样器 CPU / 内存 / 延迟 阈值比较器 是否超过阈值? 限流降级 降低并发上限 正常处理 请求通过 动态调整器 更新限流参数 核心反馈逻辑: 1. 负载采样器每 100ms 采集 CPU 使用率和内存占用率。 2. 若 CPU > 80% 或内存 > 85%,阈值比较器触发限流降级,并发上限降低 20%。 3. 若负载连续 30s 低于阈值,动态调整器逐步恢复并发上限(每次增加 10%)。

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/内存反馈)—— 动态调整前两层参数,适应机器规格和运行时变化。

三种策略并非互斥,而是互补。SRE 需要根据服务的延迟特征、资源模型和故障历史,为每个 gRPC 方法配置差异化的限流参数,并通过监控面板持续验证限流效果。