ぱろっと・すたじお

技術メモなどをまったりと / my site : http://parrot-studio.com/

RailsをRedisで「効率よく」高速化してみる(+おまけ)

仕事でコードを書く時間が減ると、別なところでコードを書きたくなるもので、
久々にチェンクロパーティーシミュレーター(以下ccpts)の
システム部分をいじっていました...φ(・ω・`)

ccpts.parrot-studio.com

以前react化したり、Rails5に置き換えたりしたわけですが・・・

parrot.hatenadiary.jp

parrot.hatenadiary.jp

・・・どうしても「遅い」ってのが問題でして(´-ω-)

react化=表示自体を全てJavaScriptで制御となると、
JSを解釈するまでメイン部分が描画されないわけで、
どうしても体感で遅くなってしまいます

「部分的react化」により、見かけ上の描画を早くすることは可能ですが、
私のスキルが追いついていないので、それはいったん置いておいて・・・Σ(・ω・ノ)ノ

私にできるのは「APIの速度改善」かな・・・ということで、
APIの速度を高速化してみたわけです

Redisのキャッシュを「効率よく」使う

APIが遅い原因はわりとシンプルで、たいていの場合は「(同期的な)I/O」です
ある程度並列処理が許される計算処理に対し、
同一のリソースに触るI/Oは排他制御等の「待ち」が発生します

I/Oといっても「ファイル」とか「ネットワーク」とかいろいろありますが、
「ネットワークでアクセスするファイルI/O」が遅いのは間違いなく、
端的にいえば「DBはどうしても遅い」ということに(´-ω-)

以前だと、DBのデータをメモリに全部乗せるとかいろいろありましたが、
KVS(現在はほぼRedis)の登場により
「高速で非同期アクセスが可能なネットワークストレージ」が登場しました

DBに対して処理時間のオーダーが文字通り「桁違い」なので、
これをキャッシュ的に生かせば、DBへのアクセスが減り、速くなる・・・はずです

だからといって、何でもかんでもRedisに突っ込めばいいわけではありません
Redisもネットワークストレージなわけで、
自分のメモリ上で処理が完結するならそっちの方が速いわけです

postd.cc

この記事は非常にすばらしく、参考になります(`・ω・´) b

そもそも、「Key-Value Store」という名の通り、
「Keyに紐付いたデータ」を取り出すのは非常に高速ですが、
「Keyのリスト」とか取り出そうとした時点で破綻しますΣ(゚Д゚)ガーン

実際、私も以前MongoDBで作ったシステムをMariaDBで作り直しています

parrot.hatenadiary.jp

用途が増え、串刺しな検索する機能が増えた結果、
RDBのような構造化された構造の方が圧倒的に速くなったわけです

つまり、当たり前のことですが・・・

  • 参照が圧倒的に多い
  • 結果取得に計算時間がかかる
  • 頻繁に内容が更新されない
    • ライフサイクルを明確に定義できる
  • Key-Valueという単純な構造に落とし込める
    • 「Keyのリスト」にアクセスする必要がない
  • 非同期アクセスしても問題ない

・・・(Redisに限らず)こういう条件を満たせなければ、
キャッシュを生かせないか、無駄に複雑化してバグの温床になるだけです(´・ω・`)

ccptsでRedisキャッシュを使う

これをふまえてccptsのAPIを高速化していくわけですが、
ccptsのAPIは概ね「検索に対してアルカナデータのリストを返す」という構造をしています

以前はクエリに対してDBを検索し、アルカナデータを構築して返していたのですが、
一つのアルカナに関連するテーブルがたくさんあり、
効率的に取得してもどうしても遅くなるわけです(´-ω-)

しかし、「アルカナのデータ」は「データの更新」をしない限り一意で、
だからこそブラウザ側でもキャッシュさせたりしているので、
「アルカナのID - アルカナデータ」という構造をキャッシュしようと

つまり、「DBで検索してDBからデータを取得」から、
「DBでIDだけ取り出して、IDに対応するアルカナデータをRedisから取得」に変えるわけです

ここで重要なのは、検索自体はDBにやらせることです
「構造化されたデータの検索」は圧倒的にDBが高速ですからね・・・

他にも、「検索画面用に返しているマスターデータ」とか、
初期表示に使う「最近追加されたアルカナリスト」とか、
決め打ちで返せるものもキャッシュしてしまいます

問題は「キャッシュの生存期間」とか「更新」についてですが、
今回の場合「データの取り込み時」にキャッシュも更新してあげれば解決です

古いキャッシュにアクセスされる可能性も、
「Keyにデータバージョンを含む」ことで回避できます(`・ω・´)

以上を実装して、先日リリースしたのがこちらです

redisのキャッシュで高速化 · parrot-studio/cc-pt-viewer@42947de · GitHub


<Redisキャッシュ対応前>

f:id:parrot_studio:20161031094518p:plain

<Redisキャッシュ対応後>

f:id:parrot_studio:20161031094603p:plain

<Redisキャッシュ対応後(ブラウザで時間測定)>

f:id:parrot_studio:20161031094611p:plain

文字通り、桁が違いましたΣ(゚Д゚;≡;゚д゚)


まあ、元々は仕事でもりもり設計していた部分ではあるのですが、
仕事のコードは持ち出せなくても、ノウハウを自分のアプリに反映しておけば、
別なところに使い回しできますからね・・・

ちなみに、お仕事の場合はメモリキャッシュ(Railsのmemstore)も使ってまして、
扱うデータに応じてメモリかRedisか選んで格納するようにしています*1

あと、キャッシュはあくまでキャッシュであって、
「消えてもDBから"必ず"復元できる」というのも徹底しています
Redisを信用してないわけではないですが、データの安全性はRDBが圧倒的ですしね

assetsの取り扱いを変える

これで体感でもそこそこ速くなったので、
Google先生のパフォーマンスチェックサイトで計測してみたのですが・・・

testmysite.thinkwithgoogle.com

・・・モバイル/PCでそれぞれ40点とさんざんでしたΣ(゚Д゚)ガーン

アドバイスに「データを圧縮して返せ( ゚Д゚)y─~~」と書かれてまして、
そういえばRailsのprecompileに圧縮版があったな・・・と思い出しました

アセットパイプライン | Rails ガイド

今まで非圧縮版にアクセスさせていたので、
ガイドの通りnginxを設定し、圧縮版にアクセスさせたところ・・・

・・・ここまで変わりました(; д ) ゚ ゚

ここからモバイルアクセスの点を上げるには、
レンダリングをブロックしているJS/CSSを改善しないといけないらしいです

Remove Render-Blocking JavaScript  |  PageSpeed Insights  |  Google Developers

とはいえ、完全reactの状態では難しく、
ファーストビューだけでもサーバサイドでレンダリングする必要があるのですが、
その問題自体は認識していて、今後の課題ですかね・・・

developers.cyberagent.co.jp

こういうことが「さらっと」(=私の手の届く範囲で)できるといいのですが(´-ω-)

まとめ

  • Redisキャッシュを使えばいいというわけではなく、使い方をちゃんと考える
  • Railsのガイドはちゃんと読む(´・ω・`)
  • 今後の課題:完全reactから一部をサーバサイドに差し戻す

*1: 昔はmemcachedが定番でしたが、Redisでもたいして速度が変わらず、永続化も可能なので、永続化が必要ならRedis、そうでなければRailsのメモリって感じで、memcachedは使ってません(´-ω-)