※6/25にサンプルコードを修正しました
※7/1にサンプルコードを修正しました&補足を入れました
最近社内のRailsを使ったプロジェクトで、外部サービスからのレスポンスを使う処理で、
いちいち外部サービスにリクエストを行って取得していてはとても時間がかかるので、
シンプルなキャッシュシステムを構築することになりました。
最初は馬場が記事にしてくれているようにファイルベースのものを用いていましたが(実際はもうちょっと複雑なものを使っていました)、
ファイル数(エントリ)が数万単位で増えた際、パフォーマンスが指数関数的に悪くなってしまったので、キーと値の格納にTokyoTyrantを利用することにしました。
TokyoTyrantの使い方はシステムエンジニアブログにも書いてみました(TokyoTyrantをRubyで使ってみた)ので、TokyoTyrantそのものについてはこちらも見てみてください。
Railsから、利用するためにlib/util/data_cache.rbといったかんじで、以下のようなモジュールを定義してみました。
任意のオブジェクトをデータ構造を保ったまま保存できるようにMarshalクラスによりシリアライズ/デシリアライズを行っています。
コントローラからはincludeして使うイメージです。
require "tokyotyrant"
module DataCache
def cache_read(key)
init_tt if !defined?(@@tokyotyrant)
v = @@tokyotyrant.get(key)
return v ? Marshal.load(v) : nil
end
def cache_write(key, value)
init_tt if !defined?(@@tokyotyrant)
@@tokyotyrant.put key, Marshal.dump(value)
end
private
def init_tt
@@tokyotyrant = TokyoTyrant::RDB.new
@@tokyotyrant.open("localhost", 1978)
end
end
これをいろんなコントローラから使っていると、自分のデスクトップから開発している分には正常に動作するのですが、
多人数が同時でアクセスしたりする環境で使うとフリーズします。
どうやらgetやputを行っているときに、並行してこれらのTokyoTyrantとの通信が発生するメソッドを呼び出すと
通信の内容がごちゃごちゃになり、TTから正常にレスポンスを取得することができなくなっていたものと思われました。
マルチスレッド動作のサポートが各方面から望まれていたRailsですが、意外にもこのようなところでも弊害が発生しました。
対処としては、以下のようにRuby標準のthreadライブラリにあるMutexを使って、
TokyoTyrantへのアクセスを行う部分をクリティカルセクションとすることで、解決しました。
require "thread"
require "tokyotyrant"
module DataCache
MTX = Mutex.new
def cache_read(key)
init_tt if !defined?(@@tokyotyrant)
v = nil
MTX.synchronize do
v = @@tokyotyrant.get(key)
end
return v ? Marshal.load(v) : nil
end
def cache_write(key, value)
init_tt if !defined?(@@tokyotyrant)
MTX.synchronize do
@@tokyotyrant.put key, Marshal.dump(value)
end
end
private
def init_tt
MTX.synchronize do
return if defined?(@@tokyotyrant)
@@tokyotyrant = TokyoTyrant::RDB.new
@@tokyotyrant.open("localhost", 1978)
end
end
end
このクリティカルセクションに限らずRailsではまだ1プロセスで大量のリクエストをさばけるほどマルチスレッド処理のスループットが
出ませんので、本番環境の実運用では複数プロセスを上げて動かすというのが一般的、というのがまだしばらく続きそうですね。
問題意識を煽るようなタイトルをつけてしまいましたが、コントローラ部分の実装がスレッドセーフかどうかを保証するのはユーザの責任であるというRailsの仕様と、get/putなどの呼び出しはスレッドセーフではないというTokyoTyrantクライアントライブラリの仕様の問題ですね。
TokyoTyrant自体はクリティカルセクションで保護してもサクサク動作しているので、今後もキャッシュ機構が必要になったら積極的に使っていこうと思います。
7/1追記:
今回実装例で挙げているモジュールでは、1つのアプリケーションプロセスで最大で1つのTokyoTyrantコネクションしか使わないため、大量にTokyoTyrantにアクセスを行う場合TokyoTyrantにいちいちつなぎ直すか、複数のコネクションをプーリングするなどの戦略が必要です。調べたところTokyoTyrantのセッション確率のコストはそんなに高くないのでgetやputを少ない回数発行するような利用の場合いちいち接続する設計にするとシンプルで、バグが少なくてよいと思います。また、アプリケーションサーバとTokyoTyrantサーバを一つのマシンで動かしていると、TTへのリクエストの並列度が増えてくるとだんだんレスポンスが悪くなるようです。(アプリサーバとTTサーバに分けて実行すると数倍の速度が出る)