lua-nginx-module の紹介 ならびに Nginx+Lua+Redisによる動的なリバースプロキシの実装案

Nginxは非常に強力なhttpdですが、独自のモジュールを実装しようとするとこれまた非常に敷居が高い印象です。

追記

この記事よりも前に http://openresty.org/#DynamicRoutingBasedOnRedis でほとんど同じ内容のエントリが書かれていました。こちらも参照ください


モジュールの開発はむずかしい

まず開発用のドキュメントはほとんどありません。必然 既存のモジュールをお手本としますが、コメントも少ないのでソースだけが頼りです。

{ファイル,ネットワーク} I/O を伴う処理では、Nginxのノンブロッキング/イベントドリブンのアーキテクチャにのっとってコールバックを駆使したCで実装する必要があり、LLで育ったゆとり脳では太刀打ちできませんでした

lua-nginx-module が代わりになるかも

なんらかのNginxモジュールを開発しなければならない場合、lua-nginx-module が代わりにならないか検討してみましょう。

lua-nginx-module を使うと Luaのコードを通してNginxを制御できます。設定ファイルで扱える変数の定義/読み書きや、HTTPリクエスト(ヘッダ、ボディ)の操作、mysqld,memcached,redis といったストレージを組み合わせて使う方法もあります。

lua-nginx-module の熱い機能の紹介

ここからは lua-nginx-module の中でも私自身が特に興味をひかれた機能について記します。日本語ブログで lua-nginx-moduleへの言及が少ないので、何らかの足しになればと思います。

また後半では Nginx + Lua + Redis を使用して動的にupstream(=バックエンド) を決めるリバースプロキシの実装例を取り上げます。

変数の操作 - upstream をLuaからいじる

ngx.var.<変数名> で設定ファイルから参照できる変数を操作できます

error_log  /dev/stderr debug;
events {
    worker_connections  256;
}

http {
    server {
        listen       8888;
        server_name  localhost;

        location / {
            # 先に空文字で初期化しておかないと 起動時にシンタックスエラーを起こす
            set $upstream "";
            rewrite_by_lua '
                ngx.var.upstream = "192.168.0.1"
            ';

            proxy_pass http://$upstream;
        }
    }
}

proxy_pass で参照できる変数を定義してみました。なんらかのロジックを組み込めば動的に proxy_pass 先を決めことができますね。

後半でもう少し有意な使い方になる例を挙げてみます

ngx.location.capture() とコルーチン

ngx.location.capture() という、擬似的なHTTPリクエストを扱える関数が用意されています。lua-nginxモジュールの中もかなり熱い関数です。

worker_processes  1;
error_log  /dev/stderr debug;

events {
    worker_connections  256;
}

http {

    server {
        listen       8080;
        server_name  localhost;

        location /1 {
            internal;
            proxy_pass http://127.0.0.1:10080/1;
        }

        location /2 {
            internal;
            proxy_pass http://127.0.0.1:10080/2;
        }

        location /3 {
            internal;
            proxy_pass http://127.0.0.1:10080/3;
        }

        location / {
            content_by_lua '
               local res1 = ngx.location.capture("/1")
               ngx.say(res1.body)

               local res2 = ngx.location.capture("/2")
               ngx.say(res2.body)

               local res3 = ngx.location.capture("/3")
               ngx.say(res3.body)
            ';
        }
    }
}

上記の設定でNginxを起動すると 1,2,3 の順番でコンテンツを返します。

一見 ngx.location.capture() の箇所でブロックしてしまう記述に見えますが、コルーチンを用いて同期的なインタフェースをしつつノンブロッキング で処理してくれるような実装になっています。

また ngx.location.capture() は HTTP GET を飛ばすインタフェースを取っていますが、実際はHTTPリクエストを生成している訳でなく 全てNginxの内部で完結する処理になっており、トラフィック/IPC(プロセス間通信) の類いは発生しません。そのため ngx.location.capture("http://example.com") のようにして他のホストにリクエストする使い方はできないのですが....

ngx.location.capture_multi という関数も用意されており、こちらは複数のリクエストを同時に発行できます。
(ここらへんの仕様は githubのドキュメントに書かれていますので、是非一度読んで見てください)


ngx.location.capture()は、mysql,redis,memcachedなどのストレージを組み合わせる事でより強力な使い方ができます

Nginx+Lua+Redisによるダイナミックリバースプロキシ

