这周五在清明假期内,提前更新文章
很多业务会有限流的场景,比如活动秒杀、社区搜索查询、社区留言功能;保护自身系统和下游系统不被巨型流量冲垮等。
在计算机网络中,限流就是控制网络接口发送或接收请求的速率,它可防止DoS攻击和限制Web爬虫。
限流,也称流量控制。是指系统在面临高并发,或者大流量请求的情况下,限制新的请求对系统的访问,从而保证系统的稳定性。限流会导致部分用户请求处理不及时或者被拒,这就影响了用户体验。所以一般需要在系统稳定和用户体验之间平衡一下。
一、场景
Go语言的限流场景非常广泛,适用于各种需要控制访问速率的场景。以下是一些常见的限流场景:
- API服务限流: 假设你正在开发一个API服务,你可以使用限流来限制对API端点的访问速率,以防止恶意攻击或过度使用API。例如,你可以限制每个IP地址的请求速率,或者根据API密钥对用户进行限流。
- Web爬虫控制: 如果你正在开发一个网络爬虫,你可能需要限制爬取网站的速率,以避免对目标网站造成过大的负担。你可以使用限流来控制爬取速率,以免被目标网站封禁IP地址。
- 消息队列消费者: 在处理消息队列时,你可能需要控制消费者的处理速率,以防止过度消费消息。你可以使用限流来控制消费者从消息队列中拉取消息的速率,确保系统稳定运行。
- 数据库访问限流: 在高并发的情况下,数据库可能成为瓶颈。你可以使用限流来控制对数据库的并发访问,以避免数据库超载或数据库连接池耗尽。
- RPC调用限流: 如果你的应用程序依赖于其他服务的RPC调用,你可能需要限制对RPC服务的调用速率,以避免对目标服务造成过大的负荷或超出服务提供商的配额。
- 任务调度: 控制任务调度器并发执行任务的速率,以避免过度消耗系统资源或超出服务协议。
- 文件IO: 控制对文件系统的读写操作,以避免过度消耗系统资源。
二、常用算法
限流算法用于控制系统的流量,防止系统被过载。
- 令牌桶算法(Token Bucket Algorithm): 令牌桶算法是一种基于令牌桶的限流算法。在这个算法中,有一个固定容量的桶,以固定的速率往桶里添加令牌。每当有请求到来时,必须从桶中获取一个令牌,如果桶中没有足够的令牌,则拒绝该请求或等待直到有足够的令牌为止。
- 漏桶算法(Leaky Bucket Algorithm): 漏桶算法是一种基于漏桶的限流算法。在这个算法中,有一个固定容量的漏桶,以固定的速率漏水。每当有请求到来时,将请求放入漏桶中,如果漏桶已满,则拒绝该请求;否则,请求在漏桶中等待一段时间后被处理。
- 计数器算法(Counter Algorithm)也叫固定窗口算法: 计数器算法是一种简单的限流算法。在这个算法中,统计一定时间窗口内的请求次数,如果请求次数超过了设定的阈值,则拒绝后续的请求。
- 滑动窗口算法(Sliding Window Algorithm): 滑动窗口算法是一种动态调整窗口大小的限流算法。在这个算法中,统计一定时间窗口内的请求次数,如果请求次数超过了设定的阈值,则拒绝后续的请求。与计数器算法不同的是,滑动窗口算法可以动态调整时间窗口的大小,适应不同的流量变化。
以下看下各个算法的图及示例:
2.1、令牌桶算法
package main
import (
"time"
"golang.org/x/time/rate"
)
func main() {
// 创建一个令牌桶,每秒产生3个令牌
limiter := rate.NewLimiter(3, 1)
// 模拟10次请求
for i := 0; i < 10; i++ {
// 获取一个令牌,如果没有可用的令牌则阻塞等待
limiter.WaitN(time.Now(), 1)
// 处理请求
handleRequest()
}
}
func handleRequest() {
// 模拟处理请求
println("Handling request...")
}
2.2、漏桶算法
package main
import (
"time"
)
func main() {
// 创建一个容量为3的漏桶,每秒漏水1个
limiter := NewLeakyBucket(3, 1)
// 模拟10次请求
for i := 0; i < 10; i++ {
// 获取一个漏桶令牌,如果漏桶已满则阻塞等待
limiter.Wait()
// 处理请求
handleRequest()
}
}
func handleRequest() {
// 模拟处理请求
println("Handling request...")
}
// 漏桶结构体
type LeakyBucket struct {
capacity int // 漏桶容量
rate time.Duration // 漏桶速率
lastLeak time.Time // 上一次漏水时间
dripAmount int // 漏水数量
}
// 创建一个新的漏桶
func NewLeakyBucket(capacity int, ratePerSecond int) *LeakyBucket {
return &LeakyBucket{
capacity: capacity,
rate: time.Second / time.Duration(ratePerSecond),
lastLeak: time.Now(),
dripAmount: 0,
}
}
// 获取一个漏桶令牌,如果漏桶已满则阻塞等待
func (lb *LeakyBucket) Wait() {
now := time.Now()
// 计算自上一次漏水以来应该漏掉的数量
lb.dripAmount += int(now.Sub(lb.lastLeak) / lb.rate)
// 如果漏桶溢满,等待一段时间
if lb.dripAmount > lb.capacity {
time.Sleep(lb.rate)
}
// 更新上一次漏水时间
lb.lastLeak = now
// 漏水一个令牌
lb.dripAmount--
}
2.3、计数器算法
package main
import (
"time"
)
func main() {
// 创建一个计数器限流器,每秒最多处理3个请求
limiter := NewCounterLimiter(3, time.Second)
// 模拟10次请求
for i := 0; i < 10; i++ {
// 判断是否允许进行请求
if limiter.Allow() {
// 处理请求
handleRequest()
} else {
// 请求被限流,打印提示信息
println("Request limited, please try again later.")
}
}
}
func handleRequest() {
// 模拟处理请求
println("Handling request...")
}
// 计数器限流器结构体
type CounterLimiter struct {
counter int // 当前计数器值
limit int // 计数器限制
interval time.Duration // 时间间隔
lastUpdate time.Time // 上次更新时间
}
// 创建一个新的计数器限流器
func NewCounterLimiter(limit int, interval time.Duration) *CounterLimiter {
return &CounterLimiter{
counter: 0,
limit: limit,
interval: interval,
lastUpdate: time.Now(),
}
}
// 判断是否允许进行请求
func (limiter *CounterLimiter) Allow() bool {
// 更新计数器值
limiter.updateCounter()
// 判断计数器值是否超过限制
if limiter.counter >= limiter.limit {
return false
}
// 计数器值加1,表示处理一个请求
limiter.counter++
return true
}
// 更新计数器值
func (limiter *CounterLimiter) updateCounter() {
// 计算距离上次更新的时间间隔
interval := time.Since(limiter.lastUpdate)
// 如果时间间隔大于限定的间隔,则重置计数器
if interval >= limiter.interval {
limiter.counter = 0
limiter.lastUpdate = time.Now()
}
}
2.4、滑动窗口算法
package main
import (
"time"
)
func main() {
// 初始化一个滑动窗口限流器,窗口大小为1秒,允许的请求数为3
limiter := NewSlidingWindowLimiter(3, 1*time.Second)
// 模拟10次请求
for i := 0; i < 10; i++ {
// 判断是否允许进行请求,如果超过限制则等待
for !limiter.Allow() {
time.Sleep(time.Millisecond * 100)
}
// 处理请求
handleRequest()
}
}
func handleRequest() {
// 模拟处理请求
println("Handling request...")
}
// 滑动窗口限流器结构体
type SlidingWindowLimiter struct {
requests []time.Time // 存储每个请求的时间戳
limit int // 允许的请求数
interval time.Duration // 时间窗口大小
}
// 创建一个新的滑动窗口限流器
func NewSlidingWindowLimiter(limit int, interval time.Duration) *SlidingWindowLimiter {
return &SlidingWindowLimiter{
requests: make([]time.Time, 0),
limit: limit,
interval: interval,
}
}
// 判断是否允许进行请求
func (limiter *SlidingWindowLimiter) Allow() bool {
// 移除时间窗口外的请求
for len(limiter.requests) > 0 && time.Since(limiter.requests[0]) > limiter.interval {
limiter.requests = limiter.requests[1:]
}
// 如果请求数超过限制,则拒绝请求
if len(limiter.requests) >= limiter.limit {
return false
}
// 记录当前请求时间
limiter.requests = append(limiter.requests, time.Now())
return true
}
在Go语言中,可以使用一些库来实现限流,例如:
Go 在 x 标准库,即 golang.org/x/time/rate 里自带了一个限流器,这个限流器是基于令牌桶算法(token bucket)实现的。
- github.com/juju/ratelimit:提供基于令牌桶算法的限流实现。
- github.com/golang/groupcache:提供的并发访问控制工具可以用于限制对共享资源的并发访问。
- github.com/uber-go/ratelimit:提供了令牌桶和漏桶算法的实现,支持更复杂的限流策略。
三、优缺点对比
这四种限流算法各有优缺点,简要的比较:
- 令牌桶算法:
- 优点:
- 令牌桶算法可以在令牌足够的情况下,突发地处理一定数量的请求。
- 算法简单,容易实现。
- 缺点:
- 实现相对复杂,需要维护令牌桶的状态。
- 无法应对突发流量,因为在令牌桶为空时无法处理任何请求。
- 优点:
- 漏桶算法:
- 优点:
- 漏桶算法可以对突发流量进行平滑处理,稳定输出。
- 算法简单,容易实现。
- 缺点:
- 无法应对突发流量,因为漏桶已满时无法处理任何请求。
- 对于突发流量的应对能力较弱。
- 优点:
- 计数器算法(固定窗口算法):
- 优点:
- 计数器算法简单直观,易于实现。
- 可以精确控制单位时间内的请求量。
- 缺点:
- 对于突发流量的应对能力较弱,无法动态调整限流窗口。
- 无法应对突发流量,因为在计数器达到限制后无法处理任何请求。
- 优点:
- 滑动窗口算法:
- 优点:
- 滑动窗口算法可以动态调整限流窗口的大小,适应不同的流量情况。
- 对于突发流量的应对能力较强。
- 缺点:
- 算法相对复杂,实现较为困难。
- 需要维护窗口中的请求记录,可能会消耗较多的内存。
- 优点:
四、参考
- x_rate
- go-zero_limiter示例+压测
- (微服务)服务治理:几种开源限流算法库/应用软件介绍和使用
- 令牌桶算法限流
- 常见限流算法