4. 流量治理:限流算法、熔断降级与灰度发布
流量治理,说白了就是给系统装上一套「交通管制系统」。没有它,突发流量一来,服务说挂就挂。我在多个高并发项目里吃过亏,后来才明白——治理不是锦上添花,是保命用的。
这一章我们聚焦三个核心手段:限流、熔断降级、灰度发布与蓝绿部署。它们分别解决「流量太猛怎么办」「下游挂了怎么办」「新版本上线怎么稳」的问题。
4.1 限流算法:令牌桶与漏桶
限流是流量治理的第一道防线。我见过太多系统因为没做限流,被一次秒杀活动直接打垮。限流算法里最经典的就是令牌桶和漏桶,它们思路不同,适用场景也不同。
4.1.1 令牌桶算法
令牌桶的思路很简单:系统以固定速率往桶里放令牌,每个请求来的时候必须拿一个令牌才能通过。桶有容量上限,令牌攒多了也存不住。
这样做的好处是——允许一定程度的突发流量。你想想看,如果桶里攒了 100 个令牌,突然来一波请求,可以一口气全放过去。这在处理秒杀开场、热点新闻推送时特别有用。
核心参数:
- 令牌填充速率:每秒往桶里放多少个令牌,决定了长期平均速率
- 桶容量:最多能攒多少个令牌,决定了突发流量的大小
- 每次请求消耗:通常消耗 1 个,也可以根据请求权重调整
我在项目中用过 Guava 的 RateLimiter,它实现的就是令牌桶变种。不过要注意,分布式场景下不能只靠本地令牌桶,得用 Redis 做分布式限流。
// 伪代码:令牌桶限流
public class TokenBucket {
private long capacity; // 桶容量
private long tokens; // 当前令牌数
private long lastRefillTime; // 上次填充时间
private long refillRate; // 每秒填充速率
public boolean tryAcquire() {
refill(); // 先补充令牌
if (tokens > 0) {
tokens--;
return true;
}
return false;
}
private void refill() {
long now = System.currentTimeMillis();
long elapsed = now - lastRefillTime;
long newTokens = elapsed * refillRate / 1000;
tokens = Math.min(capacity, tokens + newTokens);
lastRefillTime = now;
}
}
我的经验:令牌桶适合大部分业务场景。但要注意——如果桶容量设得太大,突发流量可能把下游打垮。我一般把容量设为「平均 QPS × 2」,既能应对突发,又不至于失控。
4.1.2 漏桶算法
漏桶的思路正好反过来。它不管请求来得有多猛,出去的速率是固定的。就像一个有漏洞的水桶,水进来再多,漏出去的速度也是恒定的。
这样做的好处是——输出流量平滑。不会出现忽高忽低的情况,对下游服务特别友好。
| 对比维度 | 令牌桶 | 漏桶 |
|---|---|---|
| 输出流量 | 允许突发 | 平滑 |
| 实现复杂度 | 中等 | 简单 |
| 适用场景 | 大部分业务 API | 数据库写入、消息队列 |
| 对下游保护 | 一般 | 强 |
我曾经在一个日志收集系统里用过漏桶。日志写入速率必须稳定,否则数据库扛不住。漏桶把突发的日志写入请求排队,以固定速率刷到数据库,效果很好。
避坑指南:漏桶的缺点是——如果请求持续超过处理能力,队列会无限增长,最终导致超时。我曾经遇到过队列积压到几百万,内存直接爆了。解决方案是给队列设上限,超出的请求直接丢弃或返回 429。
4.2 熔断降级策略
限流管的是「流量太大」,熔断管的是「下游挂了」。你想想看,如果 A 服务调用 B 服务,B 已经响应超时了,A 还在疯狂重试,结果就是 A 的资源也被耗尽——这叫「雪崩效应」。
熔断降级的思路很简单:发现下游有问题,就暂时切断调用,快速返回降级结果。等下游恢复了,再慢慢放行请求。
4.2.1 熔断器的三种状态
经典的熔断器有三个状态:关闭、打开、半开。
- 关闭状态:正常调用,但统计失败率。失败率超过阈值,进入打开状态。
- 打开状态:直接拒绝请求,快速返回降级结果。持续一段时间后,进入半开状态。
- 半开状态:放少量请求试探下游。如果成功,关闭熔断器;如果失败,回到打开状态。
我习惯用 Hystrix 或者 Resilience4j 来实现熔断。不过 Hystrix 已经停止维护了,新项目建议用 Resilience4j。
// 伪代码:熔断器核心逻辑
public class CircuitBreaker {
private State state = State.CLOSED;
private int failureCount = 0;
private int failureThreshold = 5; // 5次失败触发熔断
private long lastFailureTime;
private long timeout = 10000; // 10秒后尝试半开
public boolean allowRequest() {
if (state == State.OPEN) {
if (System.currentTimeMillis() - lastFailureTime > timeout) {
state = State.HALF_OPEN;
return true;
}
return false;
}
return true;
}
public void recordFailure() {
failureCount++;
if (failureCount >= failureThreshold) {
state = State.OPEN;
lastFailureTime = System.currentTimeMillis();
}
}
public void recordSuccess() {
if (state == State.HALF_OPEN) {
state = State.CLOSED;
failureCount = 0;
}
}
enum State { CLOSED, OPEN, HALF_OPEN }
}
降级策略示例:
- 返回缓存数据(比如上次查询结果)
- 返回默认值(比如空列表、0)
- 返回错误提示(比如「服务繁忙,请稍后重试」)
- 异步转同步(比如先返回一个任务 ID,后续轮询)
我的经验:熔断阈值不要设得太死。我一般把失败阈值设为 10 次/10 秒,半开试探请求设为 3 个。太敏感的话,网络抖动就会触发熔断,反而影响可用性。
4.3 灰度发布与蓝绿部署
新版本上线,最怕的就是全量发布后出问题。灰度发布和蓝绿部署就是用来解决这个问题的——让新版本先在小范围验证,没问题再全量推。
4.3.1 灰度发布
灰度发布的核心是「按比例引流」。比如先让 5% 的流量走新版本,观察一段时间没问题,再逐步提高到 10%、50%,最后全量。
在 API 网关里做灰度,通常基于请求特征来路由:
- 用户 ID 哈希:按用户 ID 取模,决定走哪个版本
- 请求头标记:比如内部测试人员带一个 X-Canary: true 头
- 地域/设备:先让某个地域或某类设备走新版本
// 伪代码:灰度路由逻辑
public class GrayRouter {
private static final int GRAY_PERCENT = 5; // 5% 灰度
public String route(String userId) {
int hash = Math.abs(userId.hashCode()) % 100;
if (hash < GRAY_PERCENT) {
return "new-version";
}
return "old-version";
}
}
避坑指南:我曾经遇到过灰度比例设成 1%,结果用户量太大,新版本还是被冲垮了。灰度发布一定要配合限流——新版本的限流阈值要设得保守一些,防止小流量也扛不住。
4.3.2 蓝绿部署
蓝绿部署的思路更简单:准备两套完全一样的环境——蓝环境和绿环境。当前只有一套在提供服务,另一套闲置。新版本部署到闲置环境,测试通过后,把流量全部切过去。
| 对比维度 | 灰度发布 | 蓝绿部署 |
|---|---|---|
| 资源消耗 | 低(共用环境) | 高(双倍环境) |
| 回滚速度 | 慢(逐步调整比例) | 快(直接切回旧环境) |
| 验证粒度 | 细(可按用户/特征) | 粗(全量切换) |
| 适用场景 | 功能验证、A/B 测试 | 版本升级、紧急回滚 |
我个人习惯:核心服务用蓝绿部署,因为回滚快,出问题能秒级恢复。非核心服务用灰度发布,省资源,还能做 A/B 测试。
网关层面的灰度实现要点:
- 网关需要维护「版本路由表」,动态更新
- 灰度策略要支持热更新,不能重启网关
- 灰度流量要打上标签,方便监控和排障
- 灰度期间要实时监控错误率和响应时间,异常自动熔断
嗯,流量治理这块内容不少,但核心就这三板斧。限流管入口,熔断管依赖,灰度管发布。把它们在网关层落地好,系统的稳定性就能上一个台阶。