• 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
 

 RSpecで現在時刻に関連するテストをするときに、Rails4.1からはTimecopを使わなくても ActiveSupport::Testing::TimeHelpers の to_travel というメソッドを使って現在時刻を設定することができます。

 たとえば、下記のように現在時刻を返すメソッドがあるとします。

   1  class Foo
   2    def now
   3      Time.now
   4    end
   5  end

 正しく現在時刻を返しているかのテストは下記のように書けます。

   1  travel_to Time.new(2014, 11, 24, 9, 30, 0) do
   2    foo = Foo.new
   3    expect(foo.now).to eq(Time.new(2014, 11, 24, 9, 30, 0))
   4  end

 travel_toで現在時刻を設定しているので、expectで評価するときに決まった時間を書いて比較できます。

 ですがもしnowメソッドでDateTime.nowを返していると、to_travelでは対応できません。

   1  class Foo
   2    def now
   3      DateTime.now # <- to_travelで設定した時間ではなく現在時刻が返る
   4    end
   5  end

 その場合にはやはりTimecopを使う必要があります。Gemfileには下記のようにgemを追加。

   1  group :test do
   2    gem 'timecop'
   3  end

 テストは下記のように記述します。

   1  Timecop.freeze(Time.new(2014, 11, 24, 9, 30, 0)) do
   2    foo = Foo.new
   3    expect(foo.now).to eq(Time.new(2014, 11, 24, 9, 30, 0))
   4  end

 travel_to が対応しているのは Time.now と Date.today のみのようです。

 travel_to(date_or_time, &block)
 rails4.1からのtravel_toをrspecで使う

posted by Png akanuma on Tue 25 Nov 2014 at 00:00

環境変数によってRailsアプリの動作を切り替えたいことがあって、
nginx + Passenger で動かしているRailsアプリに環境変数を渡す方法を調べました。

nginx の起動スクリプト(/etc/init.d/nginx)に書いても、
/etc/default/nginx に書いてもうまくいかなかったのですが、
nginx の設定ファイルの location ブロックで
passenger_set_cgi_param で設定することで環境変数が設定できます。

   1  passenger_set_cgi_param HOGE fuga;

これでRailsアプリの中で ENV['HOGE'] で "fuga" という値が取得できるように なります。

参考ページ
16.3.5. Phusion Passengerが提供するアプリケーション
8.6.1. passenger_set_cgi_param

posted by Png akanuma on Sat 22 Nov 2014 at 14:13

Rails4で入力フォームから日時を選択できるように datetimepicker を使ったのですがちゃんと動くまでに軽くはまったのでメモ。

bootstrap3-datetimepicker-rails

まずはGemfileに下記エントリを追加して bundle install

   1  gem 'momentjs-rails'
   2  gem 'bootstrap3-datetimepicker-rails'

moment と datetimepicker のJavaScriptファイルを読み込むための設定を追加します。 私は coffeescript を使っているので application.js.coffee に下記エントリ を追加します。 bootstrapの設定が入っていない場合はあわせて追加します。

   1  #= require bootstrap
   2  #= require moment
   3  #= require bootstrap-datetimepicker

続いてstylesheetの設定も追加します。 私の場合は scss を使っているので、application.css.scss に下記を追加。 ちなみに順番もこの通りじゃないとちゃんと動きません。

   1  @import "bootstrap-sprockets";
   2  @import "bootstrap";
   3  @import "bootstrap-datetimepicker";

ここまででひとまずの下準備は終了なので、実際にdatetimepickerを適用したい部分の作業をします。 今回の対象のView部分は下記のような感じです。

   1  <div class="form-group">
   2    <div class="col-md-2">
   3      <%= f.text_field :start_datetime, value: @user.start_datetime, class:
   4  "form-control", id: "user_start_datetime" %>
   5    </div>
   6  </div>

f.text_field で出力される input タグに対して datetimepicker() メソッドを 実行します。 下記内容でcoffeescriptのファイルを別途作成して読み込みます。

   1  $ ->
   2    $('#user_start_datetime').datetimepicker()

これで入力ボックスを選択した時に日時選択用のポップアップが表示されるようになります。 また、日付のフォーマットのデフォルトは MM/DD/YYYY hh:mm A/PM なので、日本の形式に変更します。 inputタグに data-date-format 属性で指定するのですが、rails の text_field タグでは追加の属性を指定できないため、先ほどのcoffeescriptを変更して動的に属性を追加します。

   1  $ ->
   2    date_format = {'data-date-format': 'YYYY/MM/DD HH:mm'}
   3    $('#user_start_datetime').attr(date_format)
   4    $('#user_start_datetime').datetimepicker()

