OpenResty または Nginx プロセスにおける最も遅い PCRE 正規表現のトレース
正規表現は、OpenResty や Nginx(OpenResty のコアコンポーネントである Lua Nginx モジュールなど)上で動作する Lua アプリケーションを含む、Web アプリケーションに遍在しています。
OpenResty は、ngx.re.*
Lua API 関数(ngx.re.match や ngx.re.find など)を提供し、PCRE 正規表現ライブラリの C API を Lua ランドに正規表現ライブラリしています。PCRE は、Just-in-Time(JIT)コンパイラを備えた Perl 互換の正規表現の非常に効率的な実装を提供します。PCRE は、Nginx コアや PHP ランタイムを含む、様々な有名なシステムソフトウェアで広く使用されています。
しかし、多くの Web 開発者は正規表現の効率性にほとんど注意を払っていません。特に PCRE のようなバックトラッキング実装では、不適切に記述された正規表現に対して病理的な動作が発生する可能性があります。さらに悪いことに、このような病理的な動作は、特定の特性を持つ比較的大きなデータ入力によってのみトリガーされる可能性があります。一部の DoS 攻撃者は、このような脆弱な正規表現パターンを特に標的にする場合もあります。
当社の OpenResty XRay 製品は、オンラインの OpenResty および Nginx プロセスをリアルタイムで分析し、脆弱で遅い正規表現を迅速に特定できる動的トレースツールを提供しています。対象の OpenResty または Nginx アプリケーションに特別なプラグインやモジュールは必要ありません。また、OpenResty XRay がサンプリングを行っていない場合、まったくオーバーヘッドは発生しません。
このチュートリアルでは、実際の例を用いてこのような高度なアナライザーをデモンストレーションします。最後に、RE2 や OpenResty Regex エンジンを含む、病理的な動作の少ない代替手段についても検討します。
システム環境
ここでは、RedHat Enterprise Linux 7 システムを例として使用します。OpenResty XRay がサポートする Ubuntu、Debian、Fedora、Rocky、Alpine などの Linux ディストリビューションであれば、同様に問題なく動作するはずです。
対象アプリケーションとして、オープンソースの OpenResty バイナリビルドを使用します。 既存の OpenResty や Nginx のバイナリ(自身でコンパイルしたものも含む)を使用することができます。 既存のサーバーインストールやプロセスに特別なビルドオプション、プラグイン、ライブラリは必要ありません。これは 動的トレーシング 技術の美しさです。真に非侵襲的なのです。
また、同じシステム上で OpenResty XRay の Agent デーモンを実行し、openresty-xray-cli
パッケージからコマンドラインユーティリティを インストールして設定 しています。
サンプル OpenResty/Nginx Lua アプリケーション
サーバーで CPU 使用率が 100% に達する非効率的な OpenResty/Nginx Lua アプリケーションがあります。これは top
コマンドラインユーティリティで確認できます:
$ top -p 3584441
...
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
3584441 nobody 20 0 41284 7184 4936 R 100.0 0.0 1:43.97 nginx
Nginx プロセスの CPU 使用率が 100.0% であり、その PID が 3584441 であることにご注目ください。この PID は、後の動的トレーシング分析で参照します。
推測せずに原因を絞り込む
C 言語の CPU フレームグラフ
CPU 使用率が高い C/C++ プロセス(Nginx プロセスを含む)を分析するには、まずそのプロセスから C 言語の CPU フレームグラフ1を取得する必要があります。OpenResty XRay は OpenResty/Nginx アプリケーションを自動的に検出し、サンプリングして様々な種類のフレームグラフを生成できますが、ここでは説明のために、コマンドラインユーティリティを手動で実行する方法を使用します。
$ orxray analyzer run lj-c-on-cpu -p 3584441
Start tracing...
Go to https://x5vrki.xray.openresty.com/targets/68/history/664712 for charts.
Nginx ワーカープロセスの PID を指定するために -p
オプションを使用します。そして、標準的な OpenResty XRay ツールである lj-c-on-cpu
を実行します。ここでは、より汎用的な c-on-cpu
ツールは使用しません。なぜなら、前者は LuaJIT と PCRE の Just-in-Time (JIT) コンパイラからの動的マシンコードフレームを自動的に展開できるからです。
この C 言語レベルの CPU フレームグラフから、以下のことが分かります:
- ほぼすべての CPU 時間が Lua コードの実行に費やされています(フレームグラフ内の
ngx_http_lua_run_thread
C 関数フレームがハイライトされていることに注目してください)。 - また、Lua コードは PCRE マッチャーも実行しています(グラフ内の
pcre_exec
関数フレームに注目してください)。 - さらに、PCRE の JIT コードが実行されていることがわかります(グラフ内の
_pcre_jit_exec
関数フレームから)。
このダイナミックトレーシングツールの強力さに注目してください。対象アプリケーションからの助けを借りずに、JIT コードフレームを透過的に展開できます2。
次のステップは、Lua レベルの CPU フレームグラフのサンプリングと生成です。これにより、Lua コード内で何が起こっているかを確認することができます。
Lua-Land CPU フレームグラフ
Lua-land CPU フレームグラフを生成するには、標準アナライザーである lj-lua-on-cpu
を以下のように使用します:
$ orxray analyzer run lj-lua-on-cpu -p 3584441
Start tracing...
Go to https://x5vrki.xray.openresty.com/targets/68/history/664870 for charts.
Lua-land フレームグラフは、JIT コンパイルされた Lua コードも表示します。これは、グラフ内の trace#3:regex.lua:721
フレームからわかります。これは、トレース ID 3
の LuaJIT トレースオブジェクトが、ソースファイル regex.lua
の 721 行目の Lua コードから開始されていることを意味します。ほぼすべての CPU 時間が単一の Lua コードパスに費やされていることがわかります。この支配的な Lua コードパスの中で最も興味深い Lua 関数フレームは regex.lua:re_match
です。その Lua ファイルには以下のようになっています:
local re_match = ngx.re.match
したがって、re_match
シンボル名は ngx.re.match
API 関数を指しています。
次の疑問は:どの正規表現がそれほど遅いのか?ということです。フレームグラフに示されているように、content_by_lua(nginx.conf:69):10
の Lua ソース行を直接参照することができます。しかし、Lua コードが汎用的で、複数の正規表現を保持する Lua テーブルを単にマッチングしているだけの場合、問題の正規表現を特定するには十分ではありません。
最も遅い正規表現
正規表現マッチングが最も CPU 時間を消費していることがわかったので、現在のライブプロセスでマッチングされた最も遅い正規表現を見つけるために、lj-slowest-ngx-re
という別の標準アナライザーを使用することができます。以下のように使用します:
$ orxray analyzer run lj-slowest-str-match-find -p 3584441 --y-var threshold_ns=1 --y-var max_hits=1000
Start tracing...
sub_regex_cache_count: 0
http: match
cache: ptr: 0x154bbc0, type: match, pat: "\w"
cache: ptr: 0x1517890, type: match, pat: "(?:.*)*css"
cache: ptr: 0x154a3a0, type: match, pat: ".*?js"
http: substitution
http: function substitution
...WARNING: reach samples count: 1000
=== start ===
max latency: 15844343 ns, ptr: 0x1517890
max latency: 13555 ns, ptr: 0x154a3a0
max latency: 8075 ns, ptr: 0x154bbc0
=== finish ===
Go to https://x5vrki.xray.openresty.com/targets/68/history/799249 for charts.
私たちは Web ブラウザで URL を開き、以下に示す棒グラフを閲覧します。
この棒グラフは、サンプリング時間枠内で実行された最も遅い正規表現の最大実行遅延を示しています。遅延はナノ秒単位で表示されています。明らかに、(?:.*)*css
が絶対的に最も遅い正規表現であり、20.6 ミリ秒以上(または 20,671,282 ナノ秒)かかっています。また、他の正規表現と比べてもはるかに遅いことがわかります。経験豊富なユーザーであれば、貪欲な量指定子 .*
と定数の接尾辞を組み合わせて使用すると、正規表現エンジンで積極的なバックトラッキングが発生する可能性があることをすぐに認識できるでしょう。この正規表現は、非貪欲版の量指定子を使用して最適化することができます:(?:.*?)css
。
最も遅い正規表現の最適化後
最も遅い正規表現 (?:.*)*css
を (?:.*?)css
に最適化した後、最適化された正規表現でアプリケーションを再読み込みし、新しい C 言語の CPU フレームグラフを再生成しました。
新しいフレームグラフの形状と特徴が以前のものとは大きく異なっていることがわかります。ボトルネックはもはや Lua コードを実行するコードパスではありません。代わりに、HTTP レスポンスデータを書き出す責任を持つ writev
システムコールがボトルネックとなっています(上記のグラフで赤色でハイライトされている部分)。グラフ内の ngx_http_output_filter
C 関数フレームに注目してください。この関数は、HTTP レスポンスヘッダーとボディデータを送信するために Nginx 出力フィルターチェーンを呼び出す責任があります。
最適化されたアプリケーションに対して lj-slowest-ngx-re
ツールを再実行してみましょう。
結果のチャートは以下の通りです。
正規表現の最大レイテンシーがはるかに短いことが分かります。わずか 58,660 ナノ秒、つまり約 59マイクロ秒 です。
PCREの実行オーバーヘッドを制限する
OpenRestyの Lua Nginx モジュールは、単一の正規表現マッチングに対して実行される基本操作に厳格な制限を設けるための lua_regex_match_limit
ディレクティブを提供しています。
例えば、以下の行を nginx.conf
ファイルに追加することができます。
lua_regex_match_limit 100000;
そして、ngx.re.match
API 関数呼び出しによって返されるエラーを必ずログに記録するようにしてください:
local m, err = re_match(content, regexes[i], "jo")
if err ~= nil then
ngx.log(ngx.ERR, "re_math failed, re: ", regexes[i], ", error: ", err)
return
end
マッチ制限を超えた場合、以下のようなエラーログメッセージが表示されます:
2022/07/13 21:05:13 [error] 3617128#3617128: *1 [lua] content_by_lua(nginx.conf:69):11: re_math failed, re: (.*)*css, error: pcre_exec() failed: -8, client: 127.0.0.1, server: localhost, request: "GET / HTTP/1.1", host: "127.0.0.1"
非バックトラッキング正規表現エンジン
PCRE は Perl 正規表現をほぼ完全にサポートしています。この柔軟性にはバックトラッキングに関連するコストが伴います。Tユーザーは、Google の RE2 や OpenResty Inc の商用 OpenResty Regex エンジンなど、バックトラッキングを行わない他の正規表現の実装も検討することをお勧めします。これらのエンジンは通常、オートマトン理論に基づくアルゴリズムを採用しています。残念ながら、RE2 はマッチングが遅くなる病理的なケースはほとんどありませんが、その平均マッチング時間は、典型的な正規表現に対して PCRE よりも約 50% 遅くなります。一方、OpenResty Regex エンジンは PCRE と同等の平均性能(時にはさらに高速)を達成し、バックトラッキングの問題もありません。
Lua の組み込み文字列パターン
標準 Lua 5.1 言語は、標準 API 関数 string.match と string.find でサポートされている独自の正規表現言語構文を定義しています。 OpenResty XRay は、このようなパターンマッチング操作を分析するための動的トレーシングツールも提供しています。このトピックについては、別のチュートリアルで取り上げる予定です。
コンテナ内のアプリケーションのトレース
OpenResty XRay ツールは、コンテナ化されたアプリケーションを透過的にトレースすることをサポートしています。 Docker と Kubernetes(K8s)コンテナの両方が透過的に動作します。通常のアプリケーションプロセスと同様に、対象のコンテナにアプリケーションや特別な権限は必要ありません。OpenResty XRay Agent デーモンは、対象のコンテナの外部(ホストオペレーティングシステム上や特権を持つ独自のコンテナ内など)で実行する必要があります。
例を見てみましょう。まず、docker ps
コマンドでコンテナ名またはコンテナ ID を確認します。
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
4465297209d9 openresty/openresty:1.19.3.1-2-alpine-fat "/usr/local/openrest…" 18 months ago Up 45 minutes angry_mclaren
ここでは、コンテナ名が angry_mclaren
です。このコンテナ内でターゲットプロセスの PID を特定することができます。
$ docker top angry_mclaren
UID PID PPID C STIME TTY TIME CMD
root 3605106 3605085 0 19:40 ? 00:00:00 nginx: master process /usr/local/openresty/bin/openresty -g daemon off;
nobody 3605692 3605106 0 19:44 ? 00:00:20 nginx: worker process
openresty
ワーカープロセスの PID は 3605692
です。その後、通常通りこの PID に対して OpenResty XRay アナライザーを実行します。
$ orxray analyzer run lj-slowest-ngx-re -p 3605692
Start tracing...
...
Go to https://x5vrki.xray.openresty.com/targets/68/history/684552 for charts.
OpenResty XRay は、長時間実行されているプロセスを特定のタイプ(「OpenResty」、「Python」など)の「アプリケーション」として自動的に検出することも可能です。
ツールの実装方法
すべてのツールは Y 言語で実装されています。OpenResty XRay は、Stap+2 または eBPF3 バックエンドのいずれかを使用してこれらを実行します。両者とも OpenResty XRay の一部であり、Linux カーネルの uprobes
および kprobes
機能に基づく 100% 非侵襲的な動的トレーシング技術を使用しています。
対象アプリケーションやプロセスからの協力は一切必要ありません。ログデータやメトリクスデータは使用せず、必要ともしません。実行中のプロセスのプロセス空間を厳密に読み取り専用で直接分析します。また、対象プロセスにバイトコードやその他の実行可能コードを一切注入しません。100% クリーンで安全です。
ツールのオーバーヘッド
このチュートリアルで紹介した動的トレーシングツール lj-slowest-ngx-re
は非常に効率的で、オンライン実行に適しています。
ツールが実行されておらず、アクティブにサンプリングしていない場合、システムおよび対象プロセスへのオーバーヘッドは厳密にゼロです。対象アプリケーションやプロセスに追加のコードやプラグインを一切注入しないため、固有のオーバーヘッドはありません。
サンプリング中、典型的なサーバーハードウェアにおいて、リクエストのレイテンシは平均で約 7 マイクロ秒(μs)しか増加しません。また、各 CPU コアで毎秒数万リクエストを処理する最速の OpenResty/Nginx サーバーにおいて、最大リクエストスループットの低下は測定不能なレベルです。
nResty XRay](https://openresty.com/jp/xray/) は動的トレーシング製品であり、実行中のアプリケーションを自動的に分析して、パフォーマンスの問題、動作の問題、セキュリティの脆弱性を解決し、実行可能な提案を提供いたします。基盤となる実装において、OpenResty XRay は弊社の Y 言語によって駆動され、Stap+、eBPF+、GDB、ODB など、様々な環境下で複数の異なるランタイムをサポートしております。
著者について
章亦春(Zhang Yichun)は、オープンソースの OpenResty® プロジェクトの創始者であり、OpenResty Inc. の CEO および創業者です。
章亦春(GitHub ID: agentzh)は中国江蘇省生まれで、現在は米国ベイエリアに在住しております。彼は中国における初期のオープンソース技術と文化の提唱者およびリーダーの一人であり、Cloudflare、Yahoo!、Alibaba など、国際的に有名なハイテク企業に勤務した経験があります。「エッジコンピューティング」、「動的トレーシング」、「機械プログラミング」 の先駆者であり、22 年以上のプログラミング経験と 16 年以上のオープンソース経験を持っております。世界中で 4000 万以上のドメイン名を持つユーザーを抱えるオープンソースプロジェクトのリーダーとして、彼は OpenResty® オープンソースプロジェクトをベースに、米国シリコンバレーの中心部にハイテク企業 OpenResty Inc. を設立いたしました。同社の主力製品である OpenResty XRay動的トレーシング技術を利用した非侵襲的な障害分析および排除ツール)と OpenResty XRay(マイクロサービスおよび分散トラフィックに最適化された多機能
翻訳
英語版の原文と日本語訳版(本文)をご用意しております。読者の皆様による他の言語への翻訳版も歓迎いたします。全文翻訳で省略がなければ、採用を検討させていただきます。心より感謝申し上げます!
-
OpenResty XRay の C 言語 CPU フレームグラフは、ほとんどのオープンソースソリューションよりもはるかに強力です。対象プロセスとの連携なしに JIT コンパイルされたマシンコードの Unwind をサポートし、インライン化された C 関数や詳細なソースファイル名、行番号もサポートしています。 ↩︎
-
オープンソースの Linux
perf
ツールチェーンには、JIT コードの展開サポートが限定的にあります。これには対象アプリケーションが特別なperf.map
ファイルを作成する必要があります。これは対象アプリケーションからの特別な支援と協力が必要であり、ユーザーがサンプリングやプロファイリングを必要としない場合でも、ファイルデータの漏洩や追加のランタイムオーバーヘッドにつながる可能性があります。 ↩︎