Skip to content
架构高级阅读约 1 分钟

高并发 / 高可用 / 多活:限流降级与流量调度 ​

本篇定位

面向中高级 SRE/架构:从容量评估(Little's Law)讲起,给到限流三种算法的可运行代码、Nginx limit_req、Sentinel 1.8.6 配置、熔断降级、权重/灰度/GSLB 流量调度,最后落到多活容量与一致性取舍。不重复 sre-capacity-dr 的灾备概念,这里给算法、参数和配置。 本篇版本:Nginx 1.30.5(stable)、Alibaba Sentinel 1.8.6、Redis 8.x、JDK 21/25 LTS。限流算法代码为 Go 与 Java 伪实现,便于移植。

一、容量评估:先算清楚再扩容 ​

1.1 小白类比:Little's Law 是"餐厅排队公式" ​

餐厅同时能坐下的人数(并发)= 每桌每分钟进来几人(到达率)× 每桌吃多久(停留时间)。运维里一模一样:

并发数 = QPS × 平均响应时间(RT,秒)

这就是 Little's Law 在 Web 场景的落地形式。它是一切容量估算的地基。

举个例子:接口 RT = 50ms(0.05s),要扛 2000 QPS,需要同时在飞的请求数 = 2000 × 0.05 = 100 个并发。如果 RT 劣化到 500ms,同样 2000 QPS 就要 1000 个并发——RT 一涨,并发需求十倍涨,系统就是这样被压垮的。

1.2 QPS / 并发 / RT 的换算与冗余 ​

已知求公式
QPS、RT并发数并发 = QPS × RT
并发、RT极限 QPSQPS = 并发 / RT
单机 QPS、机器数集群容量集群 QPS = 单机 × 机器数

压测水位与冗余系数(经验,不是教条):

  • 日常水位:单机 CPU 不超过 60%~70%,留突发余量。
  • 峰值冗余系数:按"预测峰值 × 1.5~2"备货。例如预估大促峰值 10000 QPS,单机压出来极限 3000 QPS,则至少 ceil(10000×1.5 / 3000) = 5 台,实际上 6 台留一台做故障冗余。
  • 故障冗余:N+1(挂一台不影响服务)是底线;金融/核心链路 N+2 或跨可用区。

压测水位的"拐点"

压测不是压到机器冒烟,而是找到吞吐拐点:QPS 还在涨,但 P99 开始陡增、CPU 不涨反降(说明在排队/丢弃)。拐点的 70% 作为单机安全水位。

二、限流:三种算法与代码 ​

2.1 算法对比 ​

算法思路优点缺点适用
固定窗口计数器每秒计数,到点清零简单窗口边界突发(1s 末+下 1s 初打两倍)粗粒度、低频
漏桶(Leaky Bucket)请求进桶,固定速率漏出输出绝对平滑无法应对合理突发保护下游、消息队列消费
令牌桶(Token Bucket)按速率往桶里放令牌,有令牌才放行允许一定突发(桶里攒的令牌)桶大小要调好API 限流、网关
滑动窗口把窗口切成小格滑动统计平滑、精确内存/计算略高登录/接口精细化限流

2.2 可运行代码 ​

令牌桶(Go,单机内存版,生产可换 Redis + Lua 做分布式):

go
// 令牌桶:r 个/秒,桶容量 b
type TokenBucket struct {
    rate       float64 // 每秒补充令牌数
    capacity   float64 // 桶容量
    tokens     float64
    lastTime   time.Time
}

func (b *TokenBucket) Allow() bool {
    now := time.Now()
    elapsed := now.Sub(b.lastTime).Seconds()
    // 按时间补充令牌,最多到容量
    b.tokens = math.Min(b.capacity, b.tokens+elapsed*b.rate)
    b.lastTime = now
    if b.tokens >= 1 {
        b.tokens--
        return true
    }
    return false
}

漏桶(极简思想):入口来多少都行,出口永远按固定速率处理,多余的排队或拒绝。

2.3 Nginx limit_req(网关层限流) ​

nginx
# 定义限流区:按 IP 维度,共享内存 10m,平均速率 10r/s
limit_req_zone $binary_remote_addr zone=perip:10m rate=10r/s;
# 按服务维度(不限 IP,限整体)
limit_req_zone $server_name zone=perserver:10m rate=100r/s;

server {
    location /api/ {
        # burst=20 允许突发 20 个排队;nodelay 表示排队里的请求立即处理不延时
        limit_req zone=perip burst=20 nodelay;
        limit_req_status 429;          # 被限流返回 429,别用 503
        proxy_pass http://backend;
    }
    # 连接数限流(防并发占满)
    limit_conn_zone $binary_remote_addr zone=connperip:10m;
    location /upload/ {
        limit_conn connperip 10;        # 单 IP 最多 10 个并发连接
    }
}

要点:rate=10r/s 是平均放行速率;burst 是允许排队的突发量;nodelay 让 burst 内请求不额外等一个周期。被限流要返回 429,让上游客户端退避重试,而不是 503。

2.4 Sentinel 1.8.6(应用层流控熔断) ​

Sentinel 在 Java 应用内做细粒度资源级流控与熔断。核心规则(可由控制台推送,也可代码/配置写死):

java
// 代码方式定义一条流控规则:资源 /order,QPS 超过 20 就限流
List<FlowRule> rules = new ArrayList<>();
FlowRule rule = new FlowRule();
rule.setResource("createOrder");
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);   // 按 QPS 限流
rule.setCount(20);                            // 阈值 20 QPS
rule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_DEFAULT); // 直接拒绝
// 想匀速排队用 CONTROL_BEHAVIOR_RATE_LIMITER(漏桶思想)
rules.add(rule);
FlowRuleManager.loadRules(rules);

