Lua io.popen が Nginx イベントループをブロックする問題:スループット 150 倍改善の実例
Lua の io.popen と、その IPC1 パイプハンドルに対する読み取り・クローズ操作は同期処理です。CPU と off-CPU の両面で OpenResty/Nginx のイベントループをブロックし、ワーカープロセスは待機中に他の処理を一切実行できません。ある本番環境の事例では、OpenResty XRay が対象プロセスの CPU 時間の 93.8% と off-CPU 時間の 99.8% を、io.popen とそのパイプハンドルの読み取り操作にまで遡って特定しました。OpenResty 独自のノンブロッキング lua-resty-shell ライブラリへの切り替えでスループットは約 7 倍に、さらにパイプをノンブロッキング cosocket API に置き換えることで 150 倍に向上しました。
本記事では、OpenResty XRay がネットワークセキュリティ業界のお客様の本番環境で、コード変更もプロセス再起動もなしに、このボトルネックを自動的に特定した過程を紹介します。
OpenResty XRay は動的トレーシング製品で、実行中のアプリケーションを自動的に分析し、パフォーマンスの問題、動作の問題、セキュリティの脆弱性を特定し、実行可能な提案を提供します。基盤技術として、OpenResty XRay は弊社の Y 言語によって駆動され、Stap+、eBPF+、GDB、ODB など、さまざまな環境で複数の異なるランタイムをサポートしています。
問題点:毎秒 130 リクエスト、CPU は半分以上がアイドル
お客様のオンライン OpenResty アプリケーションで深刻なパフォーマンス問題が発生しました。お客様のサーバーでの毎秒最大リクエスト数は約 130 と非常に低く、サービスがほぼ使用不可能なほど悪い状況でした。さらに、高性能 CPU を搭載したサーバーを使用しているにもかかわらず、Nginx ワーカープロセスは CPU リソースの半分以下しか利用できていませんでした。
OpenResty XRay によるイベントループブロッキングの特定
OpenResty XRay はお客様のオンラインプロセスを詳細に分析しました。以下のように、お客様側での協力は一切不要でした:
- 追加のプラグイン、モジュール、ライブラリは不要です。
- コードの注入やパッチも必要ありません。
- 特別なコンパイルや起動オプションも必要ありません。
- アプリケーションプロセスの再起動さえ必要ありません。
分析は完全に事後(ポストモーテム)方式で行われました。これは OpenResty XRay が採用している動的トレーシング技術のおかげです。
このようなパフォーマンス問題は OpenResty XRay にとって分析が容易です。CPU 操作と off-CPU 操作が同時に OpenResty/Nginx のイベントループをブロックしていることが判明しました。
CPU 操作:io.popen と file:read() のホットスポット
OpenResty XRay の自動分析レポートでは、io.popen とそれに関連する file:close() 操作が CPU 使用率の観点で非常にホットになっていました。
io.popen
CPU カテゴリーで、io.popen の問題が確認できます。これは対象プロセスが消費した総 CPU 時間の 93.8% を占めていました。
ハイライト表示された問題内の [builtin#io.popen] Lua 関数フレームに注目してください。
問題のテキストには、仮想マシン内の LuaJIT プリミティブを含む完全な Lua コードパスが表示されているため、ユーザーは該当する Lua ソースコードをすぐに特定できます。Lua 関数 run() の緑色のボックスにマウスカーソルを合わせると、Lua ソースファイル名や行番号などの詳細情報がツールチップとして表示されます。
io.popen の呼び出し位置が Lua ソースファイル /usr/local/openresty/site/lualib/cfg-utils.lua の 8 行目にあることがわかります。
file:read()
CPU カテゴリーで、file:read() の問題が確認できます。これは対象プロセスが消費した総 CPU 時間の 26.3% を占めていました。
ハイライト表示された問題内の [builtin#io.method.read] 関数フレームに注目してください。
問題のテキストには、仮想マシン内の LuaJIT プリミティブを含む完全な Lua コードパスが表示されているため、ユーザーは該当する Lua ソースコードをすぐに特定できます。Lua 関数 run() の緑色のボックスにマウスカーソルを合わせると、Lua ソースファイル名や行番号などの詳細情報がツールチップとして表示されます。
file:read() の呼び出し位置が Lua ソースファイル /usr/local/openresty/site/lualib/cfg-utils.lua の 14 行目にあることがわかります。お客様はこの行を確認し、これが以前の io.popen 呼び出しで開かれたファイルハンドル上で行われていることを確認しました。
off-CPU 操作:file:read() はどこでブロックして待つのか
ここでの “off-CPU” とは、オペレーティングシステムのスレッドがブロックされて待機状態になり、後続のコードを実行できない状態を指します。
この種の off-CPU ブロッキングは、テールレイテンシ悪化の典型的な根本原因です。関連事例として、50 万 QPS の OpenResty ゲートウェイで謎の 244ms 遅延を特定した事例もご覧ください。また、イベントループのブロッキングの深刻度を定量化する一連の分析過程(20 秒間で 43,952 件のブロッキングサンプル、最大 75 ミリ秒)については、300 RPS を超えようとしない Nginx worker プロセスの事例をご覧ください。
file:read()
診断レポートでは、file:read() の呼び出しが off-CPU 時間の観点からホットであることがわかりました。これは対象プロセスが消費した総 off-CPU 時間の 99.8% を占めており、本来ブロックしてよいのは Nginx イベントループのイベント待機操作(epoll_wait システムコールなど)だけのはずです。
ハイライト表示された問題内の [builtin#io.method.read] Lua 関数フレームに注目してください。
問題のテキストには、仮想マシン内の LuaJIT プリミティブを含む完全な Lua コードパスが表示されているため、ユーザーは該当する Lua ソースコードをすぐに特定できます。Lua 関数 run() の緑色のボックスにマウスカーソルを合わせると、Lua ソースファイル名や行番号などの詳細情報がツールチップとして表示されます。
file:read() の呼び出し位置が Lua ソースファイル /usr/local/openresty/site/lualib/cfg-utils.lua の 14 行目にあることがわかります。お客様はこの行を確認し、これも以前の io.popen 呼び出しで開かれたファイルハンドル上で行われていることを確認しました。
io.popen のノンブロッキング代替:lua-resty-shell・ngx.pipe・cosocket
上記の分析に基づき、問題の原因は io.popen やパイプファイルハンドルの読み取り・クローズ操作を含む、パイプの Lua API にあることがわかりました。これらは CPU と off-CPU 時間の両方で Nginx のイベントループを深刻にブロックしていました。したがって、解決策も明確です。
- OpenResty アプリケーションで io.popen Lua API の使用を避けます。代わりに OpenResty の lua-resty-shell ライブラリや低レベルの Lua API ngx.pipe を使用します。
- システムコマンドと IPC パイプを完全に避けます。OpenResty が提供するより効率的な cosocket API やそれを基にした高レベルのライブラリを使用します。
結果:lua-resty-shell で 7 倍、cosocket で 150 倍
お客様は弊社のアドバイスに従い、ゲートウェイアプリケーション上で標準の Lua API io.popen から OpenResty の lua-resty-shell ライブラリへの移行を行いました。その結果、約 7 倍の改善がすぐに見られました。
弊社はさらに、高コストのシステムコマンド呼び出しを完全に避けるべきだとアドバイスしました。そこでお客様はビジネスロジックを再設計し、OpenResty のノンブロッキング cosocket API を利用してメタデータを取得するようにしました。この変更により 150 倍の改善が見られ、1 つの CPU コアで 1 秒あたり数万のリクエストを処理できるようになりました。
お客様は現在のパフォーマンスに満足しています。
よくある質問
io.popen はなぜ Nginx イベントループをブロックするのですか?
io.popen とそのパイプファイルハンドルに対する読み取り・クローズ操作は同期的な Lua API です。OS スレッドはブロックされて待機状態になり、後続のコードを実行できません。本来ブロックしてよいのは、イベントループ自身のイベント待機操作(epoll_wait システムコールなど)だけです。本事例では、これらの呼び出しが対象プロセスの CPU 時間の 93.8%、off-CPU 時間の 99.8% を占めていました。
OpenResty では io.popen の代わりに何を使うべきですか?
OpenResty のノンブロッキング lua-resty-shell ライブラリ、または低レベル API の ngx.pipe を使用してください。本事例ではこれだけでスループットが 7 倍改善しました。さらに、システムコマンドの呼び出し自体を避け、ノンブロッキング cosocket API でデータを取得することで、150 倍の改善が得られました。
自社の Nginx ワーカープロセスが io.popen にブロックされているか確認するには?
OpenResty XRay は実行中のプロセスを自動的に分析します。追加のプラグインやライブラリ、コード注入、プロセスの再起動は一切不要です。診断レポートには、各ブロッキング操作の完全な Lua コードパスがソースファイル名・行番号付きで表示されます。本事例では cfg-utils.lua の 8 行目の io.popen 呼び出しを特定しました。
著者について
章亦春(Zhang Yichun)は、オープンソースの OpenResty® プロジェクトの創設者であり、OpenResty Inc. の CEO を務めています。
章亦春(GitHub ID: agentzh)は中国江蘇省生まれで、現在は米国ベイエリアに在住しております。彼は中国における初期のオープンソース技術と文化の提唱者およびリーダーの一人であり、Cloudflare、Yahoo!、Alibaba など、国際的に有名なハイテク企業に勤務した経験があります。「エッジコンピューティング」、「動的トレーシング」、「機械プログラミング」 の先駆者であり、22 年以上のプログラミング経験と 16 年以上のオープンソース経験を持っております。世界中で 4000 万以上のドメイン名を持つユーザーを抱えるオープンソースプロジェクトのリーダーとして、彼は OpenResty® オープンソースプロジェクトをベースに、米国シリコンバレーの中心部にハイテク企業 OpenResty Inc. を設立いたしました。同社の主力製品である OpenResty XRay(動的トレーシング技術を利用した非侵入型のプロファイリングおよびトラブルシューティングツール)と OpenResty Edge(マイクロサービスおよび分散トラフィックに最適化された多機能ゲートウェイソフトウェア)は、世界中の多くの上場企業および大企業から高い評価を得ております。OpenResty 以外にも、章亦春は Linux カーネル、Nginx、LuaJIT、GDB、SystemTap、LLVM、Perl など、複数のオープンソースプロジェクトに累計 100 万行以上のコードを寄与し、60 以上のオープンソースソフトウェアライブラリを執筆しております。
翻訳
英語版の原文と日本語訳版(本文)をご用意しております。読者の皆様による他の言語への翻訳版も歓迎いたします。全文翻訳で省略がなければ、採用を検討させていただきます。心より感謝申し上げます!
IPC はプロセス間通信を指します。 ↩︎

























