全件検索から全文検索へ (Rails で MySQL の全文検索)

平成24年5月15日(火) 17時05分02秒
区分
Rails
報告者:
tahara

こんにちは、tahara です。 ようやくいこーよを like '%foo%' の(SQL 力づく)全件検索から、MySQL の全文検索に変更しました。

最初は mroonga を使おうかと思ったのですが、結局は MeCab を使って MySQL の全文検索をそのまま使うことにしました。

MySQL の全文検索は MyISAM じゃないと動かないので ENGINE = MyISAM で検索用のテーブルを作成します。

class CreateFacilityIndices < ActiveRecord::Migration
  def up
    execute <<SQL
create table facility_indices (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `facility_id` int(11) NOT NULL,
  `content` text NOT NULL,
  `created_at` datetime NOT NULL,
  `updated_at` datetime NOT NULL,
  PRIMARY KEY (`id`),
  FULLTEXT INDEX (content)
) ENGINE = MyISAM DEFAULT CHARSET utf8;
SQL
    Facility.find_each {|f|f.update_facility_index()}
  end

  def down
    execute "drop table facility_indices"
  end
end

検索したいテーブル(モデル)の after_save で上記のテーブ(facilities_indices)の登録、更新を行うようにします。 また、全文検索用のスコープも作成します。 order by で floor しているのは、同じようなスコアの場合は他の条件でソートしたいめです。

class Facility < ActiveRecord::Base
  after_save :update_facility_index
  has_one(:facility_index, :dependent => :destroy)

  def update_facility_index
    index = self.facility_index || self.build_facility_index
    index.update_from_facility
  end

  scope(:scoped_by_word,
        lambda { |text|
          joins(:facility_index).
          where("MATCH (facility_indices.content) AGAINST (?)", FacilityIndex.normalize(text)).
          order(sanitize_sql(["floor(MATCH (facility_indices.content) AGAINST (?)) desc", FacilityIndex.normalize(text)]))
        })
end

FacilityIndex では次のように、全文検索のインデックスを作ります。

  1. 検索対象の検索対象フィールドを一つも文字列にコンカチします。
  2. 地域と都道府県は全文検索結果スコアへの影響を大きくするために、単純に数回繰り返します。
  3. nkf で文字種の正規化を行います。
  4. MeCab で分解します。
  5. 助詞、助動詞は除きます。
  6. 原形があれば、原形を使うようにします。
  7. 「ある」などは検索上無意味なのでストップワードしとて除外します。
  8. MeCab 前にやっちゃうと、うまくいかないカタカナひらがな変換を行います。
  9. 以上のように正規化したものを FULLTEXT INDEX の付いたカラムに登録します。

検索時にはユーザの入力した検索ワードを同様に FacilityIndex::normalize で正規化して MATCH (...) AGAINST で検索します。 上記の Facility::scoped_by_word です。

# -*- coding: utf-8 -*-
require 'nkf'
require 'MeCab'

