OpenResty中使用的NGINX知识

API7.ai

September 17, 2022

OpenResty (NGINX + Lua)

前回の記事を通じて、OpenRestyの一般的な知識を学びました。次のいくつかの記事では、OpenRestyの2つの基盤であるNGINXとLuaJITについて解説し、これらの基礎をマスターすることでOpenRestyをより深く理解できるようにします。

今日はNGINXから始めますが、ここではOpenRestyで使用される可能性のあるNGINXの基礎のみを紹介します。これはNGINXのごく一部に過ぎません。

設定に関して、OpenRestyの開発では以下の点に注意する必要があります。

  • nginx.confの設定をできるだけ少なくすること。
  • ifsetrewriteなどの複数のディレクティブの組み合わせを避けること。
  • Luaコードで解決できる場合は、NGINXの設定、変数、モジュールを使用しないこと。

これらの方法により、可読性、保守性、拡張性が最大限に高まります。以下のNGINX設定は、設定をコードとして使用する典型的な悪い例です。

location ~ ^/mobile/(web/app.htm) {
    set $type $1;
    set $orig_args $args;
    if ( $http_user_Agent ~ "(iPhone|iPad|Android)" ) {
        rewrite  ^/mobile/(.*) http://touch.foo.com/mobile/$1 last;
    }
    proxy_pass http://foo.com/$type?$orig_args;
}

これはOpenRestyで開発する際に避けるべきものです。

NGINXの設定

NGINXは設定ファイルを通じてその動作を制御します。この設定ファイルは、シンプルなDSLと考えることができます。NGINXはプロセス起動時に設定を読み取り、メモリにロードします。設定ファイルを変更した場合、NGINXを再起動またはリロードし、NGINXが再度設定ファイルを読み取るまで待つ必要があります。新しい設定が有効になるのはその時です。商用版のNGINXのみが、実行時にAPIの形でこの動的な機能の一部を提供しています。

以下の設定から始めましょう。これは非常にシンプルです。

worker_processes auto;

pid logs/nginx.pid;
error_log logs/error.log notice;

worker_rlimit_nofile 65535;

events {
    worker_connections 16384;
}

http {
    server {
        listen 80;
        listen 443 ssl;

        location / {
            proxy_pass https://foo.com;
        }
    }
}

stream {
    server {
        listen 53 udp;
    }
}

しかし、これらのシンプルな設定でも、いくつかの基本的な概念が関わっています。

まず、各ディレクティブにはそのコンテキストがあります。これは、NGINX設定ファイル内でのそのスコープです。

最上位はmainで、特定のビジネスに関係のないいくつかのディレクティブを含んでいます。例えば、worker_processespiderror_logはすべてmainコンテキストの一部です。また、コンテキスト間には階層関係があります。例えば、locationのコンテキストはserverserverのコンテキストはhttphttpのコンテキストはmainです。

ディレクティブは誤ったコンテキストで実行することはできません。NGINXは起動時にnginx.confが合法かどうかをチェックします。例えば、listen 80;をserverコンテキストからmainコンテキストに変更してNGINXサービスを起動すると、次のようなエラーが表示されます。

"listen" directive is not allowed here ......

次に、NGINXはHTTPリクエストやHTTPSトラフィックだけでなく、UDPやTCPトラフィックも処理できます。L7はHTTPに、L4はStreamにあります。OpenRestyでは、lua-nginx-modulestream-lua-nginx-moduleがそれぞれこれらに対応しています。

ここで注意すべき点は、OpenRestyがNGINXのすべての機能をサポートしているわけではなく、OpenRestyのバージョンを確認する必要があることです。OpenRestyのバージョンはNGINXと一致しており、識別が容易です。

上記のnginx.confで関わる設定ディレクティブは、NGINXのコアモジュールngx_core_modulengx_http_core_modulengx_stream_core_moduleにあります。具体的なドキュメントはリンクをクリックして確認できます。

MASTER-WORKERモード

設定ファイルを理解した後、NGINXのマルチプロセスモードを見てみましょう(以下の図を参照)。NGINXが起動すると、1つのMasterプロセスと複数のWorkerプロセス(または1つのWorkerプロセス、設定による)が存在します。

NGINX Worker Mode

まず、Masterプロセスはその名の通り「管理者」の役割を果たし、クライアントからのリクエストを処理しません。Workerプロセスを管理し、管理者からのシグナルを受け取り、Workerの状態を監視します。Workerプロセスが異常終了した場合、Masterプロセスは新しいWorkerプロセスを再起動します。

Workerプロセスは「実際に働く従業員」で、クライアントからのリクエストを処理します。これらはMasterプロセスからフォークされ、互いに独立しています。このマルチプロセスモデルは、Apacheのマルチスレッドモデルよりもはるかに進んでおり、スレッド間のロックがなく、デバッグが容易です。プロセスがクラッシュして終了しても、通常は他のワーカーの作業に影響を与えません。

OpenRestyは、NGINXのMaster-Workerモデルに独自の特権エージェントを追加しています。このプロセスはどのポートもリッスンせず、NGINXのMasterプロセスと同じ特権を持っているため、ローカルディスクファイルへの書き込み操作など、高い特権を必要とするタスクを実行できます。

