takuwan's blog

感じたことを書きますよ

5年前に自分がつくったRailsアプリをリファクタした話

8月に入り、晴れてフリーランスになりました。
ありがたいことに色々とお仕事のお話を頂きまして、今月から創業間もないスタートアップ2社に微力ながらお力添えさせていただくことにしています。楽しみです。

7月はほとんど有給消化期間で、結構のんびりしていたのですが、いろんな人にお会いしてお話したり積読本を消化したりする傍ら、5年くらい前に自分がぱっとつくってほぼ放置していたRailsアプリのリファクタ、パフォーマンス・チューニングをしていました。

30万pv/monthしかない小規模なウェブサイトですが、一週間くらい頑張ったらだいぶマシになったので、やったことを晒します。(大したことやってないですが、近況報告程度に。笑)

 

Railsアプリの概要

ikstudieという、独学の大学受験生を支えたいというコンセプトのウェブサービスが対象です。私自身、大学受験には独学で臨んだのですが、何かと不安に思うところがあって辛かったので、私のような思いをする受験生の支えになりたいと思ったのが立ち上げのきっかけでした。

以下、このサイトの特徴を挙げます。

  • 私が個人ではじめてつくったウェブサービス(手抜き、超短期開発、糞コード)
  • ここ数年、30-50万 pv/month。数日に一回程度、受験生からの質問がある。
  • リリースしてから5年くらい経つものの、Rails3系から4.2までのバージョンアップと、クリティカルなボトルネック対応以外、ほぼ手付かずで保守できていない
  • 有志で、View部分だけずっと書いてくれている仲間がいるものの、staticなページが多数出来ている
  • 当然のようにユニット・テストはない
  • モデルは30個前後の小規模アプリ
  • 当たり前品質を満たせていない(レイアウト崩れ、機能が壊れている等)
  • インフラに関しては、完全に私の実験場になっていて、これまでEC2 + capistrano2系 ー> Opsworks + Chef ー> Elastic Beanstalk ー> Terraform + Packer + (Itamae or Ansible) on AWS  ー> Google Kubernetes Engine ー> Google App Engineと色々お引越ししてきている(チューニングはしていないが、最低限のインフラ環境は整っている)

ずっとikstudieに関わってきてIT系に勤める有志の仲間たちと、これからちゃんとエンハンスをしようという話になったのが、今回のリファクタのきっかけです。中のコードがひどかったり、当たり前品質と言われるようなところに問題があったので、まずはリファクタしないと先に進めないという認識でした。

 

何はともあれまずは現状把握と解析

ぱっと調べられる範囲でいろんなツールを使って解析したり、主要ページのコードを眺めたり、全画面に遷移して挙動の確認をしてみました。

 

rubocop(linter)を走らせる

f:id:takuwan0405:20180802165103p:plain

rubocopはRubyのlinterです。autocorrectを走らせ、linterのターゲットをコントローラーとモデルだけに絞った結果、これだけoffensesが出てきました。(意外と少なかった。)

Fatなコントローラー・モデルに対して、もっと小さくしろよと怒ってるのがほとんどでした。
自分の目でコード一行一行を読んでいくともっとツッコミどころ満載だったので、あくまでrubocopで補足できる範囲での数値です。

もっとRubyっぽく柔軟に書けば読みやすいしコード量も1/3以下で済むのに、みたいな記述がたくさんあった感じです。

 

Railsアプリとしてのコード品質をrails_best_practicesでチェック

f:id:takuwan0405:20180802174305p:plain

rails_best_practicesは、その名の通り、Railsらしい記述を指南してくれる静的解析ツールです。Warningの数もさることながら、カラムにindexが付いていないだとか、結構重要なところでも怒られました。
Warningのほとんどは、viewの書き方が汚いよ・間違っているよと怒っていて、地道な修正が必要そうでした。

 

Skylightでパフォーマンスモニタリング

f:id:takuwan0405:20180802170321p:plain

Newrelicでも良かったのですが、今回ははじめてSkylightをつかってパフォーマンスモニタリングをしてみました。Newrelicのほうが機能はリッチでしたが、価格もリッチだったうえ、Skylightでも今回は困りそうになかったためです。

Typical Responseで244msは、うーんって感じですね。基本的にマシンリソースには余裕があって負荷もないので、worker、thread、connection poolとかにボトルネックがある様子もありません。Problem Responseでは1.5s以上のものも散見されました。

