OpenResty 中常见 API 的介绍
API7.ai
November 4, 2022
以前の記事では、OpenRestyの多くの重要なLua APIに慣れ親しんできました。今日は、正規表現、時間、プロセスなどに関連する他の一般的なAPIについて学びます。
正規表現関連のAPI
まず、最も一般的で重要な正規表現から見ていきましょう。OpenRestyでは、Luaのパターンマッチングではなく、ngx.re.*
が提供するAPIセットを使用して正規表現に関連するロジックを処理するべきです。これはパフォーマンス上の理由だけでなく、Luaの正規表現が自己完結型であり、PCRE
仕様ではないため、多くの開発者にとって煩わしいからです。
以前の記事ですでにいくつかのngx.re.*
APIに出会っていますが、そのドキュメントは非常に詳細です。したがって、ここではそれらを再度リストアップしません。代わりに、以下の2つのAPIを個別に紹介します。
ngx.re.split
最初に紹介するのはngx.re.split
です。文字列の分割は非常に一般的な機能であり、OpenRestyも対応するAPIを提供していますが、多くの開発者はそのような関数を見つけられず、自分で実装することを選択しています。
なぜでしょうか?ngx.re.split
APIはlua-nginx-module
にはなく、lua-resty-core
にあります。さらに、lua-resty-core
のホームページのドキュメントには記載されておらず、lua-resty-core/lib/ngx/re.md
というサードレベルのディレクトリのドキュメントに記載されています。そのため、多くの開発者はこのAPIの存在を完全に知りません。
同様に、発見しにくいAPIには、以前に紹介したngx_resp.add_header
、enable_privileged_agent
などがあります。では、この問題を迅速に解決するにはどうすればよいでしょうか?lua-resty-core
のホームページのドキュメントを読むだけでなく、lua-resty-core/lib/ngx/
ディレクトリ内の*.md
ドキュメントも読む必要があります。
lua_regex_match_limit
次に、lua_regex_match_limit
を紹介します。以前、OpenRestyが提供するNGINXコマンドについてはほとんど触れませんでした。なぜなら、ほとんどの場合、デフォルト値で十分であり、実行時に変更する必要がないからです。例外は、正規表現に関連するlua_regex_match_limit
コマンドです。
バックトラッキングNFAに基づいて実装された正規エンジンを使用する場合、カタストロフィックバックトラッキング(Catastrophic Backtracking)のリスクがあります。これは、正規表現がマッチング時に過剰にバックトラッキングし、CPUが100%になり、サービスがブロックされる現象です。
カタストロフィックバックトラッキングが発生した場合、gdb
を使用してダンプを分析するか、systemtap
を使用してオンライン環境を分析して特定する必要があります。残念ながら、事前に検出することは容易ではありません。なぜなら、特別なリクエストのみがこれを引き起こすからです。これにより、攻撃者がこれを悪用する可能性があり、ReDoS
(RegEx Denial of Service)はこの種の攻撃を指します。
ここでは、OpenRestyで以下のコードを使用して、上記の問題を簡単かつ効果的に回避する方法を紹介します。
lua_regex_match_limit
は、PCRE
正規エンジンによるバックトラッキングの回数を制限するために使用されます。これにより、カタストロフィックバックトラッキングが発生しても、その影響がCPUを飽和させない範囲に限定されます。
lua_regex_match_limit 100000;
時間関連のAPI
最も一般的に使用される時間APIはngx.now
で、現在のタイムスタンプを出力します。例えば、以下のコードです。
resty -e 'ngx.say(ngx.now())'
出力結果からわかるように、ngx.now
は小数部分を含むため、より正確です。関連するngx.time
APIは、値の整数部分のみを返します。その他、ngx.localtime
、ngx.utctime
、ngx.cookie_time
、ngx.http_time
は、主に異なる形式で時間を返したり処理したりするために使用されます。これらを使用したい場合は、ドキュメントを確認してください。理解するのは難しくないので、ここでは詳しく説明しません。
ただし、これらの現在の時間を返すAPIは、非ブロッキングネットワークIO操作によってトリガーされない場合、常にキャッシュされた値を返し、私たちが望む現在のリアルタイムの時間を返しません。以下のサンプルコードを見てください。
$ resty -e 'ngx.say(ngx.now()) os.execute("sleep 1") ngx.say(ngx.now())'
ngx.now
の2回の呼び出しの間に、Luaのブロッキング関数を使用して1
秒間スリープしていますが、出力結果からわかるように、両方のタイムスタンプは同じです。
では、非ブロッキングのスリープ関数に置き換えた場合はどうなるでしょうか?例えば、以下の新しいコードです。
$ resty -e 'ngx.say(ngx.now()) ngx.sleep(1) ngx.say(ngx.now())'
これにより、異なるタイムスタンプが出力されます。これにより、ngx.sleep
という非ブロッキングのスリープ関数にたどり着きます。この関数は、指定された時間スリープするだけでなく、もう1つの特別な目的があります。
例えば、集中的な計算を行っているコードがあり、それが多くの時間を要する場合、このコードに対応するリクエストはその間ずっとワーカーとCPUリソースを占有し、他のリクエストがキューに入り、タイムリーに応答されなくなります。この場合、ngx.sleep(0)
を挿入して、このコードが制御を放棄し、他のリクエストも処理されるようにすることができます。
ワーカーとプロセスAPI
OpenRestyは、ワーカーとプロセスに関する情報を取得するためにngx.worker.*
とngx.process.*
APIを提供しています。前者はNginxワーカープロセスに関連し、後者は一般的にすべてのNginxプロセスを指します。ワーカープロセスだけでなく、マスタープロセス、特権プロセスなども含まれます。
true
とnull
値の問題
最後に、true
とnull
値の問題を見てみましょう。OpenRestyでは、true
値とnull
値の判定は非常に面倒で混乱しやすいポイントです。
まず、Luaでのtrue
値の定義を見てみましょう。nil
とfalse
を除いて、すべてtrue
値です。
したがって、true
値には0
、空のstring
、空のtable
なども含まれます。
次に、Luaのnil
を見てみましょう。これは「未定義」を意味します。例えば、変数を宣言したが初期化していない場合、その値はnil
です。
$ resty -e 'local a ngx.say(type(a))'
また、nil
はLuaのデータ型でもあります。これら2つの定義を理解した上で、これらの定義から派生する他の問題を見ていきましょう。
ngx.null
最初の問題はngx.null
です。Luaのnil
はtable
の値として使用できないため、OpenRestyはngx.null
をtable
内のnull
値として導入しました。
$ resty -e 'print(ngx.null)' null
$ resty -e 'print(type(ngx.null))' userdata
上記の2つのコードからわかるように、ngx.null
はnull
として出力され、その型はuserdata
です。では、これをfalse
値として扱えるでしょうか?もちろんできません。ngx.null
のブール値はtrue
です。
$ resty -e 'if ngx.null then ngx.say("true") end'
したがって、nil
とfalse
のみがfalse
値であることを覚えておいてください。この点を見落とすと、例えばlua-resty-redis
を使用して以下の判定を行う場合に、簡単に落とし穴に陥ります。
local res, err = red:get("dog") if not res then res = res + "test" end
戻り値res
がnil
の場合、関数呼び出しは失敗しています。res
がngx.null
の場合、redisにキーdog
が存在しないため、キーdog
が存在しない場合にコードがクラッシュします。
cdata:NULL
2番目の問題はcdata:NULL
です。LuaJIT FFIインターフェースを介してC関数を呼び出し、その関数がNULL
ポインタを返す場合、別の種類のnull
値であるcdata:NULL
に遭遇します。
$ resty -e 'local ffi = require "ffi" local cdata_null = ffi.new("void*", nil) if cdata_null then ngx.say("true") end'
ngx.null
と同様に、cdata:NULL
もtrue
です。しかし、さらに困惑するのは、以下のコードがtrue
を出力するということで、cdata:NULL
がnil
と等価であることを意味します。
$ resty -e 'local ffi = require "ffi" local cdata_null = ffi.new("void*", nil) ngx.say(cdata_null == nil)'
では、ngx.null
とcdata:NULL
をどのように処理すべきでしょうか?アプリケーション層にこれらの面倒を気にさせるのは良い解決策ではありません。セカンドレベルのラッパーを作成し、呼び出し側にこれらの詳細を知らせない方が良いです。
cjson.null
最後に、cjson
に現れるnull
値を見てみましょう。cjson
ライブラリは、json内のNULL
をLuaのlightuserdata
にデコードし、cjson.null
として表現します。
$ resty -e 'local cjson = require "cjson" local data = cjson.encode(nil) local decode_null = cjson.decode(data) ngx.say(decode_null == cjson.null)'
Luaのnil
は、JSONによってエンコードおよびデコードされると、cjson.null
になります。想像できるように、これはngx.null
と同じ理由で導入されました。なぜなら、nil
はtable
の値として使用できないからです。
これまで、OpenRestyの多くの種類のnull
値に混乱しましたか?心配しないでください。この部分を何度か読み、自分で整理すれば、混乱することはありません。もちろん、今後if not foo then
のようなものを書くときに、それが機能するかどうかを考える必要があります。
まとめ
今日の記事では、OpenRestyで一般的に使用されるLua APIを紹介しました。
最後に、1つの質問を残します。ngx.now
の例で、yield
操作がない場合、なぜngx.now
の値が変更されないのでしょうか?コメントで意見を共有し、この記事を共有して、一緒に交流し、改善していきましょう。