ハートレイルズの技術スタッフによるブログです。

ハートレイルズの技術スタッフによるブログです。
ウェブサービス、スマートフォンアプリの制作に関連する技術的な情報を発信していきます。

2011年1月24日月曜日

Rails アプリを nginx の拡張モジュールで置き換えて高速化する方法

こんにちは、ハートレイルズの境 (@kazsakai) です。好きなエクスペンダブルズはドルフ・ラングレンです。

さて、弊社ではよくサーバーサイドを nginx+passenger+Ruby on Rails という構成でサービスを構築しています。
Rails を使っているのは社名が HeartRails だから、というわけでは全くなく、単に僕が昔から Ruby を使っていたからで、他意はありません。
passenger は今や Rails/Rack アプリ向けの標準ミドルウェアみたいな存在で、このおかげで随分 Rails の運用が楽になっています。passenger のリリース以前は手製スクリプトで Rails プロセスを制御していましたが、もうあの頃には戻りたくありません。
そして nginx です。フロントエンドの web サーバーは Apache, lighttpd と移り歩いてきましたが、今のところ nginx で落ち着いています。nginx を使っている主な理由は、
  • 高速
  • 省メモリ
  • passenger が対応
あたりです。nginx の出始めこそはロシア製で胡散臭く思われていた節もありましたが、最近は nginx を採用しているサイトの増加と passenger+Rails の勢いもあって、nginx を使っている人も多いのではないでしょうか。

この nginx、使っている人は分かると思いますが、実行ファイルに類するものは nginx バイナリ一つだけです。Apache のように modules フォルダも mod_* ファイルもありません。
このため、一見すると nginx では拡張モジュールの開発が難しい、あるいはできないように見えるかも知れません。が、実際には意外とそうでもなく、ちょっと頑張れば自分だけの拡張機能を組み込めたりします。
そして、仮に Rack や Rails を通すこと自体がボトルネックになっているような場合、そこを nginx の拡張モジュールに置き換えて Rack/Rails を通らずに済むようにすれば、パフォーマンス向上が見込めるかも知れません。
今回の記事では、Rails で行っていた処理を nginx の拡張モジュールで置き換えるにはどうするのか、そしてどれくらいパフォーマンスが変わるのかを説明したいと思います。

Rails での実装

ここではサンプルとして、リクエストされたファイル ID に対応するファイルの本体を返すような機能を考えてみます。
ファイル ID とファイルの実体の場所は memcached に全て入っていて、ファイルの長さは平均 5k バイト程度としておきます。
Rails でコントローラーを書くと、大体こんな感じでしょう。(キャッシュミスや memcache のエラー等の処理はここでは割愛します)
class FileController < ApplicationController
  @@memcache = MemCache.new('localhost:11211')
  def serve
    fname = @@memcache[params[:id]]
    path = "/mnt/files/#{fname}"
    send_file path, :disposition => 'inline'
  end
end
この機能に対してアクセスが大量に来たと想定してみます。この Rails アプリはどのくらいのアクセスまで耐えられるでしょうか?

Rails のベンチマーク

ベンチマークに使うソフトは、まあ ab でもいいんですが、ab は同一 URL に対してしか実行できないのが不満で、かと言って JMeter を使うのもオーバースペックですし何より面倒です。
そこで、今回は http_load を使うことにしました。http_load はアクセスする URL のリストを渡せるのと、秒毎の接続数を指定できるのが特長です。
http_load を使って、例えば毎秒 100 アクセスを 10 秒間続けるケースを実行してみます。
request$ ./http_load -rate 100 -seconds 10 urls.lst
http://server/file/297887: byte count wrong
http://server/file/212384: byte count wrong
http://server/file/118019: byte count wrong
http://server/file/32479: byte count wrong
http://server/file/422726: byte count wrong
http://server/file/132966: byte count wrong
http://server/file/342474: byte count wrong
http://server/file/299672: byte count wrong
790 fetches, 217 max parallel, 3.7589e+06 bytes, in 10 seconds
4758.09 mean bytes/connection
78.9997 fetches/sec, 375888 bytes/sec
msecs/connect: 9.97106 mean, 50.346 max, 0.181 min
msecs/first-response: 625.179 mean, 4148.99 max, 0.374 min
8 bad byte counts
HTTP response codes:
 code 200 -- 757
 code 502 -- 33
