アクトインディ開発者ブログ

子供とお出かけ情報「いこーよ」を運営する、アクトインディ株式会社の開発者ブログです

Rails3 への移行

こんにちは、tahara です。

突然ですが、弊社では現在エンジニアを募集しています。 仕事内容は情シス業務と自社サービの開発です。 開発は主に Ralis で PHP もときどきあります。 たまぁに Common Lisp もあます(増やしていきたいです)。 詳細はこちらをご覧ください。

それでは本題です。

いまさらではありますが、弊社で運営している http://iko-yo.net を Rails3 に移行しました。 今回はその移行作業について書いていきたいと思います。

rvm で ruby 1.9.3 をインストールし gemsent を作成する。

bash < <(curl -s https://raw.github.com/wayneeseguin/rvm/master/binscripts/rvm-installer)
rvm install ruby-1.9.3
rvm use ruby-1.9.3
gem install bundler
rvm gemset create iko-yo-rails3
rvm --rvmrc --create 1.9.3@iko-yo-rails3
cd ..
cd -

きれいする

rm -r vendor/rails
rm -r vendor/gems
rm -r vendor/plugins/*

gem を入れる

vi Gemfile

assets は使わない。。。

source 'http://rubygems.org'

gem 'rails', '3.1.0'

# Bundle edge Rails instead:
# gem 'rails',     :git => 'git://github.com/rails/rails.git'

gem 'mysql2'
gem 'jquery-rails'
gem 'exception_notification'
gem 'geokit-rails3'
gem 'jpmobile'
gem 'nokogiri'
gem 'paperclip'
gem 'restful-authentication'
gem 'ssl_requirement'
gem 'acts_as_taggable_on_steroids'
gem 'acts_as_commentable'
gem 'will_paginate'
gem 'dynamic_form'
gem 'mecab-ruby', :require => 'MeCab'
gem 'twitter'
gem 'oauth'
gem 'garb'
gem 'gdata_19', :require => 'gdata'
gem 'holiday_jp'
gem 'dalli'
gem 'newrelic_rpm'

#;; config.assets.enabled = false
#;; # Gems used only for assets and not required
#;; # in production environments by default.
#;; group :assets do
#;;   gem 'sass-rails', "  ~> 3.1.0"
#;;   gem 'coffee-rails', "~> 3.1.0"
#;;   gem 'uglifier'
#;; end

# Use unicorn as the web server
gem 'unicorn'

# Deploy with Capistrano
gem 'capistrano'
gem 'capistrano-ext'

# To use debugger
# gem 'ruby-debug19', :require => 'ruby-debug'

group :test, :development do
  # Pretty printed test output
  gem 'turn', :require => false
  gem 'spork'
  gem 'rspec-rails', "~> 2.6"
  gem 'capybara'
  gem 'ZenTest'
  gem 'autotest-stumpwm'
  gem 'remarkable_activerecord', '>=4.0.0.alpha4'
  gem 'spork'
end

gem 入れて rails3 にする。

bundle install
bundle exec rails new

ソースの編集

config/rootes.rb はがんばる。 メールまわりも完全に書きなおし。

ソースをちまちま書きかえる(以下はイメージです。実際に動作するものではありません)。

config/boot.rb に次を追加
# /home/ancient/.rvm/rubies/ruby-1.9.2-p290/lib/ruby/1.9.1/psych.rb:148:in `parse': couldn't parse YAML at line 18 column 13 (Psych::SyntaxError)
require 'yaml'
YAML::ENGINE.yamler= 'syck'

各ファイルの1行目に次を追加
# -*- coding: utf-8 -*-


helper 系メソッドに .html_safe を付加

あは以下のようなイメージでどんどん書きかえていく。

s/adapter: mysql/adapter: mysql2/ database.yml.release

s/named_scope/scope/

s/RAILS_ROOT/Rails.root/

s/returning/tap/

s/request_uri/fullpath/

s/<% form/<%= form/
s/<%= f.fields_for/<%= f.fields_for/

layout が使われなかったのは ApplicationController#initialize で super を呼んでなかったからだった。

s/(.*).merge_conditions (.*)/where(\1).where(\2)/

s/mobile_filter :hankaku => true/hankaku_filter :input => true/

s/include ActionController::UrlWriter/include Rails.application.routes.url_helpers/

s/.class_name/.name/

s/link_to_remote .*/link_to \1, :remote => true/
:complete, :before 等は js で bind('ajax:complete', ...), bind('ajax:before', ...) にする。
http://www.alfajango.com/blog/rails-3-remote-links-and-forms/

error_messages がなくなったので gem 'dynamic_form'

s/choice/sample/

s/observe_field/ふつうの手書き jQuery/

s/action mailer/スーパークラスJpmobile::Mailer::Base で書きなおす/

/self.include_root_in_json = false/d config/initializers/wrap_parameters.rb

s/model.save(false)/model.save(:validate => false)/

s/errors.or\((.*)\)/errors[\1]/

他にもいっぱいあったような気もしますが、だいたいこんな感じです。

次に実行環境まわり。

実行環境

Apache と Passenger だったのを nginx と unicorn にしました。 unicorn についているサンプルをもとに設定しました。

まずは ngix

nginx.conf

user  deployer;
worker_processes  2;

pid /var/run/nginx.pid;

events {
    worker_connections  1024;
}

http {
    client_max_body_size 50m;
    client_header_buffer_size 4k;

    include       mime.types;
    default_type  application/octet-stream;

    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    #                  '$status $body_bytes_sent "$http_referer" '
    #                  '"$http_user_agent" "$http_x_forwarded_for"';

    #access_log  logs/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  15;

    gzip  on;
    gzip_disable "msie6";
    gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;

    #open_file_cache max=2000 inactive=300s;
    #open_file_cache_valid 360s;
    #open_file_cache_min_uses 2;
    #open_file_cache_errors off;

    include /var/www/outing/current/config/unicorn/production/nginx-site.conf;
}

nginx-site.conf

upstream outing {
    # for UNIX domain socket setups:
    #server unix:/tmp/.outing.sock fail_timeout=0;
    # for TCP setups, point these to your backend servers
    server 127.0.0.1:8080 fail_timeout=0;
}

server {
    listen 80;
    root /var/www/outing/current/public;
    server_name iko-yo.net;

    location / {

        #auth_basic "Restricted";
        #auth_basic_user_file /etc/nginx/outing-password;

        if ($request_uri ~* "\.(jpg|jpeg|gif|css|png|js|ico)\?[0-9]+$") {
            expires max;
            access_log off;
            break;
        }
        if (-f $request_filename) {
            expires 24h;
            access_log off;
            break;
        }

        try_files $uri @app;
    }

    location @app {
      # an HTTP header important enough to have its own Wikipedia entry:
      #   http://en.wikipedia.org/wiki/X-Forwarded-For
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

      # enable this if and only if you use HTTPS, this helps Rack
      # set the proper protocol for doing redirects:
      # proxy_set_header X-Forwarded-Proto https;

      # pass the Host: header from the client right along so redirects
      # can be set properly within the Rack application
      proxy_set_header Host $http_host;

      # we don't want nginx trying to do something clever with
      # redirects, we set the Host: header above already.
      proxy_redirect off;

      # set "proxy_buffering off" *only* for Rainbows! when doing
      # Comet/long-poll/streaming.  It's also safe to set if you're using
      # only serving fast clients with Unicorn + nginx, but not slow
      # clients.  You normally want nginx to buffer responses to slow
      # clients, even with Rails 3.1 streaming because otherwise a slow
      # client can become a bottleneck of Unicorn.
      #
      # The Rack application may also set "X-Accel-Buffering (yes|no)"
      # in the response headers do disable/enable buffering on a
      # per-response basis.
      # proxy_buffering off;

      proxy_pass http://outing;
    }
}


# HTTPS server

server {
    listen 443;
    root /var/www/outing/current/public;
    server_name iko-yo.net;

    ssl on;
    ssl_certificate /etc/ssl/iko-yo.net/iko-yo.net.crt.cer;
    ssl_certificate_key /etc/ssl/iko-yo.net/iko-yo.net.key;

    ssl_session_timeout 5m;

    ssl_protocols SSLv3 TLSv1;
    ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv3:+EXP;
    ssl_prefer_server_ciphers on;

    try_files $uri @app;

    location @app {
      # an HTTP header important enough to have its own Wikipedia entry:
      #   http://en.wikipedia.org/wiki/X-Forwarded-For
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

      # enable this if and only if you use HTTPS, this helps Rack
      # set the proper protocol for doing redirects:
      proxy_set_header X-Forwarded-Proto https;

      # pass the Host: header from the client right along so redirects
      # can be set properly within the Rack application
      proxy_set_header Host $http_host;

      # we don't want nginx trying to do something clever with
      # redirects, we set the Host: header above already.
      proxy_redirect off;

      # set "proxy_buffering off" *only* for Rainbows! when doing
      # Comet/long-poll/streaming.  It's also safe to set if you're using
      # only serving fast clients with Unicorn + nginx, but not slow
      # clients.  You normally want nginx to buffer responses to slow
      # clients, even with Rails 3.1 streaming because otherwise a slow
      # client can become a bottleneck of Unicorn.
      #
      # The Rack application may also set "X-Accel-Buffering (yes|no)"
      # in the response headers do disable/enable buffering on a
      # per-response basis.
      # proxy_buffering off;

      proxy_pass http://outing;
    }
}

# for munin
server {
    listen 127.0.0.1;
    server_name localhost;
    location /nginx_status {
        stub_status on;
        access_log   off;
        allow 127.0.0.1;
        deny all;
    }
}

# redirect sub domain
server {
    listen 80;
    server_name *.iko-yo.net;
    rewrite ^(.*) http://iko-yo.net$1 permanent;
}

次に unicorn

次の unicorn-init.sh を /etc/init.d/outing へ ln -s します。

#!/bin/sh
### BEGIN INIT INFO
# Provides:          outing
# Required-Start:    $remote_fs $syslog
# Required-Stop:     $remote_fs $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Example initscript
# Description:       This file should be used to construct scripts to be
#                    placed in /etc/init.d.
### END INIT INFO

# sudo ln -s /var/www/outing/current/config/unicorn/production/unicorn-init.sh /etc/init.d/outing
# sudo update-rc.d outing default
# sudo update-rc.d outing enable

set -e
# Example init script, this can be used with nginx, too,
# since nginx and unicorn accept the same signals

# Feel free to change any of the following variables for your app:

. "/usr/local/rvm/environments/ruby-1.9.3-p0@iko-yo-rails3"

TIMEOUT=${TIMEOUT-60}
APP_ROOT=/var/www/outing/current
PID=$APP_ROOT/tmp/pids/unicorn.pid
CMD="unicorn_rails -E production -D -c $APP_ROOT/config/unicorn/production/unicorn.conf.rb"
INIT_CONF=$APP_ROOT/config/init.conf
action="$1"
set -u

test -f "$INIT_CONF" && . $INIT_CONF

old_pid="$PID.oldbin"

cd $APP_ROOT || exit 1

sig () {
        test -s "$PID" && kill -$1 `cat $PID`
}

oldsig () {
        test -s $old_pid && kill -$1 `cat $old_pid`
}

case $action in
start)
        sig 0 && echo >&2 "Already running" && exit 0
        $CMD
        ;;