# 一文字からも検索できるように
# sudo vi /etc/mysql/my.cfg
# [mysqld]
# ft_min_word_len = 1
class FacilityIndex < ActiveRecord::Base
  belongs_to :facility

  # Facility.find_each {|f|f.update_facility_index()}
  def update_from_facility
    facility = self.facility
    # 都府県は除く
    prefecture_name = facility.prefecture_name.try(:gsub, /(都|府|県)$/, '')
    # region と prefecture は加重する。
    self.content = FacilityIndex.normalize("#{facility.name} #{facility.kana} #{facility.pr} #{facility.description} #{([facility.prefecture.region.name]*7).join(' ')} #{([prefecture_name]*5).join(' ')}#{facility.address} #{facility.search_keyword} #{facility.features.map(&:name).join(' ')} #{facility.tag_list.join(' ')}")
    self.save!
    self
  end

  class << self

    def normalize(text)
      # UTF8
      # 半角カタカナ => 全角カタカナ
      # MIMEはデコードしない
      # 全角アルファベット、全角スペースを半角に
      #
      # ひらがな、カタカナは変換すると MeCab トークナイザが
      # 正しく動かないのでそのままにする。
      text = NKF.nkf('-WwXm0Z1', text).gsub(/[\r、。・()「」【】!?]/, '')
      mecab = MeCab::Tagger.new
      node = mecab.parseToNode(text)
      s = ''
      while node
        features = node.feature.force_encoding('UTF-8').split(/,/)
        unless %w[助詞 助動詞].include?(features[0])
          # 原形を使う。
          word = features[6]
          # 原形がなければ、表層形を使う。
          word = node.surface.force_encoding('UTF-8') if word == '*'
          unless stop_word?(word)
            s += word + ' '
          end
        end
        node = node.next
      end
      # Mecab の後にカタカナをひらがなに変換する
      NKF.nkf('-Wwh1', s)
    end

    def stop_word?(word)
      # 下の select_stop_word でひっかかったワード
      return true if ["", "ある", "市", "OK", "いる", "施設", "する", "-", "お", "れる", "できる"].include?(word)
      false
    end

    # 全件の 50% にでてくるものは MySQL の全文検索でひっかからないので、
    # そのようなワードをストップワードにする。
    def select_stop_word()
      hash = Hash.new(0)
      FacilityIndex.find_each do |x|
        x.content.split(/ /).uniq.each do |word|
          hash[word] += 1
        end
      end
      half = FacilityIndex.count / 2
      stop_words = hash.select do |k, v|
        v > half
      end
      puts stop_words
      stop_words.map {|k, v| k}
    end
  end
end

これで、全文検索ができました。 ある程度あいまいな検索ができるようになった上に、検索も速くなりました。

最後に、弊社ではシステムエンジニア、プログラマ、インフラエンジニアなどを募集しています。 おきがるにお問い合わせください。

>View Comments          このページの上へ戻る

Rails での MySQL マスタースレーブ構成

平成24年5月7日(月) 17時22分51秒
区分
Rails
報告者:
tahara

いこーよ の GW の負荷対策として MySQL のレプリケーションを使いマスタースレーブ構成にしてみました。

一番悩んだのがマスタースレーブ構成のためにどのライブリを使うか。 次のような理由から seamless_database_poolフォークして使うことにしました。

  • アプリ起動時にスレーブが落ちていても動く。 ただし、この場合は途中からスレーブが動きだしてもスレーブにはつながらない。
  • アプリ起動中にスレーブが落ちても動く。
  • 途中でスレーブが復帰すればまたスレーブにつながるようになる。
  • マスターをスレーブに含めることも、含めないこともできる。
  • マスター、および各スレーブの接続ウエイト指定ができる。

フォークする理由は次のとおりです。

  • geokit-rails が UnsupportedAdapter 例外を投げることの対策。
  • 毎回セッション使わないようにする。 Rails 3 Slave Databases: Compare Octopus to SDP を参照。
  • デバッグ時にマスターにつないでいるのか、スレーブにつないでいるのかわからないので、接続先をログ出力する。

以下、セットアップ手順を書きていきます。

マスターとスレーブ両方にレプリケーション用のアカウントを作成します。

mysql -uroot

GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO repl@'%' IDENTIFIED BY 'password';

AWS のセキュリティグループがあるので repl@'%' でよし。

マスターの my.cnf を編集します。

sudo vi /etc/mysql/my.cnf

[mysqld]
server-id              = 10
log_bin                = /var/log/mysql/mysql-bin.log

で MySQL を再起動。 sudo service mysql restart

スレーブの my.cnf を編集します。

sudo vi /etc/mysql/my.cnf

server-id               = 11
log_bin                 = /var/log/mysql/mysql-bin.log
relay_log               = /var/log/mysql/mysql-relay-bin.log
log_slave_updates       = 1
read_only               = 1
slave_load_tmpdir       = /var/tmp

slave_load_tmpdir はマシンを再起動してもファイルが消えないディレクトリにしておかないと問題があるようです。

で MySQL を再起動。 sudo service mysql restart

スレーブで次のようにしてマスターを指定します。

mysql -uroot

CHANGE MASTER TO MASTER_HOST='ec2-123-123-123-123.ap-northeast-1.compute.amazonaws.com', MASTER_USER='repl', MASTER_PASSWORD='password';
show slave status\G

マスターからデータをスレーブに投入します。