特権プロセスがNGINXのバイナリホットアップグレードメカニズムと連携すると、OpenRestyは外部プログラムに依存せずに、実行中にバイナリ全体を自己アップグレードできます。

外部プログラムへの依存を減らし、OpenRestyプロセス内で問題を解決しようとすることで、デプロイが容易になり、運用コストが削減され、プログラムエラーの発生確率が低くなります。OpenRestyの特権プロセスとngx.pipeは、この目的のために存在します。

実行フェーズ

実行フェーズもNGINXの重要な機能であり、OpenRestyの具体的な実装と密接に関連しています。NGINXには11の実行フェーズがあり、ngx_http_core_module.hのソースコードで確認できます。

typedef enum {
    NGX_HTTP_POST_READ_PHASE = 0,

    NGX_HTTP_SERVER_REWRITE_PHASE,

    NGX_HTTP_FIND_CONFIG_PHASE,
    NGX_HTTP_REWRITE_PHASE,
    NGX_HTTP_POST_REWRITE_PHASE,

    NGX_HTTP_PREACCESS_PHASE,

    NGX_HTTP_ACCESS_PHASE,
    NGX_HTTP_POST_ACCESS_PHASE,

    NGX_HTTP_PRECONTENT_PHASE,

    NGX_HTTP_CONTENT_PHASE,

    NGX_HTTP_LOG_PHASE
} ngx_http_phases;

これらの11のフェーズの役割について詳しく知りたい場合は、NGINXのドキュメントを読むことをお勧めします。ここでは詳しく説明しません。

偶然にも、OpenRestyにもNGINXのフェーズに関連する11の*_by_luaディレクティブがあります(以下の図を参照、lua-nginx-moduleのドキュメントより)。

Order of Lua NGINX Module Directives

init_by_luaMasterプロセスが作成されたときにのみ実行され、init_worker_by_luaは各Workerプロセスが作成されたときにのみ実行されます。他の*_by_luaコマンドはクライアントリクエストによってトリガーされ、繰り返し実行されます。

したがって、init_by_luaフェーズでは、Luaモジュールや読み取り専用のデータを事前にロードし、OSのCOW(Copy On Write)機能を利用してメモリを節約できます。

ほとんどの操作はcontent_by_lua内で行うことができますが、以下のように異なる機能に応じて分割することをお勧めします。

  • set_by_lua: 変数の設定。
  • rewrite_by_lua: 転送、リダイレクトなど。
  • access_by_lua: アクセス、権限など。
  • content_by_lua: 返却コンテンツの生成。
  • header_filter_by_lua: レスポンスヘッダのフィルタ処理。
  • body_filter_by_lua: レスポンスボディのフィルタ処理。
  • log_by_lua: ロギング。

例を挙げて、このように分割する利点を示します。外部に多くの平文APIが提供されていると仮定し、カスタムの暗号化・復号化ロジックを追加する必要があるとします。すべてのAPIのコードを変更する必要があるでしょうか?

location /mixed {
    content_by_lua '...';
}

もちろん、そうではありません。フェーズ機能を利用して、accessフェーズで復号化し、body filterフェーズで暗号化することで、元のcontentフェーズのコードに変更を加えることなく実現できます。

location /mixed {
    access_by_lua '...';
    content_by_lua '...';
    body_filter_by_lua '...';
}

NGINXバイナリのホットアップグレード

最後に、NGINXバイナリのホットアップグレードについて簡単に説明します。NGINXの設定ファイルを変更した後、リロードして有効にする必要があることはご存知でしょう。しかし、NGINX自体をアップグレードする場合、実行中に行うことができます。これは本末転倒のように見えるかもしれませんが、NGINXが伝統的な静的ロードバランシング、リバースプロキシ、ファイルキャッシュから始まったことを考えると理解できます。

ホットアップグレードは、古いMasterプロセスにUSR2WINCHシグナルを送ることで行われます。前者は新しいMasterプロセスを起動し、後者はWorkerプロセスを徐々にシャットダウンします。

これら2つのステップの後、新しいMasterと新しいWorkerが起動します。この時点で、古いMasterは終了しません。終了しない理由は簡単で、フォールバックが必要な場合、古いMasterHUPシグナルを送ることができるからです。もちろん、フォールバックが必要ないと判断した場合は、古いMasterKILLシグナルを送って終了させることができます。

これで、NGINXバイナリのホットアップグレードが完了します。

これについてより詳細な情報を知りたい場合は、公式ドキュメントをチェックして学習を続けてください。

まとめ

全体的に、OpenRestyで使用するのはNGINXの基礎であり、主に設定、マスター-ワーカープロセス、実行フェーズなどに関連しています。Luaコードで解決できるものは、できるだけコードで解決し、NGINXモジュールや設定を使用しないことが、OpenRestyを学ぶ際の考え方の変化です。

最後に、オープンな質問を残します。Nginxは公式にNJSをサポートしており、OpenRestyと同様にJSを使用してNGINXのロジックを制御できます。これについてどう思いますか?この記事を共有して意見を聞かせてください。