ActiveRecordのクエリは遅くても20msで返ってきていて、Viewの生成にすごい時間がかかっていました。

 

Lighthouseによるフロントからの評価

f:id:takuwan0405:20180802171838p:plain

SEOの100点が眩しいですね。(一応メディアなので、ここはクリティカルなポイントでした。)
PWAは対応する気がないので問題ないですが、PerformanceとBest Practicesが気になります。

細かい設定は色々指摘されましたが、画像周りが大変そうな印象。

 

Railsアプリのセキュリティ脆弱性をBrakemanでチェック

f:id:takuwan0405:20180802163354p:plain

やたらErrorsが出てますが、全部Parsing Errorでした。
数年前にbrakeman走らせたときはXSSのSecurity Warningが出てたりしたので(パラメーターを直に使ってSQL叩いてたところがあったので)、本当に駄目なやつは昔直してました。

 

Githubのセキュリティアラート

f:id:takuwan0405:20180802165915p:plain

自分は、つくっては放置しているリポジトリがいくつもあるので毎週怒られてます。
ikstudieもいくつかの依存ライブラリでsecurity alertが挙がっていました。(が、内容ちら見したうえでいままで後回しにしてました。)

 

使われている機能・使われていない機能の確認

管理画面を見たり、本番データを複製したDBにクエリ投げてみたりして使われていない機能をぱっと洗い出しました。

記事をマイリストする機能、いいね!する機能などが使われていないことがわかったり、使われていないカラムが沢山あることが判明しました。データ量がそんなないので、slow queryは引っかかりませんでした。

 

愚直に手動でE2Eテスト

テストケースを列挙して丁寧にやったわけではないですが、全画面の挙動を確認しました。ログイン機能が一部壊れてたり、もう色々壊れてました。

全体的にひどかったのがレスポンシブデザインで、例えばトップページでも下記のように崩れていました。

f:id:takuwan0405:20180802173316p:plain

jpmobileという、Railsのview層でUserAgentを判別できるgemが導入されていて、viewで ` request.smart_phone? ` といった感じの分岐がいたるところに入っていました。リリース当時はレスポンシブデザインだったはずなのですが。。(UserAgentで切り分けているので、スマホでikstudieを見たら崩れてはいなかったです。)

CSSに一貫した命名規則がなく(bootstrapっぽいもの、SMACSSっぽいもの、安易な命名とか混在してた)、少し触るとすぐレイアウトが崩れそうなので、それが一因してjpmobileを多用したのかなと伺えます。たぶんそんな考えてないです。

 

目標を決める

現状把握は一日くらいでぱっと済ませたので、その後4日くらいで達成する、リファクタのMUST要件(目標)をゆるく、ざっくり決めました。上から優先度高めです。

  • 壊れているレイアウト・機能の改修(UX目線での当たり前品質を満たす)
  • SkylightでのTypical Responseを80ms以下にする(UXを向上させ、SEO上のマイナスをなくす)
  • 不要な機能、コード、テーブル、カラムの除却(エンハンスしやすい形にしたい)
  • コード品質向上・維持のために、CIを導入してrubocop、rails_best_practices、brakeman、Github security alertでのwarningとerrorをゼロにする(負債を貯めないようにしたい)

 

リファクタ結果

当たり前品質(壊れたレイアウト・機能の改修)

View層で動的な部分はほぼ全部書き直しました。CSSの設計が破綻してたので、エンハンスする可能性がありそうなページは書き直しておくことで後々うれしい気持ちになりそうだったためです。問題になっていたjpmobileは極力取り除き、書き直したページは一部を除きレスポンシブデザインに戻りました。(UserAgentでのViewの条件分岐はなくしました。)
View層は私以外にも触る人がいるので、Reactjsとかは入れずに、以前から入っていたslimというテンプレートエンジンを採用し続けました。私以外の人はエンジニアとして生活しているわけではないので、学習コストがない状態にしたかったためです。

ただただ、愚直に直しただけです。
静的なページは、後日優先順位をつけて改修する必要があるかもしれません。

 

f:id:takuwan0405:20180802184816p:plain

ちなみに上の画像のフッターに出てるLINEへの送客は、どれくらい送客できるか、ikstudieのユーザー層はどうなっているかを検証・確認するためにやってみたのですが、結構良い・面白い数字が出ています。受験生の集客、つまり広告として成り立ちそうだなーとか考えています。(受験に関連するiOS/Androidアプリつくって送客しても良さそう。)

 

