アシアルブログ

アシアルの中の人が技術と想いのたけをつづるブログです

nginxで認証用proxyサーバを作成

nginxと言えば、言わずと知れた高速なwebサーバ+ロードバランサです。
とにかく軽量で高速なので、apacheをこれに置き換えて高速化という話もよく聞くようになって来ました。

先日、このnginxとmemcacheを組み合わせてセッション認証サーバを構築したので、それについて書こうと思います。

1・システム概要


今回作るシステムは、静的コンテンツを配信するサーバがすでにあり、
・認証機構(ログイン画面)を追加
・変更を最小限に抑えて開発
・スケールアウトしやすい
・アクセス数が多いので、できるだけ処理を軽くする
ことを目的とします。

この手のシステムは、既存システムに手を入れるのが普通ですが、既存システムでプログラムが動かないことには話になりません。
という事で、認証機構を追加したプロキシサーバを作成することで実現したいと思います。

仮に、PHPのみを使用して実装する場合は、下記のように出来ます。
・ログイン画面はPHP
rewriteでログイン画面系以外の全てのアクセスをindex.phpに向ける
・認証に失敗したらログイン画面にリダイレクト
・認証成功したら認証状態をセッションに記録(有効期限も記録)
・セッションはmemcacheなどで全サーバ共有
・セッションの値が有効なときはコンテンツを取得して表示

よくある方法ですが、アクセス毎にPHP実行のオーバヘッドが発生するため効率は良くありません。
そこで、Nginx側から認証できればもっと速いはず。ということで、下記のような仕様にしました。

・ログイン画面はPHP
・ログイン画面で認証成功したら、ランダムなIDを発行してCookieとmemcacheに有効期限を設定して記録しておく
・Nginx側でcookieとmemcacheのIDを読み取り、IDが存在すればそのまま表示
・IDが存在しなければログイン画面にリダイレクト

つまり、全てのアクセスでわざわざPHPを呼び出すのは非効率なので、セッションの判定部分はNginxのみで解決することにより高速化します。

2・ログイン画面


本題では無いので、あくまでも概要です。

要は、認証に成功したら下記のようにするという意味です。
cookieにauthidというキーで、ランダム値を保存
・memcacheにランダム値をキーにして、認証状態(1 or 空文字)をセット



(略)
    if (@$_POST['id']  & & @$_POST['pw']) {

        $is_member = auth($_POST['id'], $_POST['pw']); // 会員認証

        $key = generate_random_value(); // ちゃんとしたランダム値を返す関数

        // cookieをセット
        setcookie('authid', $key, COOKIE_AUTH_EXPIRE, COOKIE_PATH, COOKIE_DOMAIN);
        
        // memcacheに入れておく値。1ならばログイン成功
        $memcache->set($key, $is_member ? 1 : '', false, MEMCACHE_AUTH_EXPIRE);

	header('Location: comp.php'); // ログイン完了画面
    } else {
        header('Location: login.php'); // ログイン画面のHTML
    }


定数は適当に読み替えてください。
また、このスクリプトはかなりいい加減なのでご注意ください。

3・Nginxコンパイル


今回はサードパーティのモジュールであるevalが要るので、ダウンロードしてコンパイルします。

こちらからtarでもgitでもいいのでダウンロードし
http://www.grid.net.ru/nginx/eval.en.html

Nginxのconfigureに下記のように、モジュールを解凍した先のディレクトリを指定します
--add-module=/path/to/nginx_eval_module

あとは普通にコンパイルしてインストールしてください。

4・Nginx設定


evalは下記のように、memcached_passやproxy_passなどのレスポンスを変数(この場合は$var)に代入してくれます。



location / {
    # 会員チェック
    eval $var {
        if ($cookie_auth) {
            set $memcached_key $cookie_authid;
            memcached_pass     localhost:11211;
        }
    }
}


memcached_passは、$memcached_keyに設定された値をキーとしてmemcacheから値を取得して表示するだけです。
なので、evalモジュールを組み合わせることによりphp側で値を設定しておけば、nginx側でそのままの値を取得出来ます。

あとはこの値を比較して、リダイレクトするかしないかを判定すれば完成です。



location /login/ {
    # php設定など(割愛)
    break;
}
location / {
    # 会員チェック
    eval $var {
        if ($cookie_authid) {
            set $memcached_key $cookie_authid;
            memcached_pass     localhost:11211;
        }
    }

    # $varが1ならそのまま
    if ($var = 1) {
        proxy_pass   http://webserver.example.com:80;
        break;
    }

    # それ以外ならばログイン画面
    rewrite ^(.*)$ /login/login.php permanent;
    break;
}


簡単に書けばこんな感じです。
この場合は、memcache側に1が入っていればログイン済みという扱いにしましたが、
工夫次第で、1なら会員、0なら非会員、値が無ければ未ログインなど権限による場合分けも出来ます。


