1. 限流的目的
限流(Rate Limiting)的目的是为了控制系统或应用程序在单位时间内处理请求的数量,防止过载、保护资源、保证服务质量。具体来说,限流的主要目的包括以下几个方面:
1. 防止系统过载
通过限制单位时间内的请求数量,可以防止系统被过多的请求压垮,从而保持系统的稳定性和响应速度。
2. 提高服务质量
限流可以确保所有用户都能获得相对均衡的服务质量,避免因某些用户的过度请求导致其他用户的请求得不到及时处理。
3. 保护关键资源
某些系统资源(如数据库连接、文件句柄、API调用等)是有限的。通过限流,可以保护这些资源不被耗尽,确保系统的正常运行。
4. 防止滥用和恶意攻击
限流可以有效地防止DoS(拒绝服务)攻击和暴力破解等恶意行为,保护系统的安全性和可用性。5. 避免“雪崩效应”
在高并发情况下,如果不加限流,某个服务可能因为过载而崩溃,进而导致整个系统的级联故障。限流可以有效地避免这种“雪崩效应”。
6. 合理利用带宽
在网络传输中,限流可以帮助合理分配带宽资源,避免某些用户或操作占用过多带宽,影响整体网络性能。
2. 限流的算法
2.1 固定窗口计数器算法
package fixed_window_counter
import (
"sync"
"time"
)
// FixedWindowCounter 固定窗口计数器方法
// mu 用于同步访问,保证并发安全。
// count 记录当前时间窗口内的请求数量。
// limit 是时间窗口内允许的最大请求数量。
// windowStart 记录当前时间窗口的开始时间。
// window 是时间窗口的持续时间。
type FixedWindowCounter struct {
mu sync.Mutex
count int
limit int
windowStart time.Time
window time.Duration
}
// NewFixedWindowCounter 初始化固定窗口计数器
// limit 参数定义了每个时间窗口内允许的请求数量。
// duration 参数定义了时间窗口的大小。
func NewFixedWindowCounter(limit int, window time.Duration) *FixedWindowCounter {
return &FixedWindowCounter{
limit: limit,
window: window,
windowStart: time.Now(),
}
}
// Allow 判断是否允许访问
func (f *FixedWindowCounter) Allow() bool {
f.mu.Lock()
defer f.mu.Unlock()
now := time.Now()
if time.Since(f.windowStart) > f.window {
f.windowStart = now
f.count = 0
}
if f.count < f.limit {
f.count++
return true
}
return false
}
package main
import (
"awesomeProject1/current_limit/fixed_window_counter"
"fmt"
"time"
)
// 程序的入口点
func main() {
limiter := fixed_window_counter.NewFixedWindowCounter(10, time.Minute)
// 模拟15个请求,观察限流效果。
for i := 0; i < 2; i++ {
if limiter.Allow() {
fmt.Println("Request", i+1, "allowed")
} else {
fmt.Println("Request", i+1, "rejected")
}
}
time.Sleep(time.Second * 60)
for i := 0; i < 12; i++ {
if limiter.Allow() {
fmt.Println("Request", i+1, "allowed")
} else {
fmt.Println("Request", i+1, "rejected")
}
}
}
2.1.1. 工作原理
- 初始化:
-
NewFixedWindowCounter
函数用于初始化一个新的固定窗口计数器,接受两个参数:请求限制(limit
)和窗口大小(window
)。 -
初始化时,计数器的初始请求数量(
count
)为0,窗口的起始时间(windowStart
)为当前时间
-
-
请求判定:
-
Allow
方法判断是否允许当前请求通过。 -
首先,通过
mu.Lock()
和mu.Unlock()
来确保线程安全。 -
然后,检查当前时间与窗口起始时间的差值。如果超过了预设的窗口大小(
window
),则重置窗口起始时间和计数器。 -
接着,检查当前计数器的值是否小于限制值(
limit
)。如果是,则增加计数器并返回true
,表示允许当前请求通过;否则返回false
,表示拒绝请求。
-
2.1.2. 优缺点
-
优点
-
简单易实现:固定窗口计数器的算法非常简单,容易理解和实现。
-
低开销:每个请求只需要执行简单的计数和时间比较操作,开销较低。
-
线程安全:通过
sync.Mutex
确保了多线程环境下的线程安全
-
-
缺点
-
突发流量处理不佳:在窗口边界处可能会出现突发流量。例如,在窗口结束前的最后一秒和新窗口开始的第一秒内,可以允许接近两倍于限制值的请求量。
-
时间不够精细:固定窗口无法处理更加细粒度的时间控制,如每秒100次请求的限制。
-
2.1.3 改进方向
-
滑动窗口计数器:滑动窗口计数器通过更精细地划分时间窗口,能够更好地平滑流量,解决固定窗口的突发流量问题。
-
漏桶算法:漏桶算法能够更好地平滑请求速率,防止突发流量。
-
令牌桶算法:令牌桶算法更复杂,但能够更加灵活地控制请求速率,适用于更多场景。
总的来说,固定窗口计数器算法适用于简单的限流场景,但在需要处理突发流量或更精细的时间控制时,可能需要考虑其他更复杂的限流算法。
2.2 滑动窗口算法
package sliding_window_limiter
import (
"sync"
"time"
)
// SlidingWindowLimiter 滑动窗口限流器 // 实现每interval限流limit个请求
type SlidingWindowLimiter struct {
mu sync.Mutex
counts []int // 每个时间片的请求数
limit int // 每个时间片的限流大小,如果不同,可以设置为一个数组
windowStart time.Time
windowDuration time.Duration
interval time.Duration // 每个时间片的间隔
}
func (s *SlidingWindowLimiter) GetCounts() []int {
return s.counts
}
// NewSlidingWindowLimiter 初始化滑动窗口限流器
func NewSlidingWindowLimiter(limit int, windowDuration time.Duration, interval time.Duration) *SlidingWindowLimiter {
return &SlidingWindowLimiter{
counts: make([]int, int(windowDuration/interval)),
limit: limit,
windowDuration: windowDuration,
interval: interval,
windowStart: time.Now(),
}
}
// Allow 判断是否允许访问
func (s *SlidingWindowLimiter) Allow() bool {
s.mu.Lock()
defer s.mu.Unlock()
// 判断是否需要移动窗口
if time.Since(s.windowStart) > s.windowDuration {
s.slideWindow()
}
now := time.Now()
index := (int(now.UnixNano()-s.windowStart.UnixNano()) / int(s.interval.Nanoseconds())) % len(s.counts)
if s.counts[index] < s.limit {
s.counts[index]++
return true
}
return true
}
// 滑动窗口
func (s *SlidingWindowLimiter) slideWindow() {
copy(s.counts, s.counts[1:])
s.counts[len(s.counts)-1] = 0
s.windowStart = time.Now()
}
// main 函数是程序的入口点。
func main() {
limiter := sliding_window_limiter.NewSlidingWindowLimiter(1, time.Second, 10*time.Millisecond)
for i := 0; i < 10000000; i++ {
if limiter.Allow() {
fmt.Println(limiter.GetCounts(), len(limiter.GetCounts()))
fmt.Println("Request", i+1, "allowed")
} else {
fmt.Println("Request", i+1, "rejected")
}
}
}
2.2.1. 工作原理
-
初始化:
-
NewSlidingWindowLimiter
函数初始化滑动窗口限流器,接受三个参数:每个时间片的请求限制(limit
)、窗口的总时长(windowDuration
)和每个时间片的间隔(interval
)。 -
初始化时,计数器数组
counts
的长度为windowDuration/interval
,表示整个窗口被分割成的时间片数量。 -
窗口的起始时间
windowStart
设为当前时间。
-
-
请求判定:
-
Allow
方法判断是否允许当前请求通过。 -
通过
mu.Lock()
和mu.Unlock()
确保线程安全。 -
判断当前时间与窗口起始时间的差值是否超过了窗口总时长
windowDuration
,如果超过了则调用slideWindow
方法移动窗口。 -
计算当前时间所在的时间片索引
index
,并检查该时间片的请求数是否小于限制值limit
。如果是,则增加该时间片的请求数并返回true
,否则返回false
。
-
-
滑动窗口:
-
slideWindow
方法移动窗口,将counts
数组中的数据向前移动一位,并将最后一个时间片的值设为0。 -
更新窗口的起始时间
windowStart
为当前时间。
-
2.1.2. 优缺点
优点
-
平滑流量控制:滑动窗口限流器能够更平滑地控制请求流量,减少突发流量的影响。
-
高效:通过数组和索引计算来记录和限制请求数量,时间复杂度较低。
-
精细控制:可以通过调整
interval
来实现更加精细的时间片划分。
缺点:
-
内存占用:需要存储每个时间片的请求数量,内存使用量取决于窗口大小和时间片间隔。
-
复杂度较高:相比于固定窗口计数器,滑动窗口限流器的实现和理解稍微复杂一些。
2.1.3 改进方向
-
处理时间片滑动:当前的
slideWindow
方法每次只移动一个时间片,可能不够准确。可以根据实际情况移动多个时间片。 -
优化内存使用:如果窗口很大或时间片间隔很小,可以考虑使用更加高效的数据结构来存储时间片数据。
// 一下滑动若干片
func (s *SlidingWindowLimiter) slideWindow() {
now := time.Now()
elapsed := now.Sub(s.windowStart)
// 计算需要滑动的时间片数量
slideCount := int(elapsed / s.interval)
if slideCount >= len(s.counts) {
// 窗口完全滑动,重置所有计数
s.counts = make([]int, len(s.counts))
} else {
// 部分滑动,移动计数并重置新时间片
copy(s.counts, s.counts[slideCount:])
for i := len(s.counts) - slideCount; i < len(s.counts); i++ {
s.counts[i] = 0
}
}
// 更新窗口起始时间
s.windowStart = now
}
参考链接:https://mp.weixin.qq.com/s/EJ68f40ebapdqKTgGOG8tw