NGINX,NGINX Plusを使った流量制御 (from Official Blog)

https://cdn-1.wp.nginx.com/wp-content/themes/nginx-theme/assets/img/logo.png

本記事は、下記ブログ記事を翻訳したものです。

www.nginx.com

実案件で同様の機能を実装しており、ぜひ便利さを広めたく、
執筆者の方に許可をとって翻訳させていただきました。

もし翻訳誤りなどありましたら、下記までご連絡いただければ幸いです。

twitter.com

Queueing with nodelay,White Listingは大好きな使い方なので、
ぜひご覧いただきたいと思います。 (nodelayは、翻訳能力の問題で表現がかなりわかりにくいので、別記事で書こうと思います。。。)

以下本文です。


最も便利な機能の一つだが、しばしば勘違い・設定誤りされるNGINXの機能が、Rate Limitingです。
Rate Limitingによって、特定の期間に受け付けるHTTPリクエストの量を制御できます。
対象のメソッドは、GETリクエストのようにシンプルなもの、また、ログインフォームなどに使われるPOSTリクエストです。

Rate Limitingは、ブルートフォースアタックのリクエスト量を制限するなど、セキュリティ対策を目的として利用されるケースがあります。
上記のような例には、 protect against DDoS attacksが参考になるでしょう。
また、一般的には、大量のリクエストから後段のアプリケーションサーバを守る目的にも利用されます。

本記事では、NGINXを使ったRate Limitingの基本から、拡張的な設定について解説します。

How NGINX Rate Limiting Works

Rate Limitingは、電話通信やパケット交換方式にて帯域を制限するために広く使われているLeaky Bucketアルゴリズムを採用しています。
アルゴリズムは、バケツに水がそそがれ、バケツにあいた穴から水が漏れだすようなイメージです。もし、注がれる水の量が漏れ出す水の量を超過すると、バケツから水があふれるでしょう。
リクエストを処理する場合を想定しましょう。
バケツに注がれる水はユーザからのリクエストであり、バケツのサイズは、ユーザリクエストをFIFOアルゴリズムで処理させるために使われるキューです。
また、漏れ出す水はサーバに処理させるためにバッファから出るリクエストであり、あふれる水は、破棄され・処理されることのないリクエストです。

Configuring Basic Rate Limiting

Rate Limitingは2つのディレクティブ(limit_req,limit_req_zone)により設定されます。
下記が設定例です。

limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s;

server {
    location /login/ {
        limit_req zone=mylimit;

        proxy_pass http://my_upstream;
    }
}

limit_req_zone ディレクティブにて流量制御のパラメータを設定し、limit_req ディレクティブが定義された箇所にて流量制御が有効化されます。
(サンプルの場合、/login/ロケーションへのすべてのリクエストが対象です。)

limit_req_zone は、複数の箇所(Location)で利用できるよう、httpブロックに定義されることが一般的です。

設定には、下記3つのパラメータが必要です。

  • Key - 制御対象のリクエスト識別子を定義します。例として、NGINXの変数である$binary_remote_addr(ClientのIPアドレスをbinary化したデータを持つ)を使うケースが挙げられます。これは、それぞれのユニークなIPアドレスをキーとして、Rate パラメータの値で流量制御することを意味します。(我々はメモリ使用量を削減するため、クライアントのIPアドレスを示す$remote_addrではなく、$binary_remote_addrを使用しています。)
  • Zone - それぞれのIPアドレスがどの程度の間隔でリクエストしているか保存するための、共有メモリの領域とサイズを定義します。このメモリ領域は、それぞれのWorkerプロセスにて共有されます。この定義は、共有メモリの名前を定義するzone部と、コロン(:)で区切られたsize部の2パートを持ちます。おおよそ、16,000個のIPアドレスを保持するため場合、1MBの領域を使用します。つまり、上記の設定では160,000のIPアドレスを保持できます。もし容量が枯渇した場合、使用頻度が最も低いキーが削除されます。
  • Rate - 最大リクエスト量を定義します。例では、流量は10request/secを超過することができません。NGINXはリクエストをmsecの粒度で記録しており、この例の場合、100msecに1度のみリクエストされるよう、制御します。なぜなら、我々はburstリクエストを許容していないためです。(see the next section)もし前のリクエストから100msec以内に到着したものは、rejectされます。

limit_req_zone ディレクティブは流量制御値、および共有メモリのパラメータを定義します。
しかし、実際にリクエストを制御するものではありません。
制御を有効化するためには、limit_req ディレクティブを含んだ Location もしくは Server ブロックを定義する必要があります。
例の場合、/login/ に対するリクエストを流量制御しています。

