Vagrant VM上のUbuntuから名前解決できない

 VagrantUbuntu環境を立ち上げようとするとChefでのProvision中にabortするというのが発生していてしばらく原因が分からなかったのですが、名前解決できなかったのが問題だったようです。

 私のケースでは config.vm.box に chef/ubuntu-14.04 を指定して、 chef.run_list では apt, sqlite, redisio などの cookbook を指定した状態で vagrant up すると、下記のようにProvision中にabortしていました。

$ vagrant up
Bringing machine 'default' up with 'virtualbox' provider...
==> default: Importing base box 'chef/ubuntu-14.04'...
==> default: Matching MAC address for NAT networking...
〜〜〜中略〜〜〜
==> default: [2014-11-16T11:51:03+00:00] WARN: Cloning resource attributes for package[tar] from prior resource (CHEF-3694)
==> default: [2014-11-16T11:51:03+00:00] WARN: Previous package[tar]: /tmp/vagrant-chef-3/chef-solo-1/cookbooks/redisio/recipes/default.rb:23:in `block in from_file'
==> default: [2014-11-16T11:51:03+00:00] WARN: Current  package[tar]: /tmp/vagrant-chef-3/chef-solo-1/cookbooks/ruby_build/recipes/default.rb:34:in `block in from_file'

 このときVMの状態は abort になります。

$ vagrant status
Current machine states:

default                   aborted (virtualbox)

 provisionなしでの起動はできるので、試しに再度 vagrant up したあとに vagrant ssh してUbuntuにログインし、 sudo apt-get update してみました。

vagrant@vagrant:~$ sudo apt-get update
0% [Connecting to us.archive.ubuntu.com] [Connecting to security.ubuntu.com]Connection to 127.0.0.1 closed by remote host.
Connection to 127.0.0.1 closed.

 securiy.ubuntu.com に接続しようとしているときに終了してしまっているようです。このときVMはまたaborted状態になります。VM上からインターネットへのアクセスはできていたのですが、どうも名前解決あたりが怪しそうだと思って調べていたところ、下記記事を発見。

Virtual Box ゲストから外部ネットワークにつながらない(解決済み)

 上記サイトで紹介されている通り、config.vm.provider の設定にNAT接続時のDNSの挙動に関連する、natdnshostresolver1、natdnsproxy1 を追加します。

  config.vm.provider :virtualbox do |vb|
    vb.gui = false
    vb.customize ['modifyvm', :id, '--memory', '1024']
    vb.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]
    vb.customize ["modifyvm", :id, "--natdnsproxy1", "on"]
  end

 この設定を追加することで、VM上での名前解決要求がホストマシンのDNSサーバによって行われるようになります。

9.11.5. Enabling DNS proxy in NAT mode

9.11.6. Using the host's resolver as a DNS proxy in NAT mode

 上記設定を追加して一旦 vagrant destroy したあとに vagrant up し直したところ、無事Provisionが完了するようになりました。

Passengerがgitからinstallしたgemを認識しない

 Passenger + Nginx でアプリケーションを起動しようとしたときに、下記のようなエラーが出ました。

git://github.com/nzifnab/msgpack-rails.git (at master) is not checked out. Please run `bundle install` (Bundler::GitError)

 もちろん bundle install は実行済みです。msgpack-rails を使用するために、Gemfile に下記のような記述をしてあるのですが、色々調べたところ、Passenger が git からインストールした gem を認識してくれないようです。

gem 'msgpack-rails', :git => 'git://github.com/nzifnab/msgpack-rails.git'

 bundle install --deployment で解決するという情報も見つけたのですが、これは vendor ディレクトリにgemを持ってくるため結構な時間がかかります。デプロイでは世代管理していて、デプロイ時に新しいディレクトリを作成してデプロイしているので、開発環境のサーバへデプロイするたびに時間がかかるのはちょっと困るので他の方法を探したところ、bundle package でOKという情報を見つけました。これはgemのキャッシュを vendor ディレクトリに持つもので、すぐに終わります。ただオプションで --all を指定しないと git 経由のgemをキャッシュしてくれないようです。

bundle package --all

とすることで、git からの gem も見つけてくれるようになりました。

(参考サイト)deploying rails3 apps with bundler and phusion passenger: .bundle dir not found