stop)
        sig QUIT && exit 0
        echo >&2 "Not running"
        ;;
force-stop)
        sig TERM && exit 0
        echo >&2 "Not running"
        ;;
restart|reload)
        sig HUP && echo reloaded OK && exit 0
        echo >&2 "Couldn't reload, starting '$CMD' instead"
        $CMD
        ;;
upgrade)
        if sig USR2 && sleep 2 && sig 0 && oldsig QUIT
        then
                n=$TIMEOUT
                while test -s $old_pid && test $n -ge 0
                do
                        printf '.' && sleep 1 && n=$(( $n - 1 ))
                done
                echo

                if test $n -lt 0 && test -s $old_pid
                then
                        echo >&2 "$old_pid still exists after $TIMEOUT seconds"
                        exit 1
                fi
                exit 0
        fi
        echo >&2 "Couldn't upgrade, starting '$CMD' instead"
        $CMD
        ;;
reopen-logs)
        sig USR1
        ;;
*)
        echo >&2 "Usage: $0 <start|stop|restart|upgrade|force-stop|reopen-logs>"
        exit 1
        ;;
esac

unicorn.conf.rb

# Sample verbose configuration file for Unicorn (not Rack)
#
# This configuration file documents many features of Unicorn
# that may not be needed for some applications. See
# http://unicorn.bogomips.org/examples/unicorn.conf.minimal.rb
# for a much simpler configuration file.
#
# See http://unicorn.bogomips.org/Unicorn/Configurator.html for complete
# documentation.