上記により、それぞれのユニークなIPアドレスから/login/へのリクエストを10request per secに制限、つまり前のリクエストから100msec以内に再リクエストできないようにしています。

Handling bursts

100msec以内に2つのリクエストを受け付けるとどうなるでしょう?
2つ目のリクエストに対し、NGINXはすぐさまクライアントへステータスコード503(Service Temporarily Unavailable)を返却します。
これは、おそらく我々が望んだ形ではありません。
なぜならアプリケーションにはバースト的な傾向があるためです。
つまり我々は、設定値を超過したリクエストをバッファリングして適切なタイミングに処理したいと考えています。

これは、limit_req ディレクティブに burst パラメータを利用した例です。

location /login/ {
    limit_req zone=mylimit burst=20;

    proxy_pass http://my_upstream;
}

この burst パラメータは、クライアントが zone にて定義された流量をどの程度超過できるか、定義します。
リクエストが前のリクエストから100msec以内に到着した場合にキューにputされ、キューのサイズには20を設定しています。

もし1つのIPアドレスから同時に21のリクエストが到着した場合、NGINXは最初に到着したリクエストを後段のサーバに即座に転送し、残りの20をキューにputします。
次に、キューに入れられたリクエストを100msec毎に転送し、もしキューに入れられたリクエストの数が20を超える場合にのみ、クライアントに503を返却します。

Queueing with No Delay

busrt 設定はトラフィックの流れを適正化しますが、あなたのサイトを遅く見えさせるため実用的ではありません。
我々の例では、キュー内の20番目のリクエストは転送されるまでに2秒待たされており、この時点で、クライアントへの応答はもはや役に立たないかもしれません。
こういった状況に対応するため、burst 設定に nodelay パラメータを加えます。

location /login/ {
    limit_req zone=mylimit burst=20 nodelay;

    proxy_pass http://my_upstream;
}

nodelay パラメータを使った場合、NGINXは burst パラメータに従ってスロットをキューに割り当て、また設定された流量制御を課し、
またリクエストが"素早く"到着した場合、NGINXはキューに転送可能なスロットがある限り、ただちに転送します。
そのスロットは「利用中」とマークされ、適切な時間が経過するまで(この例では、100ミリ秒後)別の要求によって使用されるように解放されません。

前述のように、20スロットのキューが空であり、21の要求が所定のIPアドレスから同時に到着したとします。
NGINXは21個の要求すべてを直ちに転送し、キュー内の20個のスロットを転送済みとマークし、100ミリ秒ごとに1個のスロットを解放します。
(代わりに25個のリクエストがあった場合、NGINXは直ちに21個を転送し、20個のスロットにはマークを付け、4個のリクエストをステータス503で拒否します)

ここで、最初の20リクエストが転送された101msec後に、異なる20のリクエストが同時に到着したことを想定します。
その場合、1スロットのみが解放されている状態なので、NGINXは1リクエストを転送し、残りの19リクエストに503を返却します。
もし、最初の転送から501msec後に新しい20のリクエストが到着した場合、5スロットが解放されているため、NGINXは直ちに5リクエストを転送し、15リクエストを拒否します。

この効果は10request per secで制御していることと同等といえます。
nodelay オプションはリクエスト間の転送間隔を制限せずに、流量制御する場合に便利です。

Note: 多くのデプロイメントにおいて、limit_reqディレクティブではburstとnodelayパラメータの両方を有効化することを推奨しています。

Advanced Configuration Examples

NGINXのその他の機能とRate Limitingを組み合わせることで、より複雑な流量制御を実現することができます。

WhileListing

この例では、どのようにして流量制御と"ホワイトリスト“を実現するか、示します。

geo $limit {
    default 1;
    10.0.0.0/8 0;
    192.168.0.0/24 0;
}

map $limit $limit_key {
    0 "";
    1 $binary_remote_addr;
}

limit_req_zone $limit_key zone=req_zone:10m rate=5r/s;

server {
    location / {
        limit_req zone=req_zone burst=10 nodelay;

        # ...
    }
}

この例では、 geo, map ディレクティブの両方を使っています。
geo ブロックでは、ホワイトリストに含まれるIPアドレスの場合、$limitに0を、そうでない場合に$limitに1をセットしています。
その後、map ディレクティブを使って制御用のキーに値を設定します。

  • $limit が0の場合、$limit_keyに空文字列をセット
  • $limit が1の場合、$limit_keyにクライアントのIPアドレス(binary)をセット