APIレスポンスとしてのMessagePack検証

 今まではネイティブのクライアントへJsonでレスポンスを返していたのですが、新しいサービスを開発する際に、レスポンスをMessagePackにしてはどうかとクライアントサイドから提案があったので検証してみました。

 ちなみにリクエストまでMessagePackにすると、リクエスト内容が簡単に参照できなくなってデバッグや問い合わせ時の調査が大変ということで、今回はレスポンスのみに使う想定です。

検証用アプリ


 構成は下記の通りです。

  • Nginx
  • Passenger
  • Ruby2.0
  • Rails4

 今回の検証はJson圧縮とMessagePack圧縮の比較です。アプリはRails4で実装します。また、JsonはデフォルトのJSONクラスよりもoj(Optimized JSON)が早いということなのでそちらを使用しています。MessagePackの方はMsgpackRailsを使います。Gemfileには下記を追加します。

(参考) JSONの替わりに使ったOJが速い
(参考) MsgpackRails

gem 'oj'
gem 'msgpack-rails', :git => 'git://github.com/nzifnab/msgpack-rails.git'

 Controllerは下記のように実装しました。

class StaticPagesController < ApplicationController

  require 'oj'

  after_action :testcompress

  respond_to :json, :mpac

  SERIALIZE_TEST_RESPONSE = {(省略)}

  def serialize_test
    respond_to do |format|
      format.json { render :json => SERIALIZE_TEST_RESPONSE }
      format.mpac { render :text => SERIALIZE_TEST_RESPONSE.to_msgpack, :content_type => 'application/x-mpac' }
    end
  end

  def testcompress
    response.body = ActiveSupport::Gzip.compress(response.body)
  end

end
  • oj が追加されていると、MultiJSONによって自動的に oj が選択されるようです。
  • シリアライズするHashデータを SERIALIZE_TEST_RESPONSE として定義しておきます。
  • 処理内容としてはテスト用のデータをシリアライズして返すだけです。
  • また、今回はシリアライズ+圧縮を試すのですが、Rails側で圧縮した場合と、Nginxで圧縮した場合も比較してみます。Rails側で圧縮する場合の処理を testcompress メソッドに定義しておいて、after_action で gzip 処理を行います。Nginx側で圧縮する場合は after_action をコメントアウトします。

 検証方法としては、JMeterサーバを3つ用意して、100スレッドから上記APIへかけられるだけ負荷をかけてみるというやり方です。Nginxで圧縮するケースではHTTPヘッダマネージャでAccept-Encodingヘッダを設定する必要があります。また、Nginxの設定ファイルで下記の設定を有効にします。

gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/x-mpac;

 デフォルトでは application/x-mpac 以外が設定された状態でコメントアウトされていますので、コメントアウトを解除して application/x-mpac を追加してあります。

検証結果


検証結果は下記の通りです。

種別 圧縮 サンプル数 平均 中央値 90%LINE 最小値 最大値 スループット KB/sec
Json Rails 48626 1816 1805 2002 25 2258 158.57 306.30
MessagePack Rails 271372 317 321 356 5 533 889.05 1701.66
Json Nginx 48824 1785 1775 1988 27 2206 159.11 393.07
MessagePack Nginx 333783 249 248 279 4 466 1093.61 2395.42
MessagePack 非圧縮 346175 246 246 271 6 610 1134.33 19178.19

 Jsonの場合はCPUは常に振り切っている状態でしたが、MessagePackではCPUに余裕がありました。
 テスト結果のスループットとしてはMessagePack非圧縮のケースが一番速かったのですが、MessagePackのNginxでの圧縮の場合と大きな差はなく、実際のケースでは転送するデータ量を少なくすることによるメリットも大きいので、今回はMessagePackのNginx圧縮パターンを採用することにしました。

MongoDBとCouchbaseを比べてみました

 仕事で新しいサービスのDBを何にするか検討していて、MongoDBと同じような使い方ができてサーバコストが抑えられるものがないか探していたときにCouchbaseのことを思い出してハンズオンセミナーなど受けてきました。結局Couchbaseは使わないことにしたのですが、主にMongoDBと比べてどうだったかといったあたりを書いておきます。

Couchbaseについてざっくりと


 まずCouchbaseがどういったものかをざっくり説明しておくと、MongoDBと同じようなドキュメント指向のNoSQL製品で、JSONデータを格納します。
 売りにしている特徴としては下記のような点が挙げられています。

  • 容易なスケーラビリティ
  • 安定したハイパフォーマンス
  • 24時間365日安定稼働
  • 柔軟なデータモデル