最後にどのレスポンスコードが何回返ってきたかが表示されています。
200 は正常なのですが、502 は "Bad Gateway"、nginx から Rails プロセスへリクエストを転送する際に、Rails 側が詰まっていて転送できなかったことを示しています。
この例では毎秒 100 アクセスで 502 エラーが出始めているので、毎秒 100 弱あたりが正常に動作する上限と思われます。

なお、動作環境は以下の通りで、nginx+passenger+Rails のサーバーと http_load を実行するサーバーは分けています。
machine: EC2 の small instance
OS: Ubuntu 10.10 (32bit)
software:
 ruby 1.8.7 (Ruby Enterprise Edition 2010.02)
 rails 3.0.3
 passenger 3.0.2
 nginx 0.8.53
Rails 設定: production 環境 + logging をエラーのみ + x_sendfile_header を使う
passenger 設定: passenger_max_pool_size  20
この環境に対して http_load を複数回実行して暖まってから、エラーが出ないぎりぎりのラインを調べてみました。
その結果、毎秒約 90 アクセスが限界のようで、それ以上はエラーが出始めるようになりました。(EC2 なので多少のぶれはありますが)

なお、memcache にクエリする部分を改造して memcache を使わないようにしたところ、毎秒約 100 アクセスあたりまで伸びました。
memcache である程度はかかっていますが、そこがボトルネックというわけでもないようです。 また、x_sendfile_header を使う場合と使わない場合も試しましたが、ファイルが 5k バイト程度と小さいためか、こちらはほとんど差が見られませんでした。
やはり small instance では Rails で処理できる同時リクエスト数はこのあたりが限界なのかも知れません。

それでは Rack/Rails を通らないリクエストだと、nginx はどれくらいまで処理できるのでしょうか。

nginx の静的ファイル配信のベンチマーク

Rails のコードと同等の機能を再現する前に、nginx が単純な静的ファイルの配信だとどのくらいのパフォーマンスを発揮するか、調べてみます。

Rails アプリの public フォルダ以下に配置したファイルについて、ファイル名と同じパスのリクエストが来ると、passenger は静的ファイルへのリクエストと見なし、Rack/Rails を通さずに直接 passenger (とnginx) が静的ファイルの内容を返します。
これを利用し、実ファイルを public フォルダから見える位置 (public/files) に置いて、そのファイルを直接指す URL のリストを作り、http_load でベンチマークしてみました。
この場合、nginx+passenger がファイルを配信する形になります。結果は、
request$ ./http_load -rate 1000 -seconds 10 urls2.lst
9993 fetches, 71 max parallel, 4.98973e+07 bytes, in 10.0088 seconds
4993.22 mean bytes/connection
998.418 fetches/sec, 4.98532e+06 bytes/sec
msecs/connect: 2.85875 mean, 58.765 max, 0.155 min
msecs/first-response: 1.72729 mean, 49.862 max, 0.2 min
HTTP response codes:
 code 200 -- 9993
