Asial Blog

Recruit! Asialで一緒に働きませんか?

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

カテゴリ :
バックエンド(インフラ)
タグ :
Tech
nginx
eval
Linux
Time is money
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 空文字)をセット

  1. (略)
  2.     if (@$_POST['id'] && @$_POST['pw']) {
  3.         $is_member = auth($_POST['id'], $_POST['pw']); // 会員認証
  4.         $key = generate_random_value(); // ちゃんとしたランダム値を返す関数
  5.         // cookieをセット
  6.         setcookie('authid', $key, COOKIE_AUTH_EXPIRE, COOKIE_PATH, COOKIE_DOMAIN);
  7.         
  8.         // memcacheに入れておく値。1ならばログイン成功
  9.         $memcache->set($key, $is_member ? 1 : '', false, MEMCACHE_AUTH_EXPIRE);
  10. header('Location: comp.php'); // ログイン完了画面
  11.     } else {
  12.         header('Location: login.php'); // ログイン画面のHTML
  13.     }

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

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)に代入してくれます。

  1. location / {
  2.     # 会員チェック
  3.     eval $var {
  4.         if ($cookie_auth) {
  5.             set $memcached_key $cookie_authid;
  6.             memcached_pass     localhost:11211;
  7.         }
  8.     }
  9. }

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

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

  1. location /login/ {
  2.     # php設定など(割愛)
  3.     break;
  4. }
  5. location / {
  6.     # 会員チェック
  7.     eval $var {
  8.         if ($cookie_authid) {
  9.             set $memcached_key $cookie_authid;
  10.             memcached_pass     localhost:11211;
  11.         }
  12.     }
  13.     # $varが1ならそのまま
  14.     if ($var = 1) {
  15.         proxy_pass   http://webserver.example.com:80;
  16.         break;
  17.     }
  18.     # それ以外ならばログイン画面
  19.     rewrite ^(.*)$ /login/login.php permanent;
  20.     break;
  21. }

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


5・使用感

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

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

Nginx設定
  1. location /ngx/ {
  2.     eval $var {
  3.         if ($cookie_authid) {
  4.             set $memcached_key $cookie_authid;
  5.             memcached_pass     localhost:11211;
  6.         }
  7.     }
  8.     if ($var != 1) {
  9.        return 403;
  10.     }
  11.     break;
  12. }
  13. location /php/ {
  14.     include /etc/nginx/fastcgi_params;
  15.     fastcgi_pass unix:/var/run/php-fastcgi/php-fastcgi.socket;
  16.     fastcgi_index index.php;
  17.     fastcgi_param SCRIPT_FILENAME /path/to/docroot/$fastcgi_script_name;
  18.     break;
  19. }
Nginxで認証するパスと、PHPをただ実行するパスの設定をしました。

PHP
  1. $memcache_obj = new Memcache;
  2. $memcache_obj->connect('127.0.0.1', 11211);
  3. if (!$memcache_obj->get($_COOKIE['authid'])) {
  4.     header('Status: 403 Forbidden');
  5. }
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割程度残っていましので、チューニングによってはもう少し成績が上がる可能性があります。

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