Webサーバーを超えて:特権プロセスとタイマータスク
API7.ai
November 3, 2022
前回の記事では、OpenRestyのAPI、shared dict
、cosocket
を紹介しました。これらはすべてNGINXやWebサーバーの領域内で機能を実装し、低コストでメンテナンスが容易なプログラマブルなWebサーバーを提供します。
しかし、OpenRestyはそれ以上のことができます。今日は、Webサーバーを超えたOpenRestyのいくつかの機能を取り上げて紹介します。それらは、タイマータスク、特権プロセス、非ブロッキングなngx.pipe
です。
タイマータスク
OpenRestyでは、時々バックグラウンドで特定のタスクを定期的に実行する必要があります。例えば、データの同期やログのクリーンアップなどです。もしあなたがこれを設計するとしたら、どうしますか?最も簡単な方法は、これらのタスクを実行するためのAPIインターフェースを外部に提供し、システムのcrontab
を使用して定期的にcurl
を呼び出してこのインターフェースにアクセスし、間接的にこの要件を実現することです。
しかし、これは断片的であるだけでなく、運用と保守に高い複雑さをもたらします。そこで、OpenRestyはngx.timer
を提供して、このような要件を解決します。ngx.timer
は、OpenRestyによってシミュレートされたクライアントリクエストとして、対応するコールバック関数をトリガーします。
OpenRestyのタイマータスクは、以下の2種類に分けられます。
ngx.timer.at
は、1回限りのタイマータスクを実行するために使用されます。ngx.timer.every
は、固定期間のタイマータスクを実行するために使用されます。
前回の記事の最後に残した考えさせられる質問を覚えていますか?その質問は、init_worker_by_lua
でcosocket
を使用できない制限をどのように打破するかというもので、その答えはngx.timer
です。
以下のコードは、遅延0
でタイマータスクを開始します。コールバックhandler
関数を開始し、この関数内でcosocket
を使用してウェブサイトにアクセスします。
init_worker_by_lua_block {
local function handler()
local sock = ngx.socket.tcp()
local ok, err = sock:connect(“api7.ai", 80)
end
local ok, err = ngx.timer.at(0, handler)
}
これにより、この段階でcosocket
を使用できない制限を回避できます。
このセクションの冒頭で述べたユーザー要件に戻ると、ngx.timer.at
は定期的に実行する必要がある要件に対応していません。上記のコード例では、1回限りのタスクです。
では、定期的に実行するにはどうすればよいでしょうか?ngx.timer.at
APIに基づいて、2つの選択肢があるようです。
- コールバック関数内で
while true
無限ループを使用して、タスクを実行した後にしばらくsleep
することで、自分で定期的なタスクを実装できます。 - コールバック関数の最後に新しいタイマーを作成することもできます。
しかし、選択する前に、1つ明確にしておく必要があります。タイマーは本質的にリクエストであり、クライアントによって開始されたものではありません。リクエストは、タスクを完了した後に終了する必要があり、常に常駐することはできません。そうしないと、さまざまなリソースリークを引き起こす可能性があります。
したがって、while true
を使用して定期的なタスクを実装する最初の解決策は信頼できません。2番目の解決策は実行可能ですが、再帰的にタイマーを作成するため、理解しにくいです。
では、より良い解決策はあるでしょうか?OpenRestyの背後にある新しいngx.timer.every
APIは、この問題を解決するために特別に設計されており、crontab
に近い解決策です。
欠点は、タイマータスクを開始した後にキャンセルする機会がないことです。結局のところ、ngx.timer.cancel
はまだ未実装の機能です。
この時点で、あなたは問題に直面します。タイマーはバックグラウンドで実行されており、キャンセルできません。もし多くのタイマーがある場合、システムリソースを使い果たす可能性があります。
したがって、OpenRestyはlua_max_pending_timers
とlua_max_running_timers
という2つのディレクティブを提供して、これらを制限します。前者は実行待ちのタイマーの最大数を表し、後者は現在実行中のタイマーの最大数を表します。
また、Lua APIを使用して、現在待機中および実行中のタイマータスクの値を取得することもできます。以下の2つの例を参照してください。
content_by_lua_block {
ngx.timer.at(3, function() end)
ngx.say(ngx.timer.pending_count())
}
このコードは1
を出力し、1つのスケジュールされたタスクが実行待ちであることを示します。
content_by_lua_block {
ngx.timer.at(0.1, function() ngx.sleep(0.3) end)
ngx.sleep(0.2)
ngx.say(ngx.timer.running_count())
}
このコードは1
を出力し、1つのスケジュールされたタスクが実行中であることを示します。
特権プロセス
次に、特権プロセスを見てみましょう。ご存知の通り、NGINXはMaster
プロセスとWorker
プロセスに分かれており、ワーカープロセスはユーザーリクエストを処理します。lua-resty-core
で提供されるprocess.type
APIを使用して、プロセスのタイプを取得できます。例えば、resty
を使用して以下の関数を実行できます。
$ resty -e 'local process = require "ngx.process"
ngx.say("process type:", process.type())'
single
という結果が返されることがわかります。これは、resty
がMaster
プロセスではなくWorker
プロセスでNGINXを起動することを意味します。これは事実です。resty
の実装では、以下のような行でMaster
プロセスがオフになっていることがわかります。
master_process off;
OpenRestyは、privileged agent
を追加することでNGINXを拡張しています。特権プロセスには以下の特別な機能があります。
-
外部にサービスを提供しないため、どのポートも監視しません。
-
Master
プロセスと同じ特権を持ち、一般的にはroot
ユーザーの特権を持ち、Worker
プロセスでは不可能な多くのタスクを実行できます。 -
特権プロセスは
init_by_lua
コンテキストでのみ開くことができます。 -
また、特権プロセスは
init_worker_by_lua
コンテキストで実行される場合にのみ意味があります。なぜなら、リクエストがトリガーされず、content
、access
などのコンテキストには移動しないからです。
特権プロセスを有効にする例を見てみましょう。
init_by_lua_block {
local process = require "ngx.process"
local ok, err = process.enable_privileged_agent()
if not ok then
ngx.log(ngx.ERR, "enables privileged agent failed error:", err)
end
}
このコードで特権プロセスを有効にし、OpenRestyサービスを開始すると、特権プロセスがNGINXプロセスの一部であることがわかります。
nginx: master process
nginx: worker process
nginx: privileged agent process
しかし、特権がinit_worker_by_lua
フェーズで1回だけ実行されるのは良いアイデアではありません。では、特権プロセスをどのようにトリガーすべきでしょうか?
はい、答えは先ほど教えた知識に隠されています。ポートを監視しないため、端末リクエストによってトリガーされることはありません。定期的にトリガーする唯一の方法は、先ほど紹介したngx.timer
を使用することです。
init_worker_by_lua_block {
local process = require "ngx.process"
local function reload(premature)
local f, err = io.open(ngx.config.prefix() .. "/logs/nginx.pid", "r")
if not f then
return
end
local pid = f:read()
f:close()
os.execute("kill -HUP " .. pid)
end
if process.type() == "privileged agent" then
local ok, err = ngx.timer.every(5, reload)
if not ok then
ngx.log(ngx.ERR, err)
end
end
}
上記のコードは、5秒ごとにマスタープロセスにHUP
シグナルを送信する機能を実装しています。自然に、これに基づいて、特権プロセスのためのタスクがあるかどうかをデータベースをポーリングし、それらを実行するなど、よりエキサイティングなことを行うことができます。特権プロセスはroot
権限を持っているため、これは明らかに「バックドア」プログラムの一部です。
非ブロッキングなngx.pipe
最後に、非ブロッキングなngx.pipe
を見てみましょう。これは、Luaの標準ライブラリを使用して外部コマンドラインを実行し、先ほどのコード例でMaster
プロセスにシグナルを送信します。
os.execute("kill -HUP " .. pid)
当然、この操作はブロッキングされます。では、OpenRestyで外部プログラムを非ブロッキングで呼び出す方法はあるでしょうか?結局のところ、OpenRestyをWebサーバーではなく完全な開発プラットフォームとして使用している場合、これが必要です。このために、lua-resty-shell
ライブラリが作成され、それを使用してコマンドラインを呼び出すと非ブロッキングになります。
$ resty -e 'local shell = require "resty.shell"
local ok, stdout, stderr, reason, status =
shell.run([[echo "hello, world"]])
ngx.say(stdout)
このコードは、hello world
を出力する別の方法で、システムのecho
コマンドを呼び出して出力を完了します。同様に、resty.shell
をLuaのos.execute
呼び出しの代替として使用できます。
lua-resty-shell
の基盤となる実装はlua-resty-core
のngx.pipe
APIに依存しているため、この例ではlua-resty-shell
を使用してhello world
を出力していますが、ngx.pipe
を使用すると以下のようになります。
$ resty -e 'local ngx_pipe = require "ngx.pipe"
local proc = ngx_pipe.spawn({"echo", "hello world"})
local data, err = proc:stdout_read_line()
ngx.say(data)'
上記はlua-resty-shell
の実装の基盤となるコードです。ngx.pipe
のドキュメントとテストケースをチェックして、その使用方法の詳細を確認できます。したがって、ここでは詳しく説明しません。
まとめ
以上で、今日の主な内容は終わりです。上記の機能から、OpenRestyはより良いNGINXを作りながら、汎用プラットフォームの方向に近づこうとしていることがわかります。開発者が技術スタックを統一し、OpenRestyを使用して開発ニーズを解決できるようにすることを望んでいます。これは、運用と保守にとって非常に友好的です。なぜなら、OpenRestyをデプロイするだけで保守コストが低くなるからです。
最後に、考えさせられる質問を残します。NGINXのWorker
が複数存在する可能性があるため、timer
は各Worker
で1回実行されます。これはほとんどのシナリオでは受け入れられません。timer
が1回だけ実行されるようにするにはどうすればよいでしょうか?
あなたの解決策をコメントに残してください。また、この記事を同僚や友人と共有して、一緒にコミュニケーションと改善を行いましょう。