最近、私は積極的にアーランに従事し始めました。 さて、いつものように、資料を統合するために、 Mochiwebに簡単なWebサービスを実装しました 。 MochiwebはWebアプリケーションを作成するための非常にまともなフレームワークですが、1つのクライアントからのリクエストの数を制限する機能が見つかりませんでした。 だから私は自分でやった。
なぜなら クエリ速度制限機能は完全に分離されており、特定のタスクに結び付けられていないため、独立したアプリケーションで作成されたモジュールを選択し、そのソースコードをレイアウトすることにしました。
挑戦する
したがって、 Erlang / OTP 、Mochiweb、rebarがあります。 特定のユーザーからのリクエストの数を読み取り、リクエストが頻繁に送信される場合は413エラーコードを伝えたいと思います。 クライアントはIPアドレスによって識別されます。
mochiweb_request:get(peer).
を返す
mochiweb_request:get(peer).
このタスクはそれほど難しくありませんが、おそらくターンキーソリューションが誰かの時間を節約するでしょう。
アプローチ
トークンバケットアルゴリズムを使用することにしました。
簡単に説明すると、各クライアントの特定の時間間隔を、この時間間隔のユーザー要求を追加するバスケットと見なします。 バスケットの数量は限られています。 バスケットが制限までリクエストで詰まっているとすぐに、クライアントへのサービスを停止します。 なぜなら 時間が経ち、クライアントのバスケットは常に変化しており、クライアントは新しい時間間隔でサービスを受ける機会があります。
上の図では、赤は各顧客にサービスが提供されなかった時間を示しています。
写真からわかるように、クライアントAはしばらく前にリクエスト制限を受信し、それ以上送信しませんでした。クライアントBは静かに動作し、制限を超えません。クライアントBは現時点でリクエスト制限を選択し、拒否を受信しています。
クライアントからのリクエストが表示されると、時間を考慮してバスケットを作成します。 各クライアントは、独自のタイムスケールを持つことができ、バスケットの寿命を読み取ります。 バスケットのボリューム、つまりリクエストの制限は、クライアントごとに個別に設定できます。
顧客バスケットに関する情報を辞書に保存します。
キーとしての
IP + №
counter
-実際には、要求カウンター
クライアントのAPIは、1つの関数
check_rate(IP, TimeScale, Limit)
構成されています。
どこで
IP
はクライアントのIPアドレスです(ただし、任意の識別子を使用できます)
TimeScale
バスケットの有効期間(ミリ秒)。
Limit
-バスケット内のリクエストの最大数
この関数を呼び出すと、カウンターが増加し、リクエストを処理するかどうかをクライアントに返します。
実装
データを保存するには、
ordered_set
型のETSテーブルを使用します。 キーでソートされ、重複キーは許可されません。 テーブルはレコードです
[BucketID, Counter, CreatedAt, ModifiedAt]
どこで
BucketID
{IP、BucketNum}は、ordered_setのキーでもあります
Counter
-クライアントからのリクエストの数
CreatedAt
バスケットを作成する時間(ミリ秒)
ModifiedAt
カート変更時間(ミリ秒)
作成時間はデバッグのためにより多く、あなたはそれを拒否することができます
主な機能:
-spec count_hit(Id::binary(), Scale::integer(), Limit::integer()) -> {ok, continue} | {fail, Count::integer()}. %% Counts request by ID and blocks it if rate limiter fires %% ID of the client %% Scale of time (1000 = new bucket every second, 60000 = bucket every minute) %% Limit - max size of bucket count_hit(Id, Scale, Limit) -> Stamp = timestamp(), %% milliseconds since 00:00 GMT, January 1, 1970 BucketNumber = trunc(Stamp / Scale), %% with scale=1 bucket changes every millisecond Key = {BucketNumber, Id}, case ets:member(?RATERLIMITER_TABLE, Key) of false -> ets:insert(?RATERLIMITER_TABLE, {Key, 1, Stamp, Stamp }), {ok, continue}; true -> % increment counter by 1, created_time by 0, and changed_time by current one Counters = ets:update_counter(?RATERLIMITER_TABLE, Key, [{2,1},{3,0},{4,1,0, Stamp}]), % Counter, created at, changed at [BucketSize, _, _] = Counters, if (BucketSize > Limit) -> {fail, Limit}; true -> {ok, continue} end end.
まず、バスケット{IP、BucketNumber}があるかどうかを確認します。
新しいバスケットの場合、すべてがかなり平凡です-初期値で新しいバスケットを作成し、OKと言います。
既存のバスケットにカウンターを追加することはもう少し興味深いです。 etsモジュールを使用すると、指定されたルールに従ってカウンターを変更できます 。 カウンターの増分と最後の呼び出しの日付の変更をいかにエレガントに組み合わせることができるかが気に入りました。
だからコード:
Counters = ets:update_counter(?RATERLIMITER_TABLE, Key, [{2,1},{3,0},{4,1,0, Stamp}])
カウンター番号2を1増やします(これは単なるヒットカウンターです)。作成時間に0を追加し、最大値を確認しながらカウンター番号4に1を追加します。 なぜなら 最大値は0で、4番目のカウンターは常にStamp(現在時刻)に設定されます。 出力では、すべての変更可能なカウンターのリストを順番に取得します。
[Counter, CreatedTime, ChangedTime]
その結果、テーブルへの2つの呼び出しに対して、バスケットを更新または作成し、出力で呼び出しの数を受け取りました。 現在のraterlimiterの実装では、テーブルの変更の原子性は重要ではなく、同期して動作します( gen_server:call / 2の呼び出しは同期です)。
古いゴミを取り除く
時間が経つにつれて、顧客のバスケットはすべて積み重なります。 それらを削除する必要があります。
削除は定期的に行われます。 単に、指定された制限より古いバスケットはすべて削除されます。
アーランでは、すべてが美しく簡潔に見えます-
remove_old_limiters(Timeout) -> NowStamp = timestamp(), Matcher = ets:fun2ms(fun ({_,_,_,Accessed}) when Accessed < (NowStamp - Timeout) -> true end), ets:select_delete(?RATERLIMITER_TABLE, Matcher).
ets:select_deleteは、一致関数( MatchSpec )に一致するすべてのレコードを削除します。 これらのmatch_specを手動で記述するのは地獄です。 ets:fun2ms関数を使用する方がはるかに簡単です。これは、通常のErlang関数を通信関数に変換します。 たとえば、上記の関数
fun ({_,_,_,Accessed}) when Accessed < (NowStamp - Timeout) -> true end
MatchSpec形式の最終形式では、NowStamp = 1000の場合、Timeout = 0は次のようになります
[{{'_','_','_','$1'},[{'<','$1',{'-',1000,0}}],[true]}]
etsを使用するときの主なこと:fun2msは 、ソースに追加のファイルを含めることを忘れないことです-
-include_lib("stdlib/include/ms_transform.hrl").
使用する
使用量は、raterlimiterアプリケーション自体を起動するためのコードと設定をカウントせずに、check_rateの1回の呼び出しに削減されます。 Mochiwebベースのアプリケーションの例を次に示します。
{Status, _Reason} = raterlimiter:check_rate(Req:get(peer), 30000, 500), % 500 requests per 30 seconds allowed case Status of ok -> serve_request(Req, DocRoot, Path); _ -> % client wants too much information Req:respond({413,[{"Content-Type", "text/html"}, {"Retry-After", "900 (Too many requests)"}], " IP , 15 ."}) end
完全なコードはGithubで入手できます。 そこで、エラーを報告するか、プルリクエストを行うことができます。