これで日付フォーマットが変更されます。 今回は一番シンプルな形で datetimepicker を使いましたが、オプションの指定で表示形式などは色々変更できますので、試してみてもらえると良いかと思います。

参考ページ http://eonasdan.github.io/bootstrap-datetimepicker/

posted by Png akanuma on Sat 22 Nov 2014 at 14:02

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

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

   1  $ vagrant up
   2  Bringing machine 'default' up with 'virtualbox' provider...
   3  ==> default: Importing base box 'chef/ubuntu-14.04'...
   4  ==> default: Matching MAC address for NAT networking...
   5  〜〜〜中略〜〜〜
   6  ==> default: [2014-11-16T11:51:03+00:00] WARN: Cloning resource attributes for package[tar] from prior resource (CHEF-3694)
   7  ==> 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'
   8  ==> 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 になります。

   1  $ vagrant status
   2  Current machine states:
   3  
   4  default                   aborted (virtualbox)

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

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

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

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

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

   1    config.vm.provider :virtualbox do |vb|
   2      vb.gui = false
   3      vb.customize ['modifyvm', :id, '--memory', '1024']
   4      vb.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]
   5      vb.customize ["modifyvm", :id, "--natdnsproxy1", "on"]
   6    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が完了するようになりました。

posted by Png akanuma on Sun 16 Nov 2014 at 21:15

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

   1  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 を認識してくれないようです。

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

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

   1  bundle package --all

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

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

posted by Png akanuma on Wed 25 Dec 2013 at 06:01

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

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

検証用アプリ


 構成は下記の通りです。

  • Nginx
  • Passenger
  • Ruby2.0
  • Rails4

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

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

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

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

   1  class StaticPagesController < ApplicationController
   2  
   3    require 'oj'
   4  
   5    after_action :testcompress
   6  
   7    respond_to :json, :mpac
   8  
   9    SERIALIZE_TEST_RESPONSE = {(省略)}
  10  
  11    def serialize_test
  12      respond_to do |format|
  13        format.json { render :json => SERIALIZE_TEST_RESPONSE }
  14        format.mpac { render :text => SERIALIZE_TEST_RESPONSE.to_msgpack, :content_type => 'application/x-mpac' }
  15      end
  16    end
  17  
  18    def testcompress
  19      response.body = ActiveSupport::Gzip.compress(response.body)
  20    end
  21  
  22  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の設定ファイルで下記の設定を有効にします。

   1  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
JsonRails48626181618052002252258158.57306.30
MessagePackRails2713723173213565533889.051701.66
JsonNginx48824178517751988272206159.11393.07
MessagePackNginx33378324924827944661093.612395.42
MessagePack非圧縮34617524624627166101134.3319178.19

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

posted by Png akanuma on Sat 21 Dec 2013 at 18:28

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

Couchbaseについてざっくりと


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

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

MongoDBと似ているところ


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

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

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

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


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

MongoDB/Couchbase比較
項目MongoDBCouchbase
レプリケーション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が利用できるようになったらパフォーマンス検証等行ってみたいと思います。ただ無償版の状況を考えるとなかなか難しいかもしれませんが。。