# Use at least one worker per core if you're on a dedicated server,
# more will usually help for _short_ waits on databases/caches.
worker_processes 4

# Since Unicorn is never exposed to outside clients, it does not need to
# run on the standard HTTP port (80), there is no reason to start Unicorn
# as root unless it's from system init scripts.
# If running the master process as root and the workers as an unprivileged
# user, do this to switch euid/egid in the workers (also chowns logs):
user "deployer", "deployer"

# Help ensure your application will always spawn in the symlinked
# "current" directory that Capistrano sets up.
working_directory "/var/www/outing/current" # available in 0.94.0+

# listen on both a Unix domain socket and a TCP port,
# we use a shorter backlog for quicker failover when busy
#listen "/tmp/.outing.sock", :backlog => 64
listen 8080, :tcp_nopush => true

# nuke workers after 30 seconds instead of 60 seconds (the default)
timeout 60

# feel free to point this anywhere accessible on the filesystem
pid "/var/www/outing/current/tmp/pids/unicorn.pid"

# By default, the Unicorn logger will write to stderr.
# Additionally, ome applications/frameworks log to stderr or stdout,
# so prevent them from going to /dev/null when daemonized here:
stderr_path "/var/www/outing/current/log/unicorn.stderr.log"
stdout_path "/var/www/outing/current/log/unicorn.stdout.log"