熔断降级规则:

java
DegradeRule degradeRule = new DegradeRule();
degradeRule.setResource("callPayment");
degradeRule.setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO); // 按异常比例
degradeRule.setCount(0.5);        // 异常比例超过 50%
degradeRule.setTimeWindow(10);    // 熔断 10 秒,之后进入半开探测
degradeRule.setMinRequestAmount(20); // 最少 20 个请求才统计,避免偶发误判
DegradeRuleManager.loadRules(Collections.singletonList(degradeRule));

资源埋点:

java
try (Entry entry = SphU.entry("createOrder")) {
    // 正常业务逻辑
} catch (BlockException ex) {
    // 被限流/熔断:走降级逻辑,返回兜底数据
    return fallbackResult();
}

Sentinel 版本

Sentinel 最新版本为 1.8.6(sentinelguard.io 官网与 GitHub Releases)。控制台 dashboard 需单独部署;规则持久化到 Nacos/Apollo,否则重启丢失。别和"Redis Sentinel(Redis 高可用哨兵)"混淆——两者同名但完全是两个东西。

三、熔断降级与排队 ​

降级的三层策略(按代价从小到大):

  1. 读降级:下游挂了,返回缓存里的旧数据 / 默认值。
  2. 功能降级:非核心功能(推荐、积分)先关,保核心交易。
  3. 写降级:写请求先进队列/本地表,下游恢复后补写(最终一致)。

四、流量调度:权重、灰度、GSLB ​

4.1 Nginx 权重与灰度 ​

nginx
upstream backend {
    # 权重:v2 新版本只接 10% 流量,观察
    server 10.0.0.11:8080 weight=9;   # 稳定版
    server 10.0.0.12:8080 weight=1;   # 灰度版
    # 被动健康检查:失败 3 次摘除,30s 后恢复
    server 10.0.0.13:8080 max_fails=3 fail_timeout=30s;
    keepalive 32;                     # 到上游的长连接池
}

按 Header / Cookie 灰度(金丝雀):

nginx
location / {
    if ($http_x_canary = "true") { proxy_pass http://canary_upstream; }
    proxy_pass http://stable_upstream;
}

4.2 GSLB 全局负载均衡(多机房) ​

GSLB 决策维度:用户就近解析(DNS 按运营商/地域返回不同 IP)、机房健康探测(某机房连续失败就把其权重降到 0)、权重调度。它不是强一致性的"自动切流",而是基于 DNS TTL 的软切流,故障切换有 TTL 延迟(通常几十秒到几分钟)。

五、多活容量与一致性取舍 ​

模式容量一致性RTO/RPO成本适用
主备(冷备)备机不接流量强一致RTO 小时级低传统企业
同城双活两机房都接流量强/最终一致RTO 分钟级中一般互联网
异地多活多机房都接流量最终一致为主RTO 秒~分钟高大厂核心

取舍要点:

  • 单元化:按用户 ID 分片,一个用户的请求固定落到某个单元,单元间不强同步,跨单元才需要同步。这样多活不用全局强一致。
  • 数据归属:每片数据有唯一写入点(master),避免双写冲突;必须双写时用仲裁/fencing(见 expert-tough-cases 的脑裂章节)。
  • 一致性成本:跨城同步延迟(同城 1~2ms,异地 20~50ms)决定了异地多活只能做"最终一致 + 允许秒级延迟"。金融账务走同城强一致,日志/ feed 可异地最终一致。

生产红线

  • 限流阈值要和下游容量匹配:你限 1000 QPS,但数据库只能扛 500,限流就是空转。按最薄弱环节定阈值。
  • 限流一定要有"被拒绝后的客户端行为":429 + 指数退避重试,别让客户端疯狂重试把刚恢复的系统再打死(重试风暴)。
  • 熔断半开探测流量要小,且探测成功才闭合,避免"刚打开又打满"。
  • 多活不是两个机房简单复制:先想清楚数据归属和单元划分,再谈流量调度。
  • 灰度发布一定先小流量观察指标(错误率、P99),再逐步放量,配自动回滚。

相关速查 ​

官方文档 ​