UXとSEO向上(パフォーマンス・チューニング)

f:id:takuwan0405:20180802190947p:plain

Typical Responseは大体40~60msになりました。(1/5くらいになりました。)

ループを回してHTMLを生成している部分が概して遅く、Railsが標準で提供しているフラグメントキャッシュという、生成したHTMLをキャッシュする機構を多く使いました。キャッシュストア、またセッションストアにはRedisを採用しています。

N+1は意外と少なかったのですが、利用しない(検討した結果利用する予定もない)AccessLogやEventLogをinsertするクエリが各リクエストにあったり、他にも不要なクエリがいくつかあったので削ったのも少々効きました。あとはrenderメソッドのチューニングとかも効いてそうです。

キャッシュのヒット率を高めるためにキャッシュの有効期限は長めに取ってますが、記事の検索結果のページとかはヒット率が低いので、ひどいと500msとかかかってしまうケースもまだあります。

Skylightをつかってて嬉しかったこととして、週次でどれくらいパフォーマンスを改善したのかが送られてきます!StaticPageController#homeはトップページなのですが(全然Staticじゃないので命名が意味不明ですね。負債具合が伝わりそう。)、9倍になったらしいです。記事のページは141msとかかかってるので、まだ改善が必要そうですね。

 

f:id:takuwan0405:20180802190949p:plain

 

エンハンスしやすい形に(不要な機能、コード、テーブル、カラムの除却)

前述したAccessLogやEventLogもそうですが、不要なテーブルやカラムが散見されたので、凍結したりdropしました。

また、マイリストの機能は改修がちょっと大変な割には本当に使われてなかったので、一旦凍結しました。また、ログインが必要なために使われていなかった「いいね!」機能は、ログインしなくても利用できるようにしました。

コード量も全体的に削っていて、PRのコード量を単純にみたら3600行削ったようです。

f:id:takuwan0405:20180803005720p:plain

 

コード品質向上・維持 

まず、rubocop、rails_best_practices、brakeman、Github security alertでのwarningやerrorは全てゼロにしました。静的コード解析上では常にクリーンな状態を保つことである程度のコード品質を担保したかったのと、コード品質を表現する尺度を他のコントリビューターにもみえる形で設定したかったという意図です。

CIは、いまのikstudieでは無料で十分使えるCircleCIを採用し、元々あったdockerの開発環境をそのまま利用する形でぱっと構築し静的解析を走らせるようにしました。(コンテナのキャッシュとか設定してないので1回のRunで13分前後かかってて、いつか改善したいです。)

ブランチ戦略も簡単に設けて、masterとdevelopブランチはprotectし、基本的にdevelopからfeatureブランチを切る形にしました。
Staging環境をHerokuで無料で構築し、developブランチにPRがmergeされたらデプロイされるようにしました。また、masterブランチにPRがmergeされたら本番環境にデプロイされるようにもしました。

ユニットテストとかは書いてないですが、コントローラーもモデルもスリムであまり量がないので、これからエンハンスしながら書き加えていこうと思います。

 

余談

リファクタのPRを本番デプロイ後にLighthouseで確認してみたところ、数値がそこそこ改善していました。

Nginxとかの細かい修正が少なくなかったですが、Performanceに関しては、CDNとしてCloudFrontを導入してCache-Controlを長めに設定したこと、S3に置かれている画像オブジェクトを全部圧縮したこと(pngの圧縮は時間がかかりました)、またLayzr.jsをつかって一部の画像を遅延読み込みさせたこととかが、効いていそうです。

f:id:takuwan0405:20180803013031p:plain

あと余った時間でやったこととして、たまたまGoogle Cloud Load Balancerの障害に当たってしまったり、ikstudieにとってGAEは割高だと感じていたこともあって、本番環境をElastic Beanstalk(というかAWS)に移行しています。ついでにPumaのworker、thread、postgres、connection pool、GC周りとかもそれっぽい数値を改めてぱっと設定して様子見しています。

 

これから

優先順位は高くないもののまだリファクタしたいことはいくつもあるので、「心置きなく」というわけにはいきませんが、エンハンスには着手できそうです。

今年いっぱいは既存のウェブサイトの最適化を図る予定ですが、アプリの開発やリアルでのイベント開催などでもできること・やれることは沢山あるので、仮説検証をしつつコードも書いていていきます。

興味がある方はtwitterでDMください。

 

リファクタの感想

あくまで個人開発なので全然しっかりしていない開発・リファクタですが、一応満足。