毎秒 1000 アクセス余裕でした。CPU の使用率も 15% あたり。毎秒 100 アクセスで CPU 使用率が振り切っていた Rails の場合とは、一桁以上差があると見ていいでしょう。
なお、rate 1000 以上は http_load の方が対応していなかったため、
irb(main):028:0> 3.times {|i| fork { system "./http_load -rate 1000
-seconds 10 urls2.lst" } }
みたいに並列起動していましたが、3つくらい (つまり毎秒 3000) まではエラーは出ず、4つあたりからようやくエラーが出るようになりました。
これらの結果を見るに、Rack/Rails を通らない静的ファイルの配信は通る場合に比べて桁違いに軽いと言えそうです。

ここではファイル ID ではなく実ファイル名で直接アクセスしているので、Rails のサンプルとは動作が異なります。
ただ Rails のコードで行っているのはファイル ID から memcache 経由の実ファイル名への変換だけなので、この動作さえ nginx 内で実装して置き換えれば、毎秒 3000 アクセスとはいかなくとも、毎秒 1000 アクセス程度の性能は達成できるかも知れません。

それを確かめるために、nginx への独自機能の追加、つまり nginx の拡張モジュールの実装を試してみます。

nginx の拡張モジュールの準備

nginx は処理の効率のためかロシア魂なのか、拡張モジュールの動的ロードなどという軟派な機能はありません。使用するモジュールは全てビルド時に一緒にリンクして、nginx バイナリの中に含まれるようにする必要があります。
これはつまり拡張モジュールの追加は nginx 本体のビルドに影響するということであり、configure の段階から拡張モジュールを考慮しなければなりません。

具体的に、このサンプルで実装する拡張モジュール名を "http_sendfile" としましょう。
まず専用のフォルダをどこかに作り (ex. "/work/ngx_sendfile")、その下に config ファイルを配置します。
この場合、/work/ngx_sendfile/config の内容は、以下のようにしてみます。
ngx_addon_name=ngx_http_sendfile_module
HTTP_MODULES="$HTTP_MODULES ngx_http_sendfile_module"
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_sendfile_module.c"
CORE_LIBS="$CORE_LIBS -lmemcache"
ngx_addon_name はモジュール名で、nginx の HTTP モジュール群 (HTTP_MODULES) の一員にそのモジュール名を追加しています。

続いて拡張モジュールの C のソースファイル群に、独自の C ファイル "ngx_http_sendfile_module.c" を追加しています。($ngx_addon_dir はこの拡張モジュールのフォルダ、ここでは /work/ngx_sendfile を指します)
そして今回の拡張モジュールでは memcached にアクセスしたいので、そのためのライブラリ libmemcache をビルド時にリンクするよう、CORE_LIBS に "-lmemcache" を追加しています。

ここまで用意できたら、この拡張モジュールを組み込んで nginx を configure からビルドし直します。
組み込み前の configure のパラメータが、
./configure --prefix=/opt/nginx --with-http_ssl_module
--with-pcre=/data/build/pcre-8.10
--add-module=/opt/ruby-enterprise-1.8.7-2010.02/lib/ruby/gems/1.8/gems/passenger-3.0.2/ext/nginx
だったので、ここに今回の拡張モジュールのフォルダを指定するパラメータ、"--add-module=/work/ngx_sendfile" を追加してビルドを実行します (なお"nginx -V" で configure arguments が見られます) 。
./configure --prefix=/opt/nginx --with-http_ssl_module
--with-pcre=/data/build/pcre-8.10
--add-module=/opt/ruby-enterprise-1.8.7-2010.02/lib/ruby/gems/1.8/gems/passenger-3.0.2/ext/nginx
--add-module=/work/ngx_sendfile
make
ここではまだ拡張モジュールの中身の C ファイル "ngx_http_sendfile_module.c" が無いので make に失敗しますが、基本的な流れは、
  1. 専用フォルダを作ってその下に config ファイルを作り、
  2. その中で必要なソースファイルやコンパイルオプションを追加して、
  3. ソースファイルを実装して、
  4. そのフォルダを nginx の configure で指定してビルドする
だけです。Makefile 等を用意する必要も通常はありません。簡単、かどうかはともかく比較的に分かりやすい拡張方法ではないでしょうか。

それではいよいよ拡張モジュールの中身の実装に移ります。

nginx の拡張モジュールの実装 (1)

"ngx_http_sendfile_module.c" について、まずは拡張モジュールの定義に当たる部分を作りましょう。
拡張モジュールの名前や、nginx.conf の中で使える設定文字列といった情報と、nginx.conf ロード時等に行うべき処理をここで定義します。

今回の拡張モジュールでは、この部分は以下のようになりました。
const ngx_command_t ngx_http_sendfile_commands[] = {
   { ngx_string("http_sendfile"),
     NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_CONF_FLAG,
     ngx_http_sendfile,
     NGX_HTTP_LOC_CONF_OFFSET,
     0,
     NULL },
   ngx_null_command
};

static ngx_http_module_t ngx_http_sendfile_module_ctx = {
   NULL,                                  /* preconfiguration */
   ngx_http_sendfile_init,                /* postconfiguration */

   NULL,                                  /* create main configuration */
   NULL,                                  /* init main configuration */

   NULL,                                  /* create server configuration */
   NULL,                                  /* merge server configuration */

   ngx_http_sendfile_create_loc_conf,     /* create location configration */
   ngx_http_sendfile_merge_loc_conf       /* merge location configration */
};

ngx_module_t ngx_http_sendfile_module = {
   NGX_MODULE_V1,

   &ngx_http_sendfile_module_ctx,               // void                 *ctx;
   (ngx_command_t*)ngx_http_sendfile_commands,  // ngx_command_t
 *commands;
   NGX_HTTP_MODULE,                             // ngx_uint_t            type;

   NULL,                                        // ngx_int_t
 (*init_master)(ngx_log_t *log);

   NULL,                                        // ngx_int_t
 (*init_module)(ngx_cycle_t *cycle);

   NULL,                                        // ngx_int_t
 (*init_process)(ngx_cycle_t *cycle);
   NULL,                                        // ngx_int_t
 (*init_thread)(ngx_cycle_t *cycle);
   NULL,                                        // void
 (*exit_thread)(ngx_cycle_t *cycle);
   NULL,                                        // void
 (*exit_process)(ngx_cycle_t *cycle);

   NULL,                                        // void
 (*exit_master)(ngx_cycle_t *cycle);

   NGX_MODULE_V1_PADDING
};
ポイントの一つは、前述の config ファイルの HTTP_MODULES に追加した名前 "ngx_http_sendfile_module" と一致する名前で ngx_module_t を定義する点です。
nginx 本体は HTTP_MODULES 内の名前から各モジュールの ngx_module_t を見つけ、そこからそのモジュールのコマンド定義や関数ポインタのリストを辿ります。

各モジュールは設定ロード時の各段階で呼ばれる関数をそれぞれ定義できます。ここでは、ngx_http_sendfile_module_ctx に 3 種類の関数を定義しています。

また、この拡張モジュールでは、"http_sendfile" という設定名を追加して、nginx.conf の中でこの機能をオン/オフしたいところに "http_sendfile on/off" と書けるようにしました。
これを表すのに、ngx_http_sendfile_commands に "http_sendfile" コマンドを設定し、設定ロード時に "http_sendfile on/off" が現れた際に呼ばれるべき関数 ngx_http_sendfile を設定しています。

nginx の拡張モジュールの実装 (2)

指定されたのがオンなのかオフなのかを保持するための場所が必要ですが、こうした設定情報は一つの構造体にまとめるようにします。
typedef struct {
   ngx_flag_t enable;
} ngx_http_sendfile_loc_conf_t;
そして ngx_http_module_t の "create location configration", "merge location configration" に当たるところの関数として、
static void *ngx_http_sendfile_create_loc_conf(ngx_conf_t *cf)
{
   ngx_http_sendfile_loc_conf_t *conf;

   conf = ngx_palloc(cf->pool, sizeof(ngx_http_sendfile_loc_conf_t));
   if (conf == NULL) return NULL;
   conf->enable = NGX_CONF_UNSET;

   return conf;
}

static char *ngx_http_sendfile_merge_loc_conf(ngx_conf_t *cf, void
*parent, void *child)
{
   ngx_http_sendfile_loc_conf_t *prev = parent;
   ngx_http_sendfile_loc_conf_t *conf = child;

   ngx_conf_merge_value(conf->enable, prev->enable, 0);

   return NGX_CONF_OK;
}
のように用意しておけば nginx 本体によって設定情報の構造体 ngx_http_sendfile_loc_conf_t が自動的に作られたりマージされたりします。

そして、"http_sendfile on/off" が現れた際に呼ばれるよう設定した ngx_http_sendfile では、この自動的に作られた ngx_http_sendfile_loc_conf_t に対して操作を行います。
static char* ngx_http_sendfile(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
   ngx_str_t *value;
   ngx_http_sendfile_loc_conf_t *sfcf = conf;

   value = cf->args->elts;
   if (ngx_strcasecmp(value[1].data, (u_char *)"on") == 0) {
       sfcf->enable = 1;
   } else if (ngx_strcasecmp(value[1].data, (u_char *)"off") == 0) {
       sfcf->enable = 0;
   } else {
       ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "\"http_sendfile\"
must be either set to \"on\" or \"off\"");
       return NGX_CONF_ERROR;
   }
   return NGX_CONF_OK;
}
ここでは "http_sendfile" の設定が ON なら enable を 1 に、OFF なら enable を 0 に、それ以外なら設定エラーとみなすようにしています。

この location configuration についてのポイントは、設定ファイル内で異なる server や location の中で "http_sendfile" が指定された場合、それぞれ別の構造体 ngx_http_sendfile_loc_conf_t が作られて内容が入るという点です。
つまり、nginx.conf で
server {
   listen 80;
   location /file {
     http_sendfile on;
   }
   location /files {
     http_sendfile off;
   }
 }
のように指定すると、/file 以下での ngx_http_sendfile_loc_conf_t の enable は 1 に、/files 以下では 0 となります。
リクエストが来て handler で処理するときに、そのリクエストの location に対応する ngx_http_sendfile_loc_conf_t が取得できるので、その内容を見て動作を変えることができます。

nginx の拡張モジュールの実装 (3)

続いて、リクエストが来たときに実際にリクエストに対して処理を行う handler を、nginx の HTTP core に追加します。
static ngx_int_t ngx_http_sendfile_init(ngx_conf_t *cf)
{
   ngx_http_handler_pt       *h;
   ngx_http_core_main_conf_t *cmcf;

   cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module);

   h = ngx_array_push(&cmcf->phases[NGX_HTTP_CONTENT_PHASE].handlers);
   if (h == NULL) {
       return NGX_ERROR;
   }

   *h = ngx_http_sendfile_handler;

   return NGX_OK;
}
HTTP core がリクエストを処理するフェーズが幾つかあるのですが、ここでは実際にコンテンツを返すところを書き換えたいので、NGX_HTTP_CONTENT_PHASE に対して独自の handler の ngx_http_sendfile_handler を登録しています。
(なお、passenger も昔はこれと同じやり方で handler を登録していたようなのですが、何か問題があったらしく、現在は別の方法に切り替えています。が、ここではその他の一般のモジュールのやり方に合わせてみました)