posted by Png akanuma on Mon 16 Dec 2013 at 06:21

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

   1  $ rails new turntable_test -d mysql
   2        create  
   3        create  README.rdoc
   4        create  Rakefile
   5        create  config.ru
   6        create  .gitignore
   7        create  Gemfile
   8        create  app
   9        create  app/assets/javascripts/application.js
  10        create  app/assets/stylesheets/application.css
  11        create  app/controllers/application_controller.rb
  12        create  app/helpers/application_helper.rb
  13        create  app/views/layouts/application.html.erb
  14        create  app/assets/images/.keep
  15        create  app/mailers/.keep
  16        create  app/models/.keep
  17        create  app/controllers/concerns/.keep
  18        create  app/models/concerns/.keep
  19        create  bin
  20        create  bin/bundle
  21        create  bin/rails
  22        create  bin/rake
  23        create  config
  24        create  config/routes.rb
  25        create  config/application.rb
  26        create  config/environment.rb
  27        create  config/environments
  28        create  config/environments/development.rb
  29        create  config/environments/production.rb
  30        create  config/environments/test.rb
  31        create  config/initializers
  32        create  config/initializers/backtrace_silencers.rb
  33        create  config/initializers/filter_parameter_logging.rb
  34        create  config/initializers/inflections.rb
  35        create  config/initializers/mime_types.rb
  36        create  config/initializers/secret_token.rb
  37        create  config/initializers/session_store.rb
  38        create  config/initializers/wrap_parameters.rb
  39        create  config/locales
  40        create  config/locales/en.yml
  41        create  config/boot.rb
  42        create  config/database.yml
  43        create  db
  44        create  db/seeds.rb
  45        create  lib
  46        create  lib/tasks
  47        create  lib/tasks/.keep
  48        create  lib/assets
  49        create  lib/assets/.keep
  50        create  log
  51        create  log/.keep
  52        create  public
  53        create  public/404.html
  54        create  public/422.html
  55        create  public/500.html
  56        create  public/favicon.ico
  57        create  public/robots.txt
  58        create  test/fixtures
  59        create  test/fixtures/.keep
  60        create  test/controllers
  61        create  test/controllers/.keep
  62        create  test/mailers
  63        create  test/mailers/.keep
  64        create  test/models
  65        create  test/models/.keep
  66        create  test/helpers
  67        create  test/helpers/.keep
  68        create  test/integration
  69        create  test/integration/.keep
  70        create  test/test_helper.rb
  71        create  tmp/cache
  72        create  tmp/cache/assets
  73        create  vendor/assets/javascripts
  74        create  vendor/assets/javascripts/.keep
  75        create  vendor/assets/stylesheets
  76        create  vendor/assets/stylesheets/.keep
  77           run  bundle install
  78  Enter your password to install the bundled RubyGems to your system: 
  79  Fetching gem metadata from https://rubygems.org/...........
  80  Fetching gem metadata from https://rubygems.org/..
  81  Resolving dependencies...
  82  Using rake (10.1.0) 
  83  Installing i18n (0.6.9) 
  84  Using minitest (4.7.5) 
  85  Using multi_json (1.8.2) 
  86  Using atomic (1.1.14) 
  87  Using thread_safe (0.1.3) 
  88  Using tzinfo (0.3.38) 
  89  Using activesupport (4.0.1) 
  90  Using builder (3.1.4) 
  91  Using erubis (2.7.0) 
  92  Using rack (1.5.2) 
  93  Using rack-test (0.6.2) 
  94  Using actionpack (4.0.1) 
  95  Installing mime-types (1.25.1) 
  96  Using polyglot (0.3.3) 
  97  Using treetop (1.4.15) 
  98  Using mail (2.5.4) 
  99  Using actionmailer (4.0.1) 
 100  Using activemodel (4.0.1) 
 101  Using activerecord-deprecated_finders (1.0.3) 
 102  Using arel (4.0.1) 
 103  Using activerecord (4.0.1) 
 104  Using bundler (1.3.5) 
 105  Using coffee-script-source (1.6.3) 
 106  Using execjs (2.0.2) 
 107  Using coffee-script (2.2.0) 
 108  Using thor (0.18.1) 
 109  Using railties (4.0.1) 
 110  Using coffee-rails (4.0.1) 
 111  Using hike (1.2.3) 
 112  Installing jbuilder (1.5.3) 
 113  Using jquery-rails (3.0.4) 
 114  Using json (1.8.1) 
 115  Installing mysql2 (0.3.14) 
 116  Using tilt (1.4.1) 
 117  Installing sprockets (2.10.1) 
 118  Using sprockets-rails (2.0.1) 
 119  Using rails (4.0.1) 
 120  Using rdoc (3.12.2) 
 121  Using sass (3.2.12) 
 122  Using sass-rails (4.0.1) 
 123  Using sdoc (0.3.20) 
 124  Installing turbolinks (2.0.0) 
 125  Installing uglifier (2.3.2) 
 126  Your bundle is complete!
 127  Use `bundle show [gemname]` to see where a bundled gem is installed.

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

   1  Installing mysql2 (0.3.14)

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

   1  Using sqlite3 (1.3.8)

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

   1  $ diff test_app/config/database.yml turntable_test/config/database.yml 
   2  1,2c1
   3  < # SQLite version 3.x
   4  < #   gem install sqlite3
   5  ---
   6  > # MySQL.  Versions 4.1 and 5.0 are recommended.
   7  4,5c3,10
   8  < #   Ensure the SQLite 3 gem is defined in your Gemfile
   9  < #   gem 'sqlite3'
  10  ---
  11  > # Install the MYSQL driver
  12  > #   gem install mysql2
  13  > #
  14  > # Ensure the MySQL gem is defined in your Gemfile
  15  > #   gem 'mysql2'
  16  > #
  17  > # And be sure to use new-style password hashing:
  18  > #   http://dev.mysql.com/doc/refman/5.0/en/old-client.html
  19  7,8c12,14
  20  <   adapter: sqlite3
  21  <   database: db/development.sqlite3
  22  ---
  23  >   adapter: mysql2
  24  >   encoding: utf8
  25  >   database: turntable_test_development
  26  10c16,18
  27  <   timeout: 5000
  28  ---
  29  >   username: root
  30  >   password:
  31  >   socket: /tmp/mysql.sock
  32  16,17c24,26
  33  <   adapter: sqlite3
  34  <   database: db/test.sqlite3
  35  ---
  36  >   adapter: mysql2
  37  >   encoding: utf8
  38  >   database: turntable_test_test
  39  19c28,30
  40  <   timeout: 5000
  41  ---
  42  >   username: root
  43  >   password:
  44  >   socket: /tmp/mysql.sock
  45  22,23c33,35
  46  <   adapter: sqlite3
  47  <   database: db/production.sqlite3
  48  ---
  49  >   adapter: mysql2
  50  >   encoding: utf8
  51  >   database: turntable_test_production
  52  25c37,39
  53  <   timeout: 5000
  54  ---
  55  >   username: root
  56  >   password:
  57  >   socket: /tmp/mysql.sock

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

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

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

   1  mysql> show databases;
   2  +----------------------------+
   3  | Database                   |
   4  +----------------------------+
   5  | information_schema         |
   6  | mysql                      |
   7  | performance_schema         |
   8  | test                       |
   9  | turntable_test_development |
  10  | turntable_test_test        |
  11  +----------------------------+
  12  6 rows in set (0.00 sec)

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

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

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

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

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

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

