SlideShare a Scribd company logo
実録!
Railsのはまりポイント10選
        2012/03/26
    第2回 渋谷Edge Rails勉強会
         藤田 武雄




                          Copyright © Drecom Co., Ltd.
自己紹介
• 藤田 武雄
• 株式会社ドリコム
• ソーシャルゲーム事業本部
• アプリケーションエンジニア
 • 最近は主に社内ライブラリの開発を
  担当


               Copyright © Drecom Co., Ltd.
今日の内容
• Railsを使用したソーシャルゲームの開
 発時にはまったポイントをご紹介

• 実際に社内であった事例です
• ほとんどRails3採用後の話


                    Copyright © Drecom Co., Ltd.
Case1
and/or



         Copyright © Drecom Co., Ltd.
はまり例
a = b or c
 • こういう意味にしたかった
    a = (b or c)
 • 実際の動きは
    (a = b) or c
   • bの値をaに代入し、それが偽であればc
     を評価する

                    Copyright © Drecom Co., Ltd.
演算子の優先順位
• and/orは優先順位が一番低い
• 対策1: 括弧をつける
  a = (b or c)

• 対策2: &&/¦¦を使う
  a = b || c



                     Copyright © Drecom Co., Ltd.
ちなみにRails本体は
• Contributing to Ruby on Rails
• 5.3 Follow the Coding Conventionsに
  は

 • Prefer &&/¦¦ over and/or
• と書かれている
• http://guides.rubyonrails.org/
  contributing_to_ruby_on_rails.html

                               Copyright © Drecom Co., Ltd.
Case2
scope



        Copyright © Drecom Co., Ltd.
はまり例
scope :started, where(‘start_at <= ?’, Time.now)

 • どういうことが起こりうるか
  • 開始時刻になってもキャンペーンがス
      タートしない

   • 設定した時刻にバナーが切り替わら
      ない



                                         Copyright © Drecom Co., Ltd.
なぜ?
• クラスがロードされた時点でwhereの
 中にあるTime.nowの評価が行われる

• 結果、アプリケーションサーバを起動
 した時間で固定されてしまう




                 Copyright © Drecom Co., Ltd.