NginxがリクエストのHostヘッダを見て、動的にリバースプロキシする先を決める実装案です (ルーター/リクエストルーターなどの呼び名がありますが、どれがデファクトか分からないのでダイナミックリバースプロキシ と呼んでいます )

図式すると下記のような構成になります


ホスト名 -> IPの名前解決だけであればローカルネットワーク専用のDNSを立てるなどして解決できそうですが、IPに加えてポート番号の解決も必要なため Luaを通してRedisに問い合わせます。

ホスティングサーバーのような大量のドメイン(バーチャルホスト) が任意のタイミングで追加/削除される環境や、1ユーザーごとにhttpdを起動して複数のポート番号を管理するサービスの場合、 このようなリバースプロキシの利用価値が高くなります。
( 私が勤める paperboy&co. のサービス ロリポップでは、Apachemod_rewrite + MySQL + memcached を組み合わせたモジュールを作成し、ホスティングサーバ用リバースプロキシとして運用されています )

動作の詳細

さて、リバースプロキシの動作についてです。

  • NginxはRedisと実サーバーへのリバースプロキシとして動作
  • Lua は ngx.location.capture でRedis へリクエストをだし、レスポンスでupstreamを決定する
  • Redisはリクエストの { Hostヘッダ => IP:ポート } のマッピングを管理する

上記を 先に挙げた ngx.var.<変数名>ngx.location.capture() を組み合わせてLuaのコードに落とし込みます。

Redisを使ってるのに特別な意味はなく、やってみたかっただけ、です。


使用するモジュールは ↓ の通り。


以下が設定(実装)例となります

worker_processes  1;
error_log  /dev/stderr debug;

events {
    worker_connections  256;
}

http {

    server {
        listen       8888;
        server_name  localhost;

        location / {
            set $upstream "";
            rewrite_by_lua '
               local res = ngx.location.capture("/redis")
               if res.status == ngx.HTTP_OK then
                  ngx.var.upstream  = res.body
               else
                  ngx.exit(ngx.HTTP_FORBIDDEN)
               end
            ';
            proxy_pass http://$upstream;
        }

        # HostヘッダをキーにしてRedisに問い合わせ
        location /redis {
             internal;
             set            $redis_key $host;
             redis_pass     127.0.0.1:6379;
             default_type   text/html;
        }
   }
}


Redisの操作も含めた設定は https://gist.github.com/1670088 にメモってあります

実際に運用する場合はキャッシュやエラーハンドリングを厳密に詰める必要があるでしょう。上記は説明を簡単にするためのプロトタイプ実装として見てください。Redisだけでなく、MySQL(libdrizzle) や memcached などを複数組み合わせる方法も有効でしょう。

感想

memcached とか MySQL とかにプロキシするNginxモジュールって何の役に立つんだ!? 」とか思ってたのですが、Luaとの組み合わせを見て世界がひっくりかえったような衝撃を受けました。

lua-nginx-module には認証を操作するAPIもあります。ペパボの 30days album では Perlbal + memcached で画像リクエストの認証を制御していますが、lua-nginx-module + {memcached,redis,mysqld} でも同様の機能を実装できそうだなと思いました。

ところで Apacheでも mod_lua というモジュールが提供されていますが 、Apacheの内部APIへのアクセスが限定的で 後一歩のところで使い勝手がよくない印象でした。その点 lua-nginx-module はよくできた子だなーと。

あとあと、設定ファイルにロジックが紛れ込むことに抵抗がある方もいるとは思います。ただしNginxモジュールを作成して管理するコストがパないので、天秤にかけた場合 多少の見通しの悪さは黙認できるのではないでしょうか。(...Luaの部分だけ外部ファイル化もできます)

最後

Macでのざっくりビルドの手順を。

sudo brew install redis
sudo brew install lua

wget http://nginx.org/download/nginx-1.0.11.tar.gz
tar xvfz nginx-1.0.11.tar.gz
cd nginx-1.0.11

git clone https://github.com/chaoslawful/lua-nginx-module.git
wget http://people.FreeBSD.org/~osa/ngx_http_redis-0.3.5.tar.gz
tar xvfz ngx_http_redis-0.3.5.tar.gz
./configure --add-module=lua-nginx-module --add-module=ngx_http_redis-0.3.5 --prefix=/usr/local/nginx-1.0.11/

make 
sudo make install