5・使用感

簡単なテストプログラムを作ってabをかけてみました。
(純粋にモジュールのみチェックしたいので、認証成功なら0バイトのファイルを返すだけ)

webサーバ: X5680 3.33GHz 1コア 512MB
abサーバ : E5645 2.40GHz 2コア 1024MB

Nginx設定


location /ngx/ {
    eval $var {
        if ($cookie_authid) {
            set $memcached_key $cookie_authid;
            memcached_pass     localhost:11211;
        }
    }

    if ($var != 1) {
       return 403;
    }

    break;
}

location /php/ {
    include /etc/nginx/fastcgi_params;
    fastcgi_pass unix:/var/run/php-fastcgi/php-fastcgi.socket;
    fastcgi_index index.php;
    fastcgi_param SCRIPT_FILENAME /path/to/docroot/$fastcgi_script_name;
    break;
}

Nginxで認証するパスと、PHPをただ実行するパスの設定をしました。

PHP


<?php
$memcache_obj = new Memcache;
$memcache_obj->connect('127.0.0.1', 11211);

if (!$memcache_obj->get($_COOKIE['authid'])) {
    header('Status: 403 Forbidden');
}

Nginxの location /ngx/ 内でやっていることと同じ事をしています。

コマンド: ab -n 10000 -c 500 -C 'authid=hogehoge' http://xxx.xxx.xxx.xxx/ngx/test.php
     ab -n 10000 -c 500 -C 'authid=hogehoge' http://xxx.xxx.xxx.xxx/php/test.php
※/ngx/test.phpは空ファイルで、nginx側で特に設定していないのでパースされず、タダのHTML扱いになります。

5回実行した平均値

PHPのみ(spawn-fcgi) : 約500 req/s
Nginx eval使用 : 約1000 req/s
※Webサーバの性能が低いのしか用意出来なかったのと、さくらVPSからインターネット経由で某クラウド上のサーバにabかけていたので正確なデータは端折ります。

今回実験したサーバでは約2倍の性能差が出るようです。

ちなみに、vmstatも同時にとってみましたが、PHPはCPUをほぼ使い切っていたのに対し、Nginx evalの場合は2割程度残っていましので、チューニングによってはもう少し成績が上がる可能性があります。

そのうちにもう少し真面目なベンチマーク取ってみるかもしれません。
・・・誰かサーバと時間をください。

Nginx+Fastcgi+PHPでサクサク快適サイト構築!

こんにちは、井川です。連日、猛暑続きですね。熱中症には気を付けて、がんばりましょう。

今回は、軽量なWebサーバであるnginxとPHPを組み合わせて使う方法を紹介します。

Webサイトにとって、軽さはとても重要なポイントです。PHPはライトウェイトな言語でありながらも、symfonyなど最近のフレームワーク次第ではWebサイトが重くなってしまいます。特に、Apacheで多くのリクエストを同時に受け付けると、レスポンスを返さなくなることがあります。こうした場合、キャッシュを使ったり、Key/ValueストアやMongoDBなどNoSQLにしたり、スケールアウトしたりと、様々な対応が考えられます。

しかし、もっと根本的な解決方法はないでしょうか?

WebサーバとしてApacheではなく、nginxとFastcgi-PHPを使ってみましょう(lighttpdなどもありますが…)。ベンチマークでは、Apacheでリクエストを処理できなかったケースも、nginxなら全て処理できました。

実行環境


CentOS 5.5
PHP  5.3.5
Apache 2.2.3
nginx 1.0.4

nginxとは?


nginx(エンジン・エックス)は高性能なWebサーバであり、リバースプロキシ機能も持っています。nginxは2004年に公開されました。フリーのオープンソースであり、BSD系のライセンスで提供されています。Netsraftによると、トップ100万のサイトの6%がnginxを使っています。現在も着実にシェアを伸ばしています。

nginxのインストールと設定


まずは、最新のnginxをダウンロードします(記事執筆時の最新版は1.0.4)。SSLなど必要なモジュールを組み込んでコンパイルしていきます。今後のことも考え、PCREとOpenSSLを組み込んでおきます。そのため、yumでpcre、pcre-devel、openssl、openssl-develを先に入れておきます。



# yum install pcre pcre-devel openssl openssl-devel


そして、nginxのソースコードディレクトリで以下のコマンドを実行し、nginxをインストールします。



# ./configure --with-pcre --with-http_ssl_module --with-http_dav_module --with-http_flv_module --with-http_perl_module
# make
# make install


後は、サーバ起動時にnginxも起動するように、/etc/rc.local に以下を記述しておきます。



/usr/local/nginx/sbin/nginx


次にnginxの設定を行います。/usr/local/nginx/conf/nginx.confファイルを以下のように編集します。



