Redisを用いた移動におけるHTTP要求の制限
78055 ワード
だから、お客様の多くの機能を提供するこの素晴らしいAPIを作成して、多くの顧客が使用に興味を持っているが、彼らはあなたのために効果的な方法で負荷を処理することはできませんそれを使用している.スケーリングとサービスをより信頼性の高いオプションは、それだけで十分ではありませんが、負荷は不均一である可能性がありますが、使用パターンは、あなたのアプリケーションが処理されたものではない可能性がありますか、または制限を持っている可能性がありますあなたは、この時点で修正することはできませんが、これはレート制限が来るところです.
レート制限の背後にあるアイデアは、あなたがクライアントから撮影される要求の最大量を持っているということです、一度制限が達すると、定義された期間にわたって、あなたは期間の終わりに到達し、カウンタを再起動するまで要求を削除を開始します.たとえば、クライアントは60分以上の要求をすると、要求を拒否し始めると、クライアントは60分のリクエストをすることができます.
ここでの目標は、あなたのサービスをより信頼性の高いものにすることです.料金制限は、あなたの側で実装された保護です.悪い俳優か間違って設定されたクライアントがサービスがその予想された使用限界にわたっているので、サービス全体を降ろすことができないか、停電を引き起こすことができないようにすることです.理想的なシナリオでは、彼らは通常の仕事をするのに十分な要求を与えているように良いクライアントを罰していない、しかし、あなたは悪いクライアントをブロックし、サービスに大混乱をwreaking.
プロジェクトのソースコードを見つけることができますhere on Github .
私たちは、さまざまな実装の長所と短所を議論するためにRateリミッタのために複数の実現を持っています.
それで、我々には基本的な場所があります.そして、入力、出力とあらゆるカウンタによって実行される必要がある小さなインターフェースです.
それは完全にカウンタをリセットしないようにローリングウィンドウの戦略は、バーストウィンドウのためのより良い保護を提供していますが、時間ウィンドウの期間の履歴を保持します.あなたは5分のウィンドウを持っている場合、それは常にクライアントがブロックする必要があるかどうかを決定するために最後の5分で発生したトラフィックの量をカウントするだけではなく、代わりにキーを有効期限を待っている.メモリのより多くの情報(そのタイムスタンプを持つすべての要求)を保つ必要がありますが、トラフィックの迅速なバーストに対してより良い保護を提供します.
以下のようになります.
ここでの両方の実装は最初に書きます、彼らはRedisに最初にものを書きます、そして、要求が受け入れられるかどうかチェックしてください.これがコードをより簡単にして、クライアントからサーバーまでより少ないネットワークトラフィックを生成する間、彼らは、サーバーが動作するためにずっと高価になります.私たちは、これらのソートされたセットがクリーンアップされているか、またはどのように多くのリクエストを格納することができます上に制限があることを確認していない、これはあなたが取り組んでいるシステム上で発生するべきではない一般的な赤いフラグです.
私たちは、ここでのリソース使用が制限されていることを確認しなければなりません、すなわち、それはよく知られている制限を持っています、あなたがいつ書いている何かが何らかの方法で(意図的に、または、偶然に)虐待されそうであると仮定しなければならなくて、あなたのコードが何らかの方法でこれらの資源に縛られるのを確実にするべきです.Redis自体では、Aを設定することによってメモリを使い果たすつもりはないことを確認する多くの方法がありますmaxmemory どれだけのメモリが使えるかの値maxmemory-policy でない値に
read - before - writeで更新されたカウンタの実装は以下のようになります.
次に、更新されたソート済みの実装を見ます.
我々は2つの別々のカウンタを実装したので、どのように我々はそれらを利用するのですか?
それを行う方法の1つは、既存のHTTPハンドラーをラップするミドルウェアハンドラを作成することで、レート制限機能を追加することです.これにより、必要なときに速度制限付きハンドラを作成することができます.
それから、我々は
まず、前に述べたように、バースト交通.あまりにも高速に全体の制限を使用するクライアントは、まだあなたのシステムのための問題になるだろうし、ローリングウィンドウリミッターが少しこれを減少させながら、それはあなたがトラフィックの雷の群れを防ぐためにやっている唯一のものではない必要があります.もう一つの可能な解決策は、より長い期間を使用するように、代わりに毎時160リクエストをクライアントに10分000リクエストを与えるのではなく、毎分160リクエストを与え、この方法は、彼らが行うことができた最悪の10時間000の代わりに時間の短い期間で160リクエストです.
同じノートでは、100以上のリクエストのように、非常に長い期間のための制限を設定しないでください、非常にイライラしているAPIを使用しているように、あなたは100リクエストを行うと、今では再び期限が切れるために一日を待つ必要がありますので、人々がより均等に負荷を広げることができる小さな期間を使用します.
特定のクライアントからのトラフィックを完全にブロックする動的拒否リストも、このようなシステム上にある必要があります.あなたは、あなたが速度制限(NGNX/Apache Serverのような)を実行するアプリの前に座っているプロキシで使用する設定をすることができるか、あなたが使用しているCDNで(これは、CloudFlareのように)、あなたがすぐに既知の乱用者からすべてのトラフィックをブラックホールすることができるようにすることができました.
我々がここで構築した実装は失敗した解決です.そして、それはどんなエラー(REDISと話していて、抽出者を走らせて)は要求を拒否させます.これは、主にパラノイアの実装が安全であるが、サービスに最適な解決策ではないかもしれないからです.
これがよりユーザーフレンドリーであるかもしれない間、悪用がレートリミッターをオーバーロードすることができて、それから彼らが現在レートリミッターで保護を全く持っていないので、それの後でシステムをオーバーロードすることができた危険に加えます.ので、完全に失敗オープンソリューションに移動する前に、レートリミッターが失敗し、スタックのすべての部分に監視されている場合は、他の緩和策を持っていることを確認、両方のアプリとRedisサーバーので、迅速に通知している場合、それらのいずれかの失敗を開始し、トラフィックを行う.あなたは、彼らが彼らが要求を受け入れたいならば、彼らが決定をすることができるように、彼らが彼らがレート制限が失敗したことを知っているようにするために余分なHTTPヘッダーを加えるかもしれません、しかし、要求はまだ送られました.
また、料金制限を使用してあなたの主な目標は、あなたがほとんどのユーザーに可能な限り最高のサービスを提供しているように、あなたのシステムを保護する必要がありますし、いくつかの悪意のあるクライアントは、良いユーザーがそれらを使用して防ぐためにあなたのアプリケーションに大混乱を巻き起こすことを防ぎます.
レート制限の背後にあるアイデアは、あなたがクライアントから撮影される要求の最大量を持っているということです、一度制限が達すると、定義された期間にわたって、あなたは期間の終わりに到達し、カウンタを再起動するまで要求を削除を開始します.たとえば、クライアントは60分以上の要求をすると、要求を拒否し始めると、クライアントは60分のリクエストをすることができます.
ここでの目標は、あなたのサービスをより信頼性の高いものにすることです.料金制限は、あなたの側で実装された保護です.悪い俳優か間違って設定されたクライアントがサービスがその予想された使用限界にわたっているので、サービス全体を降ろすことができないか、停電を引き起こすことができないようにすることです.理想的なシナリオでは、彼らは通常の仕事をするのに十分な要求を与えているように良いクライアントを罰していない、しかし、あなたは悪いクライアントをブロックし、サービスに大混乱をwreaking.
プロジェクトのソースコードを見つけることができますhere on Github .
レート制限器の構築
私たちは、さまざまな実装の長所と短所を議論するためにRateリミッタのために複数の実現を持っています.
package redis_rate_limiter
import (
"context"
"time"
)
// Request defines a request that needs to be checked if it will be rate-limited or not.
// The `Key` is the identifier you're using for the client making calls. This could be a user/account ID if the user is
// signed into your application, the IP of the client making requests (this might not be reliable if you're not behind a
// proxy like Cloudflare, as clients can try to spoof IPs). The `Key` should be the same for multiple calls of the
// same client so we can correctly identify that this is the same app calling anywhere.
// `Limit` is the number of requests the client is allowed to make over the `Duration` period. If you set this to
// 100 and `Duration` to `1m` you'd have at most 100 requests over a minute.
type Request struct {
Key string
Limit uint64
Duration time.Duration
}
// State is the result of evaluating the rate limit, either `Deny` or `Allow` a request.
type State int64
const (
Deny State = 0
Allow = 1
)
// Result represents the response to a check if a client should be rate-limited or not. The `State` will be either
// `Allow` or `Deny`, `TotalRequests` holds the number of requests this specific caller has already made over
// the current period and `ExpiresAt` defines when the rate limit will expire/roll over for clients that
// have gone over the limit.
type Result struct {
State State
TotalRequests uint64
ExpiresAt time.Time
}
// Strategy is the interface the rate limit implementations must implement to be used, it takes a `Request` and
// returns a `Result` and an `error`, any errors the rate-limiter finds should be bubbled up so the code can make a
// decision about what it wants to do with the request.
type Strategy interface {
Run(ctx context.Context, r *Request) (*Result, error)
}
カウンタベースの実装
それで、我々には基本的な場所があります.そして、入力、出力とあらゆるカウンタによって実行される必要がある小さなインターフェースです.
package redis_rate_limiter
import (
"context"
"github.com/go-redis/redis/v8"
"github.com/pkg/errors"
"time"
)
var (
_ Strategy = &counterStrategy{}
)
const (
keyWithoutExpire = -1
)
func NewCounterStrategy(client *redis.Client, now func() time.Time) *counterStrategy {
return &counterStrategy{
client: client,
now: now,
}
}
type counterStrategy struct {
client *redis.Client
now func() time.Time
}
// Run this implementation uses a simple counter with an expiration set to the rate limit duration.
// This implementation is functional but not very effective if you have to deal with bursty traffic as
// it will still allow a client to burn through its full limit quickly once the key expires.
func (c *counterStrategy) Run(ctx context.Context, r *Request) (*Result, error) {
// a pipeline in Redis is a way to send multiple commands that will all be run together.
// this is not a transaction and there are many ways in which these commands could fail
// (only the first, only the second) so we have to make sure all errors are handled, this
// is a network performance optimization.
p := c.client.Pipeline()
incrResult := p.Incr(ctx, r.Key)
ttlResult := p.TTL(ctx, r.Key)
if _, err := p.Exec(ctx); err != nil {
return nil, errors.Wrapf(err, "failed to execute increment to key %v", r.Key)
}
totalRequests, err := incrResult.Result()
if err != nil {
return nil, errors.Wrapf(err, "failed to increment key %v", r.Key)
}
var ttlDuration time.Duration
// we want to make sure there is always an expiration set on the key, so on every
// increment we check again to make sure it has a TTl and if it doesn't we add one.
// a duration of -1 means that the key has no expiration so we need to make sure there
// is one set, this should, most of the time, happen when we increment for the
// first time but there could be cases where we fail at the previous commands so we should
// check for the TTL on every request.
if d, err := ttlResult.Result(); err != nil || d == keyWithoutExpire {
ttlDuration = r.Duration
if err := c.client.Expire(ctx, r.Key, r.Duration).Err(); err != nil {
return nil, errors.Wrapf(err, "failed to set an expiration to key %v", r.Key)
}
} else {
ttlDuration = d
}
expiresAt := c.now().Add(ttlDuration)
requests := uint64(totalRequests)
if requests > r.Limit {
return &Result{
State: Deny,
TotalRequests: requests,
ExpiresAt: expiresAt,
}, nil
}
return &Result{
State: Allow,
TotalRequests: requests,
ExpiresAt: expiresAt,
}, nil
}
この実装は通常、固定ウィンドウ戦略と呼ばれています、それは期限がいったん設定されるならば、満了時間が到着するまで、制限に達するクライアントは更なる要求をすることからブロックされることを意味します.クライアントが毎分50リクエストの制限を持ち、分の最初の5秒ですべての50リクエストを行う場合、それは別の要求を行うために55秒を待つ必要があります.これはまた、この実装の主な欠点であり、クライアントは、その制限を完全に制限(バースト性のトラフィック)を介して燃やさせるだろうし、それはまだ、このトラフィックは、全体の制限期間中に広がることを期待する可能性がありますので、あなたのサービスをオーバーロードする可能性があります.バースト交通により良い反応をするソート集合実装
それは完全にカウンタをリセットしないようにローリングウィンドウの戦略は、バーストウィンドウのためのより良い保護を提供していますが、時間ウィンドウの期間の履歴を保持します.あなたは5分のウィンドウを持っている場合、それは常にクライアントがブロックする必要があるかどうかを決定するために最後の5分で発生したトラフィックの量をカウントするだけではなく、代わりにキーを有効期限を待っている.メモリのより多くの情報(そのタイムスタンプを持つすべての要求)を保つ必要がありますが、トラフィックの迅速なバーストに対してより良い保護を提供します.
以下のようになります.
package redis_rate_limiter
import (
"context"
"github.com/go-redis/redis/v8"
"github.com/google/uuid"
"github.com/pkg/errors"
"strconv"
"time"
)
var (
_ Strategy = &sortedSetCounter{}
)
func NewSortedSetCounterStrategy(client *redis.Client, now func() time.Time) Strategy {
return &sortedSetCounter{
client: client,
now: now,
}
}
type sortedSetCounter struct {
client *redis.Client
now func() time.Time
}
// Run this implementation uses a sorted set that holds a UUID for every request with a score that is the
// time the request has happened. This allows us to delete events from *before* the current window if the window
// is 5 minutes, we want to remove all events from before 5 minutes ago, this way we make sure we roll old
// requests off the counters creating a rolling window for the rate limiter. So, if your window is 100 requests
// in 5 minutes and you spread the load evenly across the minutes, once you hit 6 minutes the requests you made
// on the first minute have now expired but the other 4 minutes of requests are still valid.
// A rolling window counter is usually never 0 if traffic is consistent so it is very effective at preventing
// bursts of traffic as the counter won't ever expire.
func (s *sortedSetCounter) Run(ctx context.Context, r *Request) (*Result, error) {
now := s.now()
// every request needs an UUID
item := uuid.New()
minimum := now.Add(-r.Duration)
p := s.client.Pipeline()
// we then remove all requests that have already expired on this set
removeByScore := p.ZRemRangeByScore(ctx, r.Key, "0", strconv.FormatInt(minimum.UnixMilli(), 10))
// we add the current request
add := p.ZAdd(ctx, r.Key, &redis.Z{
Score: float64(now.UnixMilli()),
Member: item.String(),
})
// count how many non-expired requests we have on the sorted set
count := p.ZCount(ctx, r.Key, "-inf", "+inf")
if _, err := p.Exec(ctx); err != nil {
return nil, errors.Wrapf(err, "failed to execute sorted set pipeline for key: %v", r.Key)
}
if err := removeByScore.Err(); err != nil {
return nil, errors.Wrapf(err, "failed to remove items from key %v", r.Key)
}
if err := add.Err(); err != nil {
return nil, errors.Wrapf(err, "failed to add item to key %v", r.Key)
}
totalRequests, err := count.Result()
if err != nil {
return nil, errors.Wrapf(err, "failed to count items for key %v", r.Key)
}
expiresAt := now.Add(r.Duration)
requests := uint64(totalRequests)
if requests > r.Limit {
return &Result{
State: Deny,
TotalRequests: requests,
ExpiresAt: expiresAt,
}, nil
}
return &Result{
State: Allow,
TotalRequests: requests,
ExpiresAt: expiresAt,
}, nil
}
ここでの目標はソートのキーとしてリクエストの現在のタイムスタンプを使用して、特定の時間範囲の下ですべてのリクエストをすばやく要求することができます.私たちがUUIDを値として使用しているので、ソートされたセットでこれらの要求が互いに矛盾しているというオッズは非常に低く、1回ごとに衝突をすることはあまりにも多くのトラブルではありません.重い解決策を書く
ここでの両方の実装は最初に書きます、彼らはRedisに最初にものを書きます、そして、要求が受け入れられるかどうかチェックしてください.これがコードをより簡単にして、クライアントからサーバーまでより少ないネットワークトラフィックを生成する間、彼らは、サーバーが動作するためにずっと高価になります.私たちは、これらのソートされたセットがクリーンアップされているか、またはどのように多くのリクエストを格納することができます上に制限があることを確認していない、これはあなたが取り組んでいるシステム上で発生するべきではない一般的な赤いフラグです.
私たちは、ここでのリソース使用が制限されていることを確認しなければなりません、すなわち、それはよく知られている制限を持っています、あなたがいつ書いている何かが何らかの方法で(意図的に、または、偶然に)虐待されそうであると仮定しなければならなくて、あなたのコードが何らかの方法でこれらの資源に縛られるのを確実にするべきです.Redis自体では、Aを設定することによってメモリを使い果たすつもりはないことを確認する多くの方法がありますmaxmemory どれだけのメモリが使えるかの値maxmemory-policy でない値に
no-eviction
. 我々がここで建設しているもののようなレートリミッターのためにallkeys-lru
はかなり適切なオプションです.有界カウンタ実装
read - before - writeで更新されたカウンタの実装は以下のようになります.
package redis_rate_limiter
import (
"context"
"github.com/go-redis/redis/v8"
"github.com/pkg/errors"
"time"
)
var (
_ Strategy = &counterStrategy{}
)
const (
keyThatDoesNotExist = -2
keyWithoutExpire = -1
)
func NewCounterStrategy(client *redis.Client, now func() time.Time) *counterStrategy {
return &counterStrategy{
client: client,
now: now,
}
}
type counterStrategy struct {
client *redis.Client
now func() time.Time
}
// Run this implementation uses a simple counter with an expiration set to the rate limit duration.
// This implementation is functional but not very effective if you have to deal with bursty traffic as
// it will still allow a client to burn through its full limit quickly once the key expires.
func (c *counterStrategy) Run(ctx context.Context, r *Request) (*Result, error) {
// a pipeline in redis is a way to send multiple commands that will all be run together.
// this is not a transaction and there are many ways in which these commands could fail
// (only the first, only the second) so we have to make sure all errors are handled, this
// is a network performance optimization.
// here we try to get the current value and also try to set an expiration on it
getPipeline := c.client.Pipeline()
getResult := getPipeline.Get(ctx, r.Key)
ttlResult := getPipeline.TTL(ctx, r.Key)
if _, err := getPipeline.Exec(ctx); err != nil && !errors.Is(err, redis.Nil) {
return nil, errors.Wrapf(err, "failed to execute pipeline with get and ttl to key %v", r.Key)
}
var ttlDuration time.Duration
// we want to make sure there is always an expiration set on the key, so on every
// increment we check again to make sure it has a TTl and if it doesn't we add one.
// a duration of -1 means that the key has no expiration so we need to make sure there
// is one set, this should, most of the time, happen when we increment for the
// first time but there could be cases where we fail at the previous commands so we should
// check for the TTL on every request.
// a duration of -2 means that the key does not exist, given we're already here we should set an expiration
// to it anyway as it means this is a new key that will be incremented below.
if d, err := ttlResult.Result(); err != nil || d == keyWithoutExpire || d == keyThatDoesNotExist {
ttlDuration = r.Duration
if err := c.client.Expire(ctx, r.Key, r.Duration).Err(); err != nil {
return nil, errors.Wrapf(err, "failed to set an expiration to key %v", r.Key)
}
} else {
ttlDuration = d
}
expiresAt := c.now().Add(ttlDuration)
if total, err := getResult.Uint64(); err != nil && errors.Is(err, redis.Nil) {
} else if total >= r.Limit {
return &Result{
State: Deny,
TotalRequests: total,
ExpiresAt: expiresAt,
}, nil
}
incrResult := c.client.Incr(ctx, r.Key)
totalRequests, err := incrResult.Uint64()
if err != nil {
return nil, errors.Wrapf(err, "failed to increment key %v", r.Key)
}
if totalRequests > r.Limit {
return &Result{
State: Deny,
TotalRequests: totalRequests,
ExpiresAt: expiresAt,
}, nil
}
return &Result{
State: Allow,
TotalRequests: totalRequests,
ExpiresAt: expiresAt,
}, nil
}
今、我々は増加する前に、我々はこれが許容される良い要求である場合にのみ増加することを確認してください.さもなければ、何もしようとする前に、要求を保釈して、否定してください.次に、更新されたソート済みの実装を見ます.
package redis_rate_limiter
import (
"context"
"github.com/go-redis/redis/v8"
"github.com/google/uuid"
"github.com/pkg/errors"
"strconv"
"time"
)
var (
_ Strategy = &sortedSetCounter{}
)
const (
sortedSetMax = "+inf"
sortedSetMin = "-inf"
)
func NewSortedSetCounterStrategy(client *redis.Client, now func() time.Time) Strategy {
return &sortedSetCounter{
client: client,
now: now,
}
}
type sortedSetCounter struct {
client *redis.Client
now func() time.Time
}
// Run this implementation uses a sorted set that holds an UUID for every request with a score that is the
// time the request has happened. This allows us to delete events from *before* the current window, if the window
// is 5 minutes, we want to remove all events from before 5 minutes ago, this way we make sure we roll old
// requests off the counters creating a rolling window for the rate limiter. So, if your window is 100 requests
// in 5 minutes and you spread the load evenly across the minutes, once you hit 6 minutes the requests you made
// on the first minute have now expired but the other 4 minutes of requests are still valid.
// A rolling window counter is usually never 0 if traffic is consistent so it is very effective at preventing
// bursts of traffic as the counter won't ever expire.
func (s *sortedSetCounter) Run(ctx context.Context, r *Request) (*Result, error) {
now := s.now()
expiresAt := now.Add(r.Duration)
minimum := now.Add(-r.Duration)
// first count how many requests over the period we're tracking on this rolling window so check wether
// we're already over the limit or not. this prevents new requests from being added if a client is already
// rate limited, not allowing it to add an infinite amount of requests to the system overloading redis.
// if the client continues to send requests it also means that the memory for this specific key will not
// be reclaimed (as we're not writing data here) so make sure there is an eviction policy that will
// clear up the memory if the redis starts to get close to its memory limit.
result, err := s.client.ZCount(ctx, r.Key, strconv.FormatInt(minimum.UnixMilli(), 10), sortedSetMax).Uint64()
if err == nil && result >= r.Limit {
return &Result{
State: Deny,
TotalRequests: result,
ExpiresAt: expiresAt,
}, nil
}
// every request needs an UUID
item := uuid.New()
p := s.client.Pipeline()
// we then remove all requests that have already expired on this set
removeByScore := p.ZRemRangeByScore(ctx, r.Key, "0", strconv.FormatInt(minimum.UnixMilli(), 10))
// we add the current request
add := p.ZAdd(ctx, r.Key, &redis.Z{
Score: float64(now.UnixMilli()),
Member: item.String(),
})
// count how many non-expired requests we have on the sorted set
count := p.ZCount(ctx, r.Key, sortedSetMin, sortedSetMax)
if _, err := p.Exec(ctx); err != nil {
return nil, errors.Wrapf(err, "failed to execute sorted set pipeline for key: %v", r.Key)
}
if err := removeByScore.Err(); err != nil {
return nil, errors.Wrapf(err, "failed to remove items from key %v", r.Key)
}
if err := add.Err(); err != nil {
return nil, errors.Wrapf(err, "failed to add item to key %v", r.Key)
}
totalRequests, err := count.Result()
if err != nil {
return nil, errors.Wrapf(err, "failed to count items for key %v", r.Key)
}
requests := uint64(totalRequests)
if requests > r.Limit {
return &Result{
State: Deny,
TotalRequests: requests,
ExpiresAt: expiresAt,
}, nil
}
return &Result{
State: Allow,
TotalRequests: requests,
ExpiresAt: expiresAt,
}, nil
}
Redisサーバにリクエストを追加する前に、私たちは、すでに期間の間のリクエストが多すぎたかどうかを確認します.HTTPミドルウェアとしての統合
我々は2つの別々のカウンタを実装したので、どのように我々はそれらを利用するのですか?
それを行う方法の1つは、既存のHTTPハンドラーをラップするミドルウェアハンドラを作成することで、レート制限機能を追加することです.これにより、必要なときに速度制限付きハンドラを作成することができます.
package redis_rate_limiter
import (
"fmt"
"net/http"
"strconv"
"strings"
"time"
)
var (
_ http.Handler = &httpRateLimiterHandler{}
_ Extractor = &httpHeaderExtractor{}
stateStrings = map[State]string{
Allow: "Allow",
Deny: "Deny",
}
)
const (
rateLimitingTotalRequests = "Rate-Limiting-Total-Requests"
rateLimitingState = "Rate-Limiting-State"
rateLimitingExpiresAt = "Rate-Limiting-Expires-At"
)
// Extractor represents the way we will extract a key from an HTTP request, this could be
// a value from a header, request path, method used, user authentication information, any information that
// is available at the HTTP request that wouldn't cause side effects if it was collected (this object shouldn't
// read the body of the request).
type Extractor interface {
Extract(r *http.Request) (string, error)
}
type httpHeaderExtractor struct {
headers []string
}
// Extract extracts a collection of http headers and joins them to build the key that will be used for
// rate limiting. You should use headers that are guaranteed to be unique for a client.
func (h *httpHeaderExtractor) Extract(r *http.Request) (string, error) {
values := make([]string, 0, len(h.headers))
for _, key := range h.headers {
// if we can't find a value for the headers, give up and return an error.
if value := strings.TrimSpace(r.Header.Get(key)); value == "" {
return "", fmt.Errorf("the header %v must have a value set", key)
} else {
values = append(values, value)
}
}
return strings.Join(values, "-"), nil
}
// NewHTTPHeadersExtractor creates a new HTTP header extractor
func NewHTTPHeadersExtractor(headers ...string) Extractor {
return &httpHeaderExtractor{headers: headers}
}
// RateLimiterConfig holds the basic config we need to create a middleware http.Handler object that
// performs rate limiting before offloading the request to an actual handler.
type RateLimiterConfig struct {
Extractor Extractor
Strategy Strategy
Expiration time.Duration
MaxRequests uint64
}
// NewHTTPRateLimiterHandler wraps an existing http.Handler object performing rate limiting before
// sending the request to the wrapped handler. If any errors happen while trying to rate limit a request
// or if the request is denied, the rate limiting handler will send a response to the client and will not
// call the wrapped handler.
func NewHTTPRateLimiterHandler(originalHandler http.Handler, config *RateLimiterConfig) http.Handler {
return &httpRateLimiterHandler{
handler: originalHandler,
config: config,
}
}
type httpRateLimiterHandler struct {
handler http.Handler
config *RateLimiterConfig
}
func (h *httpRateLimiterHandler) writeRespone(writer http.ResponseWriter, status int, msg string, args ...interface{}) {
writer.Header().Set("Content-Type", "text/plain")
writer.WriteHeader(status)
if _, err := writer.Write([]byte(fmt.Sprintf(msg, args...))); err != nil {
fmt.Printf("failed to write body to HTTP request: %v", err)
}
}
// ServeHTTP performs rate limiting with the configuration it was provided and if there were not errors
// and the request was allowed it is sent to the wrapped handler. It also adds rate limiting headers that will be
// sent to the client to make it aware of what state it is in terms of rate limiting.
func (h *httpRateLimiterHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
key, err := h.config.Extractor.Extract(request)
if err != nil {
h.writeRespone(writer, http.StatusBadRequest, "failed to collect rate limiting key from request: %v", err)
return
}
result, err := h.config.Strategy.Run(request.Context(), &Request{
Key: key,
Limit: h.config.MaxRequests,
Duration: h.config.Expiration,
})
if err != nil {
h.writeRespone(writer, http.StatusInternalServerError, "failed to run rate limiting for request: %v", err)
return
}
// set the rate limiting headers both on allow or deny results so the client knows what is going on
writer.Header().Set(rateLimitingTotalRequests, strconv.FormatUint(result.TotalRequests, 10))
writer.Header().Set(rateLimitingState, stateStrings[result.State])
writer.Header().Set(rateLimitingExpiresAt, result.ExpiresAt.Format(time.RFC3339))
// when the state is Deny, just return a 429 response to the client and stop the request handling flow
if result.State == Deny {
h.writeRespone(writer, http.StatusTooManyRequests, "you have sent too many requests to this service, slow down please")
return
}
// if the request was not denied we assume it was allowed and call the wrapped handler.
// by leaving this to the end we make sure the wrapped handler is only called once and doesn't have to worry
// about any rate limiting at all (it doesn't even have to know there was rate limiting happening for this request)
// as we have already set the headers, so when the handler flushes the response the headers above will be sent.
h.handler.ServeHTTP(writer, request)
}
それで、我々には何がありますか?私たちはExtractor
レート制限チェックのキーとして使用する文字列を返すインターフェイスです.HTTPインターフェイスやHTTPリクエストで利用可能な他のフィールドをチェックして、クライアントを識別するためにこのインターフェイスの複数の実装を行うことができます.これを使用する最良の方法は、クッキーやヘッダーからいくつかの周波数でIPアドレスの変更としてプルアップすることができるユーザー/アカウントIDを持っているため、アプリケーションに対して意味のある解決策を実装します.それから、我々は
RateLimiterConfig
リクエストのレート制限を実行するために必要な全てのフィールドをラップする構造体Extractor
, エーStrategy
(私たちのカウンタ)、クライアントがどのように多くのリクエストを行うことができますどのくらいの.これとすべてでhttp.Handler
ラップするには、完全に機能するHTTP Rateリミッターミドルウェアを持っていて、このハンドラーがどのように構築されているかを変更するだけで、この同じパターンを使用して他のリクエスト/レスポンスプロトコルの同じ種類のミドルウェアを構築することができます.何を心配するべきですか。
まず、前に述べたように、バースト交通.あまりにも高速に全体の制限を使用するクライアントは、まだあなたのシステムのための問題になるだろうし、ローリングウィンドウリミッターが少しこれを減少させながら、それはあなたがトラフィックの雷の群れを防ぐためにやっている唯一のものではない必要があります.もう一つの可能な解決策は、より長い期間を使用するように、代わりに毎時160リクエストをクライアントに10分000リクエストを与えるのではなく、毎分160リクエストを与え、この方法は、彼らが行うことができた最悪の10時間000の代わりに時間の短い期間で160リクエストです.
同じノートでは、100以上のリクエストのように、非常に長い期間のための制限を設定しないでください、非常にイライラしているAPIを使用しているように、あなたは100リクエストを行うと、今では再び期限が切れるために一日を待つ必要がありますので、人々がより均等に負荷を広げることができる小さな期間を使用します.
特定のクライアントからのトラフィックを完全にブロックする動的拒否リストも、このようなシステム上にある必要があります.あなたは、あなたが速度制限(NGNX/Apache Serverのような)を実行するアプリの前に座っているプロキシで使用する設定をすることができるか、あなたが使用しているCDNで(これは、CloudFlareのように)、あなたがすぐに既知の乱用者からすべてのトラフィックをブラックホールすることができるようにすることができました.
我々がここで構築した実装は失敗した解決です.そして、それはどんなエラー(REDISと話していて、抽出者を走らせて)は要求を拒否させます.これは、主にパラノイアの実装が安全であるが、サービスに最適な解決策ではないかもしれないからです.
これがよりユーザーフレンドリーであるかもしれない間、悪用がレートリミッターをオーバーロードすることができて、それから彼らが現在レートリミッターで保護を全く持っていないので、それの後でシステムをオーバーロードすることができた危険に加えます.ので、完全に失敗オープンソリューションに移動する前に、レートリミッターが失敗し、スタックのすべての部分に監視されている場合は、他の緩和策を持っていることを確認、両方のアプリとRedisサーバーので、迅速に通知している場合、それらのいずれかの失敗を開始し、トラフィックを行う.あなたは、彼らが彼らが要求を受け入れたいならば、彼らが決定をすることができるように、彼らが彼らがレート制限が失敗したことを知っているようにするために余分なHTTPヘッダーを加えるかもしれません、しかし、要求はまだ送られました.
また、料金制限を使用してあなたの主な目標は、あなたがほとんどのユーザーに可能な限り最高のサービスを提供しているように、あなたのシステムを保護する必要がありますし、いくつかの悪意のあるクライアントは、良いユーザーがそれらを使用して防ぐためにあなたのアプリケーションに大混乱を巻き起こすことを防ぎます.
Reference
この問題について(Redisを用いた移動におけるHTTP要求の制限), 我々は、より多くの情報をここで見つけました https://dev.to/mauriciolinhares/rate-limiting-http-requests-in-go-using-redis-51m7テキストは自由に共有またはコピーできます。ただし、このドキュメントのURLは参考URLとして残しておいてください。
Collection and Share based on the CC Protocol