"inspect": Apache APISIX Lua 动态调试插件
JinHua Luo
January 29, 2023
Lua動的デバッグプラグインが必要な理由
Apache APISIXには大量のLuaコードが含まれています。ソースコードを変更せずに、実行時にコード内の変数の値を検査する方法はあるでしょうか?
Luaのソースコードをデバッグのために変更することにはいくつかの欠点があります:
- 本番環境ではソースコードの変更は許可されていないことが多く、また許可すべきではありません
- ソースコードを変更するとリロードが必要になり、ビジネスプロセスの動作が妨げられます
- コンテナ化された環境ではソースコードの変更が難しい場合があります
- 一時的に生成されたコードはロールバック時に省略されやすく、メンテナンス上の問題を引き起こします
通常、変数の値を検査する必要があるのは、関数の開始時や終了時だけでなく、特定の条件が満たされたとき、例えばループがある回数に達したときや特定の条件が真になったときです。また、単に変数の値を出力するだけでは不十分な場合もあり、関連情報を外部システムに送信する必要があることもあります。さらに、このプロセスを動的にする方法や、プログラムのパフォーマンスに悪影響を与えずにこれを行う方法はあるでしょうか?
Lua動的デバッグプラグイン、inspect
は、上記の要件を満たすのに役立ちます。
- カスタマイズ可能なブレークポイント処理
- 動的なブレークポイント設定
- 複数のブレークポイントを設定可能
- ブレークポイントを一度だけトリガーするように設定可能
- パフォーマンスへの影響を制御し、特定の範囲内に保つことが可能
プラグインの動作原理
このプラグインは、LuaのDebug API
機能を活用してその機能を実装しています。インタプリタモードでは、実行される各バイトコードは特定のファイルと行番号にマッピングされます。ブレークポイントを設定するためには、行番号が期待される値と等しいかどうかを確認し、事前に定義したブレークポイント関数を実行するだけで済みます。これにより、対応する行のコンテキスト情報(upvalue、ローカル変数、スタックなどのメタデータ)を処理することができます。
APISIXはLuaのJIT実装であるLuaJITを使用しており、多くのホットコードパスは機械語にコンパイルされて実行されます。しかし、これらはDebug API
の影響を受けないため、ブレークポイントを有効にする前にJITキャッシュをクリアする必要があります。重要なのは、特定のLua関数のJITキャッシュのみをクリアすることを選択できるため、グローバルなパフォーマンスへの影響を最小限に抑えることができる点です。プログラムが実行されると、多くのJITコンパイルされたコードブロックがLuaJITでtrace
として命名されます。これらのトレースはLua関数に関連付けられており、単一のLua関数には複数のトレースが含まれる場合があります。これらは関数内の異なるホットパスを参照しています。
グローバル関数やモジュールレベルの関数については、それらの関数オブジェクトを指定してJITキャッシュをクリアすることができます。しかし、行番号が匿名関数などの他の関数タイプに対応している場合、グローバルに関数オブジェクトを取得することはできません。そのような場合、すべてのJITキャッシュをクリアするしかありません。デバッグ中に新しいトレースは生成されませんが、クリアされていない既存のトレースは引き続き実行されます。十分に制御できれば、プログラムのパフォーマンスに影響はありません。なぜなら、長時間実行されているオンラインシステムでは通常新しいトレースは生成されないからです。デバッグが終了し、すべてのブレークポイントが解除されると、システムは通常のJITモードに戻り、クリアされたJITキャッシュはホットスポットに再入したときに再生成されます。
インストールと設定
このプラグインはデフォルトで有効になっています。
conf/confg.yaml
を正しく設定してこのプラグインを有効にします:
plugins:
...
- inspect
plugin_attr:
inspect:
delay: 3
hooks_file: "/usr/local/apisix/plugin_inspect_hooks.lua"
プラグインはデフォルトで3秒ごとにファイル'/usr/local/apisix/plugin_inspect_hooks.lua'からブレークポイント定義を読み取ります。 デバッグするには、このファイルを編集するだけです。
このパスへのシンボリックリンクを作成することをお勧めします。これにより、ブレークポイントファイルの異なる履歴バージョンをアーカイブしやすくなります。
ファイルが変更されると、プラグインは以前のすべてのブレークポイントをクリアし、ブレークポイントファイルで定義された新しいブレークポイントをすべて有効にします。これらのブレークポイントはすべてのワーカープロセスで有効になります。
一般的に、ファイルを削除する必要はありません。ブレークポイントを定義する際に、いつ解除するかを指定できるからです。
このファイルを削除すると、すべてのワーカープロセスのブレークポイントが解除されます。
ブレークポイントの開始と停止のログは'WARN'ログレベルで記録されます。
ブレークポイントの定義
require("apisix.inspect.dbg").set_hook(file, line, func, filter_func)
file
、ファイル名、曖昧でないファイル名またはパスを指定できます。line
、ファイル内の行番号、ブレークポイントは行番号と密接に関連しているため、コードが変更された場合は行番号も変更する必要があります。func
、トレースをクリアする関数の名前。nil
の場合、luajit vm
内のすべてのトレースがクリアされます。filter_func
、ブレークポイントを処理するカスタムLua関数- 入力パラメータは以下の内容を含む
table
です:finfo
:debug.getinfo(level, "nSlf")
の戻り値uv
: upvaluesのハッシュテーブルvals
: ローカル変数のハッシュテーブル
- 関数の戻り値が
true
の場合、ブレークポイントは自動的に解除されます。それ以外の場合、ブレークポイントは引き続き有効です。
- 入力パラメータは以下の内容を含む
例:
local dbg = require "apisix.inspect.dbg"
dbg.set_hook("limit-req.lua", 88, require("apisix.plugins.limit-req").access,
function(info)
ngx.log(ngx.INFO, debug.traceback("foo traceback", 3))
ngx.log(ngx.INFO, dbg.getname(info.finfo))
ngx.log(ngx.INFO, "conf_key=", info.vals.conf_key)
return true
end)
dbg.set_hook("t/lib/demo.lua", 31, require("t.lib.demo").hot2, function(info)
if info.vals.i == 222 then
ngx.timer.at(0, function(_, body)
local httpc = require("resty.http").new()
httpc:request_uri("http://127.0.0.1:9080/upstream1", {
method = "POST",
body = body,
})
end, ngx.var.request_uri .. "," .. info.vals.i)
return true
end
return false
end)
--- さらにブレークポイント ...
demo
ブレークポイントは、いくつかの情報を整理して外部サーバーに送信することに注意してください。また、使用されているresty.http
ライブラリはcosocket
に基づく非同期ライブラリです。
OpenRestyの非同期APIを呼び出すときは、timer
を使用して遅延させて送信する必要があります。なぜなら、ブレークポイントで実行される関数は同期処理でありブロッキングされるため、nginx
のメインプログラムに戻って非同期処理を行うことができないからです。そのため、遅延させる必要があります。
使用例
リクエストボディの内容に基づいてルートを決定する
次の要件があるとします:リクエストボディに文字列APISIX: 666
が含まれるPOSTリクエストのみを受け入れるルートをどのように設定できますか?
ルート設定にはvars
フィールドがあり、nginx
変数の値をチェックしてルートがマッチするかどうかを判断するために使用できます。nginx
が提供する$request_body
変数にはリクエストボディの値が含まれているため、この変数を使用して要件を実装できます。
ルートを設定してみましょう:
curl http://127.0.0.1:9180/apisix/admin/routes/var_route \
-H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d '
{
"uri": "/anything",
"methods": ["POST"],
"vars": [["request_body", "~~", "APISIX: 666"]],
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org": 1
}
}
}'
次に、これを試してみます:
curl http://127.0.0.1:9080/anything
{"error_msg":"404 Route Not Found"}
curl -i http://127.0.0.1:9080/anything -X POST -d 'hello, APISIX: 666.'
HTTP/1.1 404 Not Found
Date: Thu, 05 Jan 2023 03:53:35 GMT
Content-Type: text/plain; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Server: APISIX/3.0.0
{"error_msg":"404 Route Not Found"}
奇妙ですね、なぜこのルートにマッチしないのでしょうか?
次に、nginxのこの変数のドキュメントを見てみましょう:
この変数の値は、リクエストボディがメモリバッファに読み込まれたときに、proxy_pass、fastcgi_pass、uwsgi_pass、scgi_passディレクティブによって処理される場所で利用可能になります。
つまり、この変数を使用する前にリクエストボディを読み取る必要があります。
ルートをマッチングするとき、この変数は空でしょうか?inspect
プラグインを使用してこれを確認できます。
ルートをマッチングするコードの行を見つけました:
apisix/init.lua
...
api_ctx.var.request_uri = api_ctx.var.uri .. api_ctx.var.is_args .. (api_ctx.var.args or "")
router.router_http.match(api_ctx)
local route = api_ctx.matched_route
if not route then
...
515行目の変数request_body
を確認しましょう。これはrouter.router_http.match(api_ctx)
です。
ブレークポイントの設定
ファイル/usr/local/apisix/example_hooks.lua
を編集します:
local dbg = require("apisix.inspect.dbg")
dbg.set_hook("apisix/init.lua", 515, require("apisix").http_access_phase, function(info)
core.log.warn("request_body=", info.vals.api_ctx.var.request_body)
return true
end)
ブレークポイントファイルへのシンボリックリンクを作成します:
ln -sf /usr/local/apisix/example_hooks.lua /usr/local/apisix/plugin_inspect_hooks.lua
ログを確認して、ブレークポイントが有効になっていることを確認します。
2023/01/05 12:02:43 [warn] 1890559#1890559: *15736 [lua] init.lua:68: setup_hooks():
set hooks: err: true, hooks: ["apisix\/init.lua#515"], context: ngx.timer
ルートマッチングを再トリガーします:
curl -i http://127.0.0.1:9080/anything -X POST -d 'hello, APISIX: 666.'
ログを確認します:
2023/01/05 12:02:59 [warn] 1890559#1890559: *16152
[lua] [string "local dbg = require("apisix.inspect.dbg")..."]:39:
request_body=nil, client: 127.0.0.1, server: _,
request: "POST /anything HTTP/1.1", host: "127.0.0.1:9080"
もちろん、request_body
は空です!
解決策
リクエストボディを読み取る必要があることがわかっているので、vars
を使用してこれを行うことはできません。代わりに、ルートのfilter_func
フィールドを使用して要件を実現できます。
curl http://127.0.0.1:9180/apisix/admin/routes/var_route \
-H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d '
{
"uri": "/anything",
"methods": ["POST"],
"filter_func": "function(_) return require(\"apisix.core\").request.get_body():find(\"APISIX: 666\") end",
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org": 1
}
}
}'
確認してみましょう:
curl http://127.0.0.1:9080/anything -X POST -d 'hello, APISIX: 666.'
{
"args": {},
"data": "",
"files": {},
"form": {
"hello, APISIX: 666.": ""
},
"headers": {
"Accept": "*/*",
"Content-Length": "19",
"Content-Type": "application/x-www-form-urlencoded",
"Host": "127.0.0.1",
"User-Agent": "curl/7.68.0",
"X-Amzn-Trace-Id": "Root=1-63b64dbd-0354b6ed19d7e3b67013592e",
"X-Forwarded-Host": "127.0.0.1"
},
"json": null,
"method": "POST",
"origin": "127.0.0.1, xxx",
"url": "http://127.0.0.1/anything"
}
問題解決!
ログレベルによってブロックされているログを出力する
一般的に、本番環境ではINFO
レベルのログを有効にしませんが、時には詳細な情報を確認する必要があります。どうすればよいでしょうか?
通常、直接INFO
レベルを設定してリロードすることはしません。これには2つの欠点があります:
- ログが多すぎてパフォーマンスに影響し、確認が難しくなります
- リロードすると永続的な接続が切断され、オンライントラフィックに影響します
通常、特定のポイントのログを確認するだけで十分です。例えば、APISIXはetcd
を設定配布データベースとして使用していることは知られていますが、データプレーンにルート設定が増分的に更新されたときに、いつ、どのような具体的なデータが更新されたかを確認できますか?
apisix/core/config_etcd.lua
local function sync_data(self)
...
log.info("waitdir key: ", self.key, " prev_index: ", self.prev_index + 1)
log.info("res: ", json.delay_encode(dir_res, true), ", err: ", err)
...
end
増分同期のためのLua関数はsync_data()
ですが、etcd watchからの増分データをINFO
レベルで出力します。
そこで、inspect
プラグインを使用して表示してみましょう。ルートリソースの変更のみを表示します。
/usr/local/apisix/example_hooks.lua
を編集します:
local dbg = require("apisix.inspect.dbg")
local core = require("apisix.core")
dbg.set_hook("apisix/core/config_etcd.lua", 393, nil, function(info)
local filter_res = "/routes"
if info.vals.self.key:sub(-#filter_res) == filter_res and not info.vals.err then
core.log.warn("etcd watch /routes response: ", core.json.encode(info.vals.dir_res, true))
return true
end
return false
end)
このブレークポイント処理関数のロジックは、フィルタリング能力を明確に示しています。watch
のkey
が/routes
で、err
が空の場合、etcdから返されたデータを一度だけ出力し、その後ブレークポイントを解除します。
sync_data()
はローカル関数なので、直接参照することはできません。
その場合、set_hook
の3番目のパラメータをnil
に設定するしかありません。これにはすべてのトレースをクリアする副作用があります。
上記の例では、シンボリックリンクを作成しているので、編集後にファイルを保存するだけで済みます。ブレークポイントは数秒後に有効になります。ログを確認して確認できます。
ログを確認することで、必要な情報を取得できます。これはWARN
ログレベルで出力されます。
また、データプレーンでetcdの増分データを取得した時刻も表示されます。
2023/01/05 14:33:10 [warn] 1890562#1890562: *231311
[lua] [string "local dbg = require("apisix.inspect.dbg")..."]:41:
etcd watch /routes response: {"headers":{"X-Etcd-Index":"24433"},
"body":{"node":[{"value":{"uri":"\/anything",
"plugins":{"request-id":{"header_name":"X-Request-Id","include_in_response":true,"algorithm":"uuid"}},
"create_time":1672898912,"status":1,"priority":0,"update_time":1672900390,
"upstream":{"nodes":{"httpbin.org":1},"hash_on":"vars","type":"roundrobin","pass_host":"pass","scheme":"http"},
"id":"reqid"},"key":"\/apisix\/routes\/reqid","modifiedIndex":24433,"createdIndex":24429}]}}, context: ngx.timer
結論
Lua動的デバッグは重要な補助機能です。APISIXのinspect
プラグインを使用することで、以下のような多くのことができます:
- 問題の原因を特定し、トラブルシューティングを行う
- ブロックされているログを出力し、必要に応じてさまざまな情報を取得する
- デバッグを通じてLuaコードを学ぶ
詳細については、関連ドキュメントを参照してください。