1. 限流\流控的本质
限流的本质是为了保护系统,防止过载或者针对恶意流量进行拦截,确保核心业务的可用性和稳定性。
2. 限流机制类型
2.1. 本地限流 (Local Rate Limiting)
单节点限流是指在单个服务实例内部进行的限流,不涉及跨服务实例的协调。它通常用于保护单个服务实例的及其或下游依赖资源。
2.2. 分布式限流
分布式限流是指在多个服务实例之间共享限流状态,以实现全局的限流。
基于 Redis 的分布式限流
设计:利用 Redis 的原子操作(如 INCR、SETNX、EXPIRE)来实现计数器、令牌桶或滑动窗口
- 原理: 利用 Redis 的原子操作(如 INCR、SETNX、EXPIRE)来实现计数器、令牌桶或滑动窗口。
- 优点: Redis 性能高,支持集群,易于部署和维护。
- 实现方式:
- 计数器: 使用 INCR 命令对某个 key 进行计数,并设置 EXPIRE。
- 滑动窗口: 使用 Redis 的 ZSET(有序集合)存储请求的时间戳,通过 ZRANGEBYSCORE 和 ZREM 来维护窗口内的请求。
- 令牌桶: 模拟令牌桶的逻辑,每次请求尝试从 Redis 中获取令牌(例如,一个 key 代表当前令牌数,通过 DECR 操作)。
基于 Redis 的分布式限流代码实现
|
|
基于消息队列的分布式限流 (流量削峰)
典型场景,用户秒杀下单,将下单数据(用户、商品、数量等关键数据)推送到 MQ,再异步消费 MQ 数据处理复杂的下单业务
- 原理: 将请求放入消息队列,后端消费者以恒定速率从队列中取出并处理。
- 优点: 流量削峰填谷,保护后端系统。
- 缺点: 引入异步处理,增加系统复杂性,对实时性要求高的场景不适用。
- Go 实现: 使用 Kafka、RabbitMQ、NATS 等消息队列客户端。
基于 ZooKeeper/Etcd 的分布式限流
实际还是基于分布式选举获取一个锁,基于锁的数量判断师傅超额,还是有些复杂了
- 原理: 利用 ZooKeeper/Etcd 的分布式锁或顺序节点特性来实现。例如,每个请求尝试获取一个分布式锁,或者在某个路径下创建临时顺序节点,通过节点数量来判断是否超限。
- 优点: 强一致性,适用于对一致性要求较高的场景。
- 缺点: 性能相对 Redis 低,延迟较高,不适合高并发场景。
- Go 实现: 使用 go.etcd.io/etcd/client/v3 或 github.com/go-zookeeper/zk。
本地+分布式两者结合
针对非常大流量网站,之前的业务中有做过本地+分布式限流的结合
- 节点本地限流拦截一道
- 再走分布式限流拦截一道
3. 限流策略实现
3.1. 令牌桶 (Token Bucket)
令牌入桶,桶口令牌消耗随请求流量可大可小,消耗光了则阻塞等待新的入桶令牌
- 原理: 桶中以恒定速率放入令牌,请求需要消耗桶令牌才能被处理,如果桶中没有令牌,请求将被拒绝或排队
- 优点:桶支持囤积一定令牌,所以允许一定程度的突发流量,平滑请求速率
- Go 实现:
- 使用
time.Ticker
或time.After
定时生成令牌。 - 使用
sync.Mutex
或chan
保护令牌桶的并发访问。
- 使用
令牌桶代码示例
|
|
3.2. 漏桶 (Leaky Bucket)
令牌入桶,桶口大小固定,每间隔 T 时间间隔从桶口漏一个令牌,请求消耗令牌
- 原理: 请求以恒定速率从桶中流出,如果桶满,新请求将被拒绝
- 优点: 强制输出速率恒定,对下游系统有很好的保护作用
- Go 实现: 通常使用带缓冲的 chan 模拟漏桶,或者结合 time.Ticker 控制出队速率
漏桶策略代码示例
|
|
3.3. 计数器 (Fixed Window Counter)
- 原理: 在一个固定时间窗口内,统计请求数量,达到阈值则拒绝。
- 优点: 实现简单。
- 缺点: 存在“临界问题”,即在窗口边缘可能允许两倍的流量。
- Go 实现: Redis 的 ADD 操作,超过阈值拒绝,下个 T 清空计数器,重新累加
令牌和计数器区别
- 计数器有阈值设置,累加达到阈值后,需要在下个 T 时间后置空,可能有两倍流量问题;
- 令牌可以有初始容量,消耗了令牌后,还可以在每间隔 T 时间后有流入的令牌(稍微缓和一些,不会有两倍流量问题) – 细水长流
3.4. 滑动窗口计数器 (Sliding Window Counter)
更加平滑的限流
- 原理: 将时间窗口划分为更小的子窗口,每个子窗口维护一个计数器。通过滑动窗口,可以更平滑地统计请求数量,避免固定窗口的临界问题。
- 优点: 解决了固定窗口的临界问题,更精确。
- Go 实现: 需要维护一个队列或环形缓冲区来存储子窗口的计数。
4. 开源解决方案
4.1. 一些限流开源库
alibaba/sentinel-golang
的特性功能比较多,go-redis/redis_rate
非常简单
golang.org/x/time/rate
: Go 官方提供的限流库,实现了令牌桶算法,功能强大且易于使用uber-go/ratelimit
: Uber 开源的 Go 语言限流库,提供了漏桶速率限制算法的 Golang 实现go-redis/redis_rate
: 基于 go-redis 的 Redis 分布式限流库,提供了令牌桶和滑动窗口的实现alibaba/sentinel-golang
: 阿里巴巴开源的流量控制组件 Sentinel 的 Go 语言实现。它提供了熔断、降级、限流等多种流量治理能力,支持分布式限流(通过集成外部存储如 Redis)。- 特点: 规则动态配置、实时监控、多种限流模式(QPS、并发线程数)、流控效果(快速失败、排队等待)。
- 分布式限流实现: Sentinel 本身是单机限流,但可以通过扩展数据源(如 Redis)来实现分布式限流。例如,将限流规则和计数器存储在 Redis 中,各个 Sentinel 实例从 Redis 获取并更新状态
uber-go/ratelimit 示例
|
|
go-redis/redis_rate 示例
非常简洁
|
|
5. API 网关集中式限流
将所有服务的限流逻辑集中到一个独立的限流服务中。所有请求在进入业务服务之前,都需要先经过限流服务进行校验,常见就是接入层网关、业务网关等
常见方式有:
- API Gateway/Sidecar 模式
5.1. API Gateway/Sidecar 模式
- 原理: 在 API Gateway 层或通过 Sidecar 代理模式,拦截所有请求,并在该层进行统一的限流判断。
- 优点:
- 统一管理: 所有限流规则集中管理,易于配置和维护。
- 透明性: 对业务服务无侵入。
- 全局视角: 可以实现更复杂的全局限流策略(如用户维度、IP 维度、API 维度)。
- Go 实现:
- API Gateway: 使用 Go 语言开发一个高性能的 API Gateway,集成限流逻辑。例如,基于 Gin、Echo 等 Web 框架,结合上述分布式限流方案。
- Envoy Proxy + Go Filter: Envoy 是一个高性能的 L7 代理,支持通过 Wasm 或外部 GRPC 服务扩展其功能。可以编写 Go 语言的 GRPC 服务作为 Envoy 的外部授权服务或限流服务,Envoy 将请求转发给该服务进行限流判断。
- Kong/APISIX + Go Plugin: Kong 和 APISIX 是流行的 API Gateway,支持通过插件扩展功能。可以编写 Go 语言插件来实现自定义限流逻辑。
- 自研限流服务: 如果业务需求非常特殊,可以考虑使用 Go 语言自研一个高性能的限流服务,对外提供 GRPC 或 HTTP 接口供其他服务调用,该服务内部可以采用 Redis 等作为状态存储
6. 常见限流策略 (Rate Limiting Strategies)
6.1. 基于 QPS (Queries Per Second) / TPS (Transactions Per Second)
最常见的限流方式,限制单位时间内允许的请求数量,适用场景: 大多数 API 接口、服务入口。
6.2. 基于并发连接数/线程数
限制同时处理的请求数量,适用场景: 数据库连接池、RPC 调用下游服务、CPU 密集型任务。
Go 实现: 使用带缓冲的 chan
作为信号量,或者 semaphore
库。
使用 semaphore 示例
PS. 实际和带缓冲通道处理起来很类似~
|
|
6.3. 其他限流策略
- 基于用户 ID/IP 地址,例如登录场景,根据请求的 User-ID 或 IP 作为限流 key,防止恶意攻击或滥用
- 基于 API 接口/资源路径,例如一些资源接口,需要根据请求的 URL 路径或方法作为限流 key,对不同的 API 接口设置不同的限流阈值。
- 基于系统负载/资源利用率(类似动态调节): 需要实时监控系统指标,并根据指标动态更新限流规则。这通常需要结合监控系统(如 Prometheus)和配置中心
- 多维度组合限流: 例如之前提到的单节点本地限流+分布式限流结合
6.4. 限流后的处理方式 (Flow Control Effects)
当请求被限流时,如何处理这些请求也很重要,包括:
- 快速失败 (Fail Fast): 直接拒绝请求,返回错误码(如 HTTP 429 Too Many Requests),简单直接,快速释放资源,适用大多数同步 API
- 排队等待 (Queueing): 将请求放入队列,等待有可用资源时再处理,例如下单处理
- 优点: 提高请求成功率,平滑流量
- 缺点: 增加请求延迟,需要考虑队列溢出和超时
- Go 实现: 使用带缓冲的 chan 或自定义队列
- 降级 (Degradation):当流量过大时,返回一个默认值、缓存数据或简化版的服务响应,而不是拒绝请求
- 适用场景: 非核心功能、推荐系统、搜索结果等
- 优点: 保证核心功能可用性,提升用户体验
- 重试 (Retry):注意重试可能引发雪崩
- 客户端收到限流错误后,在一定延迟后进行重试
- 优点: 提高请求成功率。
- 缺点: 可能加剧系统压力,需要实现指数退避等策略。
7. 一个业务限流的实例
典型 Case: 在用户登录场景,需要对登录的用户 UID 进行限频,防止用户恶意攻击
几个关键点:
- 使用 Redis+Lua 脚本确保了原子操作
- 借助Redis的SZET有序集合实现,对应的Key表示限流实体(比如某个用户登录),针对SET集合中每个元素设置+T的过期时间,统计未过期集合中元素个数与阈值比较确定是否被限流了
|
|
8. 总结与最佳实践
- 选择合适的算法: 令牌桶和滑动窗口是分布式限流的常用选择,它们在平滑性和精确性上表现良好
- 选择合适的存储: Redis 是分布式限流的首选,其高性能和原子操作非常适合
- Lua 脚本原子性: 在 Redis 中实现复杂限流逻辑时,务必使用 Lua 脚本来保证操作的原子性,避免竞态条件。
- 可配置性: 限流规则应该可配置,最好支持动态更新,无需重启服务。
- 监控与告警: 实时监控限流效果,包括被拒绝的请求数量、当前流量等,并设置告警。
- 灰度发布: 新的限流策略上线前,进行灰度发布,逐步验证效果。
- 与熔断、降级结合: 限流是流量控制的第一道防线,但它不能解决所有问题。结合熔断(防止雪崩效应)和降级(保证核心功能可用)可以构建更健壮的系统。
- 客户端适配: 告知客户端限流策略,并建议客户端实现重试、指数退避等机制。
- 压测验证: 在生产环境上线前,进行充分的压力测试,验证限流效果和系统稳定性。