ここに handler を登録しておくと、HTTP core は、リクエストを処理するフェーズが NGX_HTTP_CONTENT_PHASE に到達したときに、そこに登録されている各 handler を順に呼び出してくれます。

最後に、ようやく handler の中身です。
static ngx_str_t ngx_http_sendfile_root = ngx_string("/files/");
static ngx_int_t ngx_http_sendfile_handler(ngx_http_request_t *r)
{
   ngx_http_sendfile_loc_conf_t *sfcf;
   u_char *p;
   struct memcache *mc;
   char *mc_res, *path;
   int len;
   ngx_str_t uri;

   if (!(r->method & (NGX_HTTP_GET|NGX_HTTP_HEAD))) {
       return NGX_DECLINED;
   }
   sfcf = ngx_http_get_module_loc_conf(r, ngx_http_sendfile_module);
   if (!sfcf || !sfcf->enable) {
       return NGX_DECLINED;
   }

   p = &r->uri.data[r->uri.len-1];
   len = 0;
   while (*p >= '0' && *p <= '9') { p--; len++; }
   p++;
   if (len <= 0) return NGX_DECLINED;

   mc = mc_new();
   mc_server_add(mc, "127.0.0.1", "11211");
   mc_res = mc_aget(mc, (char*)p, len);
   path = mc_res + 4;

   uri.len = ngx_http_sendfile_root.len + strlen(path);
   uri.data = ngx_pnalloc(r->pool, uri.len);
   if (uri.data == NULL) return NGX_HTTP_INTERNAL_SERVER_ERROR;
   ngx_memcpy(uri.data, ngx_http_sendfile_root.data,
ngx_http_sendfile_root.len);
   ngx_memcpy(uri.data + ngx_http_sendfile_root.len, (u_char*)path,
strlen(path));

   free(mc_res);
   mc_free(mc);

   //ngx_log_error(NGX_LOG_ALERT, r->connection->log, 0, path);
   return ngx_http_internal_redirect(r, &uri, &r->args);
}
ポイントは、handler が返す値によって HTTP core は次の handler の呼び出しに進むのか、あるいはそこで処理を終了するのかといった動作が切り替わる点です。
handler が自分で処理せずに後続の handler に任せたい場合は NGX_DECLINED を返したり、そこで処理が完了した場合は NGX_DONE を返すといった具合です。