ホワイトリストIPアドレスのために$limit_keyに空文字列をセット、もしくはクライアントIPアドレスをセットしています。
$limit_keyの値が空文字列の場合、limit_req_zone ディレクティブにおいて制御が有効化されず、つまりホワイトリストに記載されたIPアドレス(10.0.0.0/8 and 192.168.0.0/24 subnets)は流量制御されません。
また、その他のすべてのIPアドレスは5request per secで流量制御されます。

なお、limit_req ディレクティブはロケーション:/に対して、最大10のバースト値を設定おり、nodelayオプションを有効化しています。

Including Multiple limit_req Directives in a Location

複数の limit_req を一つのロケーションに設定することが可能です。
リクエストがマッチする全ての制御が適用され、最も制限の厳しい制御が使用されます。
例えば、一つ以上のディレクトリによって遅延させられている場合、最も長い遅延が使用されます。
また同様に、あるディレクティブが拒絶する条件に一致する場合、他のディレクティブが許容していても、リクエストは拒絶されます。

前述のホワイトリストの設定を拡張した例が以下です。

http {
    # ...

    limit_req_zone $limit_key zone=req_zone:10m rate=5r/s;
    limit_req_zone $binary_remote_addr zone=req_zone_wl:10m rate=15r/s;

    server {
        # ...
        location / {
            limit_req zone=req_zone burst=10 nodelay;
            limit_req zone=req_zone_wl burst=20 nodelay;
            # ...
        }
    }
}

ホワイトリストに列挙されるIPアドレスは最初の流量制御(req_zone)に合致しませんが、2つ目の流量制御(req_zone_wl)に合致します。
それにより、15 requests per secに制御されます。
また、ホワイトリストに列挙されないIPアドレスは双方の流量制御に合致し、より厳しい設定が適用され、5 requests per secに制御されます。

Logging

NGINXログは下記の例のようにRate Limitingによって遅延・拒絶されたログを出力します。

2015/06/13 04:20:00 [error] 120315#0: *32086 limiting requests, excess: 1.000 by zone "mylimit", client: 192.168.1.2, server: nginx.com, request: "GET / HTTP/1.0", host: "nginx.com"

ログエントリーには下記のフィールドが含まれます。

  • limiting requests – 制御対象リクエストのログ番号を示します。
  • excess – このリクエストが表す設定されたレートを超えるミリ秒あたりのリクエストの数
  • zone – 制御されたレート制限を定義するゾーン
  • client – リクエストを行ったクライアントのIPアドレス
  • server – サーバーのIPアドレスまたはホスト名
  • request – クライアントからの実際のHTTP要求
  • host – HTTPリクエストヘッダー(Host)の値

デフォルトでは、上記の例の[エラー]に示すように、NGINXは拒絶されたリクエストをエラーレベルで記録します。
(遅延要求は1つ下のレベルでログに記録されるため、デフォルトでinfoです)。
ログレベルを変更するには、limit_req_log_levelディレクティブを使用します。
ここでは、拒否された要求を警告レベルで記録するように設定します。

location /login/ {
    limit_req zone=mylimit burst=20 nodelay;
    limit_req_log_level warn;

    proxy_pass http://my_upstream;
}

Error Code Sent to Client

デフォルトでは、クライアントが流量制御値を超過した際、NGINXはステータスコード503を返却します。
limit_req_status ディレクティブを使用することで、異なるステータスコードをセットすることが可能です。
(下記の例では444を使用)

location /login/ {
    limit_req zone=mylimit burst=20 nodelay;
    limit_req_status 444;
}

Denying All Requests to a Specific Location

もし特定のURLへのすべてのアクセスを拒否したい場合、location ブロックに deny all ディレクティブを使用することで制御することが可能です。

location /foo.php {
    deny all;
}

Conclusion

我々は流量制御に関する多くの機能を実現しています。
例えば、複数のロケーションにおいて流量制御を実現する機能や、burst , nodelay パラメータといった流量制御機能を拡張する機能などです。
また、同様にホワイトリストブラックリストに含まれるIPアドレスに異なる流量制御値を与える拡張的な機能や、どのようにして拒絶・遅延されたかログに記録する機能を有しています。

今日からfree-30day trialを始めるか、我々にデモを依頼をいただくなど、ぜひ、NGINX Plusの流量制御を試してみてください。