posted by Png akanuma on Wed 11 Dec 2013 at 06:29

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

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

   1  $ brew install mysql
   2  Warning: It appears you have MacPorts or Fink installed.
   3  Software installed with other package managers causes known problems for
   4  Homebrew. If a formula fails to build, uninstall MacPorts/Fink and try again.
   5  ==> Downloading https://downloads.sf.net/project/machomebrew/Bottles/mysql-5.6.1
   6  ######################################################################## 100.0%
   7  ==> Pouring mysql-5.6.15.mavericks.bottle.tar.gz
   8  ==> Caveats
   9  A "/etc/my.cnf" from another install may interfere with a Homebrew-built
  10  server starting up correctly.
  11  
  12  To connect:
  13      mysql -uroot
  14  
  15  To have launchd start mysql at login:
  16      ln -sfv /usr/local/opt/mysql/*.plist ~/Library/LaunchAgents
  17  Then to load mysql now:
  18      launchctl load ~/Library/LaunchAgents/homebrew.mxcl.mysql.plist
  19  Or, if you don't want/need launchctl, you can just run:
  20      mysql.server start
  21  ==> /usr/local/Cellar/mysql/5.6.15/bin/mysql_install_db --verbose --user=akanuma
  22  ==> Summary
  23   /usr/local/Cellar/mysql/5.6.15: 9410 files, 349M

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

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

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

   1  $ launchctl load ~/Library/LaunchAgents/homebrew.mxcl.mysql.plist 
   2  $ ps aux | grep mysql
   3  akanuma          2558   0.0  0.0  2423468    332 s000  R+   10:54PM   0:00.00 grep mysql
   4  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
   5  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コマンドで接続できることを確認します。

   1  $ mysql -uroot
   2  Welcome to the MySQL monitor.  Commands end with ; or \g.
   3  Your MySQL connection id is 1
   4  Server version: 5.6.15 Homebrew
   5  
   6  Copyright (c) 2000, 2013, Oracle and/or its affiliates. All rights reserved.
   7  
   8  Oracle is a registered trademark of Oracle Corporation and/or its
   9  affiliates. Other names may be trademarks of their respective
  10  owners.
  11  
  12  Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

posted by Png akanuma on Tue 10 Dec 2013 at 23:15

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


試験環境


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

  • Ruby2.0, Rails4
  • アプリケーションサーバ:1台(VM)
  • JMeterサーバ:3台(VM)
  • JMeterクライアント:1台(通常の作業PC)
    サーバ構成
    hostanameCPU仮想コア数(Per CPU)MemoryDisk用途
    loadtest01248192MB20GBAPサーバ
    loadtest02114096MB20GBJMeterサーバ
    loadtest03114096MB20GBJMeterサーバ
    loadtest04114096MB20GBJMeterサーバ


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


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

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


処理内容


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

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

   1  class StaticPagesController < ApplicationController
   2  
   3    def fibonacci
   4      @fibonacci = calc_fibonacci(params[:n].to_i)
   5    end
   6  
   7    private
   8  
   9      def calc_fibonacci(n)
  10        if (n < 2)
  11          return n;
  12        else
  13          p2 = 0;
  14          p1 = 1;
  15          2.upto(n) { p2, p1 = p1, p2 + p1 }
  16          return p1;
  17        end 
  18      end
  19  
  20  end


JMeterシナリオ


JMeterのシナリオとしては、

  • JMeterスレッド数:100
  • Ramp up:0
  • ループ数:無限ループ
  • 継続時間:5分間

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


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


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


Puma

Puma検証
ワーカプロセス数スレッド数サンプル数平均値(msec)中間値(msec)90%LINE (msec)最小値(msec)最大値(msec)スループット(/sec)KB/secエラー数
1500430792090197823791292519141.68414.67多発
1008241811365571071646414799.432339.490
10032243620362601136616890805.372356.8332
64322428253655113557103157802.352347.941
3232241934366467606252504798.242335.890
1664237270373313396298546782.002288.340
1696231996382376446241748762.492231.260

  • ワーカプロセス数1のケースでは全くCPUを使い切れていませんが、他のケースではCPUを使い切った状態で動いていました。
  • スレッド数を増加させてもリソースの使用量はあまり変化しませんでしたが、プロセス数を増やすと使用量がかなり増えます。
  • メモリに余裕があってもCPUが振り切る方が早いです。


Unicorn

Unicorn検証
ワーカプロセス数サンプル数平均値(msec)中間値(msec)90%LINE (msec)最小値(msec)最大値(msec)スループット(/sec)KB/secエラー数
1429411345572505537717624711.7434.54433
886866026562137217714119322.1565.15389
32429431465325284451183552103.48304.38280
6419125444017037526115819498.541466.4648
962471913601702614982454773.582275.693
1282560503432524521320587836.002459.820
160254117347291537158825835.542458.540
192252158349302588135746833.942453.870
2242297233832755929299799697.122045.152

  • ワーカプロセス数が1の場合は1CPUしか使われない
  • ワーカプロセス数をコア数と同じ8にした場合でも、すべてのコアが使われるわけではない
  • ワーカプロセス数をコア数の4倍の64まで増やすとかなりCPUが使われるようになってきて、128まで増やすとCPUはほぼ使い切られて、メモリの使用率も高くなる。
  • ワーカプロセス数を192まで増やした時点でメモリもほぼ使い切られ、224まで増やすとメモリが足りなくなり、Swapが発生して遅くなる。


Passenger

Passenger検証
プロセス数サンプル数平均値(msec)中間値(msec)90%LINE (msec)最小値(msec)最大値(msec)スループット(/sec)KB/secエラー数
16518813761282132512248690215.15646.070
82451533603553682972441810.572433.990
162467373573533812792339815.772448.770
242411653663603912812414797.272392.300

  • プロセス数が1だと4コアしか使われていない
  • プロセス数をコア数と同じにするとどのコアもかなり使われるようになる。メモリは余裕あり。
  • プロセス数をコア数以上に増やしていってもCPU, メモリの使用率はあまり変化しない。CPUは80%まで使われることが多いが、アイドルが10%前後残っていて、使い切ることはない


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


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

総合
APサーバサンプル数平均値(msec)中間値(msec)90%LINE (msec)最小値(msec)最大値(msec)スループット(/sec)KB/sec
Puma241811365571071646414799.432339.49
Unicorn252158349302588135746833.942453.87
Passenger2467373573533812792339815.772448.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
Unicorn23075121013411675.32221.50
Passenger2309211912511875.38226.67

  • UnicornとPassengerでほぼ差はなし
  • Passengerは最初の比較時に他と比べて最小値が遅いことが懸念だったが、実際の状況に近いアクセス数では 問題なく、Unicornが負荷が低い状態でもメモリリソースを消費するのに対してPassengerはリソースの消費が少ないこと から、Passengerを第一候補と考える。


検証時の問題点


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


Passenger

  • デフォルトの状態で負荷をかけたところ、下記のようなエラーが多発してHTTPステータスコード 503が返される。

   1  [ 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ドメインソケットで行う設定で負荷をかけたところ、下記エラーが多発。

   1  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

   1  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"

   1  [2013-11-20 14:44:09.109250] ERROR worker=114 PID:26737 timeout (31s > 30s),
   2        killing
   3        [2013-11-20 14:44:09.119595] ERROR reaped #<Process::Status: pid 26737 SIGKILL
   4        (signal 9)> worker=114
   5        [2013-11-20 14:44:10.140398]  INFO worker=114 ready

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

   1  send_timeout 300;
   2  proxy_connect_timeout 300;
   3  proxy_read_timeout 300;

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

posted by Png akanuma on Sat 7 Dec 2013 at 23:14