このコードでは、まず
sfcf = ngx_http_get_module_loc_conf(r, ngx_http_sendfile_module);
   if (!sfcf || !sfcf->enable) {
       return NGX_DECLINED;
   }
のあたりでこのリクエストの location に対応する ngx_http_sendfile_loc_conf_t を取得し、その enable が 1 でない場合はこの機能が有効でない location なので NGX_DECLINED を返しています。

また、その後の
p = &r->uri.data[r->uri.len-1];
   len = 0;
   while (*p >= '0' && *p <= '9') { p--; len++; }
   p++;
   if (len <= 0) return NGX_DECLINED;

   mc = mc_new();
   mc_server_add(mc, "127.0.0.1", "11211");
   mc_res = mc_aget(mc, (char*)p, len);
   path = mc_res + 4;
が、リクエストされたパスに含まれるファイル ID を memcache に問い合わせて、実ファイル名を取得する部分です。
(なお、ポインタを +4 しているのは、memcache に入っていた値が通常の文字列ではなく ruby の文字列を marshalling したもので、MRI だと頭に 4 バイト余分な情報が付いていたからです)

これによって、"aa/aa.dat" というような実ファイル名が返ってくるので、
uri.len = ngx_http_sendfile_root.len + strlen(path);
   uri.data = ngx_pnalloc(r->pool, uri.len);
   if (uri.data == NULL) return NGX_HTTP_INTERNAL_SERVER_ERROR;
   ngx_memcpy(uri.data, ngx_http_sendfile_root.data,
ngx_http_sendfile_root.len);
   ngx_memcpy(uri.data + ngx_http_sendfile_root.len, (u_char*)path,
strlen(path));
のところで "/files/aa/aa.dat" という internal な URI のパスに変換し、
return ngx_http_internal_redirect(r, &uri, &r->args);
で内部的にリダイレクトしています。実ファイルへの URI パスを渡して内部リダイレクトすると、あとは HTTP core がそのファイルをリクエスト元に返してくれます。

