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

  1. \n
  2. \n
  3. \n
  4. \n
  5. \n
  6. \n
  7. \n
  8. \n
  9. \n
  10. \n
  11. \n
  12. \n
  13. \n
  14. \n
  15. \n
  16. \n
  17. \n
  18. \n
  19. \n
  20. \n
  21. \n
  22. \n
  23. \n
  24. \n
  25. \n
  26. \n
  27. \n
  28. \n
  29. \n
  30. \n
  31. \n
  32. \n
  33. \n
  34. \n
  35. \n
  36. \n
  37. \n
  38. \n
  39. \n
  40. \n
  41. \n
  42. \n
  43. \n
  44. \n
  45. \n
  46. \n
  47. \n
  48. \n
  49. \n
  50. \n
  51. \n
  52. \n
  53. \n