• 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 というメソッドを使って現在時刻を設定することができます。

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

ruby>>
class Foo
def now
Time.now
end
end
<<--

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

ruby>>
travel_to Time.new(2014, 11, 24, 9, 30, 0) do
foo = Foo.new
expect(foo.now).to eq(Time.new(2014, 11, 24, 9, 30, 0))
end
<<--

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

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

ruby>>
class Foo
def now
DateTime.now # <- to_travelで設定した時間ではなく現在時刻が返る
end
end
<<--

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

ruby>>
group :test do
gem 'timecop'
end
<<--

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

ruby>>
Timecop.freeze(Time.new(2014, 11, 24, 9, 30, 0)) do
foo = Foo.new
expect(foo.now).to eq(Time.new(2014, 11, 24, 9, 30, 0))
end
<<--

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

 travel_to(date_or_time, &block)

 rails4.1からのtravel_toをrspecで使う

posted by akanuma akanuma on Tue 25 Nov 2014 at 00:03 with 0 comments

環境変数によってRailsアプリの動作を切り替えたいことがあって、

nginx + Passenger で動かしているRailsアプリに環境変数を渡す方法を調べました。

nginx の起動スクリプト(/etc/init.d/nginx)に書いても、

/etc/default/nginx に書いてもうまくいかなかったのですが、

nginx の設定ファイルの location ブロックで

passenger_set_cgi_param で設定することで環境変数が設定できます。

sh>>
passenger_set_cgi_param HOGE fuga;
<<--

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

参考ページ

16.3.5. Phusion Passengerが提供するアプリケーション

8.6.1. passenger_set_cgi_param

posted by akanuma akanuma on Sat 22 Nov 2014 at 14:14 with 0 comments
Rails4で入力フォームから日時を選択できるように datetimepicker を使ったのですがちゃんと動くまでに軽くはまったのでメモ。 [bootstrap3-datetimepicker-rails](https://github.com/TrevorS/bootstrap3-datetimepicker-rails) まずはGemfileに下記エントリを追加して bundle install ruby>> gem 'momentjs-rails' gem 'bootstrap3-datetimepicker-rails' <<-- moment と datetimepicker のJavaScriptファイルを読み込むための設定を追加します。 私は coffeescript を使っているので application.js.coffee に下記エントリ を追加します。 bootstrapの設定が入っていない場合はあわせて追加します。 js>> #= require bootstrap #= require moment #= require bootstrap-datetimepicker <<-- 続いてstylesheetの設定も追加します。 私の場合は scss を使っているので、application.css.scss に下記を追加。 ちなみに順番もこの通りじゃないとちゃんと動きません。 css>> @import "bootstrap-sprockets"; @import "bootstrap"; @import "bootstrap-datetimepicker"; <<-- ここまででひとまずの下準備は終了なので、実際にdatetimepickerを適用したい部分の作業をします。 今回の対象のView部分は下記のような感じです。 html>>
<%= f.text_field :start_datetime, value: @user.start_datetime, class: "form-control", id: "user_start_datetime" %>
<<-- f.text_field で出力される input タグに対して datetimepicker() メソッドを 実行します。 下記内容でcoffeescriptのファイルを別途作成して読み込みます。 js>> $ -> $('#user_start_datetime').datetimepicker() <<-- これで入力ボックスを選択した時に日時選択用のポップアップが表示されるようになります。 また、日付のフォーマットのデフォルトは MM/DD/YYYY hh:mm A/PM なので、日本の形式に変更します。 inputタグに data-date-format 属性で指定するのですが、rails の text_field タグでは追加の属性を指定できないため、先ほどのcoffeescriptを変更して動的に属性を追加します。 js>> $ -> date_format = {'data-date-format': 'YYYY/MM/DD HH:mm'} $('#user_start_datetime').attr(date_format) $('#user_start_datetime').datetimepicker() <<-- これで日付フォーマットが変更されます。 今回は一番シンプルな形で datetimepicker を使いましたが、オプションの指定で表示形式などは色々変更できますので、試してみてもらえると良いかと思います。 参考ページ [http://eonasdan.github.io/bootstrap-datetimepicker/](http://eonasdan.github.io/bootstrap-datetimepicker/)
posted by akanuma akanuma on Sat 22 Nov 2014 at 14:03 with 0 comments

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

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

sh>>
$ 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 になります。

sh>>
$ vagrant status
Current machine states:

default aborted (virtualbox)
<<--

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

sh>>
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 を追加します。

ruby>>
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が完了するようになりました。

posted by akanuma akanuma on Sun 16 Nov 2014 at 21:28 with 0 comments

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

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

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

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

sh>>
bundle package --all
<<--

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

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

posted by akanuma akanuma on Wed 25 Dec 2013 at 06:01 with 0 comments

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

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

検証用アプリ


 構成は下記の通りです。

  • Nginx
  • Passenger
  • Ruby2.0
  • Rails4

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

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

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

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

ruby>>
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の設定ファイルで下記の設定を有効にします。

sh>>
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 を追加してあります。

検証結果


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

|caption=検証結果
|
|種別,圧縮,サンプル数,平均,中央値,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圧縮パターンを採用することにしました。

posted by akanuma akanuma on Sat 21 Dec 2013 at 19:00 with 0 comments

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

Couchbaseについてざっくりと


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

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

MongoDBと似ているところ


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

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

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

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


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

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

posted by akanuma akanuma on Tue 17 Dec 2013 at 06:21 with 0 comments

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

sh>>
$ 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が使用されます。

sh>>
Installing mysql2 (0.3.14)
<<--

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

sh>>
Using sqlite3 (1.3.8)
<<--

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

diff>>
$ 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を作成します。

sh>>
$ bundle --binstubs
$ bin/rake db:create
<<--

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

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

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

sh>>
$ 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にテーブルを作成します

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

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

sh>>
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)
<<--

posted by akanuma akanuma on Thu 12 Dec 2013 at 08:03 with 0 comments

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

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

sh>>
$ 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が起動するように設定します。

sh>>
$ 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コマンドで起動します。

sh>>
$ 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コマンドで接続できることを確認します。

sh>>
$ 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.
<<--

posted by akanuma akanuma on Tue 10 Dec 2013 at 23:19 with 0 comments

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


# 試験環境

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

  • Ruby2.0, Rails4
  • アプリケーションサーバ:1台(VM)
  • JMeterサーバ:3台(VM)
  • JMeterクライアント:1台(通常の作業PC)
    |caption=サーバ構成
    |
    |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サーバのアーキテクチャの比較は下記の通りです。

|caption=各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でメモ化を使ってフィボナッチ数を求める

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

|caption=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

|caption=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

|caption=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サーバを決定する

|caption=総合
|
|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と比 べると遅いのが懸念点。

|caption=定数スループットタイマを使って実際のアクセス数に近い負荷で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が返される。

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

sh>>
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://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

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

sh>>
[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の設定にも下記を追加しました。

sh>>
send_timeout 300;
proxy_connect_timeout 300;
proxy_read_timeout 300;
<<--

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

posted by akanuma akanuma on Sun 8 Dec 2013 at 00:06 with 0 comments