mysqldump -uroot --single-transaction --all-databases --master-data=1 | ssh -C deployer@ec2-54-248-109-31.ap-northeast-1.compute.amazonaws.com mysql -uroot

スレーブでレプリケーションを開始します。

mysql -uroot

start slave;
show slave status\G

Rails で seamless_database_pool を使うようにします。

application_controller.rb でデフォルトでマスターを使うようにします。

class ApplicationController < ActionController::Base
  include SeamlessDatabasePool::ControllerFilter
  use_database_pool :all => :master # 個別にスレーブを使うように指定する。

スレーブを参照したいコントローラで次のように書きます。 アクション毎に指定できます。

class FacilitiesController < ApplicationController
  use_database_pool [:index, :show, :map_xhr] => :persistent

application_controller.rb で全てスレーブ参照にしても更新系の SQL はちゃんとマスターにいくのですが、 レプリケーションの遅延があった場合よくあるアップデートして show にリダイレクで古い情報を表示してしまう問題と、 ごく一部のアクションが全クエリーのほとんどをしめるという理由からデフォルトマスターで、個々にスレーブを使うように指定する方針にしました。

おかげさまで、今回の GW の瞬間的なピークは New Relic で 3,092rpm を記録し、 Google Analytics リアルタイムのアクディブユーザで 1,900 を越えました。 AWS のオートスケールでアプリサーバも1台から5台まで自動的にスケールしました。

いこーよ のご利用ありがとうございました。

最後に、弊社ではシステムエンジニア、プログラマ、インフラエンジニアなどを募集しています。 おきがるにお問い合わせください。

>View Comments          このページの上へ戻る

Amazon Web Service にいこーよ

平成24年4月16日(月) 16時20分38秒
区分
AWS
報告者:
tahara

いこーよ を Amazon Web Service (AWS) に移行しました。 御蔭様でいこーよのアクセス数は伸びてきており、これで3回目の引越しです。

移行に際して一番悩んだのがオートスケールを前提としたデプロイ、監視でした。 オートスケールの設定をしている場合、インスタンスは負荷に応じて起動したり削除されたりします。 当然 IP アドレスも都度変っていきます。

  • cap production deploy:migrations する時に生きているサーバはどれ?
  • オートスケールでインスタンスが起動した時、ちゃんと最新のソースで起動するにはどうする?
  • munin でリソース監視したいけど、監視対象サーバはどれ?

といったところが、全くわかりませんでした。 世のオートスケール利用者の方々はどうされているんでしょうか。

いろいろ試行錯誤し、結局のところ次のようにしました。

  • unicorn を passenger に変更
    • unicorn に問題があったわけではありません。
  • cap は DB サーバにのみ行う。
  • passenger が動くアプリサーバは DB サーバのデプロイディレクトリを nfs マウント。

これでデプロイは DB サーバに対してだけ行えばよく、アプリの再起動も DB サーバで touch tmp/restart.txt を行えば、それを nfs マウントしているアプ リサーバの passenger がみんな再起動する、という仕組みです。

munin の方はうまい方法を思い付かなかったのでシェルスクリプトを書きました。 DB サーバは Elastic IP を使っているのでホスト名が固定です。 ec2-describe-instances の出力から DB サーバを除いて起動しているホスト名を取得し、 /etc/munin/munin.conf を都度生成します。

#!/bin/sh

export JAVA_HOME=/usr/lib/jvm/java-7-openjdk-i386
export EC2_HOME=/opt/ec2-api-tools
export PATH=$EC2_HOME/bin:$PATH
export EC2_PRIVATE_KEY=/foo/pk-KKKKKKKKKKKK.pem
export EC2_CERT=/foo/cert-XXXXXXXXXXXXXXX.pem
export EC2_URL=https://ec2.ap-northeast-1.amazonaws.com

ec2-describe-instances --show-empty-fields | \
  sed -n '/^INSTANCE.*running/p' | \
  sed -n '/54-248-122-232/!p' | \
  awk '{ print $4, $5; }' | \
  sed -e '=' | \
  sed -e 'N;s/\(.*\)\n\(.*\) \(.*\)/[iko-yo.net;ap\1]\n    address \2\n    use_node_name yes\n/' > /tmp/aws-munin.conf.tmp

