ActiveResourceを使ったRailsアプリをRedisで高速化した
ActiveResource とは
ActiveResourceはRESTful APIをマッピングしActiveRecord のモデルとして利用可能にするgemで、これを使うとActiveRecordでDB操作を行うのと同じようにRESTful APIを利用できます。
ボトルネックになることもある
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のスロークエリの解析や、stackprof、rblineprofによるRubyのコードのプロファイリングを行うとより詳細にボトルネックを見つけることが可能です。 また、Redisでキャッシュすることによる高速化は、ActiveResourceを使ったアプリケーションに限定されるテクニックではなく、様々なアプリケーションで使うことができます。やっていきましょう。