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ファイルを指定のストレージに配置してくれる優れものです。
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'
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にロック機構をつけるのは簡単でした。