このような作業を経て、拡張モジュールに最低限必要なファイルが完成しました。

nginx の拡張モジュールのビルド

モジュール本体のソースコードも用意できたら、あとはビルドして nginx 本体ごとデプロイするだけです。
./configure --prefix=/opt/nginx --with-http_ssl_module
--with-pcre=/data/build/pcre-8.10
--add-module=/opt/ruby-enterprise-1.8.7-2010.02/lib/ruby/gems/1.8/gems/passenger-3.0.2/ext/nginx
--add-module=/work/ngx_sendfile
make
sudo make install
なお、config ファイルを変えない限りは configure からやり直す必要はなく、make 以降だけで十分です。

エラーもなく make と make install が完了したら、あとは適宜 nginx.conf を編集して、nginx を起動します。
今回の nginx.conf の server 部分は、以下のように設定しました。
server {
       listen 80;
       server_name server;
       root /work/files/public;
       location /file {
           http_sendfile on;
       }
       location /files {
           http_sendfile off;
       }
   }
"/file/:id" へアクセスされると、/file の http_sendfile は on なので今回の拡張モジュールの機能が動作して "/files/aa/aa.dat" といった実ファイルへ内部リダイレクトします。
一方リダイレクト先での "/files/aa/aa.dat" では /files の http_sendfile は off のため、拡張機能は動作せずに通常の動作、つまり静的ファイルの配信を行います。
実際に /file/1 等へアクセスしてみると、実ファイルの本体が返ってきました。