http {
    include       mime.types;
    default_type  application/octet-stream;
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';
    sendfile        on;
    keepalive_timeout  65;
    #gzip  on;

    server {
        listen        80;
        server_name   localhost;
        server_tokens off;
        
        root ドキュメントルートへのパス;
        index index.html index.php;

        access_log  logs/access.log  main;
        error_log   logs/error_log;

        location / {
            try_files $uri /index.php;
        }
        location ^~ /index.php/ {
            try_files $uri /index.php;
        }
        location ~ \.php($|/) {
            include fastcgi_params;
            fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
            fastcgi_pass   127.0.0.1:9000;
        }
        location ~ /\.ht {
            deny  all;
        }
    }
}


FastCGIのインストールと設定


FastCGICGIを拡張したものです。通常のCGIは処理毎にプロセスを破棄するため、オーバーヘッドが発生し、パフォーマンスの低下につながります。一方で、FastCGIはプロセスをメモリ上に永続化することで、大量の要求も円滑に処理します。

まずは、yumでspawn-fcgiをインストールします(epelからインストール可能です)。epelをレポジトリに追加していない場合は、以下で追加しておきます。



wget http://download.fedoraproject.org/pub/epel/5/i386/epel-release-5-4.noarch.rpm
rpm –ivh http://download.fedoraproject.org/pub/epel/5/i386/epel-release-5-4.noarch.rpm


spawn-fcgiのインストール



# yum install spawn-fcgi


次に、php-fastcgi起動用のシェルスクリプトphp-fastcgi)を作成し、/etc/rc.d/init.d/php-fastcgi と配置します。シェルスクリプトの詳細はこちらのサイトを参考にしました。



#!/bin/sh
#
# spawn-fcgi   Start and stop FastCGI processes
#
# chkconfig:   - 80 20
# description: Spawn FastCGI scripts to be used by web servers
 
# Source function library.
. /etc/rc.d/init.d/functions
 
RETVAL=0
SPAWNFCGI="/usr/bin/spawn-fcgi"
PHPFCGI="/usr/bin/php-cgi"
FCGIPORT="9000"
FCGIADDR="127.0.0.1"
PHP_FCGI_CHILDREN=5
PHP_FCGI_MAX_REQUESTS=1000
ALLOWED_ENV="PATH USER"
USER=apache
GROUP=apache
PIDFILE=/var/run/phpfcgi.pid
 
ALLOWED_ENV="$ALLOWED_ENV PHP_FCGI_CHILDREN PHP_FCGI_MAX_REQUESTS FCGI_WEB_SERVER_ADDRS"
 
case "$1" in
  start)
        PHPFCGI_START=$"Starting ${NAME} service: "
        echo -n $PHPFCGI_START
 
        # clean environment
        E=
        for i in $ALLOWED_ENV; do E="$E $i=${!i}"; done
        daemon $SPAWNFCGI -a ${FCGIADDR} -p ${FCGIPORT} -u ${USER} -g ${GROUP} -P ${PIDFILE} -C ${PHP_FCGI_CHILDREN} -f ${PHPFCGI}
        RETVAL=$?
        ;;
  stop)
        echo -n "Stopping php-fcgi: "
        killproc -p $PIDFILE phpfcgi
        echo
        RETVAL=$?
        ;;
  *)
        echo "Usage: $0 {start|stop}"
        exit 1
esac
exit $RETVAL


後は、php-fastcgiをサービスに登録しておきます。



# chmod 755 /etc/rc.d/init.d/php-fastcgi
# /sbin/chkconfig --add php-fastcgi
# /sbin/chkconfig php-fastcgi on
# /sbin/service php-fastcgi start


後は、通常通りブラウザからアクセスすれば、サイトを閲覧できます。

ベンチマーク


symfony1.4を使って、"Hello world"を表示するだけの簡単なスクリプトを使います。ツールはApacheBenchを使い、Webサーバ上で実行します。Apache+mod_php (MaxClients 256) とnginx+FastCGI+PHPを比較しました。

①リクエスト数1000回、同時接続数50
 Apache: 26[requests/sec](失敗0)
 nginx : 27[requests/sec](失敗0)

②リクエスト数1000回、同時接続数100
 Apache: 半数以上が失敗
 nginx : 27[requests/sec](失敗0)

③リクエスト数1000回、同時接続数200
 Apache: 7~8割が失敗
 nginx : 21[requests/sec](失敗0)

同時接続数が増えた場合、Aapcheはレスポンスを返せなくなっています。一方で、nginxは同時接続数が増えてもリクエストを処理し、レスポンスを返しました。nginx+FastCGI+PHP構成が常に応答した理由は、リクエストを受け取る部分と、アプリケーションの処理を担う部分が分かれていることにあります。PHPの処理が重くても、サーバへの影響は少なくてすみます。その他、nginxの方がメモリ等のリソースの使用が少ないようです。


nginxにはキャッシュやリバースプロキシの機能もあります。興味があれば、ぜひ試してみて下さい。