sed -e '/KOKO/r /tmp/aws-munin.conf.tmp' -e '/KOKO/d' /etc/munin/munin.conf.template > /etc/munin/munin.conf

/etc/munin/munin.conf.template はこんな感じ。

...前略...

# DB サーバ
[iko-yo.net;db.iko-yo.net]
    address db.iko-yo.net

# アプリサーバ
KOKO

...後略...

さて DB サーバ m1.large x 1, アプリサーバ c1.medium x 1 で本番投入してみたのですが、DB サーバが重い。 その夜急遽 DB サーバを c1.xlarge に変更。 c1.xlarge にしたらリソースが余ったで、アプリも DB サーバで動かして c1.medium のアプリサーバ停止。 という構成になりました。

きっと GW か夏休みのピークにはオートスケールの出番が来るかと思います。

ちなみに最近のピーク時のアクセスは nginx のリクエスト数でいうと 90 request/second で New Relic の Throughput でいうと 770 rpm くらいです。 はやく DB の全件 like 検索しているのをなんとかしたいところです。

最後に、弊社ではシステムエンジニア、プログラマ、インフラエンジニアなどを募集しています。 お気軽るにお問い合わせください。

>View Comments          このページの上へ戻る

Rails 3.2.2 へのアップクレード (ssl_allowed が allowed されない件)

平成24年3月21日(水) 16時07分50秒
区分
Rails
報告者:
tahara

こんにちは tahara です。

先日 いこーよ を Rails 3.1.0 から Rails 3.2.2 にアップグレードしました。 今日はその模様を報告したいと思います。

まず Gemfile で Rails のバージョン指定を 3.2.2 にし bundle update しました。

gem 'rails', '3.2.2'

Asset Pipeline は使っていなので、 jquery-rails-2.0.1/vendor/assets/javascripts/jquery_ujs.js を public/javascripts にコピー。

config/environments/development.rb に次を追加。 開発環境で遅いクエリーは自動的に explain してくれるのはいいですね。 log/development.log に出力されます。

  # Raise exception on mass assignment protection for Active Record models
  config.active_record.mass_assignment_sanitizer = :strict

  # Log the query plan for queries taking more than this (works
  # with SQLite, MySQL, and PostgreSQL)
  config.active_record.auto_explain_threshold_in_seconds = 0.5

で、動かしてみたのですが ssl_requirement でエラーになりました。 どうやら bartt-ssl_requirement を使うのがよさそう。 Gemfile を次のように書き替えて bundle install したらうまく動きました。

gem 'bartt-ssl_requirement', '~>1.4.0', :require => 'ssl_requirement'

と思ったのですが、ssl_allowed しているアクションが http から https にリ ダイレクトされてしまう現象に遭遇しました。 もともとの ssl_requirement では ssl_allowed が ssl_required より優先されていたのに、 bartt-ssl_requirement では ssl_required の方が優先されるようになっていました。 ここは github の慣例にならって fork し、ssl_allowed が優先されるようにしました。 修正は一行です。

という感じで いこーよ をRails 3.2.2 にアップグレードできました。

弊社ではエンジニアを募集しています。 詳細はこちらを御覧ください。

>View Comments          このページの上へ戻る

CoffeeScript を使って Titanium でアプリを作る (Common Lisp バージョン)

平成24年2月27日(月) 15時10分07秒
区分
Titanium
報告者:
tahara

こんにちは、tahara です。

CoffeeScript を使って Titanium でアプリを作るにはいくつか方法があるようです。

弊社ではこのような場合 Common Lisp を使います(個人的に)。

コードは下記のとおり。 もし動かしてみたいという方がいらっしゃるようでしたら、 defparameter しているものを環境に合わせて変更してください。 あと Quicklisp でインストールできるものの他に https://github.com/quek/info.read-eval-print.series-ext も必要になります。

エラーがあれば repl に表示されます。 一度 Titanium Studio から Emulator でアプリを起動していれば、 CoffeeScript の保存で自動的に Eumlator での実行まで行います。 快適です。

;;;; CoffeeScript を使って Titanium で Android アプリを作る
;;;;
;;;; 参考にしたサイト
;;;; http://a-h.parfe.jp/einfach/archives/2011/0106235955.html

