RailsでTokyoTyrantを使ってみたらフリーズした

※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サーバに分けて実行すると数倍の速度が出る)

デザインも頼めるシステム開発会社をお探しならBPS株式会社までどうぞ 開発エンジニア積極採用中です! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

渡辺 正毅

1984年生。サンフランシスコ育ち。大学から憧れの日本に留学し、そのまま移住。2006年慶應大学SFC卒。2007年BPS株式会社設立。いい国ですよね。もっとよくしたい。好きになってくれる人を増やしたい。

渡辺 正毅の書いた記事

夏のTechRachoフェア2019

週刊Railsウォッチ

インフラ

ActiveSupport探訪シリーズ