こんにちは。ecbeing金澤です。
レビューのSaaSサービス「ReviCo(レビコ)」を開発しています。
はじめに:ReviCoは1周年を迎えました
少し前になりますが、4月にReviCoは正式ローンチ1周年を迎えました。
おかげさまで多くの企業様にご利用頂いているのですが、
ユーザーが増えるにつれて、トラフィックとデータ量も増えていき、
トラフィックとデータ量が増えるにつれて、パフォーマンスは落ちていきました…
私自身、SaaSサービスのプロダクトリードは初経験だったのですが、
もっとも痛感したことは
「SaaSは生き物」
サービスは日々成長していきますが、ただ身体だけ大きくなっていけば良いわけではなく、骨や内臓など身体を支える構造も一緒に強くしていったり、血液や酸素などの栄養も沢山流れるようにしていかないと、バランスが崩れて弱ったり動けなくなったりしてしまうんだなぁと…。
要は、機能追加と並行して、トラフィック増に耐えられる基盤の整備や、データ量に合わせたチューニングをしなきゃいかんよ、ということに気付けた1年でした。
というわけで、この記事では、ローンチからの今まで、ReviCoが大きくなる傍らで実施したパフォーマンス改善施策についてまとめたいと思います。
1年前のReviCo
ローンチ時のReviCoのインフラ構成はこんな感じでした。
VPCやサブネットなどは省きますが、いわゆるWebサーバーとDBサーバーという超単純なつくりでした。
CloudFrontはWAF利用のみで、リクエストは素通りでした。
パフォーマンス改善のためにやったこと
CDNキャッシュをきちんと設定する
効果:★★★
難度:★
CloudFrontのCache Statisticsでキャッシュヒット率のランキングが見れるので、静的ファイルやGET系のAPIでキャッシュに乗っていないものは片っ端から突っ込んでいきました。
基本中の基本ではありますが、WebサーバーやDBサーバーの負荷が一番劇的に下がったのはこの対応でした。
スロークエリをきちんと対処する
効果:★★★
難度:★★
最初はスロークエリログすら出していなかったので、まずその設定からしました。
Auroraでログを出すには、クラスター側とインスタンス側の両方に設定を入れてやる必要があり、そこで少し手間取りました。
(詳細はクラスメソッドさんの記事で紹介されています)
スロークエリも、最初はめちゃくちゃ出るので心が折れかけましたが、実行計画を見ながら根気よく1つずつ潰していきました。インデックスの貼り漏れなど割とすぐ解決できたのが多かったのが幸いでした…。
表示処理を優先するようロジックを見直す
効果:★★★
難度:★★
ReviCoはWebサイトの任意の場所にウィジェットとしてレビューを表示できるのですが、そのロジックが
jQuery.when(
// レビュー表示データ取得API,
// レビュー絞り込みメニュー取得API,
// レビュー閲覧記録送信API,
// etc…
).done(
// レビュー表示処理
);
のようになっており、高負荷時などにAPIのレスポンスが1つでも遅くなると、レビュー表示が遅くなるという致命的な弱点がありました。
なので、
jQuery.when(
// レビュー表示データ取得API
).done(
// レビュー表示処理
);
// レビュー絞り込みメニュー取得
// レビュー閲覧記録送信
// etc…
のように、レビューの表示に関連する処理を何よりも優先するようにして、ファーストビューに影響のない処理やイベント送信などは後から処理するようロジックを組み替えました。
中間テーブルを作って重いSQLが毎回走らないようにする
効果:★★★
難度:★★★
集計系のSQLはデータ量が増えるにつれて遅くなりがちなので、中間テーブル(マテリアライズドビュー的な役割のテーブル)を作り、SELECT時間の短縮を図りました。
効果は抜群でしたが、実データとのタイムラグを作りたくなかったので、実データの更新箇所すべてに中間テーブルの更新処理を入れた結果、実データの更新操作が徐々に遅くなっているという新たな課題が…。
(今思えば多少のタイムラグは許容できるので、SQSなどでイベント連携して非同期で処理する方式を検討中です)
ElastiCacheを導入する
効果:★★★
難度:★★★
セッション管理をAuroraからElastiCache(Redis)に移動しました。
ReviCoは.NET Coreで動いているのですが、Azure Cache for Redisに繋げる記事を参考にしたらElastiCacheのRedisにも接続できました。
また、SELECTした結果をRedisにキャッシュする対応も順次進めています。
データ的に独立しているテーブルをAuroraからDynamoDBに移動する
効果:★★
難度:★★
最初はデータを何でもかんでもAuroraに突っ込んでいましたが、ワンタイムトークンなど他のテーブルとの関連が無いデータはDynamoDBに移動しました。
DynamoDBは有効期限が切れたデータを勝手に消してくれるのが便利ですが、キャパシティユニットのオートスケールに少し時間が掛かるので、ターゲット使用率をいくつにすればスパイクアクセスにある程度耐えられるのかを定期的に見直しています。今は70%にしています。
(今思えばElastiCacheで十分だったような気もします…)
書き込みDBと読み込みDBを分離する
効果:★
難度:★★★
Auroraではリードレプリカを柔軟にスケール出来るようになっていますが、プログラム側で接続先を切り替えることができず、全SQLが書き込みDBに繋ぎに行っていました。
なので、SELECT系のクエリは読み込みDBに繋ぎに行けるようプログラムを改修し、DBにはCPU使用率ベースのオートスケール設定を入れて、高負荷時にDBが張り付かないような構成に変えました。
ただ、最初からDBを分離する想定でシステムを作っていなかったので、トランザクション内でUPDATE→SELECTをしているような処理は分離できず、結局スロークエリの筆頭に上がるようなSQLを数個だけ対応して諦めました…。
APIのキャッシュヒット率を上げる
効果:失敗…
難度:★★
レビューデータを取得しているAPIで、リクエストパラメーターが
?param=A¶m=B¶m=C&...
となっているものがあり、このパラメーターの並びが完全一致しないとキャッシュにヒットしないので、キャッシュヒット率を下げる原因になっていました。
そこで、
?param=A
?param=B
?param=C
のようにパラメーターを分割してAPIを複数回呼ぶようにすれば、キャッシュヒット率が上がるのではないか?と考えました。
が…本番リリースしたところ、リクエスト数が一気に数倍になってしまい、キャッシュが完成する前にDBサーバーがギブアップしてしまったため、泣く泣くリリースを切り戻しました…。
まとめ
施策 | 効果 | 難度 |
---|---|---|
CDNキャッシュをきちんと設定する | ★★★ | ★ |
スロークエリをきちんと対処する | ★★★ | ★★ |
表示処理を優先するようロジックを見直す | ★★★ | ★★ |
中間テーブルを作って重いSQLが毎回走らないようにする | ★★★ | ★★★ |
ElastiCacheを導入する | ★★★ | ★★★ |
データ的に独立しているテーブルをAuroraからDynamoDBに移動する | ★★ | ★★ |
書き込みDBと読み込みDBを分離する | ★ | ★★★ |
APIのキャッシュヒット率を上げる | 失敗… | ★★ |
今のReviCo
そんなわけで、色々対応した結果、今はこんな感じになっています。
なんだかクラウドっぽくなってきた!!
おわりに:今後の展望など
パフォーマンスチューニングは本番環境相当のトラフィック量・データ量が無いと効果検証がしづらいのですが、ステージング環境にそれが用意できなかったため、本番にリリースしてみて効果を見つつ次の施策を考える、というやり方を繰り返してきました。
ReviCoでは2週間に1度のサイクルでリリースしているのですが、リリースした施策が全く効果が無かった時の苦悩と、バチッと効果が出た時の爽快感は、自社サービスを責任持って開発している立場だからこそ感じられるものだなと思っています。
最後に、次の打ち手としてこのようなことをやっていきたいと考えています。打てる策に(お金以外の)物理的制約が無いのがクラウドの凄いところですね。
■ ElasticSearch導入
ReviCoでは様々な条件でレビューの絞り込みができるのですが、SQLのWhere句では限界が来ているので、検索エンジンを導入してDBサーバーの負荷とレスポンス速度を劇的に改善したいです。ファセットも活用したい。
■ API Gateway + Lambda でECSの負荷軽減
ワンタイムトークンの発行APIなんかはECSで処理する必要もなさそうなので、API GatewayとLambdaで処理するようにして、ECSのコンテナ数を減らしにいきたいです。
■ Aurora Serverless v2導入
夢のようなサービスだと思います。早くGAしないかな。
お知らせ
ecbeingではサービスの成長のため一緒に試行錯誤してくれる仲間を募集しています!