软件系统限流-1

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

    1. NewSlidingWindowLimiter 函数初始化滑动窗口限流器,接受三个参数:每个时间片的请求限制(limit)、窗口的总时长(windowDuration)和每个时间片的间隔(interval)。

    2. 初始化时,计数器数组 counts 的长度为 windowDuration/interval,表示整个窗口被分割成的时间片数量。

    3. 窗口的起始时间 windowStart 设为当前时间。

  2. 请求判定

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

    2. 通过 mu.Lock()mu.Unlock() 确保线程安全。

    3. 判断当前时间与窗口起始时间的差值是否超过了窗口总时长 windowDuration,如果超过了则调用 slideWindow 方法移动窗口。

    4. 计算当前时间所在的时间片索引 index,并检查该时间片的请求数是否小于限制值 limit。如果是,则增加该时间片的请求数并返回 true,否则返回 false

  3. 滑动窗口

    1. slideWindow 方法移动窗口,将 counts 数组中的数据向前移动一位,并将最后一个时间片的值设为0。

    2. 更新窗口的起始时间 windowStart 为当前时间。

2.1.2. 优缺点

优点

  1. 平滑流量控制:滑动窗口限流器能够更平滑地控制请求流量,减少突发流量的影响。

  2. 高效:通过数组和索引计算来记录和限制请求数量,时间复杂度较低。

  3. 精细控制:可以通过调整 interval 来实现更加精细的时间片划分。

缺点:

  1. 内存占用:需要存储每个时间片的请求数量,内存使用量取决于窗口大小和时间片间隔。

  2. 复杂度较高:相比于固定窗口计数器,滑动窗口限流器的实现和理解稍微复杂一些。

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

 

你可能感兴趣的:(软件系统相关知识学习,网络,算法,开发语言)