Webサーバーを超えて:特権プロセスとタイマータスク

API7.ai

November 3, 2022

OpenResty (NGINX + Lua)

前回の記事では、OpenRestyのAPI、shared dictcosocketを紹介しました。これらはすべて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_luacosocketを使用できない制限をどのように打破するかというもので、その答えは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_timerslua_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という結果が返されることがわかります。これは、restyMasterプロセスではなくWorkerプロセスでNGINXを起動することを意味します。これは事実です。restyの実装では、以下のような行でMasterプロセスがオフになっていることがわかります。

master_process off;

OpenRestyは、privileged agentを追加することでNGINXを拡張しています。特権プロセスには以下の特別な機能があります。

  • 外部にサービスを提供しないため、どのポートも監視しません。

  • Masterプロセスと同じ特権を持ち、一般的にはrootユーザーの特権を持ち、Workerプロセスでは不可能な多くのタスクを実行できます。

  • 特権プロセスはinit_by_luaコンテキストでのみ開くことができます。

  • また、特権プロセスはinit_worker_by_luaコンテキストで実行される場合にのみ意味があります。なぜなら、リクエストがトリガーされず、contentaccessなどのコンテキストには移動しないからです。

特権プロセスを有効にする例を見てみましょう。

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-corengx.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回だけ実行されるようにするにはどうすればよいでしょうか?

あなたの解決策をコメントに残してください。また、この記事を同僚や友人と共有して、一緒にコミュニケーションと改善を行いましょう。