redis-mutexで排他制御

例えばサーバが2台あったとして同じ時間に全く同じ内容のcronスクリプトが実行されるとする、その処理がDBのバッチ処理など大きなコストを必要とするなら両方のサーバで実行されるのは無駄でしかないし場合によってはデータの不整合なども発生するかもしれない。そんな時はredis-mutexを使って排他制御を行うことができる。

なにはともあれredisをインストール ※Macの場合のインストール方法です

$ brew install redis

redis-mutexというgemをインストール。

$ gem install redis-mutex
Successfully installed redis-mutex-2.1.1

排他制御を行いたい部分をRedis::Mutex.with_lockブロックで囲むだけでOK、簡単ですね。

#!/usr/bin/env ruby

require 'redis-mutex'

def main
  Redis::Classy.db = Redis.new(host:"localhost", port: "6379")

  Redis::Mutex.with_lock(:your_lock_name, block: 0) do
    sleep 5
    puts "Do exclusively!"
  end
rescue
  puts 'failed to acquire lock!'
end

if __FILE__ == $0
  main()
end

このスクリプトをコンソールを2つ開いてそれぞれ実行すると最初に実行したほうだけwith_lockブロックで囲んだコードが実行されます。

# 1番目に実行
$ ./redis-mutex-example.rb
Do exclusively!

# 2番目に実行
$ ./redis-mutex-example.rb
failed to acquire lock!

実装にはredisのSETNXとSETEXを使っていると思われる。redisはrebuld.fmでredis-mutexの作者の江島さんが言ってたようにサーバの垣根をも超えたスーパーグローバル変数みたいな感じの使い方ができるし、リアルタイムランキングの実装にも向いてるし、RDBMSではちょっとやりにくい部分をうまく補ってくれるツールな感じ。AWSのElasticacheでもサポートされて今後どんどん伸びていきそうなKVSですね。

事前にprecompileしたassetsをS3に配置してdeployを高速化する方法

Railsで作られているサービスとおもわれるbasecampやgithubはassetsファイルをアプリケーションサーバとは別のCDN(akamaiやCloudFront)サービスから配信している。

CDNに配置する事のメリットとして世界中のEdgeサーバからassetsを配信できるのでどこからアクセスしてもページロードが速い事が一番大きいが、
副次的な効果として事前にassets:precompileすることが可能なためcapistranoでのdeploy時にprecompileしない戦略を取ることによって大幅にdeploy時間を短縮できるメリットがある。
(assetsをgit等ににコミットしてしまってprecompileせずにdeployする戦略もあるがコミットログが汚くなるため避けたい)

今回はasset_syncというgemを使ってS3にassetsを配置しつつcapistranoでdeployする方法を記述します。
このgemはassets:precompileした時に自動的にassetsファイルを指定のストレージに配置してくれる優れものです。

AWSに登録

詳細は省くがaccess_key_idとsecret_access_keyを取得しておく

assetsを配置するためのS3のbucketを作成します

今回は下記のbucketを作成します.

Bucket Name: hakutoitoi-assets

GemFileにassset_syncを追加

gem 'asset_sync'
$ bundle install

config/environments/production.rbを編集

# デフォルト以外のパスにassetsを配置したい場合は指定します(デフォルトは/assets)
# config.assets.prefix = "/path/to/assets"

# 事前に作成しておいたS3のbucketのURLを指定します
config.action_controller.asset_host = "//hakutoitoi-assets.s3.amazonaws.com"

assets_syncの設定ファイルを作成する

if defined?(AssetSync)
  AssetSync.configure do |config|
    config.fog_provider = 'AWS'
    config.aws_access_key_id = "XXXXXXXXXXXXX"
    config.aws_secret_access_key = "XXXXXXXXXXXXX"
    config.fog_directory = "hakutoitoi-assets"

    # アップロードするS3エンドポイントを明示的に指定
    config.fog_region = 'ap-northeast-1'
  end
end

開発マシンでprecompileする

# precompileすると自動的にS3にassetsがsyncされます
$ bundle exec rake assets:precompile RAILS_ENV=production

できあがったassetsからmanifest.ymlだけをコミットする

このファイルがdeployされたソースコードにないとrailsはassetsのパスを解決することができずassetsを利用しているページは500エラーになってしまいます

