软件系统限流-1
package leaky_bucket
import (
"fmt"
"time"
)
// 漏桶算法
// LeakyBucket 漏桶算法
type LeakyBucket struct {
queue chan struct{}
}
// NewLeakyBucket 创建一个漏桶算法
func NewLeakyBucket(bucketSize int) *LeakyBucket {
return &LeakyBucket{
queue: make(chan struct{}, bucketSize),
}
}
// Add 向漏桶中添加一个请求
func (l *LeakyBucket) Add() bool {
select {
case l.queue <- struct{}{}:
return true
default:
return false
}
}
// Remove 从漏桶中移除一个请求
func (l *LeakyBucket) Remove() {
for range l.queue {
fmt.Println("Request handled", time.Now().Format("2006-01-02 15:04:05"))
time.Sleep(100 * time.Millisecond)
}
}
func main() {
limiter := leaky_bucket.NewLeakyBucket(10)
go limiter.Remove() // 模拟处理请求
for i := 0; i < 15; i++ {
if limiter.Add() {
fmt.Println("Request", i+1, "allowed")
} else {
fmt.Println("Request", i+1, "rejected")
}
}
time.Sleep(time.Second * 5)
}
这段代码实现了一个简单的漏桶算法(Leaky Bucket Algorithm),用于流量控制。漏桶算法是一种经典的流量整形和限流算法,通过将请求放入一个固定容量的桶中,并以固定速率从桶中移除请求,来控制请求的速率。
初始化:
NewLeakyBucket
函数创建一个新的漏桶,接受一个参数 bucketSize
,表示桶的容量。
使用 make(chan struct{}, bucketSize)
创建一个带缓冲的通道 queue
,其缓冲区大小为 bucketSize
。
添加请求:
Add
方法向漏桶中添加一个请求。
使用 select
语句尝试向 queue
通道中发送一个空结构体 struct{}{}
。如果通道未满,发送成功并返回 true
;如果通道已满,发送失败并返回 false
。
移除请求:
Remove
方法从漏桶中移除请求。
使用 for range
语句从 queue
通道中读取请求,并处理每个请求,通道关闭的时候,循环会退出。处理请求的过程中,使用 time.Sleep
模拟处理时间。
优点:
简单易懂:实现和理解相对简单,不需要复杂的数据结构。
恒定速率:通过固定的移除速率,能够平滑请求流量,防止突发流量。
防止过载:通过固定容量的桶,能够防止系统过载。
缺点:
固定处理速率:处理速率是固定的,无法动态调整,可能不适应瞬时高峰流量。
单一线程:当前实现中,Remove
方法只能在单一线程中运行,无法利用多核处理能力。
动态调整处理速率:可以根据系统负载,动态调整请求的处理速率。【可以加入time.NewTicker】
并发处理:使用多个 goroutine 来处理请求,提高处理能力。
package leaky_bucket
import (
"fmt"
"sync"
"time"
)
type LeakyBucketV2 struct {
queue chan struct{} // 漏桶队列
stopChan chan struct{} // 停止信号
wg sync.WaitGroup // 等待所有请求处理完成
}
// NewLeakyBucketV2 创建一个漏桶算法
func NewLeakyBucketV2(bucketSize int) *LeakyBucketV2 {
return &LeakyBucketV2{
queue: make(chan struct{}, bucketSize),
stopChan: make(chan struct{}),
}
}
// Add 向漏桶中添加一个请求
func (l *LeakyBucketV2) Add() bool {
select {
case l.queue <- struct{}{}:
return true
default:
return false
}
}
// Remove 从漏桶中移除一个请求
func (l *LeakyBucketV2) Remove() {
for {
select {
case <-l.queue:
l.wg.Add(1)
go func() {
defer l.wg.Done()
// 处理请求
fmt.Println("Request handled", time.Now().Format("2006-01-02 15:04:05"))
}()
case <-l.stopChan:
l.wg.Wait()
return
}
}
}
// Stop 停止处理请求
func (l *LeakyBucketV2) Stop() {
close(l.stopChan)
}
func main() {
limiter := leaky_bucket.NewLeakyBucketV2(10)
go limiter.Remove() // 模拟处理请求
for i := 0; i < 15; i++ {
if limiter.Add() {
fmt.Println("Request", i+1, "allowed")
} else {
fmt.Println("Request", i+1, "rejected")
}
}
time.Sleep(time.Second * 5)
limiter.Stop()
}
package token_bucket
import (
"sync"
"time"
)
type TokenBucket struct {
mu sync.Mutex
capacity int // 桶的容量
tokens int // 当前令牌数量
rate float64 // 令牌产生速率
lastFill time.Time // 上次填充令牌的时间
}
// NewTokenBucket 创建一个令牌桶
func NewTokenBucket(capacity int, rate float64) *TokenBucket {
return &TokenBucket{
capacity: capacity,
tokens: capacity,
rate: rate,
lastFill: time.Now(),
}
}
// Allow 判断是否允许访问
func (t *TokenBucket) Allow() bool {
t.mu.Lock()
defer t.mu.Unlock()
// 计算令牌数量
now := time.Now()
t.tokens += int(now.Sub(t.lastFill).Seconds() * t.rate)
if t.tokens > t.capacity {
t.tokens = t.capacity
}
// 判断是否有令牌
if t.tokens > 0 {
t.tokens--
t.lastFill = now
return true
}
return false
}
func main() {
limiter := token_bucket.NewTokenBucket(10, 2)
for i := 0; i < 15; i++ {
if limiter.Allow() {
fmt.Println("Request", i+1, "allowed")
time.Sleep(1000 * time.Millisecond) // 延迟时间一秒,这样每次请求都不会拒绝,如不加,可能因为执行太快导致还没来得及放令牌
} else {
fmt.Println("Request", i+1, "rejected")
}
}
}
初始化:
NewTokenBucket
函数创建一个新的令牌桶,接受两个参数:桶的容量 capacity
和令牌产生速率 rate
。
初始化时,令牌桶的容量 capacity
和当前令牌数量 tokens
都设置为 capacity
,表示桶是满的。
令牌产生速率 rate
表示每秒生成的令牌数量。
lastFill
记录上次填充令牌的时间。
请求判定:
Allow
方法判断是否允许当前请求通过。
使用 mu.Lock()
和 mu.Unlock()
确保线程安全。
计算从 lastFill
到当前时间 now
之间经过的时间,并根据令牌生成速率 rate
计算新的令牌数量,并更新当前令牌数量 tokens
。
如果当前令牌数量 tokens
大于桶的容量 capacity
,则将其设置为 capacity
。
判断当前令牌数量 tokens
是否大于0,如果大于0,表示有可用的令牌,允许通过并减少一个令牌,同时更新 lastFill
为当前时间;否则,不允许通过。
优点:
灵活性高:通过调整令牌生成速率和桶的容量,可以灵活地控制请求的速率和突发流量。
平滑流量:通过令牌的生成和消耗,可以平滑请求流量,防止突发流量对系统的冲击。
简单易懂:实现相对简单,易于理解和使用。
缺点:
时间计算精度:时间计算使用 time.Now()
和 time.Sub()
,在高并发场景下可能存在一定的时间精度问题。
锁竞争:使用互斥锁 sync.Mutex
来保证线程安全,在高并发场景下可能会导致锁竞争,影响性能。
单一桶实现:当前实现为单一桶,无法支持多级限流或复杂的流量控制场景。
优化时间计算:可以使用更高精度的时间计算方法,减少时间计算误差。
减少锁竞争:可以使用无锁算法或更细粒度的锁来减少锁竞争,提高性能。
支持多级限流:可以扩展实现,支持多级限流和复杂的流量控制场景。
// 多级限流,参考
package token_bucket
import (
"sync"
"time"
)
// TokenBucket 令牌桶结构
type TokenBucket struct {
mu sync.Mutex
capacity int // 桶的容量
tokens int // 当前令牌数量
rate float64 // 令牌产生速率
lastFill time.Time // 上次填充令牌的时间
}
// NewTokenBucket 创建一个令牌桶
func NewTokenBucket(capacity int, rate float64) *TokenBucket {
return &TokenBucket{
capacity: capacity,
tokens: capacity,
rate: rate,
lastFill: time.Now(),
}
}
// Allow 判断是否允许访问
func (t *TokenBucket) Allow() bool {
t.mu.Lock()
defer t.mu.Unlock()
// 计算令牌数量
now := time.Now()
t.tokens += int(now.Sub(t.lastFill).Seconds() * t.rate)
if t.tokens > t.capacity {
t.tokens = t.capacity
}
// 判断是否有令牌
if t.tokens > 0 {
t.tokens--
t.lastFill = now
return true
}
return false
}
// MultiLevelLimiter 多级限流器
type MultiLevelLimiter struct {
globalBucket *TokenBucket
buckets map[string]*TokenBucket
mu sync.Mutex
}
// NewMultiLevelLimiter 创建多级限流器
func NewMultiLevelLimiter(globalCapacity int, globalRate float64) *MultiLevelLimiter {
return &MultiLevelLimiter{
globalBucket: NewTokenBucket(globalCapacity, globalRate),
buckets: make(map[string]*TokenBucket),
}
}
// AddBucket 添加特定资源的令牌桶
func (m *MultiLevelLimiter) AddBucket(resource string, capacity int, rate float64) {
m.mu.Lock()
defer m.mu.Unlock()
m.buckets[resource] = NewTokenBucket(capacity, rate)
}
// Allow 判断是否允许访问特定资源
func (m *MultiLevelLimiter) Allow(resource string) bool {
// 先检查全局令牌桶
if !m.globalBucket.Allow() {
return false
}
// 再检查特定资源的令牌桶
m.mu.Lock()
bucket, exists := m.buckets[resource]
m.mu.Unlock()
if exists {
return bucket.Allow()
}
// 如果没有特定资源的令牌桶,则默认允许
return true
}
package main
import (
"fmt"
"token_bucket"
"time"
)
func main() {
limiter := token_bucket.NewMultiLevelLimiter(100, 10) // 全局限流器,容量100,速率10个令牌/秒 limiter.AddBucket("API_A", 10, 5) // 为API_A添加令牌桶,容量10,速率5个令牌/秒 limiter.AddBucket("API_B", 20, 10) // 为API_B添加令牌桶,容量20,速率10个令牌/秒
for i := 0; i < 15; i++ {
go func(i int) {
if limiter.Allow("API_A") {
fmt.Printf("Request %d for API_A allowed\n", i)
} else {
fmt.Printf("Request %d for API_A denied\n", i)
}
}(i)
}
time.Sleep(2 * time.Second) // 等待一段时间让所有goroutine执行完
}
// 这是一个伪代码案例,演示实现逻辑
package main
import (
"fmt"
"github.com/gin-gonic/gin" // 引入Gin框架,用于构建Web服务器和处理HTTP请求
"net/http"
"sync" // 引入sync包,用于同步原语,如互斥锁
"time" // 引入time包,用于时间相关操作
)
// TokenBucket 结构体实现令牌桶限流算法。
// 它包含互斥锁mu用于同步访问,capacity代表桶的容量,
// tokens表示当前桶中的令牌数,refillRate是令牌的填充速率(每秒),
// lastRefill记录上次填充的时间。
type TokenBucket struct {
mu sync.Mutex
capacity int
tokens int
refillRate float64
lastRefill time.Time
}
// NewTokenBucket 函数创建并初始化一个新的TokenBucket实例。
// 它设置桶的容量和填充速率,并将初始令牌数设为容量的值。
func NewTokenBucket(capacity int, refillRate float64) *TokenBucket {
return &TokenBucket{
capacity: capacity,
tokens: capacity, // 初始化时桶被填满
refillRate: refillRate,
lastRefill: time.Now(), // 记录创建时的时间作为上次填充时间
}
}
// Allow 方法用于检查是否允许通过当前请求。
// 它首先获取锁,然后计算自上次填充以来应该添加的令牌数,
// 更新桶中的令牌数,但不超过桶的容量。
// 如果桶中至少有一个令牌,它将减少一个令牌并返回true,表示请求被允许。
// 如果桶为空,则返回false,表示请求被拒绝。
func (tb *TokenBucket) Allow() bool {
tb.mu.Lock() // 获取锁,保证操作的原子性
defer tb.mu.Unlock()
now := time.Now() // 获取当前时间
// 计算自上次填充以来经过的秒数,然后乘以填充速率,得到应添加的令牌数
tokensToAdd := int(tb.refillRate * (now.Sub(tb.lastRefill).Seconds()))
tb.tokens += tokensToAdd // 更新桶中的令牌数
if tb.tokens > tb.capacity {
tb.tokens = tb.capacity // 确保不超过桶的容量
}
if tb.tokens > 0 {
tb.tokens-- // 处理请求,减少一个令牌
tb.lastRefill = now // 更新上次填充时间为当前时间
return true
}
return false // 如果桶为空,返回false
}
// Middleware 函数返回一个Gin中间件,该中间件使用TokenBucket来限流。
// 如果TokenBucket的Allow方法返回false,中间件将中断请求处理,
// 并返回HTTP状态码429(Too Many Requests)和错误信息。
// 如果请求被允许,中间件将调用c.Next()继续执行后续的处理链。
func Middleware(tb *TokenBucket) gin.HandlerFunc {
return func(c *gin.Context) {
// 在处理请求之前,调用TokenBucket的Allow方法检查是否允许请求
if !tb.Allow() {
// 如果请求被限流,返回错误信息和状态码
c.JSON(http.StatusTooManyRequests, gin.H{"error": "too many requests"})
c.Abort() // 中断请求处理
return
}
// 如果请求未被限流,继续执行后续的处理链
c.Next()
}
}
func main() {
// 创建一个Gin的默认实例,用于Web服务
r := gin.Default()
// 创建TokenBucket实例,用于限流控制
tb := NewTokenBucket(10, 1.0) // 桶的容量为10,每秒填充1个令牌
// 使用上面定义的限流中间件
r.Use(Middleware(tb))
// 定义一个简单的路由,当访问/hello路径时,返回JSON格式的消息
r.GET("/hello", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "hello world"})
})
// 启动Gin服务器,默认监听在0.0.0.0:8080
r.Run()
}
令牌桶算法:
TokenBucket
结构体实现了令牌桶算法,用于流量控制。
capacity
表示桶的容量,即令牌的最大数量。
tokens
表示当前桶中的令牌数量。
refillRate
表示令牌的填充速率,即每秒生成的令牌数量。
lastRefill
记录上次填充的时间。
令牌桶的初始化:
NewTokenBucket
函数用于创建并初始化一个新的令牌桶,设置桶的容量和填充速率,并将初始令牌数设为容量的值。
请求判定:
Allow
方法用于判断是否允许当前请求通过。
计算自上次填充以来应该添加的令牌数,并更新桶中的令牌数,但不超过桶的容量。
如果桶中有可用的令牌,减少一个令牌并返回 true
,表示请求被允许;否则,返回 false
,表示请求被拒绝。
Gin中间件:
Middleware
函数返回一个Gin中间件,该中间件使用 TokenBucket
来限流。
如果 TokenBucket
的 Allow
方法返回 false
,中间件将中断请求处理,并返回HTTP状态码429(Too Many Requests)和错误信息。
如果请求被允许,中间件将调用 c.Next()
继续执行后续的处理链。
优点
简单易懂:
令牌桶算法简单易懂,实现和使用都非常直观。
灵活性高:
可以轻松调整令牌桶的容量和填充速率,以适应不同的流量需求。
精细控制:
可以为不同的API或服务设置不同的令牌桶,实现精细的流量控制。
保护系统:
有效防止流量突发导致的系统过载,保护后端服务的稳定性。
缺点
锁竞争:
使用互斥锁 sync.Mutex
来保证线程安全,在高并发场景下可能会导致锁竞争,影响性能。
单点故障:
如果令牌桶限流器本身出现问题(例如内存泄漏、死锁等),可能导致整个服务不可用。
分布式部署问题:
在分布式部署环境中,需要保证多个实例之间的限流策略一致,这可能需要额外的协调机制。
代理层限流是在网络通信的代理服务器层面实现限流,例如使用Nginx或HAProxy等代理服务器。这种方法可以在请求到达后端服务之前对它们进行限制,从而保护后端服务不受过多请求的冲击。
// nginx限流配置实例
http {
# 定义一个限流区域,使用共享内存存储状态
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=1r/s;
server {
# 监听80端口
listen 80;
# 定义一个location块,用于匹配特定的请求路径
location /api/ {
# 应用限流规则
limit_req zone=mylimit burst=5 nodelay;
# 代理请求到后端服务
proxy_pass http://backend/;
}
}
}
这段Nginx配置实现了一个限流机制,主要用于控制进入 /api/
路径的请求速率。下面是详细解释:
配置解释:
1. 定义限流区域
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=1r/s;
$binary_remote_addr
:使用客户端IP地址作为关键字。
zone=mylimit:10m
:定义一个名称为 mylimit
的限流区域,使用10MB的共享内存来存储限流状态。
rate=1r/s
:设置请求速率为每秒1个请求。
2. 服务器块
server {
listen 80;
...
}
listen 80;
:监听80端口,处理HTTP请求。
3. 位置块
location /api/ {
limit_req zone=mylimit burst=5 nodelay;
proxy_pass http://backend/;
}
limit_req zone=mylimit burst=5 nodelay;
:
zone=mylimit
:使用之前定义的 mylimit
限流区域。
burst=5
:允许最多5个突发请求(请求可以临时超出速率限制,但不能超过5个)。
nodelay
:表示即使突发请求也不需要延迟处理,直接返回限流响应。
proxy_pass http://backend/;
:将请求代理到后端服务 http://backend/
。
优点
全局控制:
在代理层可以对所有流量进行统一的限流控制,不需要修改后端服务的代码。
减轻后端负担:
通过在代理层进行限流,可以减少后端服务的负载,防止流量突发导致后端过载。
灵活配置:
可以根据不同的路径、客户端IP等进行灵活的限流配置,满足不同的业务需求。
快速响应:
代理层限流在Nginx层面处理,可以快速响应限流请求,减少处理延迟。
独立性:
限流逻辑独立于应用代码,方便维护和调整。
缺点
复杂配置:
Nginx限流配置较为复杂,需要仔细调试和验证配置的正确性。
精度问题:
限流的精度可能不如应用层限流,因为Nginx的限流基于共享内存统计,可能存在一定的误差。
单点故障:
如果Nginx代理层出现故障,可能导致整个系统的请求无法处理。需要做好高可用性配置,如负载均衡和故障转移。
缺少业务上下文:
代理层限流无法获取具体的业务上下文信息(如用户身份、请求内容),只能基于请求速率和客户端IP等进行限流。
资源开销:
在高并发场景下,Nginx的共享内存和限流统计可能会带来额外的资源开销,需要合理配置和监控。
代理层限流是一种有效的流量控制手段,通过Nginx等反向代理服务器,可以对流量进行统一的限流管理,从而保护后端服务的稳定性和可用性。然而,在实际应用中,需要根据具体的业务需求和系统架构,合理配置限流策略,并结合应用层限流等手段,达到最佳的流量控制效果。
在硬件层(如负载均衡器)实现限流,可以在请求到达应用服务器之前进行控制。