lambdaを渡す
scope :started, lambda { where('start_at
<= ?', Time.now) }
 • lambdaの中身は都度評価される
 • edgeではwhereを直接書くやり方は
   deprecatedとなった

 • https://github.com/rails/rails/commit/
   0a12a5f8169685915cbb7bf4d0a7bb48
   2f7f2fd2


                                   Copyright © Drecom Co., Ltd.
Case3
クラスのインスタンス変数



            Copyright © Drecom Co., Ltd.
インスタンス変数の遅延
初期化
@foo ||= bar
 • 初期化の手間を省くためによく使われ
   る

 • barの実行結果をキャッシュする効果も
 • クラスのインスタンス変数では注意が
   必要



                 Copyright © Drecom Co., Ltd.
よくない例
class Baz
 def self.foo
   @foo ||= bar
 end
end
 • barがDBから取得した値だとDBを更新
   しても返り値が変化しない!


                   Copyright © Drecom Co., Ltd.
起こったこと
• データを追加したのに反映されない
• よくわからんからunicornを再起動
• うまくいった

• そんなこともありました

                  Copyright © Drecom Co., Ltd.
なぜ?
• 通常はオブジェクトのインスタンスの
 寿命は1リクエストの間だけ

• 一方、クラス自体はリクエストを返し
 た後も生き続ける

• クラスのインスタンス変数に入れる
  とずっと保持される



                 Copyright © Drecom Co., Ltd.
対策
• むやみにクラスのインスタンス変数に
 入れない

• 意味をわかっていてやるのであればOK



                Copyright © Drecom Co., Ltd.
Case4
textあふれ



          Copyright © Drecom Co., Ltd.
データ破損
• textカラムにJSONを格納していた
• ある日突然データが壊れた!




                  Copyright © Drecom Co., Ltd.
原因
• mysqlのtextは65535バイトまで
• それ以上のものを保存すると後ろが切
 れるが保存自体エラーにはならない

• 読み込むとJSONとしては壊れているの
 でパースエラー




                   Copyright © Drecom Co., Ltd.
対策
• その1: validates_length_ofを設定
• その2: 文字数を減らす
• その3: mediumtext(16MBまで)などに
 変更する

• どれぐらい増えるか見積もりしておけば
 はみ出ないように設計見直しできるはず

• AR::Store(YAML)にも気をつけよう
                       Copyright © Drecom Co., Ltd.
Case5
index名長さ制限



         Copyright © Drecom Co., Ltd.
index名の長さ
• 複数カラムにindexを張った時に長さ制
  限に引っかかった

• mysqlは64バイトまでしか使えない
index_テーブル名_on_カラム名_and_カラ
ム名_…




                     Copyright © Drecom Co., Ltd.
index名を短くする
• デフォルトを変更するモンキーパッチ
  をあてた

• テーブルごとにuniqueになればよいの
  でテーブル名不要
カラム名_and_カラム名_...




                    Copyright © Drecom Co., Ltd.
パッチ
module ActiveRecord
 module ConnectionAdapters
  module SchemaStatements
   def index_name(table_name, options)
    if Hash === options
      if options[:column]
        "#{Array.wrap(options[:column]) * '_and_'}"
      elsif options[:name]
        options[:name]
      else
        raise ArgumentError, "You must specify the index name"
      end
    else
      index_name(table_name, :column => options)
    end
   end
  end
 end
end
                                                           Copyright © Drecom Co., Ltd.
Case6
tinyint



          Copyright © Drecom Co., Ltd.
tinyintの扱いの罠
• アプリリリース前にDBAの協力の下、
 テーブル構成をチューニングした

 • int → tinyint
• するとアプリの動作がおかしくなった
• DBに入っている数値は変わってないの
 に取ってきた値が全然違う!



                 Copyright © Drecom Co., Ltd.
原因と対策
• ActiveRecordはtinyintのカラムを
 booleanとして扱う

• 対策: boolean以外ではtinyintを使わな
 い(smallintにするなど)




                        Copyright © Drecom Co., Ltd.
Case7
参照分散時のミス



       Copyright © Drecom Co., Ltd.
data_fabric
• 参照分散のためのplugin
• 利用者数が増え、masterだけでさばききれな
 くなったため導入

• action単位でaround_filterをかけて振り分け
 • slaveの情報を元にmaster側を更新しない
   ようにaction単位にした

• アクセスの多いマイページなどはslaveへ逃が
 す


                        Copyright © Drecom Co., Ltd.
事故発生!
• あるときレプリケーション遅延が起
 こってslaveの古い情報を元にmasterに
 更新がかかってしまった

• データ不整合発生!
• そんな実装はしていない認識だった
• 調査するとマイページのviewで呼び出
 しているhelperからupdateが…

                    Copyright © Drecom Co., Ltd.
対策
• helperからupdateしない!
• 参照振り分け用around_filterの外側に
 before_filterを設定し、update処理部
 分を移した




                     Copyright © Drecom Co., Ltd.
Case8
ランキング集計



          Copyright © Drecom Co., Ltd.
ランキング集計
• イベントランキングを毎時集計
• 一人ずつselectして順位をupdateする実装
 になっていた

• 参加ユーザ数が増えると集計時間がどんど
 ん長くなる

• 集計時間が30分を超えた時点で状況を検知
• 前月までは大丈夫だったのに…
                     Copyright © Drecom Co., Ltd.
対応前のサーバ負荷
• DBサーバのCPU使用率




                 Copyright © Drecom Co., Ltd.
対策
• 方針としてはtransaction数を減らす
• 裏で新規テーブルを作成して、1000件
  ずつbulk insert

• 全部入ったらrenameで入れ替え
• bulk insertにはactiverecord-import
  を使用



                            Copyright © Drecom Co., Ltd.
コード例
CREATE TABLE IF NOT EXISTS
tmp_scores LIKE scores;
TRUNCATE TABLE tmp_scores;
class TmpScore < ActiveRecord::Base; end
TmpScore.import([:user_id, :value, :rank],
scores)
RENAME TABLE scores TO scores_old,
tmp_scores TO scores;
DROP TABLE scores_old;
                                 Copyright © Drecom Co., Ltd.
対応後のサーバ負荷




            Copyright © Drecom Co., Ltd.
別の方法
• Redisのsorted setを使う
• リアルタイムにランキングを更新でき
 る

• 数万人規模でも大丈夫
• そのままでは同率順位を表現できない

                 Copyright © Drecom Co., Ltd.
Case9
double submit
 protection


            Copyright © Drecom Co., Ltd.
double submit protection

• 二重送信を検知してくれるplugin
• 二重送信を防ぎたいフォーム(orリンク)
  のあるページでsessionにtokenを保存
  し、パラメータにも同じ物をつける

• 次のページでは両者を付きあわせて、
  session内のtokenを無効にする



                     Copyright © Drecom Co., Ltd.
sessionの罠
• Railsのsessionの扱いではまった
• レスポンスを返すまでsessionの実体が
 更新されない!

• 処理に時間がかかるとすり抜けてしま
 う




                  Copyright © Drecom Co., Ltd.
処理の流れ
user             app          session      memcache
       request         load

                       write

                   ココが問題
   response                             write




                                           Copyright © Drecom Co., Ltd.
対策
• レスポンス返すまで待っていられない
 のでsessionを使うのをやめる

• pluginに手を入れてtokenをRedisに保
 存するように変更




                      Copyright © Drecom Co., Ltd.
Case10
daemon-spawn +
    resque


            Copyright © Drecom Co., Ltd.
daemon-spawn
• rubyプログラムをデーモン化するため
 のライブラリ

• resqueのworkerをデーモン化した時に
 問題が起こった




                   Copyright © Drecom Co., Ltd.
何が起こったか
• rails runnerで起動していた
• redisへのコネクションを張るpluginが
 あった

• forkするときにコネクションがコピー
 されて全workerで共有されてしまい
 redisへの接続が不安定に



                    Copyright © Drecom Co., Ltd.
対策
• rails runnerを使わなくした
• 自前で必要なものをrequireするスクリ
 プトに書き換え




                  Copyright © Drecom Co., Ltd.
もうひとつの問題
• stopした時に処理中のプロセスが落と
 される

• 処理中ステータスの変なデータが残っ
 てしまった




                 Copyright © Drecom Co., Ltd.
原因
• daemon-spawnのstop時のシグナルが
 デフォルトではTERM

• rescueはTERMを受け取ると処理中で
 も即終了してしまう




                    Copyright © Drecom Co., Ltd.
対策
• daemon-spawnの起動スクリプトで明
 示的にQUITを指定した

 • ドキュメントに載ってない
 • 実装を読んでみて指定できることがわ
  かった




                   Copyright © Drecom Co., Ltd.
まとめ
• 社内ではまった例をご紹介しました
• pluginではまったときは実装を追いか
 けることも必要




                  Copyright © Drecom Co., Ltd.
ご清聴ありがとうございました




            Copyright © Drecom Co., Ltd.

More Related Content

実録!Railsのはまりポイント10選

Editor's Notes