ActiveResourceを使ったRailsアプリをRedisで高速化した

ActiveResource とは

ActiveResourceはRESTful APIマッピングActiveRecord のモデルとして利用可能にするgemで、これを使うとActiveRecordでDB操作を行うのと同じようにRESTful APIを利用できます。

github.com

ボトルネックになることもある

ActiveResourceを頻繁に使うとAPIに過剰にアクセスしているのと同じこととなり、ボトルネックとなりえます。 以下は今回、チューニングの対象となったアプリのNginxのアクセスログkataribeでパースしたものです。

$ cat /var/log/nginx/access.log | kataribe
...
TOP 12 Slow Requests
 1  14.933  GET / HTTP/1.1
 2  14.898  GET / HTTP/1.1
 3  14.722  GET / HTTP/1.1
 4  14.692  GET / HTTP/1.1
 ...

rootへのGETリクエストが14秒台後半で非常に遅いことがわかります。これはActiveResourceを使って取ってきたデータを一覧するページで、ActiveResourceボトルネックとなっていると推測できました。

Redisでキャッシュする

オンメモリ KVSのDBであるRedisにキャッシュすることで高速化を図ります。Redisはメモリ上でデータを管理するため、Read/Writeともに高速でキャッシュとして適しています。ActiveResourceを使ってデータを引っ張ってくるところをキャッシュしてみました。

RedisとRailsアプリケーションの接続にはmperham/connection_pool: Generic connection pooling for Rubyを使いました。コネクションプールというのはDBのコネクションをあらかじめ一定数確立しておいて使いまわす手法で、これを使うとDBへの接続に必要となるオーバーヘッドをカットできWeb/DBの双方の負荷を下げることができます。また、Web/DB間の接続を使いまわすことで同時接続数を節約します。MongoDB gem、ActiveRecord gemは自前でコネクションプールを持っていますが、Redis gemは持っていないので、このgemを使って接続することでコネクションプールを使えるようにしました。以下がRedisへの接続のために追加したファイルです。

config/initializers/redis.rb

# frozen_string_literal: true

# Load the redis.yml configuration file
redis_config = YAML.load_file(Rails.root + 'config/redis.yml')[Rails.env]

Redis.current = ConnectionPool.new(size: 10, timeout: 5) do
  Redis.new host: redis_config['host'], port: redis_config['port']
end

config/redis.yml

default: &default
  host: localhost
  port: 6379

development:
  <<: *default

test:
  <<: *default

production:
  <<: *default

Redisへのアクセス

Redisへのアクセスはcontroller内で以下のように行えますが、ここで注意しないといけないのは、keyがハッシュであるオブジェクトをJSONに変換しRedisにいれると、Redisから取り出しJSONにした際にkeyがstringになるところです。with_indifferent_accessを使うなどして対処しましょう。

Redis.current.with do |redis|
  test_user = { :name => "tkmru", :email => "tkmru@hoge"}
  redis.set('test_user', test_user.to_json)
  test_user = JSON.parse(redis.get('test_user')) # {"name"=>"tkmru", "email"=>"tkmru@hoge"}
  p test_user[:name]                             # nil
  p test_user['name']                            # tkmru
  test_user = test_user.with_indifferent_access
  p test_user[:name]                             # tkmru
  p test_user['name']                            # tkmru
end

結果

$ cat /var/log/nginx/access.log | kataribe
...
TOP 10 Slow Requests
 1  3.566  GET /hoge/231 HTTP/1.1
 2  1.866  GET / HTTP/1.1
 3  1.482  GET /hoge/240 HTTP/1.1
 4  1.479  GET /hoge/226 HTTP/1.1
 5  1.439  GET / HTTP/1.1
 6  1.415  GET /hoge/238 HTTP/1.1
 7  1.410  GET / HTTP/1.1
 8  1.293  GET / HTTP/1.1
 9  1.119  GET /hoge/243 HTTP/1.1
...

14秒台後半だったrootへのGETリクエストが1.4秒ちょっとで終わりました。 /hoge 以下もキャッシュすればもっと早くできそう。

おわりに

今回は原因が自明であったため、kataribeによるNginxのアクセスログのプロファイリングしか行いませんでしたが、pt-query-digestによるMySQLのスロークエリの解析や、stackprofrblineprofによるRubyのコードのプロファイリングを行うとより詳細にボトルネックを見つけることが可能です。 また、Redisでキャッシュすることによる高速化は、ActiveResourceを使ったアプリケーションに限定されるテクニックではなく、様々なアプリケーションで使うことができます。やっていきましょう。