中间件——流量洪峰下的系统保护 / Middleware for Protecting Systems Under Traffic Spikes
📅 创建时间:2026-05-08 🏷️ 标签:#API网关 #限流 #熔断 #令牌桶 #链路追踪 #服务发现 📚 前置知识:[[00-backend-overview]] [[01-redis-deep]](Redis 限流) 📚 相关知识:[[06-network]](TCP 连接数) [[08-concurrency]](线程池)
场景:双十一零点,你的系统被自己的流量打垮了
┌─────────────────────────────────────────────────────────────┐
│ │
│ 双十一零点,10 万用户同时涌进。 │
│ │
│ 用户体验: │
│ 前端页面:正常 │
│ API 网关:正常 │
│ 用户服务:正常 │
│ 商品服务:正常 │
│ 订单服务:❌ 超时 │
│ │
│ 运营后台: │
│ 发现所有服务正常,只有订单服务超时 │
│ │
│ 为什么? │
│ → 10 万请求同时涌入订单服务 │
│ → 订单服务线程池耗尽 │
│ → 后续请求全部排队 → 超时 │
│ │
│ 雪上加霜: │
│ → 超时的请求没有快速失败 │
│ → 它们还占用着线程资源(等待) │
│ → 订单服务越来越慢 → 最终崩溃 │
│ │
└─────────────────────────────────────────────────────────────┘这一章,我们理解如何用中间件保护系统:限流(控制流量)、熔断(快速失败)、网关(统一入口)、链路追踪(问题定位)。
第1节:限流——为什么你需要"拒绝服务"
问题抽象
餐厅有 20 张桌子,门口排了 100 人。
方案 A(无限制):100 人全部进去 → 挤爆 → 所有人体验极差
方案 B(限流):门口只放 20 人 → 20 人正常用餐 → 剩下 80 人排队
但方案 B 也有问题:排队的 80 人不知道要等多久。
→ 需要告诉用户"你是第几号,预计等待 X 分钟"推演:限流的三个维度
┌─────────────────────────────────────────────────────────────┐
│ 限流的三个维度 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 按谁限流: │
│ • 用户维度:每个用户每分钟最多 100 请求 │
│ • IP 维度:每个 IP 每分钟最多 200 请求 │
│ • 接口维度:/order 每秒最多 1000 请求 │
│ • 全局维度:整个系统每秒最多 10 万请求 │
│ │
│ 2. 按什么限流: │
│ • QPS(每秒请求数) │
│ • 并发数(同时处理的请求数) │
│ • 连接数(TCP 连接数) │
│ │
│ 3. 限流后怎么办: │
│ • 直接拒绝(返回 429 Too Many Requests) │
│ • 排队等待(返回"排队中,预计 X 分钟") │
│ • 降级处理(返回默认响应,不调下游服务) │
│ │
└─────────────────────────────────────────────────────────────┘第2节:限流算法——从计数器到令牌桶
算法一:固定窗口计数器(最简单,有缺陷)
┌─────────────────────────────────────────────────────────────┐
│ 固定窗口计数器 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 窗口:1 分钟 │
│ 限制:100 次/分钟 │
│ │
│ 0:00-1:00: 100 次请求(已满) │
│ 1:00-1:01: 又来了 100 次(窗口重置,重新计数) │
│ │
│ 问题:临界突变 │
│ 0:59: 来了一波 100 次请求 │
│ 1:00: 窗口重置,新的一波 100 次请求 │
│ → 1 秒内处理了 200 个请求(超出预期!) │
│ │
└─────────────────────────────────────────────────────────────┘算法二:滑动窗口(更精确,但实现复杂)
┌─────────────────────────────────────────────────────────────┐
│ 滑动窗口算法 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 将 1 分钟分成 6 个 10 秒窗口 │
│ │
│ 当前时刻 1:05,过去 6 个窗口的请求数: │
│ [1:00] [1:01] [1:02] [1:03] [1:04] [1:05] │
│ 10 20 15 18 12 10 │
│ 总计:85 < 100,允许通过 │
│ │
│ 当前时刻 1:10,过去 6 个窗口的请求数: │
│ [1:05] [1:06] [1:07] [1:08] [1:09] [1:10] │
│ 15 20 20 20 20 15 │
│ 总计:110 > 100,限流 │
│ │
│ 优点:没有临界突变 │
│ 缺点:需要维护 6 个窗口的状态 │
│ │
└─────────────────────────────────────────────────────────────┘算法三:漏桶(让流量平滑)
┌─────────────────────────────────────────────────────────────┐
│ 漏桶算法 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────┐ │
│ │ │ │
│ │ 桶 │ │
│ │ [请求][请求][请求]... │ │
│ │ ↓ 漏出(固定速率) │ │
│ └──────────────────────────────┘ │
│ ↓ │
│ 消费者(固定处理速度) │
│ │
│ 特点: │
│ • 无论上游来多少请求,下游都以固定速率处理 │
│ • 桶满了,请求被丢弃 │
│ • 流量平滑,但突发请求无法加速处理 │
│ │
└─────────────────────────────────────────────────────────────┘算法四:令牌桶(推荐,同时处理突发和限流)
┌─────────────────────────────────────────────────────────────┐
│ 令牌桶算法(推荐) │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────┐ │
│ │ │ │
│ │ 桶 │ ← 容量 = burst │
│ │ (令牌) │ burst = 允许的突发量 │
│ │ 以固定速率放入 │ 令牌放入速率 = qps │
│ └──────────────────────────────┘ │
│ ↓ │
│ 请求来了 → 必须拿到令牌才能处理 │
│ 拿到令牌 → 处理请求 → 令牌消耗 │
│ 没令牌 → 请求被限流 │
│ │
│ 举例: │
│ 桶容量 = 100(burst) │
│ 令牌放入速率 = 10/秒 │
│ │
│ 平时:每秒处理 10 个请求 │
│ 突发:一次性可以处理 100 个请求(桶里有 100 个令牌) │
│ 突发后:需要等令牌慢慢补充 │
│ │
│ 优点: │
│ ✅ 允许一定的突发流量(用户体验更好) │
│ ✅ 平滑限流(令牌补充是均匀的) │
│ │
└─────────────────────────────────────────────────────────────┘令牌桶 Python 实现
python
import time
import threading
import math
class TokenBucket:
def __init__(self, qps=100, burst=200):
self.qps = qps
self.burst = burst
self.tokens = burst # 初始令牌数
self.last_update = time.time()
self.lock = threading.Lock()
def acquire(self, tokens=1, blocking=False):
with self.lock:
now = time.time()
# 补充令牌
elapsed = now - self.last_update
self.tokens = min(
self.burst,
self.tokens + elapsed * self.qps
)
self.last_update = now
if self.tokens >= tokens:
self.tokens -= tokens
return True # 获取到令牌
else:
if blocking:
wait_time = (tokens - self.tokens) / self.qps
time.sleep(wait_time)
self.tokens = min(self.burst, self.tokens + wait_time * self.qps)
return True
return False # 令牌不足
# 使用
limiter = TokenBucket(qps=100, burst=200)
@app.route("/order")
def create_order():
if not limiter.acquire():
return jsonify({"error": "Too Many Requests"}), 429
# 处理订单...限流实战:Sentinel(Spring Cloud Alibaba)
java
// Sentinel:阿里的限流降级框架(比 Hystrix 更轻量)
@Configuration
public class SentinelConfig {
@Bean
public SentinelResourceHolder sentinelResourceHolder() {
return new SentinelResourceHolder();
}
}
// 使用注解:限流处理
@SentinelResource(value = "createOrder",
blockHandler = "createOrderBlockHandler",
fallback = "createOrderFallback")
public Result createOrder(OrderDTO dto) {
// 业务逻辑
return orderService.create(dto);
}
// 限流后的处理(快速失败)
public Result createOrderBlockHandler(OrderDTO dto, BlockException e) {
return Result.fail(429, "系统繁忙,请稍后再试");
}
// 降级后的处理(服务不可用时的兜底)
public Result createOrderFallback(OrderDTO dto, Throwable e) {
return Result.fail(503, "服务降级,请稍后再试");
}第3节:熔断——如何让故障不蔓延
场景:订单服务依赖库存服务,但库存服务挂了
┌─────────────────────────────────────────────────────────────┐
│ │
│ 订单服务 ──▶ 库存服务 │
│ │
│ 库存服务挂了: │
│ → 订单服务调用库存服务 → 超时 │
│ → 订单服务线程池等待 → 积压 │
│ → 订单服务越来越慢 │
│ → 订单服务也挂了! │
│ │
│ 连锁反应:一个服务挂了 → 拖垮了另一个服务 → 整个系统崩溃 │
│ │
└─────────────────────────────────────────────────────────────┘推演:熔断器的三个状态
┌─────────────────────────────────────────────────────────────┐
│ 熔断器三状态 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 状态 1:Closed(关闭) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 正常:请求通过 → 统计成功/失败 │ │
│ │ 失败率 < 阈值 → 继续关闭 │ │
│ │ 失败率 > 阈值 → 跳闸!进入 Open │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ 状态 2:Open(打开) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 熔断:所有请求直接失败(快速失败) │ │
│ │ 不调用下游服务 → 保护下游服务 │ │
│ │ 等待一段时间 → 进入 Half-Open │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ 状态 3:Half-Open(半开) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 试探:放行一个请求 │ │
│ │ 成功 → 关闭熔断器,恢复正常 │ │
│ │ 失败 → 重新打开熔断器 │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘熔断 vs 限流的区别
┌─────────────────────────────────────────────────────────────┐
│ 熔断 vs 限流的区别 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 限流:保护系统不被流量打垮 │
│ → 流量是正常的,但系统处理不过来 │
│ → 拒绝部分请求,但不是服务不可用 │
│ │
│ 熔断:保护系统不被下游故障拖垮 │
│ → 流量正常,但下游服务有问题 │
│ → 主动拒绝请求,让下游服务喘口气 │
│ │
│ 两者配合: │
│ 限流(流量控制) + 熔断(故障隔离) │
│ │
└─────────────────────────────────────────────────────────────┘第4节:API 网关——统一的流量入口
场景:多个服务,怎么统一鉴权/限流/路由?
没有网关:
用户 → 用户服务(自己鉴权)
用户 → 商品服务(自己鉴权)
用户 → 订单服务(自己鉴权)
每个服务都要写一套鉴权/限流/路由代码。
新加一个服务 → 要复制粘贴 3 份。
有网关:
用户 → API 网关(统一处理)→ 各服务网关的核心职责
┌─────────────────────────────────────────────────────────────┐
│ API 网关职责 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 路由转发 │
│ /api/users/** → 用户服务 │
│ /api/products/** → 商品服务 │
│ /api/orders/** → 订单服务 │
│ │
│ 2. 统一鉴权 │
│ → 验证 JWT Token │
│ → 验证 API Key │
│ → 验证 IP 黑名单 │
│ │
│ 3. 统一限流 │
│ → 按用户限流 │
│ → 按接口限流 │
│ → 按 IP 限流 │
│ │
│ 4. 协议转换 │
│ → HTTP → Dubbo / gRPC(内部通信) │
│ → REST → GraphQL │
│ │
│ 5. 统一日志/监控 │
│ → 请求日志 │
│ → 调用链追踪(OpenTelemetry) │
│ │
└─────────────────────────────────────────────────────────────┘Kong 网关实战配置
yaml
# Kong 配置:通过声明式 YAML 部署
services:
- name: order-service
url: http://order-service:8080
routes:
- name: order-route
paths:
- /api/orders
methods:
- GET
- POST
plugins:
# JWT 鉴权
- name: jwt
config:
key_claim_name: kid
# 限流(按用户,每分钟 100 次)
- name: rate-limiting
config:
minute: 100
policy: redis
redis_host: redis
hide_client_headers: false
# CORS
- name: cors
consumers:
- username: user_1001
jwt_secrets:
- secret: my-secret-key第5节:链路追踪——为什么问题总是难定位
场景:订单接口超时,怎么快速定位是哪个环节慢?
┌─────────────────────────────────────────────────────────────┐
│ │
│ 订单接口耗时 5 秒。 │
│ │
│ 是 MySQL 慢? │
│ 是 Redis 慢? │
│ 是库存服务慢? │
│ 是支付服务慢? │
│ │
│ 没有任何追踪工具 → 一个个排查 → 花了 2 小时 │
│ │
│ 有链路追踪 → 看到每个环节的耗时 → 1 分钟定位问题 │
│ │
└─────────────────────────────────────────────────────────────┘OpenTelemetry 链路追踪原理
┌─────────────────────────────────────────────────────────────┐
│ OpenTelemetry 链路追踪 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Trace(一次请求的完整链路): │
│ │
│ Span A(订单服务) │
│ ├── Span B(查询用户)耗时 5ms │
│ ├── Span C(查询商品)耗时 10ms │
│ ├── Span D(调用库存服务)耗时 3s ← 问题在这里! │
│ │ ├── Span D1(HTTP) │
│ │ └── Span D2(DB 查询) │
│ └── Span E(创建订单)耗时 8ms │
│ │
│ Trace ID:贯穿整个请求的唯一 ID │
│ Parent ID:谁调用了我 │
│ Span ID:我自己的唯一 ID │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ HTTP Header 传递 Trace ID: │ │
│ │ │ │
│ │ Header: traceparent: 00-{TraceID}-{SpanID}-01 │ │
│ │ │ │
│ │ 网关生成 TraceID → 传递给下游 → 所有服务共享 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘升华:系统保护的分层策略
┌─────────────────────────────────────────────────────────────┐
│ 系统保护分层策略 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 第一层:限流(入口控制) │
│ → 网关层:总 QPS 限制 │
│ │
│ 第二层:熔断(故障隔离) │
│ → 服务间调用:当下游故障时快速失败 │
│ │
│ 第三层:降级(兜底策略) │
│ → 服务不可用时,返回默认响应或缓存数据 │
│ │
│ 第四层:隔离(资源隔离) │
│ → 线程池隔离:不同服务用不同线程池 │
│ → 舱壁模式:一个服务耗尽资源不影响其他 │
│ │
│ 一句话总结: │
│ 限流是"不要来太多",熔断是"它病了别找它", │
│ 降级是"它病了给个替代方案",隔离是"别让它们互相传染"。 │
│ │
└─────────────────────────────────────────────────────────────┘"AI 可查 vs 必须理解"清单
AI 可查:
✅ Sentinel / Hystrix / Resilience4j 的具体配置参数
✅ Kong / APISIX / Spring Cloud Gateway 的路由配置语法
✅ OpenTelemetry 的埋点代码
必须理解:
🔴 令牌桶的 burst 参数含义(突发流量处理能力)
🔴 熔断器三状态的转换条件
🔴 限流和熔断的使用场景区别
🔴 为什么需要链路追踪(快速定位问题)学习状态:🟡 开始学习