(eval-when (:compile-toplevel :load-toplevel :execute)
  (require :alexandria)
  (require :bordeaux-threads)
  (require :cl-ppcre)
  (require :info.read-eval-print.series-ext))

(info.read-eval-print.series-ext:sdefpackage
 :compile-coffee
 (:use :cl))

(in-package :compile-coffee)

(defparameter *builder.py*
  "~/.titanium/mobilesdk/linux/1.8.1/android/builder.py"
  "Titanium のビルドコマンド
iPhone で動かす場合は s/android/iphone/ でいいかもしれない")

(defparameter *project-dir*
  "~/Titanium\\ Studio\\ Workspace/outing-app"
  "Titanium のプロジェクトディレクトリ")

(defparameter *android-sdk*
  "~/local/opt/android-sdk-linux"
  "Android SDK のディレクトリ")

(defparameter *interval* 0.3
  "ファイルの変更監視間隔")

(defvar *compile-titanium-process* nil)

(defun escape-sh-arg (arg)
  (ppcre:regex-replace-all " " arg "\\ "))

(defun sh-async (control-string &rest format-arguments)
  (let* ((command (apply #'format nil control-string format-arguments))
         (process (progn (format *terminal-io* "~&~a" command)
                         (sb-ext:run-program "/bin/sh"
                                             (list "-c" command)
                                             :wait nil
                                             :output :stream
                                             :error :stream)))
         (streams (list (sb-ext:process-output process)
                        (sb-ext:process-error process)))
         (threads (labels ((cat (stream)
                             (bordeaux-threads:make-thread
                              (lambda ()
                                (collect-stream
                                 *terminal-io*
                                 (delete #\Return
                                         (scan-stream stream #'read-line))
                                 #'write-line)))))
                    (collect (cat (scan 'list streams))))))
    (bordeaux-threads:make-thread
     (lambda ()
       (let ((exit-code (sb-ext:process-exit-code (sb-ext:process-wait process))))
         (collect-ignore (progn
                           (bordeaux-threads:join-thread (scan 'list threads))
                           (close (scan 'list streams))))
         (if (zerop exit-code)
             (prog1 t (format *terminal-io* "~&ok"))
             nil))))
    process))

(defun sh (control-string &rest format-arguments)
  (let* ((command (apply #'format nil control-string format-arguments))
         (process (progn (format *terminal-io* "~&~a" command)
                         (sb-ext:run-program "/bin/sh"
                                             (list "-c" command)
                                             :wait nil
                                             :output :stream
                                             :error :stream)))
         (streams (list (sb-ext:process-output process)
                        (sb-ext:process-error process)))
         (threads (labels ((cat (stream)
                             (bordeaux-threads:make-thread
                              (lambda ()
                                (collect-stream
                                 *terminal-io*
                                 (delete #\Return
                                         (scan-stream stream #'read-line))
                                 #'write-line)))))
                    (collect (cat (scan 'list streams)))))
         (exit-code (sb-ext:process-exit-code (sb-ext:process-wait process))))
    (collect-ignore (progn
                      (bordeaux-threads:join-thread (scan 'list threads))
                      (close (scan 'list streams))))
    (if (zerop exit-code)
        (prog1 t (format *terminal-io* "~&ok"))
        nil)))

(defun compile-titanium ()
  (when *compile-titanium-process*
    (sb-ext:process-kill *compile-titanium-process*
                         sb-posix:sigterm)
    (setf *compile-titanium-process* nil))
  (setf *compile-titanium-process*
        (sh-async "~a run ~a ~a" *builder.py* *project-dir* *android-sdk*)))

(defun compile-coffee (file)
  (when (sh "coffee -o ~a -c ~a"
            (escape-sh-arg (directory-namestring file))
            (escape-sh-arg (namestring file)))
    (compile-titanium)))

(defun run ()
  (sb-cltl2:compiler-let ((series::*suppress-series-warnings* t))
    (collect-ignore
     (compile-coffee
      (choose-if (complement
                  (lambda (x)
                    (alexandria:starts-with-subseq ".#" (file-namestring x))))
                 (scan-file-change
                  (format nil "~a/**/*.coffee" *project-dir*)
                  :interval *interval*))))))

;; 実行
;; (bordeaux-threads:make-thread #'run :name "compile-coffee")

>View Comments          このページの上へ戻る

WordPress で同じ URL のまま User-Agent によって別のページを表示する

平成24年2月21日(火) 15時30分47秒
区分
WordPress
報告者:
tahara

こんにちは tahara です。

今回は WordPress です。

http://example.com/foo がリクエストされた時、クライアントがスマホだったら、 リダイレクトすることなく、同じ URL のまま、スマホ用のページを表示するには、 どうしたらいいか? 「、」が多い。

これがリダクレクトして別 URL になっても OK だよ、なら話は簡単だったのですが、 WordPress のパーマリンクの関連でかなり悩みました。

mod_rewrite で /foo が来たら /smartphon/foo に書き替えればいいや、と思っ ていたのですが、WordPress のパーマリンクは /smartphon/foo に書き替えて も最初のブラウザの要求の /foo をもとにページを返してきます。

で、結局は自分自身に proxy して解決しました。 しかし、自分自身への proxy は http://httpd.apache.org/docs/current/mod/mod_rewrite.html によると

doesn't make sense, not supported

らしいです。 とりあえず思ったとおりに動いてはいますが、他にいい方法はないものでしょうか?

.htaccess

<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /

# for smartphone
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_URI} !^/smartphone.*$
RewriteCond %{REQUEST_URI} !^/index.php
RewriteCond %{HTTP_USER_AGENT} (iPhone|iPod|Android|BlackBerry) [NC]
RewriteCond %{HTTP_USER_AGENT} !iPad [NC]
RewriteCond %{HTTP_COOKIE} !wptouch_switch_toggle=normal
RewriteRule .* - [E=SMART_PHONE_P:T]

RewriteCond %{ENV:SMART_PHONE_P} =T
RewriteRule ^$ http://example.com/smartphone [P,L]

RewriteCond %{ENV:SMART_PHONE_P} =T
RewriteRule ^foo$ http://example.com/smartphone/foo [P,L]

RewriteCond %{ENV:SMART_PHONE_P} =T
RewriteRule ^foo/bar$ http://example.com/smartphone/bar [P,L]

# for WordPress permlink
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]

</IfModule>

>View Comments          このページの上へ戻る

アクトインディ技術部隊報告書を Elephant から Rucksack に移行

平成24年2月6日(月) 17時09分17秒
区分
Common Lisp
報告者:
tahara

こんにちは、tahara です。

前々回のRails3 への移行に続いて移行ネタです。

本ブログ「アクトインディ技術部隊報告書」は Common Lisp (SBCL) + Hunchentoot + Elephant で動いていました。 しかし、1年2ヶ月ぶりのエントリを書こうとした時、新規エントリの登録ができなくなっていました。 Elephant をアップグレードしたのが原因だったのですが、 新しいバージョンにマイグレーションするのがめんどうだったので、 Rucksack に移行してしまいました。

Rucksack は同時に一トランザクションしか時行できなかったのですが、 そのへんは適当に改造して同時に複数トランザクション実行できるようにしました。 そのソースはこちら https://github.com/quek/rucksack です。

Hunchentoot と Rucksack の連携は次のように行います。 Hunchentoot のリダイレクによる throw と Rucksack のトランザクション制御を 両立するためにすこしめんどうなことになってまいます。

(defmethod hunchentoot::acceptor-dispatch-request ((self my-acceptor) request)
  (let (response
        (handler-done t))
    (rucksack:with-transaction ()
      ;; hunchentoot のリダイレクトのハンドリング
      (catch 'hunchentoot::handler-done
        (setf response (call-next-method))
        (setf handler-done nil)))
    (if handler-done
        (throw 'hunchentoot::handler-done nil)
        response)))

というわけで「アクトインディ技術部隊報告書」復活しましたので、 今後ともよろしくお願いいたします。

>View Comments          このページの上へ戻る

デプロイ時の Unicorn リスタートが失敗する件

平成24年1月30日(月) 16時14分22秒
区分
Rails
報告者:
tahara

こんにちは tahara です。

デプロイ時の Unicorn リスタートがときどき失敗して悩んでいました。 幸い本番環境では発生せず、ステージング環境と開発環境で発生していました。

リスタートは unicorn-4.1.1/examples/init.sh の upgrade を使っています。 upgrade は sig USR2 && sleep 2 && sig 0 && oldsig QUIT とい一連の流れになっています。 調べてみると sig USR2 で新しい PID ファイルが作成されるのですが、 sig 0 の時点でまだそれができていなくて失敗していました。 本番環境はサーバの性能が高いので sleep 2 で間に合っていましたが、 ステージング環境等では間に合わなかったんですね。

そこで、次のように sig 0 が成功するまで一定期間リトライするようにしました。

upgrade)
        echo -n "sig USR2"
        if sig USR2
        then
            sleep 1
            n=$UPGRADE_TIMEOUT
            while ! sig 0 && test $n -ge 0
            do
                printf '.' && sleep 1 && n=$(( $n - 1 ))
            done
            echo
            if test $n -lt 0 && ! sig 0
            then
                echo >&2 "sig SUR2 failed!"
                exit 1
            fi

            echo -n "oldsig QUIT"
            if 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
                echo "ok"
                exit 0
            fi
        fi
        echo
        echo >&2 "Couldn't upgrade, starting '$CMD' instead"
        $CMD
        ;;

これでうまくリスタートできるようになりました。

ついでに、何かの拍子に古い方のプロセスに QUIT を送れなかった時の対策として、 Unicorn の設定ファイルで QUIT を送るようにしました。

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!

  # oldsig QUIT
  old_pid = "#{server.config[:pid]}.oldbin"
  if File.exists?(old_pid) && server.pid != old_pid
    begin
      Process.kill("QUIT", File.read(old_pid).to_i)
    rescue Errno::ENOENT, Errno::ESRCH
    end
  end
end

これなら init.sh の方では sig USR2 だけで、後は Unicorn に古いプロセスの QUIT を まかせればいいかと思いましたが、 そうすると新しいプロセスのワーカが動き出すまで古いワーカが処理を行ってしまい、 DB のマイグレーションを行った時なんかは悲しいことになりそうです。

上記のように init.sh で古いプロセスを QUIT する場合は、 新しいプロセスのワーカが動き出すまでリクエストは待たされるので、 まだこちらの方がいいんじゃないかしらん、というところです。

>View Comments          このページの上へ戻る

Rails3 への移行

平成24年1月24日(火) 23時01分53秒
区分
Rails
報告者:
tahara

こんにちは、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 環境ではどんどんメモリをくっていきます。 これもなぜでしょう。。。

>View Comments          このページの上へ戻る

なんでもbrowse-url-at-point

平成22年11月11日(木) 14時46分55秒
区分
Emacs
報告者:
chiba

こんにちは、Chibaです!
ネタ切れなので、ちょっとした自前便利Emacs lispの紹介です!
browse-url-at-pointとは、ポイント位置にURLの文字があれば、それをブラウザで開くというものです。 このbrowse-url-at-pointで使われている、thing-at-pointという関数が肝なのですが、この関数は、ポイント(カーソル)がある場所のオブジェクトを取得できるというEmacsの関数です。
この関数なのですが使い方次第では非常に便利です。
browse-url-at-pointでは、URLが決め打ちですが、thing-at-point(ポイント位置のオブジェクトを取得)とbrowse-url(ブラウザで開く)を組み合せることによって似たようなものを簡単に作成することができます。
例えば、ポイント位置のRedmineのチケット(#1234というような形式)を開きたい場合は、色々手抜きですが、

(defun show-ticket-at-point ()
  (interactive)
  (browse-url (format "https://example.com/issues/show/%s"
                      (thing-at-point 'word))))
のように書けると思います。
簡単な割には便利ですので、エディタ上の情報からブラウザで何か開きたいと思った時には、工夫して色々作成してみてはいかがでしょう。

>View Comments          このページの上へ戻る

技師部隊からの
お知らせ

【求人】エンジニア募集 しています。

本頁の来客数
十六万千二百名

メンバー一覧

アクトインディ技師部隊員名簿

アクトインディ技師部元隊員

アクトインディへ

投稿する

カテゴリー

アクトインディ

aaaa