kakakakakku blog

Weekly Tech Blog: Keep on Learning!

nginx でリクエストを制限できるモジュール「ngx_http_limit_req_module」

nginx でリクエストを制限できるモジュール「ngx_http_limit_req_module」を使うと,Throttling や DoS 対策など,リクエストの過剰な増加に nginx で対応できるようになる.挙動を確認するため,Docker Compose を使って検証環境を構築した.

nginx.org

検証環境

今回 Docker Compose を使って,nginx と Sinatra を起動する検証環境を構築した.コンテナは計4種類で,以下の構成図にまとめた.今回は default.conf の異なる3種類の nginx (Frontend1-3) の挙動を確認する.

  • Frontend1 (nginx) : limit_req
  • Frontend2 (nginx) : limit_req + burst
  • Frontend3 (nginx) : limit_req + burst + limit_req_status
  • Backend (Sinatra)

f:id:kakku22:20190825131113p:plain

リクエストを投げるために今回は Vegeta を使う.

github.com

特に設定はなく,すぐに docker-compose up で起動できるようにした.なお,Docker Compose の設定などは GitHub に公開した.

$ docker-compose up

$ curl http://localhost:8080
Hello, backend!

$ curl http://localhost:8081
Hello, backend!

$ curl http://localhost:8082
Hello, backend!

github.com

検証 1 : limit_req

まず,Frontend1 の設定(一部)を以下に載せる.ngx_http_limit_req_module の基本設定となる limit_req_zone$binary_remote_addr を指定し,IP アドレスごとに制限をしている.他にも $server_name を指定し,サーバごとに制限をすることもできる.そして今回は rate=1r/s を指定し,1秒間に1リクエストを許容する.

http {
    limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;

    server {
        (中略)
        location / {
            limit_req zone=one;
            proxy_pass http://backend;
        }
    }
}

さっそく Vegeta でリクエストを投げてみる.今回は -rate=10-duration=5s を指定し「1秒間に10リクエストを5秒間」とする.結果レポートの中で注目するポイントは「200:5 503:45」で,1秒間に1リクエストを許容しているため,残った45リクエストは「503 Service Temporarily Unavailable」になっている.

$ echo 'GET http://localhost:8080' | vegeta attack -rate=10 -duration=5s > result.bin

$ vegeta report -type=text result.bin
Requests      [total, rate, throughput]  50, 10.21, 1.02
Duration      [total, attack, wait]      4.898020366s, 4.896749649s, 1.270717ms
Latencies     [mean, 50, 95, 99, max]    2.134954ms, 1.814726ms, 3.90771ms, 6.088868ms, 6.088868ms
Bytes In      [total, mean]              22305, 446.10
Bytes Out     [total, mean]              0, 0.00
Success       [ratio]                    10.00%
Status Codes  [code:count]               200:5  503:45
Error Set:
503 Service Temporarily Unavailable

nginx のアクセスログは以下のように出力されていた.

frontend1_1  | 2019/08/25 00:00:00 [error] 6#6: *3 limiting requests, excess: 0.100 by zone "one", client: 172.21.0.1, server: localhost, request: "GET / HTTP/1.1", host: "localhost:8080"
frontend1_1  | 172.21.0.1 - - [25/Aug/2019:00:00:00 +0000] "GET / HTTP/1.1" 503 494 "-" "Go-http-client/1.1" "-"

検証 2 : limit_req + burst

次に,Frontend2 の設定(一部)を以下に載せる.Frontend1 との差は burst=10nodelay で,今回は burst を指定することにより,制限を超えたリクエストをキューに溜められるようになる.さらに nodelay を合わせて指定し,キューに溜まったリクエストを遅延処理しないようにできる.リクエストを制限しながら緩和できる設定となり,よく使うことになりそう.

http {
    limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;

    server {
        (中略)
        location / {
            limit_req zone=one burst=10 nodelay;
            proxy_pass http://backend;
        }
    }
}

Frontend2 にも Vegeta でリクエストを投げてみる.結果レポートを確認すると「200:15 503:35」となり,burst に設定したリクエストも処理できた.残った35リクエストは「503 Service Temporarily Unavailable」になっている.

$ echo 'GET http://localhost:8081' | vegeta attack -rate=10 -duration=5s > result.bin

$ vegeta report -type=text result.bin
Requests      [total, rate, throughput]  50, 10.20, 3.06
Duration      [total, attack, wait]      4.901103127s, 4.899598364s, 1.504763ms
Latencies     [mean, 50, 95, 99, max]    3.344031ms, 1.78755ms, 13.669119ms, 17.032244ms, 17.032244ms
Bytes In      [total, mean]              17515, 350.30
Bytes Out     [total, mean]              0, 0.00
Success       [ratio]                    30.00%
Status Codes  [code:count]               200:15  503:35
Error Set:
503 Service Temporarily Unavailable

検証 3 : limit_req + burst + limit_req_status

最後は,Frontend3 の設定(一部)を以下に載せる.Frontend2 との差は limit_req_status で,特に API リクエストを制限する場合など,レスポンスコードとして「503 Service Temporarily Unavailable」ではなく「429 Too Many Requests」を返すべき場面もあると思う.今回は limit_req_status を指定することにより,レスポンスコードを指定している.

http {
    limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;

    server {
        (中略)
        location / {
            limit_req zone=one burst=10 nodelay;
            limit_req_status 429;
            proxy_pass http://backend;
        }
    }
}

Frontend3 にも Vegeta でリクエストを投げてみる.結果レポートを確認すると「200:15 429:35」となり,残った35リクエストは「429 Too Many Requests」になっている.

$ echo 'GET http://localhost:8082' | vegeta attack -rate=10 -duration=5s > result.bin

$ vegeta report -type=text result.bin
Requests      [total, rate, throughput]  50, 10.20, 3.06
Duration      [total, attack, wait]      4.905475679s, 4.903894373s, 1.581306ms
Latencies     [mean, 50, 95, 99, max]    3.614673ms, 2.121613ms, 8.324474ms, 34.016092ms, 34.016092ms
Bytes In      [total, mean]              6140, 122.80
Bytes Out     [total, mean]              0, 0.00
Success       [ratio]                    30.00%
Status Codes  [code:count]               200:15  429:35
Error Set:
429 Too Many Requests

nginx のアクセスログは以下のように出力されていた.

frontend3_1  | 2019/08/25 00:00:00 [error] 6#6: *1 limiting requests, excess: 10.210 by zone "one", client: 172.21.0.1, server: localhost, request: "GET / HTTP/1.1", host: "localhost:8082"
frontend3_1  | 172.21.0.1 - - [25/Aug/2019:00:00:00 +0000] "GET / HTTP/1.1" 429 169 "-" "Go-http-client/1.1" "-"

まとめ

nginx でリクエストを制限できるモジュール「ngx_http_limit_req_module」を試した.IP アドレスごとなど,設定した単位ごとにリクエストを制限でき,burst を設定すれば緩和もできる.モジュールに実装されたレベルの制限なら nginx で実現できそう.実際に ngx_http_limit_req_module.c など,実装を紐解くと,正確には r/s は1秒間に許容するリクエストではないらしく,本番環境などトラフィックのある環境に導入するときには様々なシナリオを考えて負荷テストをしておく必要がありそう.

関連記事

kakakakakku.hatenablog.com