Token Bucket
令牌桶是一种用于分组网络和电信网络算法,可用于检查数据包形式的数据传输是否符合定义的带宽和突发性限制。
令牌桶算法基于固定容量桶类比,令牌通常代表一个字节单位或者预定大小的单个数据包,以固定速率添加到桶中。当要检查数据包是否符合定义限制时,将检查桶以查看当时是否包含足够的令牌。如果足够,则移除对应量令牌。若不足,则桶内容不改变,而是通过以下方式处理不合格的数据包
- 丢弃
- 积累足够令牌,再排入队列进行后续传输
- 可能会被传输,但被标记不合格,一旦网络过载,可能就会被丢弃
type TokenBucket struct {
lastCheck time.Time
timeUnit time.Duration
tokens int64
bucket int64
forwardCallback func(packet string)
dropCallback func(packet string)
}
func (b *TokenBucket) handle(packet string) {
now := time.Now()
timePassed := now.Sub(b.lastCheck)
b.lastCheck = now
tpNum := timePassed.Nanoseconds()
tuNum := b.timeUnit.Nanoseconds()
// 求的当前桶中令牌数量
b.bucket = b.bucket + tpNum*b.tokens/tuNum
// 若求得当前桶令牌数量超过了最大数量,则赋为最大数量
if b.bucket > b.tokens {
b.bucket = b.tokens
}
// 若桶中数量小于1,则丢弃
if b.bucket < 1 {
b.dropCallback(packet)
return
}
// 否则消费一枚令牌
b.bucket--
b.forwardCallback(packet)
}
Leaky Bucket
漏桶类比于如果注入的水平均速率超过桶的泄露速率,或者如果谁超过桶的容量,那么将会漏出。
可以用于确定某些离散事件序列是否符合平均和峰值速率定义限制。
漏桶算法有两个版本
- 类比于计数器,仅用于检查流量或事件是否符合限制。当每个数据包到达进行检查或发生事件点时,计数器便会递增,同时也以固定速率递减
此时完全相当于令牌桶 - 类比于流量中的队列,用于直接控制该流量。数据包到达队列,相当于将水添加到桶中;然后将这些数据包从队列中删除,通常以固定速率向前传输,相当于水从桶泄露。因此,服务队列的速率直接控制流量前向传输的速率。
这种情况下,相比于令牌桶强行限制了数据的传输速率,而令牌桶则只是限制了平均速率
type LeakyBucket struct {
occupied int
bucketSize int
outputSpeed int
forwardCallback func(packet string)
dropCallback func(packet string)
}
func (b *LeakyBucket) handle(packet string) {
// 计算剩余容量
var left int
left = b.bucketSize - b.occupied
// 若输入速度
if left < 1 {
b.dropCallback(packet)
return
}
b.occupied++
b.occupied += b.outputSpeed
return
}
Fixed Window
在这个算法中,我们可以将时间分为固定窗口,然后将到来的事件映射到这些窗口。
为了理解这个算法,可以想象一列正在行驶的火车,然后有一堆奇奇怪怪的乘客要登上行驶中火车车厢,若车厢满了就不能上去
type FixedWindow struct {
recordedTime time.Time
fixedWindowDuration time.Duration
allowance int
capacity int
forwardCallback func(packet string)
dropCallback func(packet string)
}
func NewFixedWindow(fixedWindowDuration time.Duration, allowance, capacity int, forwardCallback func(packet string), dropCallback func(packet string)) *FixedWindow {
return &FixedWindow{
recordedTime: time.Now(),
fixedWindowDuration: fixedWindowDuration,
allowance: allowance,
capacity: capacity,
forwardCallback: forwardCallback,
dropCallback: dropCallback,
}
}
func (w *FixedWindow) handle(packet string) {
// 若上次记录的时间超过当前时间一定间隔,则更新记录时间为当前时间,并设置允许通过数量为容量
if time.Now().Sub(w.recordedTime) >= w.fixedWindowDuration {
w.recordedTime = time.Now()
w.allowance = w.capacity
}
// 若通过容量不足,则调用丢弃回调方法
if w.allowance < 1 {
w.dropCallback(packet)
return
}
// 若通过容量充足,则将通过容量递减,且调用前向回调
w.allowance--
w.forwardCallback(packet)
return
}
Sliding Window
不像固定窗口,滑动窗口并不限制固定时间内的请求。
在固定窗口中,存在允许在窗口边缘出现巨大爆发,因为可以结合当前窗口和下一个窗口来发送请求。滑动窗口试图通过计数器来解决这个问题。
让我们用一个例子来解释。假设有一个时间单位为tu秒,容量为c的速率限制器,上次消费的时间为,上周期消费容量为,当前周期已经消费数量为,那么在时间t消费的容量为
type SlidingWindows struct {
capacity int
timeUnit time.Duration
forwardCallback func(packet string)
dropCallback func(packet string)
recordTime time.Time
preCount int
consumedCount int
}
func (w *SlidingWindows) handle(packet string) {
// 若上次消费时间到现在超过一定周期,则设置记录上次消费时间为当前时间,上次消费周期消费次数为当前消费次数,当前周期消费次数为0
d := time.Now().Sub(w.recordTime)
if d >= w.timeUnit {
w.recordTime = time.Now()
w.preCount = w.consumedCount
w.consumedCount = 0
}
// 求得当前周期可用消费次数
ds := int(d.Nanoseconds())
tu := int(w.timeUnit.Nanoseconds())
r := w.preCount*(ds/tu) + w.consumedCount
// 若当前周期可用消费次数超过容量,则丢弃
if r > w.capacity {
w.dropCallback(packet)
return
}
// 否则消费次数加1,并前向回调
w.consumedCount++
w.forwardCallback(packet)
return
}
Ref
- https://en.wikipedia.org/wiki/Token_bucket
- https://en.wikipedia.org/wiki/Leaky_bucket
- https://dev.to/satrobit/rate-limiting-using-the-fixed-window-algorithm-2hgm