最小のRackアプリケーションをHerokuにデプロイしてベンチマークを計測してみた

Heroku の Ruby スタックの性能が知りたかったので、 下記ような最小の Rack アプリケーションを Heroku にデプロイし、 Apatch Bentch でベンチマークを計測してみた。 Rack アプリケーションは unicorn とか使わず rackup コマンドで動かしている。

require "rack"

class SampleApp
  def call(env)
    [200, { "Content-Type" => "text/html" }, ["Hello World"]]
  end
end

run SampleApp.new

計測結果は下表の通り。すべてのリクエストが成功したものだけを載せている。

合計リクエスト発行数&同時接続数 Requests per second[#/sec] Time per request(mean, across all concurrent requests)[ms]
-n 100 -c 100 33.51 29.838
-n 1000 -c 100 57.03 17.536
-n 1000 -c 1000 36.64 27.924
-n 3000 -c 1000 45.05 22.199
-n 2500 -c 2500 41.33 24.193
-n 3000 -c 2500 45.79 21.838

同時接続数は 2500、合計リクエスト数 3000 あたりが上限だった。

ActiveRecord enums で列挙型の値を取り出す方法

Rails のモデルで次のように列挙型を定義した場合

class Customer < ActiveRecord::Base
  enum payment_system: { credit_card: 1, bank_transfer: 2 }
end

列挙型の値を取り出すときは次のように書く。

Customer.payment_systems[:credit_card] #=> 1

ビューやヘルパーで使うからメモしておく。

環境や開発者に依存しない設定は Rails の config に追加すればよかった

ヘッダーに表示するサービス名や、フッターに表示するコピーライトを変更しやすいように、 設定ファイルとして YAML で抜き出そうと思っていたけど、 Rails の config に設定を追加できることを今さら知った。

例えば、config/application.rb に

module RailsSample
  class Application < Rails::Application
    # ...

    # タイトルとコピーライトを設定に追加
    config.app_name = "サンプルサービス"
    config.copyright = "tnakamura"
  end
end

と書いておけば、

Rails.application.config.app_name #=> サンプルサービス
Rails.application.config.copyright #=> tnakamura

という風に利用できた。 設定を追加したら、Rails アプリの再起動が必要だけど。

環境や開発者に依存する設定は dotenv 使うとして、 そういった環境に依存しない設定は config/application.rb に書けばいいや。

Rails のドキュメントはちゃんと読んでおくべきだったな。

Sidekiq をバックエンドに ActiveJob を導入

Heroku に 30 秒でレスポンスを返さないといけないルールがあったのを忘れていたので、 急遽 Rails アプリで時間がかかる処理を非同期にすることにした。

Rails で非同期というと Resque や Sidekiq が今のところ人気だけど、 今回は Rails 4.2 で追加予定の ActiveJob を使うことにした。 バックエンドには Sidekiq。

Redis をインストール

バックエンドに使う Sidekiq は Redis が必要なので、 Vagrant + Chef + Berkshelf で構築している開発環境にインストールする。

Berksfile に

cookbook "redisio"

を追加し

rm -rf cookbooks
berks vendor cookbooks

で Cookbook をインストール。

redisio のレシピを使うように Vagrantfile を

VAGRANTFILE_API_VERSION = "2"

Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
  # ...

  config.vm.provision "chef_solo" do |chef|
    chef.cookbooks_path = ["./chef/cookbooks", "./chef/site-cookbooks"]

    # ...

    # Redis のレシピを追加
    chef.add_recipe "redisio::install"
    chef.add_recipe "redisio::enable"
  end
end

という風に修正したら、vagrant provision で chef-solo を実行。

ActiveJob と Sidekiq をインストール

ここからは vagrant ssh で開発環境に入っての作業。

Gemfile に

gem "activejob", "4.2.0.beta1", require: "active_job/railtie"
gem "sidekiq"
gem "sinatra"

を追加して bundle install

ActiveJob は gem を直接使う。 Rails 4.1 のアプリで使いたかったので、activesuport 4.2.0.beta2 が不要な、4.2.0.beta1 を使用。 active_job/railtie を require しないと、ActiveJob が Rails に読み込まれなかった。

あとは config/environment/development.rb で ActiveJob の設定を記述。 バックエンドで Sidekiq を使うように指定する。

Rails.application.configure do
  # ...

  # ActiveJob のバックエンドに Sidekiq を指定
  config.active_job.queue_adapter = :sidekiq
end

ローカルにインストールした Redis を使うので、Sidekiq の Redis 設定は省略可能。

Sidekiq の Web コンソールをマウント

ちゃんとジョブが実行されたか確認したいので、Sidekiq の Web コンソールをマウントする。 sinatra もインストールしたのは、これが理由。 Sidekiq の Web コンソールは Sinatra を別にインストールしないといけない。

config/routes.rb を次のように修正。

Rails.application.routes.draw do
  # ...

  # Sidekiq の Web コンソールをマウント
  mount Sidekiq::Web => "/sidekiq"
end

ジョブを作成

次のコマンドでジョブを生成。

bundle exec railg generate job calc_score

すると

class CalcScoreJob < ActiveJob::Base
  def perform(*args)
  end
end

みたいな雛形が app/jobs/calc_score_job.rb に生成されるので、 perform メソッドに非同期で実行したい処理を書く。 こんな感じ。

class CalcScoreJob < ActiveJob::Base
  def perform(*args)
    project_id = args[0]
    project = Project.find(project_id)
    project.calc_score # 30 秒以上かかる集計処理
  end
end

コントローラーでジョブを使う

コントローラーで作成したジョブを使ってみる。

ドキュメントには perform_later を使うと書いてあったけど、 RubyGems.org からインストールした 4.2.0.beta1 にはまだ実装されていないので、NoMethodError。 代わりに古い APIenqueue を呼び出す。

class ProjectsController < ApplicationController

  # ...

  def calc_score
    # 本当は
    # CalcScoreJob.perform_later(data)

    # ジョブ実行
    CalcScoreJob.enqueue(@project.id)
    respond_to do |format|
      format.html { redirect_to @project }
    end
  end
end

ジョブを実行

Rails プロジェクト直下に、次の内容を書いた Procfile を作成する。

web: bundle exec unicorn -p $PORT -c config/unicorn.rb
worker: bundle exec sidekiq

Foreman を使って Rails サーバーと Sidekiq ワーカーを起動。

web: bundle exec unicorn -p $PORT -c ./config/unicorn.rb
worker: bundle exec sidekiq

コントローラーのアクションを呼び出してジョブを実行したら、 Sidekiq の Web コンソールで確認してみる。

ブラウザで http://localhost:8080/sidekiq にアクセスして

f:id:griefworker:20140928122506p:plain

Processed の数が増えていれば、ActiveJob のバックエンドとして Sidekiq がちゃんと動いたことになる。

Capybara + Poltergeist を使ってテストするための環境を Vagrant + Chef で構築

プライベートで開発に関わっている Rails アプリが完成に近づいてきたので、 Capybara と RSpec を使ってインテグレーションテストを書くことにした。

JavaScript で動きをつけたページもきちんとテストしたいので、 JavaScript ドライバに Poltergeist を選択。

以下、作業メモ。

Chef で Phantomjs をインストール

まずは、Poltergeist が依存している、ヘッドレスブラウザの Phantomjs のインストールが必要。

Phantomjs はコミュニティ Cookbook を使ってインストールする。

Berkfile に

cookbook "phantomjs"

の1行を追加し、

berks vendor cookbooks

でインストール。既に cookbooks フォルダが存在する場合は、先に削除しておくこと。

開発環境は Vagrant で構築しているので、Vagrantfile に

Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
  # ...

  config.vm.provision "chef_solo" do |chef|
    chef.cookbooks_path = ["./chef/cookbooks", "./chef/site-cookbooks"]

    # ...

    # Phantomjs のレシピを追加
    chef.add_recipe "phantomjs::default"
  end
end

を記述して

vagrant provision

を実行。

Capybara と Poltergeist をインストール

ここからは vagrant ssh仮想マシンに入って作業する。

Rails プロジェクトの Gemfile に

group :test do
  # 下記を追加
  gem "capybara"
  gem "database_cleaner"
  gem "poltergeist"
end

を記述。投入したテストデータがちゃんと削除されるように、database_cleaner もあわせて使う。

bundle install

を実行してインストール。

Capybara と Poltergeist と DatabaseCleaner を有効にする

spec_helper.rb に、Capybara と Poltergeist と DatabaseCleaner の設定を記述。

# ...

# Capybara の設定
require 'capybara/rspec'
require 'capybara/rails'
require 'capybara/poltergeist'
Capybara.javascript_driver = :poltergeist

RSpec.configure do |config|
  # ...

  # database_cleaner の設定
  config.before(:suite) do
    DatabaseCleaner.strategy = :transaction
    DatabaseCleaner.clean_with(:truncation)
  end
  config.around(:each) do |example|
    DatabaseCleaner.cleaning do
      example.run
    end
  end
end

これで準備完了

あとは spec/features フォルダ下にフィーチャーを書いて、

bin/rake spec:features

でテストを走らせればいい。

下記のように、js: true オプションを指定した箇所は Poltergeist を使って実行されるようになる。

describe "JavaScript を使ったページのテスト", js: true do
  it "JavaScript が実行されたかのテスト" do
    # テスト内容を書く
  end
end

環境や開発者ごとに異なる設定を記述するのに dotenv が便利だった

Rails アプリ開発中、メール送信機能をテストするには GmailSMTP サーバーを使うのが手っ取り早い。

その際、ActionMailer の設定に Gmail のアカウントとパスワードを書く必要があるけど、 開発者ごとに違うし、そもそもアカウントとパスワードをソースコードに直接書きたくない。

そういった環境や開発者ごとに異なる情報を記述するために、 dotenv(dotenv-rails) を導入してみた。

Gemfile に

gem "dotenv-rails", groups: [:development, :test]

を追加して bundle install

プロジェクトのルートディレクトリに .env ファイルを作成し、環境変数にセットしたい情報を記述する。

GMAIL_ADDRESS="your-address@gmail.com"
GMAIL_PASSWORD="your-password"

すると、config/environment/development.rb の ActionMailer の設定が次のように書ける。

config.action_mailer.delivery_method = :smtp
config.action_mailer.raise_delivery_errors = true
config.action_mailer.smtp_settings = {
  enable_starttls_auto: true,
  address: "smtp.gmail.com",
  port: 587,
  domain: "smtp.gmail.com",
  authentication: "plain",
  user_name: ENV["GMAIL_ADDRESS"],
  password: ENV["GMAIL_PASSWORD"]
}

bin/rails server で開発サーバーを起動すれば、.env ファイルに書いた環境変数が自動で読み込まれて ENV にセットされる。

.env ファイルをリポジトリから除外するように .gitignore に追加しておけば、 Gmail のアカウントとパスワードをソースコードに記述せずに、 心置きなくメール送信をテストできる。

RubyMotion をアンインストール

Apple の新言語 Swift を使い始めていて、思いのほか書き心地が良かったので、 iOS アプリ開発は RubyMotion から Swift に移行することにした。

そこで、後戻りできないように、MacBook から RubyMotion をアンインストール。

sudo rm -rf /Library/RubyMotion
sudo rm /usr/bin/motion
rm ~/Library/RubyMotion

書き心地というか、楽しさでは RubyMotion も負けていないんだけど、 ライセンス更新に毎年 10000 円ほどかかるのがツライ。 iOS Developer Program で 7800 円かかるから余計にね。