こんにちは、昨日親知らずを抜いた影響で、右頬に違和感があり、全く生産性のない男こと、P山 です。今日は、先日テレビCM を放映し、6/11から6/19まで開催したSUZURIのTシャツセール を迎えるに当たり、主に僕が担当した、SUZURIで行った負荷対策について紹介します。
SUZURIについて
SUZURI は写真を1枚アップロードするだけで、Tシャツやマグカップなどのオリジナルグッズをかんたんに作成、販売できるサービスです。また、アイテムごとに「トリブン」を販売価格に追加することが可能で、自分が作成したアイテムの売上の一部を受け取ることができます。こういった手軽さから、日本全国のクリエーターにご利用いただいています。またスリスリ神宮のようにお気持ちをもとにアイテムを作ることができるサービスなど、僕らが大切にしている「もっとおもしろくできる」を体現しているサービスです。
アプリケーションとしてはRuby on Railsを利用しており、実行環境は主にHerokuを利用しています。詳細な技術スタックはこちらをご参照ください。
どのように対策したか
セールを迎えるに当たり、想定トラフィック数を社内で推測し、その値をもとにアプリケーションのチューニングを進めました。想定トラフィックを再現するベンチマーカーを基盤チームの たくたかが準備して、そのベンチマークをパスするまで改善するというアプローチと、僕がDatadogの示す、ボトルネックをアクセスが多いエンドポイント順に潰していくという両軸から対応を進めました。
N+1問題
SUZURIのアプリケーションはブラウザアプリとスマートフォンアプリに別れており、それぞれから呼ばれるエンドポイントにN+1が多くありました。特に、アプリケーションの成長とともに、モデルのリレーションが増えており、古くからある実装で、新しいモデルの先読みが漏れている箇所が多くあり、ActiveRecordの preload
,eager_load
,includes
を入れて回る活動を行いました。事前に、これらをCIで検知できるようにbullet を導入してあったのですが、内部用のAPIなどでうまくCIでカバーできていない箇所があったので、そういった箇所をしもじゅーが対応しました。
ユニットテストで、bulletがN+1を検知すると、どこに問題があるのかに加えて、下記のようにどのようなコードを追加するべきかも推薦してくれるので非常に便利です。
2009-08-25 20:40:17[INFO] USE eager loading detected:
Post => [:comments]·
Add to your query: .includes([:comments])
2009-08-25 20:40:17[INFO] Call stack
/Users/richard/Downloads/test/app/views/posts/index.html.erb:8:in `each'
/Users/richard/Downloads/test/app/controllers/posts_controller.rb:7:in `index'
READMEより引用 https://github.com/flyerhzm/bullet
HTML/JSONキャッシュ
Railsにはいくつかのキャッシュ手法 があります。これらを活用して、様々なリソースをキャッシュをすることはもちろん行ったのですが、それでもパフォーマンスが出ない、もしくは著しくコールされるエンドポイントは、HTML/JSONごとキャッシュする戦略を取りました。具体的にはこのような実装を行っています。
def index
bl = Proc.new do
# 何かしらの処理
end
if use_cache?
c = Rails.cache.fetch('cache_key') do
bl.call
render_to_string
end
# 書き換えたい部分は書き換える
c.gsub!(/hoge/, 'fuga')
render inline: c and return
end
bl.call
end
HTMLやJSONをキャッシュしてしまい、DBクエリなどをショートカットするのは高速である反面、意図しない動作を引き起こすリスクが非常に高く、基本的にはさけるべき戦略だと考えています。一方で今回のようなユースケースで限定的に利用するのであれば、非常に有用であるため、そこはトレードオフしました。
エンドポイントごとのレートリミットを設ける
CM放映に伴い、例えば特定の商品や、クリエーターのページにアクセスが集中した場合に、SUZURI全体が落ちることを避ける目的で、rack-attackを利用してエンドポイントごとに、レートリミットを設けました。
Rack::Attack.throttle('path limits', limit: 5000000000000000, period: 1.seconds) do |req|
if req.get?
req.path
end
end
こうすることで特定のパスにアクセスが集注しても、SUZURI全体に影響が波及しづらくなっています。なお、設定値においては負荷試験の実績値から合理的な値を算出して設定しました。また本来こういった制限は前段にWEBプロキシーサーバなどを配置して、スロットリングするのが定石ではありますが、時間とチームリソースのトレードオフがあり、Rackのレイヤーでスロットリングすることとしました。
リリースフラグを利用した機能の死活
先のレートリミットと似た役割で、例えばサイトの負荷が高いときに推薦システムを経由させないことや、検索機能などを非活性化することで、サイトの回遊、商品購入は可能だが、一部の機能が限定されている状態を作り出すために、リリースフラグを導入しました。本来リリースフラグは新しい機能を安全にマージしたり、段階的にリリースしたりすることに使うものですが、WEBUIからかんたんにサイトの挙動を変更できるため、導入しました。
導入したリリースフラグのソフトウェアはUnleash で、導入当初あらゆるところで「アンラッシュ」とまるでパトラッシュのようにあらゆるところで豪語していた日々を取り戻したいです。
Unleashを利用すると、下記のようなUIでリリースフラグを管理できます。
作成したリリースフラグはRubyのコードで下記のように判定することができます。
if @unleash.is_enabled?("owada-example", unleash_context)
puts "we are owada"
else
puts "we are yamashita"
end
このように外部からかんたんに挙動を変えることができるので、新機能のリリースだけではなく、ちょっとしたトグルにも非常に便利です。
最後に
今回、SUZURIのセール対策ということで、技術基盤チームから数名、SUZURI事業部に支援を行うことになり、セール中にサービスを絶対に落とさないために、主に時間とのトレードオフを多く行って、いろいろな対策をすすめて、なんとかサイトをほとんど落とすことなくセールを無事乗り切ることができました。ペパボではこの規模の会社でしかやれない、インフラからフロントエンドまで、また事業部をまたいだ活躍ができるポジションが多くあるので、興味があるかたはぜひTwitterのDMなり、採用ページからご連絡いただければ幸いです。