MongoDBと似ているところ


 また、下記のような思想はMongoDBと共通しています。

  • データは全部メモリに載せる
  • パフォーマンス出すにはサーバを追加する(スケールアウト)

 データが100GBあれば、masterノードのメモリの合計がそれ以上になるようにしましょう、という点では両者とも同じ思想で、サーバスペック的にもCPUよりもメモリを多く積んだサーバを必要とします。

比較して気になったところなど


 逆にMongoDBと比較して違う点で気になったところ等は下記のようなところです。

項目 MongoDB Couchbase
レプリケーション master-slaveレプリケーション master-masterレプリケーション
ノード追加 コマンドで設定変更 Webの管理画面から操作可能
自動Failover 台数制限なし 同時に一台まで
クエリ SQLライクな柔軟なクエリ MapReduceのみ
ドキュメント削除時 ドキュメント削除時にすべて消える メタデータが残ってしまう
商用版と無償版 違いはサポートの有無 商用版がベースなのでバージョンアップなどもまず商用版から

 それぞれ簡単に説明していきます。

  • master-masterレプリケーション

    MongoDBではレプリケーションの構成はmaster-slaveになりますので、masterノードのレプリカはslaveサーバが保持します。なのでシャーディング構成をとっている場合で1レプリカセット3台で構成している場合は、1シャード増やす場合には1レプリカセット追加ということになって3ノード増やす必要があります。これに対してCouchbaseはmaster-masterレプリケーションなので、あるmasterノードのレプリカは他のmasterノードが保持します。なので1シャード追加する場合にも1ノード増やすだけでOKです。masterが他のmasterのレプリカを持つ分、1ノードあたりのデータ量はMongoDBと比べると多くなりますが、レプリカデータは普段は非Activeになっていてアクセスされないので、その分のメモリは足りていなくても運用できるとのことでした。(実際の動作検証はしてません。。)

  • 管理画面からノード追加

    MongoDBではクラスタへノードを追加する場合、コマンドラインから操作する必要がありますが、CouchbaseではWebの管理画面から操作することができます。追加後のリバランスもそのまま管理画面から実行可能で、データ量が増えてもリバランスの所要時間はあまり変わらないとのことです。また、Mongoではシャードキーを自分で設定しますが、Couchbaseでは自動で振り分けが行われるので、データの内容から置かれているサーバを特定することはできません。リバランス処理の負荷はやはり高いので、すでに負荷でいっぱいいっぱいの状態のクラスタでノード追加、リバランスを行うのはつらいものがあります。

  • 自動Failoverは一台まで

    MongoDBではレプリケーションがmaster-slave構成ということもあり、Failoverは同時に何シャードで起こっても大丈夫ですが、Couchbaseでは自動Failoverは1ノードまでなので、最初の1ノードを復旧する前にもう一台落ちた場合には、クラスタが停止します。管理画面から手動でFailoverさせることは可能です。

  • クエリはMapReduceのみ

    今回Couchbaseの採用を見送った最大の理由はこれなのですが、MongoDBではSQLライクな柔軟なクエリが使用できるのに対して、CouchbaseではクエリはすべてMapReduceで行う必要があります。前もってMapReduceの処理を定義しておき、アプリケーションなどからはどのMapReduce処理かを指定して結果を参照します。開発時の効率を考えるとすべての処理をMapReduceで書くのはつらいものがあるので今回は見送りました。来年出るバージョン3.0ではSQLライクなクエリであるN1QL(ニッケル)が実装されるそうですので、期待したいところです。

  • ドキュメントを消してもメタデータが残る

    Couchbaseではドキュメントを消した場合にもドキュメントのメタデータは消されずに残ってしまうので、容量を圧迫します。この残ったメタデータを削除する処理の実行頻度を管理画面から指定できるのですが、削除処理は負荷が高いようなので、運用時にはこういった点も考慮する必要があります。

  • 商用版ありき

    OSSには元々オープンソースとして始まったものに商用のサポートなどがつくケースが多いですが、Couchbaseは元々商用製品として始まったもののソースを公開している形になります。なのであくまでメインは商用版で、バージョンアップやパッチの公開等もまず商用版に対して行われ、Couchbaseのエンジニアの手が空いたときにOSS版がメンテナンスされることになります。現状バージョンアップについては商用版の半年遅れぐらいでOSS版が追随しているようですが、今後はさらに間隔が開いたり、機能やデータ容量の制限がかかってくることもあるかもしれません。無償で利用できるものを探している場合はこの辺りがネックになりそうです。現在の商用版の最新バージョンは2.2ですが、OSS版はまだ2.1です。次のバージョンの3.0はOSSではいつ公開されるのか。。


 今回は実際に性能の検証等を行うまでに使わないという判断をしてしまいましたが、いくつか公開されているベンチマークではCouchbaseがMongoDBなどより良い結果を出しているものもあるので、N1QLが利用できるようになったらパフォーマンス検証等行ってみたいと思います。ただ無償版の状況を考えるとなかなか難しいかもしれませんが。。

