nginx でリクエストを制限できるモジュール「ngx_http_limit_req_module」を使うと,Throttling や DoS 対策など,リクエストの過剰な増加に nginx で対応できるようになる.挙動を確認するため,Docker Compose を使って検証環境を構築した.
検証環境
今回 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)
リクエストを投げるために今回は Vegeta を使う.
特に設定はなく,すぐに 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!
検証 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=10
と nodelay
で,今回は 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秒間に許容するリクエストではないらしく,本番環境などトラフィックのある環境に導入するときには様々なシナリオを考えて負荷テストをしておく必要がありそう.