hibomaのはてなダイアリー このページをアンテナに追加 RSSフィード

 

2012::02::05

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

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


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

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

{ファイル,ネットワーク} 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ヘッダを見て、動的にリバースプロキシする先を決める実装案です (ルーター/リクエストルーターなどの呼び名がありますが、どれがデファクトか分からないのでダイナミックリバースプロキシ と呼んでいます )

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

f:id:hiboma:20120205212614p:image


ホスト名 -> 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

2012::02::01

寿司ダイアリー

寿司ブログからお誘いがこないので ランチで寿司を食べた。

2012::01::28

[] MacBook Pro, MacBook air (Snow Leopard) のバッテリーの容量を半年モニタしたグラフ

私物のMacBook Pro (13inch) , MacBook air (11inch) と、会社で私用している MacBook Pro (15inch) のバッテリー容量を 半年程 はてなグラフに数値を投稿してモニタリングしていたので 公開します。


発端

Macのバッテリーに関する情報 /usr/sbin/ioreg を実行して取得できます

バッテリーの最大容量となる数値は ioreg -l | grep MaxCapacity で得られる値を利用したらよいようです 。

(この数値は 「このMacについて」>「詳しい情報」 > 「電源」でも確認できます )

ということで 毎日のcronで ioregのMaxCapacityの値を取得はてなグラフに投稿、という形式で半年ほどモニタリングしました。


使用したスクリプト

#!/usr/bin/perl

use strict;
use warnings;
use LWP::UserAgent;
use DateTime;
use Mac::IORegistry::Battery;

my $mac     = shift || die;
my $battery = Mac::IORegistry::Battery->get;
my $ua      = LWP::UserAgent->new;

$ua->credentials('graph.hatena.ne.jp:80', '', 'hiboma', 'ひみつのぱすわーど');
my $res = $ua->post( 'http://graph.hatena.ne.jp/api/post', {
    graphtype => 'bars',
    graphname => "${mac}::Battery",
    date      => DateTime->now->ymd,
    value     => $battery->{MaxCapacity}, 
});
warn $res->content unless $res->code == 201;

http://developer.hatena.ne.jp/ja/documents/graph/apis/rest に登録されているコードのまんまです


Mac::IORegistry::Battery は拙作のモジュールで /usr/sbin/ioreg -r -n AppleSmartBattery の出力をハッシュに変換するだけの単純コードです。コードは github に置いてあります

使用しているOSバージョンは全て Snow Leopard です。(未だにLion導入してないので...計測していません)


結果


半年程計測して下記のようなグラフが得られました。(縦軸の単位は mAh )

hibomac::Battery


  • (B) 会社MacBook Pro 15inch 。電源ケーブルは持ち運びに応じてつけたり外したり

MacbookPro::Battery


  • (C) 私物MacBook Air 11inch。2010年末?に購入。電源ケーブルは持ち運びに応じてつけたり外したり

hibomair::Battery

グラフの赤線は ioregで得られる DesignCapacity という値です。 設計上の最大容量をさすようです。


観察

いずれのグラフでも時間が経つにつれてバッテリーの容量が減っていますね

常時電源ケーブルをつけている (A)のMacは値の変動がほぼありません。容量が急激に減っている時期がありますが、一時期サーバーとしての使用を止めて起動していない時期でした。

(B)と(C)のMacは 値の変動激しいですね。持ち運びして充電ケーブルをつけていない環境での使用時間も多いです。一時的に値が回復している時期もありますが、長期的に見ると下降しています。


その他

Appleが書いている http://www.apple.com/jp/batteries/notebooks.html によると

Appleは、ノートブックを電源コンセントに常時接続しておくことを推奨していません。

理想なのは通勤電車の中でノートブックを使い、オフィスで充電のために電源につなぐといった利用方法です。

このような使い方をすると、バッテリー液が流動している状態に保てます。

一方、オフィスでデスクトップコンピュータを使っていて、ノートブックは旅行出張で時々使うだけ、という場合は、毎月最低1回はバッテリーを充放電することをお勧めします。

という使い方が良いようです。これらの手順に従って管理していれば 容量がヘタるのを抑えられたかもしれません。(減ってもあんまり気にしてないんですが)


また、エントリをまとめるにあたって調べ直して分かったのですが CycleCount などの値で充電/放電の回数も取れるようですね。こちらも合わせて取っておけばよかったなと。グラフの値が上下しているタイミングと照らし合わせることで 面白い結果になったかも。


参考にしたエントリ