RailsでMySQLを使う

 Ruby on Rails で普通にプロジェクトを作成すると sqlite3 が使われるようになっていますが、mysql を使用するにはプロジェクト作成時に -d オプションで mysql を指定します。

$ rails new turntable_test -d mysql
      create  
      create  README.rdoc
      create  Rakefile
      create  config.ru
      create  .gitignore
      create  Gemfile
      create  app
      create  app/assets/javascripts/application.js
      create  app/assets/stylesheets/application.css
      create  app/controllers/application_controller.rb
      create  app/helpers/application_helper.rb
      create  app/views/layouts/application.html.erb
      create  app/assets/images/.keep
      create  app/mailers/.keep
      create  app/models/.keep
      create  app/controllers/concerns/.keep
      create  app/models/concerns/.keep
      create  bin
      create  bin/bundle
      create  bin/rails
      create  bin/rake
      create  config
      create  config/routes.rb
      create  config/application.rb
      create  config/environment.rb
      create  config/environments
      create  config/environments/development.rb
      create  config/environments/production.rb
      create  config/environments/test.rb
      create  config/initializers
      create  config/initializers/backtrace_silencers.rb
      create  config/initializers/filter_parameter_logging.rb
      create  config/initializers/inflections.rb
      create  config/initializers/mime_types.rb
      create  config/initializers/secret_token.rb
      create  config/initializers/session_store.rb
      create  config/initializers/wrap_parameters.rb
      create  config/locales
      create  config/locales/en.yml
      create  config/boot.rb
      create  config/database.yml
      create  db
      create  db/seeds.rb
      create  lib
      create  lib/tasks
      create  lib/tasks/.keep
      create  lib/assets
      create  lib/assets/.keep
      create  log
      create  log/.keep
      create  public
      create  public/404.html
      create  public/422.html
      create  public/500.html
      create  public/favicon.ico
      create  public/robots.txt
      create  test/fixtures
      create  test/fixtures/.keep
      create  test/controllers
      create  test/controllers/.keep
      create  test/mailers
      create  test/mailers/.keep
      create  test/models
      create  test/models/.keep
      create  test/helpers
      create  test/helpers/.keep
      create  test/integration
      create  test/integration/.keep
      create  test/test_helper.rb
      create  tmp/cache
      create  tmp/cache/assets
      create  vendor/assets/javascripts
      create  vendor/assets/javascripts/.keep
      create  vendor/assets/stylesheets
      create  vendor/assets/stylesheets/.keep
         run  bundle install
Enter your password to install the bundled RubyGems to your system: 
Fetching gem metadata from https://rubygems.org/...........
Fetching gem metadata from https://rubygems.org/..
Resolving dependencies...
Using rake (10.1.0) 
Installing i18n (0.6.9) 
Using minitest (4.7.5) 
Using multi_json (1.8.2) 
Using atomic (1.1.14) 
Using thread_safe (0.1.3) 
Using tzinfo (0.3.38) 
Using activesupport (4.0.1) 
Using builder (3.1.4) 
Using erubis (2.7.0) 
Using rack (1.5.2) 
Using rack-test (0.6.2) 
Using actionpack (4.0.1) 
Installing mime-types (1.25.1) 
Using polyglot (0.3.3) 
Using treetop (1.4.15) 
Using mail (2.5.4) 
Using actionmailer (4.0.1) 
Using activemodel (4.0.1) 
Using activerecord-deprecated_finders (1.0.3) 
Using arel (4.0.1) 
Using activerecord (4.0.1) 
Using bundler (1.3.5) 
Using coffee-script-source (1.6.3) 
Using execjs (2.0.2) 
Using coffee-script (2.2.0) 
Using thor (0.18.1) 
Using railties (4.0.1) 
Using coffee-rails (4.0.1) 
Using hike (1.2.3) 
Installing jbuilder (1.5.3) 
Using jquery-rails (3.0.4) 
Using json (1.8.1) 
Installing mysql2 (0.3.14) 
Using tilt (1.4.1) 
Installing sprockets (2.10.1) 
Using sprockets-rails (2.0.1) 
Using rails (4.0.1) 
Using rdoc (3.12.2) 
Using sass (3.2.12) 
Using sass-rails (4.0.1) 
Using sdoc (0.3.20) 
Installing turbolinks (2.0.0) 
Installing uglifier (2.3.2) 
Your bundle is complete!
Use `bundle show [gemname]` to see where a bundled gem is installed.

 オプションなしでプロジェクトを作成したときとの違いは、作成時の出力で下記のように示されている通り、mysqlのgemが使用されます。