# combine REE with "preload_app true" for memory savings
# http://rubyenterpriseedition.com/faq.html#adapt_apps_for_cow
preload_app true
GC.respond_to?(:copy_on_write_friendly=) and
  GC.copy_on_write_friendly = true

before_fork do |server, worker|
  # the following is highly recomended for Rails + "preload_app true"
  # as there's no need for the master process to hold a connection
  defined?(ActiveRecord::Base) and
    ActiveRecord::Base.connection.disconnect!

  # The following is only recommended for memory/DB-constrained
  # installations.  It is not needed if your system can house
  # twice as many worker_processes as you have configured.
  #
  # # This allows a new master process to incrementally
  # # phase out the old master process with SIGTTOU to avoid a
  # # thundering herd (especially in the "preload_app false" case)
  # # when doing a transparent upgrade.  The last worker spawned
  # # will then kill off the old master process with a SIGQUIT.
  # old_pid = "#{server.config[:pid]}.oldbin"
  # if old_pid != server.pid
  #   begin
  #     sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU
  #     Process.kill(sig, File.read(old_pid).to_i)
  #   rescue Errno::ENOENT, Errno::ESRCH
  #   end
  # end
  #
  # Throttle the master from forking too quickly by sleeping.  Due
  # to the implementation of standard Unix signal handlers, this
  # helps (but does not completely) prevent identical, repeated signals
  # from being lost when the receiving process is busy.
  # sleep 1
end

after_fork do |server, worker|
  # per-process listener ports for debugging/admin/migrations
  # addr = "127.0.0.1:#{9293 + worker.nr}"
  # server.listen(addr, :tries => -1, :delay => 5, :tcp_nopush => true)

  # the following is *required* for Rails + "preload_app true",
  defined?(ActiveRecord::Base) and
    ActiveRecord::Base.establish_connection

  # if preload_app is true, then you may also want to check and
  # restart any other shared sockets/descriptors such as Memcached,
  # and Redis.  TokyoCabinet file handles are safe to reuse
  # between any number of forked children (assuming your kernel
  # correctly implements pread()/pwrite() system calls)
end

問題は unicorn の upgrade (sudo service outing upgrade) で失敗する場合があること。 なぜでしょう。。。

あと unicorn は production 環境ではメモリ使用量が少ないのですが、 development 環境ではどんどんメモリをくっていきます。 これもなぜでしょう。。。