ngx_http_limit_req_moduleの説明
説明はほぼほぼ省きます。とりあえず公式を読むのが一番良さそうです。
ngx_http_limit_req_moduleモジュール (0.7.21) は、定義されたキーごとのリクエストの処理レート、特に一つのIPアドレスから来るリクエストの処理レートを制限します。制限は"leaky bucket"メソッドを使って行われます。
リーキーバケットは以下の記事がわかりやすかったです。
yourmystar-engineer.hatenablog.jp
結論(?)
- burstなし = 設定した流量[req/sec]以上は全部エラー
- burstあり = burstに設定した接続は流量[req/sec]に沿ってリクエストを処理する(遅延処理)
- burstあり + nodelayあり = burstに設定した値分の同時接続を許可しそれ以上は即時エラー。(スロットリング)
サンプルコード
以下のサンプルを使って実験していきます。
nginx
1秒あたり1リクエストを受け付ける。
limit_req_zone $binary_remote_addr zone=test:10m rate=1r/s; server { listen 80; server_name localhost; location / { limit_req zone=test burst=10 nodelay; proxy_pass http://app:8080; } }
python
同時接続数とかをわかりやすくするために3秒sleepするようにしている。
#!/usr/bin/env python import time from flask import Flask app = Flask(__name__) @app.route("/") def index(): time.sleep(3) return "<h1>Hello, Flask!</h1>" if __name__ == "__main__": app.run(host="0.0.0.0", port=8080, debug=True)
実験
abコマンドを使って10コネクションで100リクエストを投げます。abはpreflyght的に最初に1リクエスト投げますが一旦そこは無視してざっくり結果で進めていきたいと思います。(failedの数がおかしくなったりするがとりあえずこれを使う)
ab -c 10 -n 100 http://localhost/
burstなし
Time taken for tests: 6.022 seconds Failed requests: 98 Requests per second: 16.61 [#/sec] (mean) Time per request: 602.162 [ms] (mean)
burstあり
Time taken for tests: 103.915 seconds Failed requests: 0 Requests per second: 0.96 [#/sec] (mean) Time per request: 10391.490 [ms] (mean)
burstあり + nodelay
Time taken for tests: 9.064 seconds Failed requests: 85 Requests per second: 11.03 [#/sec] (mean) Time per request: 906.393 [ms] (mean)
結果
burstなしとburstありはとてもわかりやすかった。burstなし = 1[req/sec]を超えるリクエストは全て503を返しアプリまでの到達は0となる。一方burstありの場合は1[req/sec]になるようにnginxでリクエストをキューイングしていてくれる。アプリも最大同時接続数は1となる。やはりわかりにくのが「burstあり + nodelay」だ。
nodelayって何
burst を指定すると、上限を超えたリクエストは burst に指定した数だけキューイングされ超えると503が帰ります。キューイングされたリクエストはそれぞれ指定されたレートにそって処理を行われます(スロットリング)。nodelayはその名の通り処理を遅延させないためのオプションです。
nodelayを指定しない場合はクライアントはコネクションを運良く掴んだ場合でも1[req/sec]でのレートに制限され最悪キューイングの数 * 処理秒数を待つことになってしまいます。上記の実験結果のbusrtありを見るとわかりますがburst10で1[req/sec]で制限すると処理時間が3秒のエンドポイントへのリクエストでは最大30秒クライアントは待たされてしまいます。それを防ぐのがnodelayオプションです。
nodelayオプションを付けるとある基準に従ってリクエストを即処理するか弾くかを決めるようになります。(nodelayはオプションと謳ってますがあまりにもburstありと機能の毛色が違う気がするので分けても良さそうな気がしました)。burstあり + nodelayの結果をもう一回見てみると以下のようになっています。
Time taken for tests: 9.064 seconds Failed requests: 85 Requests per second: 11.03 [#/sec] (mean) Time per request: 906.393 [ms] (mean)
実行時間は全体で9秒で11[req/sec]が処理されています。バックエンドに到達したリクエスト数は15となっています。ここでポイントとなるのがバックエンドへの同時接続数です。nodelayオプションなしの場合はバックエンドの処理は全てシーケンシャル(1[req/sec]を指定しているので)でしたがnodelayを指定すると同時接続数はburstの値と同じ10となっています。
app_1 | 172.29.0.3 - - [25/Oct/2021 14:37:54] "GET / HTTP/1.0" 200 - app_1 | 172.29.0.3 - - [25/Oct/2021 14:37:54] "GET / HTTP/1.0" 200 - app_1 | 172.29.0.3 - - [25/Oct/2021 14:37:54] "GET / HTTP/1.0" 200 - app_1 | 172.29.0.3 - - [25/Oct/2021 14:37:54] "GET / HTTP/1.0" 200 - app_1 | 172.29.0.3 - - [25/Oct/2021 14:37:54] "GET / HTTP/1.0" 200 - app_1 | 172.29.0.3 - - [25/Oct/2021 14:37:54] "GET / HTTP/1.0" 200 - app_1 | 172.29.0.3 - - [25/Oct/2021 14:37:54] "GET / HTTP/1.0" 200 - app_1 | 172.29.0.3 - - [25/Oct/2021 14:37:54] "GET / HTTP/1.0" 200 - app_1 | 172.29.0.3 - - [25/Oct/2021 14:37:54] "GET / HTTP/1.0" 200 - app_1 | 172.29.0.3 - - [25/Oct/2021 14:37:54] "GET / HTTP/1.0" 200 -
nodelay有無での用途
nodelayはありはバックエンドへの同時接続数を絞りたい場合に使えてnodelayなしは大量にキューに入れて厳密に流量を制御したい場合に使えるのかなと思った。
aws lambdaとかで見られるスロットリングは前者でスロットルが発生したら429を返すような実装ができる。良識あるクライアントならエクスポネンシャルバックオフなりで良い感じにリトライしてくれることを期待しても良さそう。(内部向けのサービスとかで制限したいケースとかでとても便利そう)
後者の用途としては良識のない攻撃なんかだろうか。例えば複数コネクションで大量リクエストを送ってくるタイプのクライアントなら流量[req/sec]を少なめに設定してburst値を上げることで処理遅延が走るため結果待ちの時間が発生し大量リクエストを投げづらくなる。
具体的な実装とか(読んではいない)
ソースを見た感じはこの辺が具体的にリミットを操作している箇所になっている。実際にリミットがかかるかどうかの探索は赤黒木を用いて実装されてるみたいなのでapacheで使ってたツールのようにO(N)にはならないようでした。(zoneを100MBとかにしても対した負荷にはならなそう)
まとめというか感想
nodelayの挙動を見つつ用途を考えるととても難しかった。誰向けの制限なのかで使い方が変わるのかな〜という感じの結論でした。mapとかgeoを使ってアクセス元の情報を元に制限方法を動的に変えたりなどnginxでできることはまだまだありそうだなと感じた調査になりました。