Installing mysql2 (0.3.14)

 また、sqlite3は使用されなくなるので、下記のようなsqlite3に関する出力はなくなります。

Using sqlite3 (1.3.8)

 作成される config/database.yml の差分は下記の通りです。

$ diff test_app/config/database.yml turntable_test/config/database.yml 
1,2c1
< # SQLite version 3.x
< #   gem install sqlite3
---
> # MySQL.  Versions 4.1 and 5.0 are recommended.
4,5c3,10
< #   Ensure the SQLite 3 gem is defined in your Gemfile
< #   gem 'sqlite3'
---
> # Install the MYSQL driver
> #   gem install mysql2
> #
> # Ensure the MySQL gem is defined in your Gemfile
> #   gem 'mysql2'
> #
> # And be sure to use new-style password hashing:
> #   http://dev.mysql.com/doc/refman/5.0/en/old-client.html
7,8c12,14
<   adapter: sqlite3
<   database: db/development.sqlite3
---
>   adapter: mysql2
>   encoding: utf8
>   database: turntable_test_development
10c16,18
<   timeout: 5000
---
>   username: root
>   password:
>   socket: /tmp/mysql.sock
16,17c24,26
<   adapter: sqlite3
<   database: db/test.sqlite3
---
>   adapter: mysql2
>   encoding: utf8
>   database: turntable_test_test
19c28,30
<   timeout: 5000
---
>   username: root
>   password:
>   socket: /tmp/mysql.sock
22,23c33,35
<   adapter: sqlite3
<   database: db/production.sqlite3
---
>   adapter: mysql2
>   encoding: utf8
>   database: turntable_test_production
25c37,39
<   timeout: 5000
---
>   username: root
>   password:
>   socket: /tmp/mysql.sock

 sqlite3 の設定が mysql 用の設定に変更されているのがわかります。username, password などは適宜変更します。
 binstubsを実行してから、rake でDBを作成します。

$ bundle --binstubs
$ bin/rake db:create

 mysqlにログインしてDBが作成されていることを確認します。

mysql> show databases;
+----------------------------+
| Database                   |
+----------------------------+
| information_schema         |
| mysql                      |
| performance_schema         |
| test                       |
| turntable_test_development |
| turntable_test_test        |
+----------------------------+
6 rows in set (0.00 sec)

 動作確認用のサンプルとしてUserModelを作成します。

$ bin/rails generate model User name:string email:string
      invoke  active_record
      create    db/migrate/20131211224446_create_users.rb
      create    app/models/user.rb
      invoke    test_unit
      create      test/models/user_test.rb
      create      test/fixtures/users.yml

 migrate を実行してDBにテーブルを作成します

$ bin/rake db:migrate
==  CreateUsers: migrating ====================================================
-- create_table(:users)
   -> 0.0110s
==  CreateUsers: migrated (0.0111s) ===========================================

 下記のようにテーブルが作成されていれば成功です。

mysql> desc users;
+------------+--------------+------+-----+---------+----------------+
| Field      | Type         | Null | Key | Default | Extra          |
+------------+--------------+------+-----+---------+----------------+
| id         | int(11)      | NO   | PRI | NULL    | auto_increment |
| name       | varchar(255) | YES  |     | NULL    |                |
| email      | varchar(255) | YES  |     | NULL    |                |
| created_at | datetime     | YES  |     | NULL    |                |
| updated_at | datetime     | YES  |     | NULL    |                |
+------------+--------------+------+-----+---------+----------------+
5 rows in set (0.00 sec)

