前書き
達成1を制限する前の記事では、3つの電流制限スキームについてすでに説明しています。
- ランダムな拒否
- カウンター方式
- スライド時間ウィンドウに基づく電流制限
残りの数個はもともとすぐに終了する予定でしたが、3ヶ月が経過するとは思っていませんでした。とても恥ずかしかったです。今回は主に以下の2つのアルゴリズムを実装します
- トークンバケットアルゴリズム
- ファンネルアルゴリズム
アルゴリズムの特定の実装は、githubhttps://github.com/shidawuhen/asap/tree/master/controller/limitで確認できます。
まず、これら2つのアルゴリズムを紹介しましょう。
トークンバケットアルゴリズム
アルゴリズムの説明
トークンバケット:これは、ネットワークトラフィックシェーピング(トラフィックシェーピング)およびレート制限(レート制限)で最も一般的に使用されるアルゴリズムです。トークンバケットアルゴリズムの概略図は次のとおりです。
一般的なプロセスは次のとおりです。
a。トークンを特定のレートでトークンバケットに入れます
b。事前設定されたマッチングルールに従って、パケットが最初に分類され、マッチングルールを満たさないパケットは、トークンバケットによって処理され、直接送信される必要はありません。
c。一致ルールを満たすパケットは、トークンバケットで処理する必要があります。バケットに十分なトークンがある場合、メッセージは引き続き送信でき、トークンバケット内のトークンの量は、メッセージの長さに応じてそれに応じて減少します。
d。トークンバケットにトークンが不足している場合、メッセージを送信できません。メッセージは、バケットに新しいトークンが生成された場合にのみ送信できます。これにより、メッセージのフローをトークン生成のレート以下に制限でき、フローを制限するという目的を達成できます。
固定サイズのトークンバケットは、一定の速度でトークンを継続的に生成できます。トークンが消費されない場合、または消費率が生成率よりも低い場合、トークンはバケットがいっぱいになるまで増加し続けます。後で生成されたトークンはバケットからオーバーフローします。最終バケットに格納できるトークンの最大数は、バケットのサイズを超えることはありません。
トークンバケットには通常2つの値があり、1つはバケット容量であり、もう1つは時間単位に配置されるトークンの量であるため、トークンバケットはバーストを許可できます。バケット容量が単位時間あたりに配信されるトークンの量よりも多く、単位時間あたりの消費量が配信される量よりも少ない場合、トークンの数は最終的に最大バケット容量に達します。このとき、多数のリクエストが到着すると、すべてのトークンが消費され、バーストを許可する効果が実現します。
アルゴリズムの実装
アルゴリズムにはいくつかのコアポイントがあります
- トークンの数を更新したいので、ロックする必要があります
- トークンを定期的にバケットに入れる方法は2つあります。1つはゴルーチンを開始して定期的に入れる方法、もう1つはトークンが十分にあるかどうかを判断するときに状況に応じてトークンを入れる方法です。今回の実装では2番目の方法を使用し、アーキテクチャ全体がより単純になります。
package limit
import (
"github.com/gin-gonic/gin"
"net/http"
"sync"
"time"
)
// @Tags limit
// @Summary 令牌桶拒流
// @Produce json
// @Success 200 {string} string "成功会返回ok"
// @Failure 502 "失败返回reject"
// @Router /limit/tokenreject [get]
type TokenBucket struct {
rate int64 //固定的token放入速率, r/s
capacity int64 //桶的容量
tokens int64 //桶中当前token数量
lastTokenSec int64 //桶上次放token的时间戳 s
lock sync.Mutex
}
func (l *TokenBucket) Allow() bool {
l.lock.Lock()
defer l.lock.Unlock()
now := time.Now().Unix()
l.tokens = l.tokens + (now-l.lastTokenSec)*l.rate // 先添加令牌
if l.tokens > l.capacity {
l.tokens = l.capacity
}
l.lastTokenSec = now
if l.tokens > 0 {
// 还有令牌,领取令牌
l.tokens--
return true
} else {
// 没有令牌,则拒绝
return false
}
}
func (l *TokenBucket) Set(r, c int64) {
l.rate = r
l.capacity = c
l.tokens = 0
l.lastTokenSec = time.Now().Unix()
}
func CreateTokenBucket()*TokenBucket{
t := &TokenBucket{
}
t.Set(1,5)
return t
}
var tokenBucket *TokenBucket = CreateTokenBucket()
func TokenReject(c *gin.Context) {
//fmt.Println(tokenBucket.tokens)
if !tokenBucket.Allow() {
c.String(http.StatusBadGateway, "reject")
return
}
c.String(http.StatusOK, "ok")
}
漏出バケットアルゴリズム
アルゴリズムの説明
漏出バケットを計測ツール(メーターとしての漏出バケットアルゴリズム)として使用すると、トラフィックシェーピング(トラフィックシェーピング)とフロー制御(TrafficPolicing)に使用できます。漏出バケットアルゴリズムの説明は次のとおりです。
- 一定の一定速度で水滴を流出させる、固定容量の漏出バケット。
- バケツが空の場合、水滴が流出する必要はありません。
- 水滴は、とにかく漏出バケットに注ぐことができます。
- 流入する水滴がバケツの容量を超えると、流入する水滴はオーバーフロー(廃棄)され、漏出バケットの容量は変わりません。
電流制限のリーキーバケット方式は理解しやすいです。一定の割合で水滴を落とすバケットがあるとします。リクエストの数やリクエストの割合が大きくても、一定の割合で流出します。システムに対応して、固定レートに基づいています。リクエストが処理されるレート。
概略図は次のとおりです。
アルゴリズムの実装
関連情報を参照した後、3つの主要なアルゴリズムの実装があります。このような実装を検討した結果、誤解されているのではないかと思い、カウント拒否を使うのは不便だと感じています。私の理解が間違っているなら、あなたも私に言うことができます。
3つの方法は次のとおりです。
トークンバケットアルゴリズムのバリエーション
バケットのサイズは、単位時間あたりに流出できる最大量であり、この種の記述はありません。
逆流症の変種を数える
このメソッドは、使用可能なスペースを指定された時間の初期値に設定します。
package limit
import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
"sync"
"time"
)
type LeakyBucket struct {
// 容量
capacity int64
// 剩余大小
remaining int64
// 下一次的重置容量时间
reset time.Time
// 重置容量时间间隔
rate time.Duration
mutex sync.Mutex
}
func (b *LeakyBucket) Allow() bool {
b.mutex.Lock()
defer b.mutex.Unlock()
if time.Now().After(b.reset) {
// 需要重置
b.reset = time.Now().Add(b.rate) // 更新时间
b.remaining = b.capacity // 重置剩余容量
}
fmt.Println(b.remaining)
if b.remaining > 0 {
// 判断是否能过
b.remaining--
return true
}
return false
}
func (b *LeakyBucket) Set(r time.Duration, c int64) {
b.rate = r
b.capacity = c
b.remaining = c
b.reset = time.Now().Add(b.rate)
}
func CreateLeakyBucket(r time.Duration,c int64) *LeakyBucket {
t := &LeakyBucket{
}
t.Set(r, c)
return t
}
var leakyBucket *LeakyBucket = CreateLeakyBucket(time.Second*2,10)
func LeakyReject(c *gin.Context) {
if !leakyBucket.Allow() {
c.String(http.StatusBadGateway, "reject")
return
}
c.String(http.StatusOK, "ok")
}
真の固定金利
このアルゴリズムの実装は、uberチームのオープンソースのgithub.com/uber-go/ratelimitに基づいています。
この実装により、リクエストが多数ある場合、各リクエストが指定された時間間隔で実行されることが保証されます。100個のリクエストを処理するように1を設定すると、10ミリ秒ごとに1つが処理されます。
長時間リクエストがない場合でも、リクエストは短時間で処理されます。もちろん、この状況は簡単に修復でき、修正方法を考えることができます。
package limit
import (
"fmt"
"github.com/andres-erbsen/clock"
"github.com/gin-gonic/gin"
"net/http"
"sync"
"time"
)
//真固定速率
type Clock interface {
Now() time.Time
Sleep(time.Duration)
}
type limiter struct {
sync.Mutex // 锁
last time.Time // 上一次的时刻
sleepFor time.Duration // 需要等待的时间
perRequest time.Duration // 每次的时间间隔
maxSlack time.Duration // 最大的富余量
clock Clock // 时钟
}
// Take 会阻塞确保两次请求之间的时间走完
// Take 调用平均数为 time.Second/rate.
func (t *limiter) Take() time.Time {
t.Lock()
defer t.Unlock()
now := t.clock.Now()
// 如果是第一次请求就直接放行
if t.last.IsZero() {
t.last = now
return t.last
}
// sleepFor 根据 perRequest 和上一次请求的时刻计算应该sleep的时间
// 由于每次请求间隔的时间可能会超过perRequest, 所以这个数字可能为负数,并在多个请求之间累加
t.sleepFor += t.perRequest - now.Sub(t.last)
fmt.Println(t.sleepFor)
// 我们不应该让sleepFor负的太多,因为这意味着一个服务在短时间内慢了很多随后会得到更高的RPS。
if t.sleepFor < t.maxSlack {
t.sleepFor = t.maxSlack
}
// 如果 sleepFor 是正值那么就 sleep
if t.sleepFor > 0 {
t.clock.Sleep(t.sleepFor)
t.last = now.Add(t.sleepFor)
t.sleepFor = 0
} else {
t.last = now
}
return t.last
}
func NewLimiter(rate int) *limiter {
l := &limiter{
perRequest: time.Second / time.Duration(rate),
maxSlack: -10 * time.Second / time.Duration(rate),
}
if l.clock == nil {
l.clock = clock.New()
}
return l
}
var rl = NewLimiter(100) // per second,每秒100个请求
func LeakyRejectFixedRate(c *gin.Context) {
prev := time.Now()
for i := 0; i < 10; i++ {
now := rl.Take()
fmt.Println(i, now.Sub(prev))
prev = now
}
c.String(http.StatusOK, "ok")
}
デモンストレーションの結果は次のとおりです。
総括する
トークンバケットとリーキーバケットアルゴリズムを学び、過去数年間の作業の特定のシナリオと組み合わせた後、トークンバケットアルゴリズムの実用的な価値はより大きいと感じています。
以下は、トークンバケットとリーキーバケットの比較です。
- トークンバケットは、固定レートでトークンをバケットに追加します。リクエストが処理されるかどうかは、バケット内のトークンが十分かどうかによって異なります。トークンの数がゼロになると、新しいリクエストは拒否されます。
- リーキーバケットは一定の固定レートでリクエストを流出させ、着信リクエストのレートは任意です。着信リクエストの数がリーキーバケットの容量に達すると、新しい着信リクエストは拒否されます。
- トークンバケットは平均流入速度を制限し(トークンが処理できる限りバースト要求を許可し、一度に3つのトークンと4つのトークンをサポートします)、ある程度のバーストトラフィックを許可します。
- 漏出バケットは一定の流出速度を制限し(つまり、流出速度は固定の一定値です。たとえば、流出速度は1であり、次回は1と2にすることはできません)、それによってバースト流入速度が平滑化されます。
- トークンバケットはある程度のバーストを可能にし、リーキーバケットの主な目的は流入速度をスムーズにすることです。
- 2つのアルゴリズムは同じ方法で実装できますが、方向が逆であり、同じパラメーターに対して得られる電流制限効果は同じです。
最後に、さまざまな電流制限アルゴリズムの比較を示します。
データ
やっと
私の記事が気に入ったら、私の公式アカウント(プログラマーMala Tang)をフォローしてください。
私の個人的なブログは次のとおりです:https://shidawuhen.github.io/
以前の記事のレビュー:
技術
- スパイクシステム
- 分散システムとコンセンサスプロトコル
- マイクロサービスのサービスフレームワークとレジストリ
- Beegoフレームワークの使用法
- マイクロサービスについて話す
- TCPパフォーマンスの最適化
- 電流制限の実現1
- Redisは分散ロックを実装しています
- Golangソースコードのバグ追跡
- トランザクションの原子性、一貫性、耐久性の実現原理
- 詳細なCDNリクエストプロセス
- 一般的なキャッシュ手法
- サードパーティの支払いに効率的に接続する方法
- ジンフレームワークの簡潔なバージョン
- InnoDBのロックとトランザクションの簡単な分析
- アルゴリズムの概要
研究ノート
考え