OpenResty 中常见 API 的介绍

API7.ai

November 4, 2022

OpenResty (NGINX + Lua)

以前の記事では、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_headerenable_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.localtimengx.utctimengx.cookie_timengx.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プロセスを指します。ワーカープロセスだけでなく、マスタープロセス、特権プロセスなども含まれます。

truenull値の問題

最後に、truenull値の問題を見てみましょう。OpenRestyでは、true値とnull値の判定は非常に面倒で混乱しやすいポイントです。

まず、Luaでのtrue値の定義を見てみましょう。nilfalseを除いて、すべてtrue値です。

したがって、true値には0、空のstring、空のtableなども含まれます。

次に、Luaのnilを見てみましょう。これは「未定義」を意味します。例えば、変数を宣言したが初期化していない場合、その値はnilです。

$ resty -e 'local a ngx.say(type(a))'

また、nilはLuaのデータ型でもあります。これら2つの定義を理解した上で、これらの定義から派生する他の問題を見ていきましょう。

ngx.null

最初の問題はngx.nullです。Luaのniltableの値として使用できないため、OpenRestyはngx.nulltable内のnull値として導入しました。

$ resty -e 'print(ngx.null)' null
$ resty -e 'print(type(ngx.null))' userdata

上記の2つのコードからわかるように、ngx.nullnullとして出力され、その型はuserdataです。では、これをfalse値として扱えるでしょうか?もちろんできません。ngx.nullのブール値はtrueです。

$ resty -e 'if ngx.null then ngx.say("true") end'

したがって、nilfalseのみがfalse値であることを覚えておいてください。この点を見落とすと、例えばlua-resty-redisを使用して以下の判定を行う場合に、簡単に落とし穴に陥ります。

local res, err = red:get("dog") if not res then res = res + "test" end

戻り値resnilの場合、関数呼び出しは失敗しています。resngx.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:NULLtrueです。しかし、さらに困惑するのは、以下のコードがtrueを出力するということで、cdata:NULLnilと等価であることを意味します。

$ resty -e 'local ffi = require "ffi" local cdata_null = ffi.new("void*", nil) ngx.say(cdata_null == nil)'

では、ngx.nullcdata: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と同じ理由で導入されました。なぜなら、niltableの値として使用できないからです。

これまで、OpenRestyの多くの種類のnull値に混乱しましたか?心配しないでください。この部分を何度か読み、自分で整理すれば、混乱することはありません。もちろん、今後if not foo thenのようなものを書くときに、それが機能するかどうかを考える必要があります。

まとめ

今日の記事では、OpenRestyで一般的に使用されるLua APIを紹介しました。

最後に、1つの質問を残します。ngx.nowの例で、yield操作がない場合、なぜngx.nowの値が変更されないのでしょうか?コメントで意見を共有し、この記事を共有して、一緒に交流し、改善していきましょう。