dalliの挙動を検証してみた

Rails4でmemcachedをキャッシュストアで使う定番gemといえばdalliRailsGuideに書いてある。ただ、このgemが実際どうゆう挙動をするのかググってみてもあまり日本語の情報が見つからなかったので試してみた。

環境

Ruby-2.0.0p353
Rails-4.0.2
dalli-2.7.0
正常時のパフォーマンス

Requests per second:    8.65 [#/sec] (mean)
Time per request:       578.181 [ms] (mean)
Time per request:       115.636 [ms] (mean, across all concurrent requests)

memcachedサーバが2台ある場合、あるキーは2台のサーバに分散してストアされてしまうのか?

Railsでは下記のようにconfig.cache_storeの設定をconfig/environments/環境名.rbに記述するが、この時に複数台のmemcachedサーバを書くことができる。

  config.cache_store = :dalli_store, "cache1.hakutoitoi.com", 
                                     "cache2.hakutoitoi.com"

ここで疑問に思ったのが、あるキー(ここでは仮にkey1とする)がcache1.hakutoitoi.comにストアされてその後のアクセスでcache2.hakutoitoi.comにもストアされてしまうかどうか。もしサーバが正常に動いているのにこれをやられてしまうとキャッシュヒット率が落ちてしまい悲しいことになる。

公式ドキュメントには下記のように書いてある。

uses the exact same algorithm to choose a server so existing memcached clusters with TBs of data will work identically to memcache-client.

TB級のデータならmemcached clustersと全く同じサーバ選択のアルゴリズムを使うってことらしいので、多分key1はずっと同じサーバにストアしてくれるはず。
実際にapache benchを使って1000回連続でリクエストしてみたがcache2.hakutoitoi.comにストアされることはなかった。

ab -n 1000 -c 10 "http://dalli-test.hakutoitoi.com/"

書き込まれた形跡はなし。

telnet cache2.hakutoitoi.com 11211
stats items
END

memcachedサーバが2台ある場合に1台落ちたらどうなるのか?

どちらかのサーバが落ちた場合にどうゆう挙動になるのか、考えられるのは2つのパターン

  1. cache1にストアされるリクエストはキャッシュしない
  2. cache1が落ちてるのでcache2にストアする

これに関してはfailoverという設定を追加することでどちらの挙動になるか選択できる。

  config.cache_store = :dalli_store, "cache-1.hakutoitoi.com", 
                                     "cache-2.hakutoitoi.com",
                                     { failover: true }

上記の設定を追加すると、cache1-hakutoitoi.comが落ちていてもcache2にストアしてくれて、cache1が落ちてる事を知らせるログが出る。

I, [2014-02-10T07:55:18.694561 #19929]  INFO -- : cache1.hakutoitoi.com:11211 failed (count: 3) Errno::ECONNREFUSED: Connection refused - connect(2)

パフォーマンスは半分くらいに落ちた。

$ ab -n 10 -c 5 "http://dallitest.hakutoitoi.com/"
Requests per second:    4.67 [#/sec] (mean)
Time per request:       1071.762 [ms] (mean)
Time per request:       214.352 [ms] (mean, across all concurrent requests)

failover: falseの場合延々とキャッシュしないので下記ログが出続けてパフォーマンスは大きく悪化する。

I, [2014-02-10T07:35:14.837841 #18979]  INFO -- : cache1.hakutoitoi.com:11211 failed (count: 4) Timeout::Error: execution expired
E, [2014-02-10T07:35:14.838389 #18979] ERROR -- : DalliError: No server available
$ ab -n 10 -c 5 "http://dallitest.hakutoitoi.com/"
Requests per second:    0.39 [#/sec] (mean)
Time per request:       12678.444 [ms] (mean)
Time per request:       2535.689 [ms] (mean, across all concurrent requests)

memcachedサーバが全て落ちたらどうなるのか?

つなげるmemcachedサーバがないとRailsアプリはどうなるのか?
結果はキャッシュをしないで正常なレスポンスを返すだった。

I, [2014-02-10T08:22:35.956360 #20576]  INFO -- : cache1.hakutoitoi.com:11211 failed (count: 0) EOFError: end of file reached
I, [2014-02-10T08:22:36.126920 #20576]  INFO -- : cache1.hakutoitoi.com:11211 failed (count: 1) Errno::ECONNREFUSED: Connection refused - connect(2)
W, [2014-02-10T08:22:36.127159 #20576]  WARN -- : cache1.hakutoitoi.com:11211 is down
I, [2014-02-10T08:22:36.130691 #20576]  INFO -- : cache2.hakutoitoi.com:11211 failed (count: 0) EOFError: end of file reached
I, [2014-02-10T08:22:36.308573 #20576]  INFO -- : cache2.hakutoitoi.com:11211 failed (count: 1) Errno::ECONNREFUSED: Connection refused - connect(2)
W, [2014-02-10T08:22:36.308748 #20576]  WARN -- : cache2.hakutoitoi.com:11211 is down
E, [2014-02-10T08:22:36.309556 #20576] ERROR -- : DalliError: No server available

最悪の結果であるページエラーにならなくてよかったが、データベースにアクセスが集中して落ちる可能性が高いので迅速な復旧作業が必要だろう。

まとめ

memcachedを複数台構成にして設定しておいても1台落ちた場合にパフォーマンスは落ちるので、サーバーダウンのアラートを受けた場合は迅速にRails側で設定ファイルを変更して1台構成にしてdeployする。
その後落ちたmemcachedを復旧させるもしくはAWSなどを使っているなら別Nodeを立ち上げて、立ち上がったら2台構成に戻すなどの手動オペレーションは必要だと思った。

active_adminの認証を他のページに設定する方法

active_adminrailsで定義したモデルを操作するwebインターフェースを数コマンド叩くだけで簡単に定義してくれるお手軽なツールです。まれにあるページに対して認証を定義したい時がありますがactive_adminの認証を簡単に設定する方法があります。

認証を設定したいページにbefore_action(Rails3ならbefore_filter)を追加するだけ。

class ToolController < ApplicationController
  before_action :authenticate_admin_user!
end

こうするとactive_adminと同じIDとパスワードを使ってログイン認証をページにつけることができます。実際はactive_adminが内部的に使っているdeviseの機能ですが。

CoffeeScriptで定義したクラスを別のcoffeescriptファイルから呼び出す方法

普通にcoffeescriptを書くとglobal汚染を防ぐためにfunctionの中に入れられてしまうのでwindowに領域を確保してそこに格納しておく必要がある。HogeClassの前にHogeProjectを定義しているのはnamespaceみたいなもので他のjsがHogeClassを定義している時のための保険みたいなものです。

呼び出されるクラス

window.HogeProject = {}

class HogeClass
  constructor: ->

  say: =>
    alert "say good bye!"

window.HogeProject.HogeClass = HogeClass

呼び出すスクリプト

jQuery ->
  hoge = new HogeProject.HogeClass
  hoge.say()

この手法はRailsなどコンパイルオプションが使えない場合に有用です。