なお、この例では passenger を使わないようにしてしまいましたが、実際には一部の機能に対するリクエストだけを nginx の拡張モジュールで処理し、そこで処理できない場合やそれ以外のリクエストは passenger+Rails に任せる、といった併用が現実的でしょう。

nginx+拡張モジュールのベンチマーク

これでようやく初めの Rails のコードとほぼ等価な動作を行う機能を、nginx とその拡張モジュールだけで実現することができました。
静的ファイルへの直接アクセスに比べ、memcached へのクエリ、内部リダイレクトという動作が入っていますが、どれくらいのパフォーマンスが出るか http_load で試してみます。
request$ ./http_load -rate 1000 -seconds 10 urls.lst
9969 fetches, 92 max parallel, 4.9603e+07 bytes, in 10.0245 seconds
4975.72 mean bytes/connection
994.465 fetches/sec, 4.94818e+06 bytes/sec
msecs/connect: 3.01519 mean, 72.736 max, 0.167 min
msecs/first-response: 8.32156 mean, 79.847 max, 0.366 min
HTTP response codes:
 code 200 -- 9969
毎秒 1000 アクセスはクリア。CPU の使用率は 25% 前後と、静的ファイルの場合の 15% よりはさすがに重くなっていますが、同じ桁のパフォーマンスは達成できていると見ていいでしょう。
実際、毎秒 1500 アクセスあたりまでは大丈夫で、そこから先は memcache 周りのエラーが発生し始めました。これ以上は拡張モジュール内の memcache 周りのコードの改善か、サーバー側のチューニングが必要になってきそうです。
が、とりあえずの目標の毎秒 1000 アクセスは達成できたので、ここではこれでよしとしましょう。

結論

ウェブアプリのサーバーは、通常だとデータベースへのクエリがボトルネックになる場合が多いです。
が、そこを解消し、各種設定のチューニングをして、なおアプリケーションサーバーが重い場合は、「アプリケーションサーバーをアップグレードする/台数を増やす」というのが一般的かつ妥当な対処方法でしょう。
普通はそうなのですが、もしも最もリソースを消費している箇所が C/C++ 言語で記述可能であれば、本記事のように nginx の拡張モジュールとして実装して置き換えることで、大幅な性能向上が見込める可能性があります。

もちろん、現実のアプリケーションはサンプルのように極端に簡単なものではありませんが、上記例の実装中で libmemcache をリンクして使っているように、nginx は C のバイナリにリンクできるライブラリであれば取り込んで使うことができます。(現に passenger の大部分は boost を使って C++ で書かれています)
そのため、本物の C/C++ プログラマがその気になれば大抵のことは問題無く実現できるでしょう。

LL 言語のみを使ってマシンスペック頼みでパフォーマンスを確保するのが昨今の流行かも知れませんが、時には C/C++ レベルの泥臭い作業でボトルネックを潰すのも、技術者として面白いのではないでしょうか。

参考

nginx のモジュールを開発するのであれば、nginx 本体に同梱されているモジュールや passenger の nginx 拡張のソースコードか、あるいは
Emiller's Guide to Nginx Module Development
が参考になると思います。なお、この URL は非常に参考になりそうですが、僕がこれを発見したのは今回のコードを書いた後でした。

この記事を気に入ってくださった方は、ぜひブックマーク、リツイートいただけると幸いです。

この記事をはてなブックマークに追加

ハートレイルズのファンページはこちら。今後ともどうぞよろしくお願いいたします。

ハートレイルズでは現在、新規のウェブサービス、ソーシャルアプリ (ゲーム)、スマートフォンアプリの受託開発案件を絶賛募集中です。どうぞお気軽にご相談ください! (スタートアップの方は投資/育成ページをご覧ください。)

0 件のコメント:

コメントを投稿