限流与背压控制
前言
双十一零点,几亿用户同时涌入——服务器扛得住吗? 任何系统都有处理能力的上限。当请求量超过系统承载能力时,如果不加控制,结果就是所有人都用不了。限流和背压就是保护系统不被"压垮"的两道防线。
这篇文章会带你学什么?
学完这章后,你将获得:
- 限流必要性:理解为什么需要主动拒绝部分请求来保护系统
- 限流算法:掌握令牌桶、漏桶、滑动窗口三种核心算法的原理和差异
- 背压机制:理解当上游速度超过下游时的处理策略
- 多层限流:了解从客户端到网关到服务的多层限流架构
- 实战能力:知道在什么场景下选择什么限流策略
| 章节 | 内容 | 核心概念 |
|---|---|---|
| 第 1 章 | 为什么需要限流 | 雪崩效应、服务保护 |
| 第 2 章 | 限流算法 | 令牌桶、漏桶、滑动窗口 |
| 第 3 章 | 背压控制 | 缓冲区、丢弃策略、弹性扩容 |
| 第 4 章 | 多层限流架构 | 客户端、网关、服务端 |
| 第 5 章 | 实战与选型 | Nginx、Redis、Sentinel |
0. 全景图:为什么要"拒绝"用户?
这听起来很反直觉——我们不是应该服务好每一个用户吗?但现实是:不拒绝一部分请求,所有请求都会失败。
想象一个只能坐 100 人的餐厅,突然涌进来 1000 人。如果不限流,结果不是 1000 人都能吃上饭,而是厨房崩溃、服务员瘫痪,1000 人谁都吃不上。正确的做法是在门口排队限流,让 100 人先进去,其余人等候。
限流的核心目标
- 保护系统:防止过载导致服务完全不可用
- 公平分配:确保已接受的请求能正常处理
- 优雅降级:被限流的请求收到明确的 429 状态码,而不是超时或 500 错误
1. 限流算法:三种经典方案
限流的核心问题是:在单位时间内,最多允许多少个请求通过? 不同的算法在精确度、突发流量处理、实现复杂度上各有取舍。
限流算法对比
选择算法,点击"发送请求"观察效果
通过0
拒绝0
剩余令牌5
令牌桶
以固定速率往桶里放令牌,每个请求消耗一个令牌。桶满时多余令牌丢弃。允许一定程度的突发流量(桶里有存量令牌时)。
| 算法 | 原理 | 突发流量 | 精确度 | 实现复杂度 |
|---|---|---|---|---|
| 令牌桶 | 固定速率放令牌,请求消耗令牌 | 允许(桶中有存量) | 高 | 中 |
| 漏桶 | 请求排队,固定速率处理 | 不允许(完全平滑) | 高 | 中 |
| 滑动窗口 | 统计窗口内请求数 | 部分允许 | 较高 | 低 |
| 固定窗口 | 按时间窗口计数 | 边界处可能突发 | 低 | 最低 |
选哪个算法?
- API 限流:令牌桶最常用,允许合理的突发流量
- 流量整形:漏桶适合需要恒定输出速率的场景
- 简单计数:滑动窗口实现简单,适合大多数 Web 应用
2. 背压控制:当上游比下游快
限流解决的是"外部请求太多"的问题,而背压(Backpressure)解决的是"内部组件速度不匹配"的问题。
当生产者产生数据的速度持续超过消费者处理数据的速度时,中间的缓冲区会不断膨胀,最终导致内存溢出或数据丢失。背压机制就是让消费者能够"反向通知"生产者减速。
背压控制 (Backpressure)
当生产速度超过消费速度时会发生什么?
生产速率:6/s
消费速率:3/s
生产者
6/s
缓冲区 (0/20)
正常运行
消费者
3/s
背压处理策略:
丢弃策略
缓冲区满时直接丢弃新数据
如:日志采集、实时监控指标
阻塞策略
缓冲区满时让生产者等待
如:Go channel、Java BlockingQueue
采样策略
只处理部分数据,跳过其余
如:高频传感器数据降采样
弹性扩容
动态增加消费者数量
如:K8s HPA 自动扩缩容
背压的四种策略
- 丢弃(Drop):缓冲区满时丢弃新数据或旧数据,适合实时性要求高但允许丢失的场景
- 阻塞(Block):让生产者暂停,等消费者处理完再继续,适合数据不能丢失的场景
- 采样(Sample):只处理部分数据,适合高频数据流
- 弹性扩容(Scale):动态增加消费者数量,适合云原生环境
3. 多层限流架构
生产环境中,限流不是在某一个点做就够了,而是需要多层防护,每一层解决不同粒度的问题。
| 层级 | 位置 | 限流粒度 | 工具 |
|---|---|---|---|
| 客户端 | 前端/App | 按钮防抖、请求节流 | lodash.throttle、debounce |
| CDN/WAF | 边缘节点 | IP 级别、地域级别 | Cloudflare Rate Limiting |
| API 网关 | 入口网关 | 路由级别、用户级别 | Nginx limit_req、Kong |
| 服务端 | 应用内部 | 接口级别、资源级别 | Sentinel、Resilience4j |
| 数据库 | 存储层 | 连接数、QPS | 连接池配置、慢查询熔断 |
限流的 HTTP 规范
被限流的请求应该返回 429 Too Many Requests 状态码,并在响应头中包含:
Retry-After: 建议客户端多久后重试(秒数或日期)X-RateLimit-Limit: 限流上限X-RateLimit-Remaining: 剩余配额X-RateLimit-Reset: 配额重置时间
4. 实战选型
| 场景 | 推荐方案 | 说明 |
|---|---|---|
| Nginx 入口限流 | limit_req_zone | 基于漏桶算法,配置简单 |
| 分布式限流 | Redis + Lua 脚本 | 令牌桶或滑动窗口,多实例共享计数 |
| Java 微服务 | Sentinel / Resilience4j | 支持熔断、降级、热点限流 |
| Node.js API | express-rate-limit | 简单易用,支持 Redis 存储 |
| Go 服务 | golang.org/x/time/rate | 标准库令牌桶实现 |
总结
限流和背压是保护系统稳定性的两道关键防线。限流控制外部流量的涌入速度,背压协调内部组件的处理速度。
回顾本章的关键要点:
- 限流的必要性:不拒绝部分请求,所有请求都会失败
- 三种核心算法:令牌桶(允许突发)、漏桶(完全平滑)、滑动窗口(简单精确)
- 背压机制:丢弃、阻塞、采样、扩容四种策略
- 多层防护:从客户端到数据库,每层解决不同粒度的问题
- 429 规范:被限流时返回标准状态码和限流头信息
延伸阅读
- Stripe 的限流实践 - 支付系统的限流设计
- Nginx limit_req 文档 - Nginx 限流模块
- Alibaba Sentinel - 面向分布式服务的流量控制组件
- Resilience4j - Java 轻量级容错库
- Token Bucket 算法详解 - 令牌桶算法的数学原理