$ git commit public/assets/manifest.yml
$ git push origin master

追記:
Rails3の場合はmanifest.ymlだがRails4からはmanifest-md5hash.jsonが生成されるようになるのでこちらをコミットする必要があるので注意。詳しくはEdge RailsGuideを参照。
http://edgeguides.rubyonrails.org/asset_pipeline.html#precompiling-assets

$ git commit public/assets/manifest-51c7803d58f0eceb1bfb69a398259469.json

Capfileでdeploy/assetsの読み込みをしている場合はコメントアウトする

# こうしないとdeploy時にprecompileしてしまいます
# load 'deploy/assets'

deployにかかる時間の比較(AWS smallインスタンスの場合)

# precompileをするdeploy
$ time cap deploy --quiet
real    1m48.629s
user    0m1.269s
sys     0m0.697s

# precompileをしないdeploy
$ time cap deploy --quiet
real    0m17.132s
user    0m0.942s
sys     0m0.541s

active_adminにロック機構を導入する

active_adminをインターネットに公開する場合にアカウントロックを設けたいなぁと思った。

いろいろ調べていたらdeviceでアカウントロックを後から追加するの記事がとても参考になりました。
要約するとactive_adminはdeviceという認証フレームワークを使って作らているのでdevice側にあるアカウントロック機構を使えば簡単に実装できるよってことだと思います。

手順は本当に簡単でした。

1. models/admin_user.rbにロック機構(lockableモジュール)を結びつける宣言をする

class AdminUser < ActiveRecord::Base
  devise :database_authenticatable, 
         :recoverable, :rememberable, :trackable, :validatable, 
         :lockable # <= コレ追加

  # Setup accessible (or protected) attributes for your model
  attr_accessible :email, :password, :password_confirmation, :remember_me
  # attr_accessible :title, :body
end

2. config/initializer/device.rbにお好みのアカウントロック仕様を設定する

ログインに5回失敗したら1分間ロックするという仕様の例。

-  # config.unlock_strategy = :both
+  config.unlock_strategy = :time
 
# Number of authentication tries before locking an account if lock_strategy
# is failed attempts.
-  # config.maximum_attempts = 20
+  config.maximum_attempts = 5

# Time interval to unlock the account if :time is enabled as unlock_strategy.
-  # config.unlock_in = 1.hour
+  config.unlock_in = 1.minute

3. model AdminUserにロック用のスキーマを追加するマイグレーションを作る

class AddLockableColumnsAdminUser < ActiveRecord::Migration
  def change
    add_column :admin_users, :failed_attempts, :integer, :default => 0
    add_column :admin_users, :unlock_token, :string
    add_column :admin_users, :locked_at, :datetime
  end
end

4. マイグレーション実行

$ rake db:migrate

以上でOK。

最初はactive_adminレコードは下記のようになっている。

+-----------------+--------------+-----------+
| failed_attempts | unlock_token | locked_at |
+-----------------+--------------+-----------+
|               0 | NULL         | NULL      |
+-----------------+--------------+-----------+

active_adminのログインに1回失敗するとfailed_attemptsに1加算される。

+-----------------+--------------+-----------+
| failed_attempts | unlock_token | locked_at |
+-----------------+--------------+-----------+
|               1 | NULL         | NULL      |
+-----------------+--------------+-----------+

5回失敗するとlocked_atにロックされた時間が設定されて設定ファイルで指定した1分が経過するまではログインできなくなる。

+-----------------+--------------+---------------------+
| failed_attempts | unlock_token | locked_at           |
+-----------------+--------------+---------------------+
|               6 | NULL         | 2013-02-08 16:22:35 |
+-----------------+--------------+---------------------+

1分経過後再びログインに失敗するとfailed_attemptsがリセットされてlocked_atが再びNULLになります。

+-----------------+--------------+-----------+
| failed_attempts | unlock_token | locked_at |
+-----------------+--------------+-----------+
|               1 | NULL         | NULL      |
+-----------------+--------------+-----------+

ログインに成功するとfaild_attemptsは0にリセットされるようです。

+-----------------+--------------+-----------+
| failed_attempts | unlock_token | locked_at |
+-----------------+--------------+-----------+
|               0 | NULL         | NULL      |
+-----------------+--------------+-----------+

まとめ

active_adminにロック機構をつけるのは簡単でした。