软件系统限流-2

软件系统限流-1

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),用于流量控制。漏桶算法是一种经典的流量整形和限流算法,通过将请求放入一个固定容量的桶中,并以固定速率从桶中移除请求,来控制请求的速率。 

1.1. 工作原理
  1. 初始化

    1. NewLeakyBucket 函数创建一个新的漏桶,接受一个参数 bucketSize,表示桶的容量。

    2. 使用 make(chan struct{}, bucketSize) 创建一个带缓冲的通道 queue,其缓冲区大小为 bucketSize

  2. 添加请求

    1. Add 方法向漏桶中添加一个请求。

    2. 使用 select 语句尝试向 queue 通道中发送一个空结构体 struct{}{}。如果通道未满,发送成功并返回 true;如果通道已满,发送失败并返回 false

  3. 移除请求

    1. Remove 方法从漏桶中移除请求。

    2. 使用 for range 语句从 queue 通道中读取请求,并处理每个请求,通道关闭的时候,循环会退出。处理请求的过程中,使用 time.Sleep 模拟处理时间。

1.2. 优缺点

优点:

  1. 简单易懂:实现和理解相对简单,不需要复杂的数据结构。

  2. 恒定速率:通过固定的移除速率,能够平滑请求流量,防止突发流量。

  3. 防止过载:通过固定容量的桶,能够防止系统过载。

缺点:

  1. 固定处理速率:处理速率是固定的,无法动态调整,可能不适应瞬时高峰流量。

  2. 单一线程:当前实现中,Remove 方法只能在单一线程中运行,无法利用多核处理能力。

2.1.3 改进方向
  • 动态调整处理速率:可以根据系统负载,动态调整请求的处理速率。【可以加入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()
}

2. 令牌桶算法

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")
       }
    }
}
2.1. 工作原理
  1. 初始化

    1. NewTokenBucket 函数创建一个新的令牌桶,接受两个参数:桶的容量 capacity 和令牌产生速率 rate

    2. 初始化时,令牌桶的容量 capacity 和当前令牌数量 tokens 都设置为 capacity,表示桶是满的。

    3. 令牌产生速率 rate 表示每秒生成的令牌数量。

    4. lastFill 记录上次填充令牌的时间。

  2. 请求判定

    1. Allow 方法判断是否允许当前请求通过。

    2. 使用 mu.Lock()mu.Unlock() 确保线程安全。

    3. 计算从 lastFill 到当前时间 now 之间经过的时间,并根据令牌生成速率 rate 计算新的令牌数量,并更新当前令牌数量 tokens

    4. 如果当前令牌数量 tokens 大于桶的容量 capacity,则将其设置为 capacity

    5. 判断当前令牌数量 tokens 是否大于0,如果大于0,表示有可用的令牌,允许通过并减少一个令牌,同时更新 lastFill 为当前时间;否则,不允许通过。

2.2. 优缺点

优点:

  1. 灵活性高:通过调整令牌生成速率和桶的容量,可以灵活地控制请求的速率和突发流量。

  2. 平滑流量:通过令牌的生成和消耗,可以平滑请求流量,防止突发流量对系统的冲击。

  3. 简单易懂:实现相对简单,易于理解和使用。

缺点:

  1. 时间计算精度:时间计算使用 time.Now()time.Sub(),在高并发场景下可能存在一定的时间精度问题。

  2. 锁竞争:使用互斥锁 sync.Mutex 来保证线程安全,在高并发场景下可能会导致锁竞争,影响性能。

  3. 单一桶实现:当前实现为单一桶,无法支持多级限流或复杂的流量控制场景。

2.3 改进方向
  • 优化时间计算:可以使用更高精度的时间计算方法,减少时间计算误差。

  • 减少锁竞争:可以使用无锁算法或更细粒度的锁来减少锁竞争,提高性能。

  • 支持多级限流:可以扩展实现,支持多级限流和复杂的流量控制场景。

// 多级限流,参考
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执行完
}

3. 限流的不同实现方式