MacにMySQLをインストール

 私物のMacBook AirMySQLをインストールしました。せっかくなので手順をメモしておきます。

 インストール自体はHomebrewでコマンド一発です。

$ brew install mysql
Warning: It appears you have MacPorts or Fink installed.
Software installed with other package managers causes known problems for
Homebrew. If a formula fails to build, uninstall MacPorts/Fink and try again.
==> Downloading https://downloads.sf.net/project/machomebrew/Bottles/mysql-5.6.1
######################################################################## 100.0%
==> Pouring mysql-5.6.15.mavericks.bottle.tar.gz
==> Caveats
A "/etc/my.cnf" from another install may interfere with a Homebrew-built
server starting up correctly.

To connect:
    mysql -uroot

To have launchd start mysql at login:
    ln -sfv /usr/local/opt/mysql/*.plist ~/Library/LaunchAgents
Then to load mysql now:
    launchctl load ~/Library/LaunchAgents/homebrew.mxcl.mysql.plist
Or, if you don't want/need launchctl, you can just run:
    mysql.server start
==> /usr/local/Cellar/mysql/5.6.15/bin/mysql_install_db --verbose --user=akanuma
==> Summary
 /usr/local/Cellar/mysql/5.6.15: 9410 files, 349M

 インストール後の各手順については上記のインストール時の出力に示されています。
 まずOS起動時にMySQLが起動するように設定します。

$ ln -sfv /usr/local/opt/mysql/*.plist ~/Library/LaunchAgents
/Users/akanuma/Library/LaunchAgents/homebrew.mxcl.mysql.plist -> /usr/local/opt/mysql/homebrew.mxcl.mysql.plist

 launchctlコマンドで起動します。

$ launchctl load ~/Library/LaunchAgents/homebrew.mxcl.mysql.plist 
$ ps aux | grep mysql
akanuma          2558   0.0  0.0  2423468    332 s000  R+   10:54PM   0:00.00 grep mysql
akanuma          2555   0.0  5.3  3081112 448512   ??  S    10:54PM   0:00.40 /usr/local/Cellar/mysql/5.6.15/bin/mysqld --basedir=/usr/local/Cellar/mysql/5.6.15 --datadir=/usr/local/var/mysql --plugin-dir=/usr/local/Cellar/mysql/5.6.15/lib/plugin --bind-address=127.0.0.1 --log-error=/usr/local/var/mysql/Hiroakis-MacBook-Air.local.err --pid-file=/usr/local/var/mysql/Hiroakis-MacBook-Air.local.pid
akanuma          2467   0.0  0.0  2436436   1008   ??  S    10:54PM   0:00.02 /bin/sh /usr/local/opt/mysql/bin/mysqld_safe --bind-address=127.0.0.1

 mysqlコマンドで接続できることを確認します。

$ mysql -uroot
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 1
Server version: 5.6.15 Homebrew

Copyright (c) 2000, 2013, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

Rails APサーバの比較検証(Puma, Unicorn, Passenger)

 仕事でRailsを使うことになり、APサーバの選定にあたってPuma, Unicorn, Passenger の比較検討を行いました。方法としてはJMeterでAPサーバにデプロイしたRailsアプリケーションに対して負荷をかけられるだけかけるというやり方です。


試験環境


試験の環境としては下記の構成です。

サーバ構成

hostaname CPU 仮想コア数(Per CPU) Memory Disk 用途
loadtest01 2 4 8192MB 20GB APサーバ
loadtest02 1 1 4096MB 20GB JMeterサーバ
loadtest03 1 1 4096MB 20GB JMeterサーバ
loadtest04 1 1 4096MB 20GB JMeterサーバ


サーバアーキテクチャの比較


各APサーバのアーキテクチャの比較は下記の通りです。

Puma Unicorn Passenger
デプロイモデル Reverse Proxy Reverse Proxy Nginx Module
Process/Thread Multi Processes / Multi Threads Multi Processes / Single Thread Multi Processes / Single Thread(OSS Edition)
Multi Processes / Multi Threads(Commercial Edition)
その他特徴 Workerプロセスごとにスレッドを立ち上げる Pull型(Workerプロセス側からキューのタスクを取得しにいく) トラフィックに応じてプロセス数を自動調整。商用Editionのみマルチスレッド利用可能


処理内容


処理内容としては、純粋にAPサーバの処理能力の比較にするため、DBアクセス等はせず、フィボナッチ数列を計算して返す処理を行います。ロジックは書きサイトを参考にしました。

Rubyでメモ化を使ってフィボナッチ数を求める

class StaticPagesController < ApplicationController

  def fibonacci
    @fibonacci = calc_fibonacci(params[:n].to_i)
  end

  private

    def calc_fibonacci(n)
      if (n < 2)
        return n;
      else
        p2 = 0;
        p1 = 1;
        2.upto(n) { p2, p1 = p1, p2 + p1 }
        return p1;
      end 
    end

end


JMeterシナリオ


JMeterのシナリオとしては、 * JMeterスレッド数:100 * Ramp up:0 * ループ数:無限ループ * 継続時間:5分間

という内容です。
JMeterのシナリオファイルはこちらから取得できます。


APサーバごとの設定値による差の検証


まずは各APサーバごとに、設定値の変更による差を検証しました。


Puma

ワーカプロセス数 スレッド数 サンプル数 平均値(msec) 中間値(msec) 90%LINE (msec) 最小値(msec) 最大値(msec) スループット(/sec) KB/sec エラー数
1 500 43079 2090 1978 2379 129 2519 141.68 414.67 多発
100 8 241811 365 57 1071 6 46414 799.43 2339.49 0
100 32 243620 362 60 1136 6 16890 805.37 2356.83 32
64 32 242825 365 51 1355 7 103157 802.35 2347.94 1
32 32 241934 366 46 760 6 252504 798.24 2335.89 0
16 64 237270 373 31 339 6 298546 782.00 2288.34 0
16 96 231996 382 37 644 6 241748 762.49 2231.26 0
  • ワーカプロセス数1のケースでは全くCPUを使い切れていませんが、他のケースではCPUを使い切った状態で動いていました。
  • スレッド数を増加させてもリソースの使用量はあまり変化しませんでしたが、プロセス数を増やすと使用量がかなり増えます。
  • メモリに余裕があってもCPUが振り切る方が早いです。


Unicorn

ワーカプロセス数 サンプル数 平均値(msec) 中間値(msec) 90%LINE (msec) 最小値(msec) 最大値(msec) スループット(/sec) KB/sec エラー数
1 4294 11345 572 50553 77 176247 11.74 34.54 433
8 8686 6026 562 13721 77 141193 22.15 65.15 389
32 42943 1465 325 2844 51 183552 103.48 304.38 280
64 191254 440 170 375 26 115819 498.54 1466.46 48
96 247191 360 170 261 49 82454 773.58 2275.69 3
128 256050 343 252 452 13 20587 836.00 2459.82 0
160 254117 347 291 537 15 8825 835.54 2458.54 0
192 252158 349 302 588 13 5746 833.94 2453.87 0
224 229723 383 275 592 9 299799 697.12 2045.15 2
  • ワーカプロセス数が1の場合は1CPUしか使われない
  • ワーカプロセス数をコア数と同じ8にした場合でも、すべてのコアが使われるわけではない
  • ワーカプロセス数をコア数の4倍の64まで増やすとかなりCPUが使われるようになってきて、128まで増やすとCPUはほぼ使い切られて、メモリの使用率も高くなる。
  • ワーカプロセス数を192まで増やした時点でメモリもほぼ使い切られ、224まで増やすとメモリが足りなくなり、Swapが発生して遅くなる。


Passenger

プロセス数 サンプル数 平均値(msec) 中間値(msec) 90%LINE (msec) 最小値(msec) 最大値(msec) スループット(/sec) KB/sec エラー数
1 65188 1376 1282 1325 1224 8690 215.15 646.07 0
8 245153 360 355 368 297 2441 810.57 2433.99 0
16 246737 357 353 381 279 2339 815.77 2448.77 0
24 241165 366 360 391 281 2414 797.27 2392.30 0
  • プロセス数が1だと4コアしか使われていない
  • プロセス数をコア数と同じにするとどのコアもかなり使われるようになる。メモリは余裕あり。
  • プロセス数をコア数以上に増やしていってもCPU, メモリの使用率はあまり変化しない。CPUは80%まで使われることが多いが、アイドルが10%前後残っていて、使い切ることはない


各APサーバの最適値同士を比較


各APサーバの最適値同士を比較して、使用するAPサーバを決定する

APサーバ サンプル数 平均値(msec) 中間値(msec) 90%LINE (msec) 最小値(msec) 最大値(msec) スループット(/sec) KB/sec
Puma 241811 365 57 1071 6 46414 799.43 2339.49
Unicorn 252158 349 302 588 13 5746 833.94 2453.87
Passenger 246737 357 353 381 279 2339 815.77 2448.77
  • Pumaはスループットが一番低いのに加えて最大値、90%LINEの遅さ、不安定さで候補から除外。
  • Passengerは安定しているが最小値が他の2つに比べて大きい。90%LINE、最大値は優秀。
  • Unicornはスループットが一番大きいのと、最小値、平均値も悪くない。最大値、90%LINEがPassengerと比 べると遅いのが懸念点。

定数スループットタイマを使って実際のアクセス数に近い負荷でUnicornとPassengerを比較

APサーバ サンプル数 平均値(msec) 中間値(msec) 90%LINE (msec) 最小値(msec) 最大値(msec) スループット(/sec) KB/sec
Unicorn 23075 12 10 13 4 116 75.32 221.50
Passenger 23092 11 9 12 5 118 75.38 226.67
  • UnicornとPassengerでほぼ差はなし
  • Passengerは最初の比較時に他と比べて最小値が遅いことが懸念だったが、実際の状況に近いアクセス数では 問題なく、Unicornが負荷が低い状態でもメモリリソースを消費するのに対してPassengerはリソースの消費が少ないこと から、Passengerを第一候補と考える。


検証時の問題点


検証時に発生した問題点を参考までに記載しておきます。


Passenger

  • デフォルトの状態で負荷をかけたところ、下記のようなエラーが多発してHTTPステータスコード 503が返される。
[ 2013-11-20 12:21:13.0578 17419/7fbbbc9da700 Pool2/Group.h:331 ]: Request queue is full. Returning an error

リクエストのQueueがあふれたことによるもののようです。Queue のサイズのデフォルトは100なので、無制限にするために passenger_max_request_queue_size を0に設定したところ、エラーは出なくなりました。


Unicorn

  • Nginxとの連携をUnixドメインソケットで行う設定で負荷をかけたところ、下記エラーが多発。
2013/11/20 11:32:35 [error] 27462#0: *622742 connect() to unix:///home/test_user/server-proto/unicorn.sock failed (11: Resource temporarily unavailable) while connecting to upstream, client: 192.168.51.234, server: loadtest01.test.com, request: "GET /fibonacci?n=1000 HTTP/1.1", upstream: "http://unix:///home/test_user/server-proto/unicorn.sock:/fibonacci?n=1000", host: "loadtest01.test.com:3090"

下記サイトなどを参考に、TCPポートによる連携に変更しました。

http://www.faultserver.com/q/answers-need-to-increase-nginx-throughput-to-an-upstream-unix-soc ket-linux-kernel-tun-398972.html

変更後に負荷をかけたところ、エラーは概ね解消しました。負荷をかけ続けていると、件数は少なくなったもののエラーが発生しました。HTTPステータスコードは 502 Bad Gateway

2013/11/20 14:45:29 [error] 25979#0: *254606 upstream prematurely closed connection while reading response header from upstream, client: 192.168.51.232, server: loadtest01.nubee.com, request: "GET /fibonacci?n=1000 HTTP/1.1", upstream: "http://127.0.0.1:3070/fibonacci?n=1000", host: "loadtest01.test.com:3090"
[2013-11-20 14:44:09.109250] ERROR worker=114 PID:26737 timeout (31s > 30s),
      killing
      [2013-11-20 14:44:09.119595] ERROR reaped #<Process::Status: pid 26737 SIGKILL
      (signal 9)> worker=114
      [2013-11-20 14:44:10.140398]  INFO worker=114 ready

検証のためにUnicorn側のタイムアウト設定を大幅に増やしました。30秒 → 300秒に変更。Nginxの設定にも下記を追加しました。

send_timeout 300;
proxy_connect_timeout 300;
proxy_read_timeout 300;

これでひとまずエラーは回避できました。。スループットの検証のためにタイムアウト値を大きくしていますが、実際の環境ではタイムアウト値は小さく設定する必要があります。