Railsのsession_storeやcache_storeとして、memcachedを使うことはよくあると思います。
今回は、memcachedのRuby用クライアントgem, memcache-clientやdalliにて、Passengerやunicornのfork後の初期化をきちんと書かないと、悲惨なことになるというお話です。
※他のgemやmemcached以外でも当てはまります。
必要な初期化処理
以下の処理を、config/environments/production.rbや、config/initializers/session_store.rbなど、どこでも良いので初期化コードに書いておく必要があります。
dalliを利用している場合
https://github.com/mdesjardins/dalli/tree/
# passengerの場合
if defined?(PhusionPassenger)
PhusionPassenger.on_event(:starting_worker_process) do |forked|
Rails.cache.reset if forked
ObjectSpace.each_object(ActionDispatch::Session::DalliStore) { |obj| obj.reset }
end
end
# unicornの場合
after_fork do |server, worker|
if defined?(ActiveSupport::Cache::DalliStore) && Rails.cache.is_a?(ActiveSupport::Cache::DalliStore)
Rails.cache.reset
ObjectSpace.each_object(ActionDispatch::Session::DalliStore) { |obj| obj.reset }
end
end
memcache-clientを利用している場合
http://petelacey.tumblr.com/post/3073967460/on-forking-application-servers-and-memcached-in-rails
これをやらないと何が起きるのか
「本番環境でのみ」「たまに」、取得されるデータが壊れたり、違うデータが取得されたりします。
なんでそんなことに
Passengerやunicornは(他のもそうだと思いますが)、最初にRailsのプロセスを立ち上げたあと、リクエストを捌くworker processをforkして増やします。
この際、既にmemcachedへの接続が確立されている場合、そのsocketもそのままforkされるので、1個のコネクションを複数プロセスが共有する形になってしまいます。
2個以上のプロセスがほぼ同時にリクエストを発行した場合、結果が混ざったり順番が入れ替わったりして、壊れたデータや入れ替わったデータが取得されてしまいます。
ちなみに、SQLなどでも同じことは起こるのですが、サンプルのunicorn.conf.rbには初めからこのような初期化コードが入っているため、知らなくても動いてしまったりします。
before_fork do |server, worker|
defined?(ActiveRecord::Base) and
ActiveRecord::Base.connection.disconnect!
end
上記の初期化コードは、確立されたコネクションをいったん切って、それぞれのプロセスで新たにmemcachedサーバに接続しなおしています。
恐ろしいことに
当然ながらworker processが2つ以上同時にリクエストしないと発生しないため、ローカル環境で1人で試していてもたいてい気づきません。
もちろん、webrickの1インスタンスで実行していても、再現しません。
さらに、複数workerでも、一定以上の頻度でアクセスしないと、あまり再現しません。
結論
ドキュメントはよく読みましょう。
本番に近い構成で、しっかりテストしましょう。