3.1. 应用层限流
// 这是一个伪代码案例,演示实现逻辑
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()
}
  1. 令牌桶算法

    1. TokenBucket 结构体实现了令牌桶算法,用于流量控制。

    2. capacity 表示桶的容量,即令牌的最大数量。

    3. tokens 表示当前桶中的令牌数量。

    4. refillRate 表示令牌的填充速率,即每秒生成的令牌数量。

    5. lastRefill 记录上次填充的时间。

  2. 令牌桶的初始化

    1. NewTokenBucket 函数用于创建并初始化一个新的令牌桶,设置桶的容量和填充速率,并将初始令牌数设为容量的值。

  3. 请求判定

    1. Allow 方法用于判断是否允许当前请求通过。

    2. 计算自上次填充以来应该添加的令牌数,并更新桶中的令牌数,但不超过桶的容量。

    3. 如果桶中有可用的令牌,减少一个令牌并返回 true,表示请求被允许;否则,返回 false,表示请求被拒绝。

  4. Gin中间件

    1. Middleware 函数返回一个Gin中间件,该中间件使用 TokenBucket 来限流。

    2. 如果 TokenBucketAllow 方法返回 false,中间件将中断请求处理,并返回HTTP状态码429(Too Many Requests)和错误信息。

    3. 如果请求被允许,中间件将调用 c.Next() 继续执行后续的处理链。

优缺点

优点

  1. 简单易懂

    1. 令牌桶算法简单易懂,实现和使用都非常直观。

  2. 灵活性高

    1. 可以轻松调整令牌桶的容量和填充速率,以适应不同的流量需求。

  3. 精细控制

    1. 可以为不同的API或服务设置不同的令牌桶,实现精细的流量控制。

  4. 保护系统

    1. 有效防止流量突发导致的系统过载,保护后端服务的稳定性。

缺点

  1. 锁竞争

    1. 使用互斥锁 sync.Mutex 来保证线程安全,在高并发场景下可能会导致锁竞争,影响性能。

  2. 单点故障

    1. 如果令牌桶限流器本身出现问题(例如内存泄漏、死锁等),可能导致整个服务不可用。

  3. 分布式部署问题

    1. 在分布式部署环境中,需要保证多个实例之间的限流策略一致,这可能需要额外的协调机制。

3.2. 代理层限流

代理层限流是在网络通信的代理服务器层面实现限流,例如使用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/

 优缺点

优点

  1. 全局控制

    1. 在代理层可以对所有流量进行统一的限流控制,不需要修改后端服务的代码。

  2. 减轻后端负担

    1. 通过在代理层进行限流,可以减少后端服务的负载,防止流量突发导致后端过载。

  3. 灵活配置

    1. 可以根据不同的路径、客户端IP等进行灵活的限流配置,满足不同的业务需求。

  4. 快速响应

    1. 代理层限流在Nginx层面处理,可以快速响应限流请求,减少处理延迟。

  5. 独立性

    1. 限流逻辑独立于应用代码,方便维护和调整。

缺点

  1. 复杂配置

    1. Nginx限流配置较为复杂,需要仔细调试和验证配置的正确性。

  2. 精度问题

    1. 限流的精度可能不如应用层限流,因为Nginx的限流基于共享内存统计,可能存在一定的误差。

  3. 单点故障

    1. 如果Nginx代理层出现故障,可能导致整个系统的请求无法处理。需要做好高可用性配置,如负载均衡和故障转移。

  4. 缺少业务上下文

    1. 代理层限流无法获取具体的业务上下文信息(如用户身份、请求内容),只能基于请求速率和客户端IP等进行限流。

  5. 资源开销

    1. 在高并发场景下,Nginx的共享内存和限流统计可能会带来额外的资源开销,需要合理配置和监控。

 总结

        代理层限流是一种有效的流量控制手段,通过Nginx等反向代理服务器,可以对流量进行统一的限流管理,从而保护后端服务的稳定性和可用性。然而,在实际应用中,需要根据具体的业务需求和系统架构,合理配置限流策略,并结合应用层限流等手段,达到最佳的流量控制效果。

3.3  硬件层限流

在硬件层(如负载均衡器)实现限流,可以在请求到达应用服务器之前进行控制。

你可能感兴趣的:(软件系统相关知识学习,中间件,算法,学习)