🔥

【性能改善】今夜勝ちたい。パフォーマンスボトルネックは消去法で捉えよ!

2023/12/20に公開

これを見たということは、あなたは、今まさに性能問題に直面している真っ最中?
よくぞこの記事を開いてくださいました。任せてください。
あなたが渇望する ”これやっときゃ性能のボトルネックが大体わかる” 方法をお教えします。

みなさんこんにちは。運送管理SaaSを開発するアセンド株式会社で、まだ見ぬ物流の最高のデータモデルを探究し続けている宮津です。
この記事は「はじめてのアドベントカレンダー Advent Calendar 2023」に参加しています。

パフォーマンス問題は突然に

突然ですが、あなたが開発しているWebアプリにおいて、緊急性の高いパフォーマンス課題に直面しました。お客さんからは「なんだか遅い」「業務が回らなくて困る」などの声が上がっています。そしてあなたは、「自分の開発したあそこが不味かったかな?」「画面挙動が遅いということはフロントエンドかな?」と原因についてあれこれ思いを巡らしながら、この問題にどう向き合うか考えています。

どう考えるか?

パフォーマンス改善のトピックにおいて、 「推測するな、計測せよ」 とはよく用いられるフレーズですが、私自身はこの言葉を 「改善の手を打つ前に客観的な根拠を持ってボトルネックを明らかにしろ」 と解釈しています。あなたが手を動かす前にあれこれ思いを巡らすことは決して悪いことではありません。
経験上、パフォーマンス改善に長けた人は消去法の思考が徹底されています。あれかな?これかな?と当たりを探るのではなく、 あれではない、これではない、ならばこれしかない、と、絞り込むようにして犯人を特定します。誰しもパフォーマンス問題を起こそうと思って開発をしないので、思いもよらない箇所がボトルネックになっていることが大半です。そのため、この処理は大丈夫だろう、という思い込みを潰すことが重要になってきます。決して、「この処理はシンプルだし、しばらく触られてないから、原因の可能性はないとする」とはしないでください。それは ”確か” ではありません。

ケーススタディ

では、実際に弊社で起こったパフォーマンス問題を例に、ボトルネックを特定していきましょう。
弊社が提供している運送管理SaaSロジックスにて、ドライバーさんの勤務状況を可視化するダッシュボード機能があるのですが、想定していたよりも大量のデータを集計することになったため、グラフがなかなか表示されない事象に直面しました。

1. フロントエンド、バックエンド、DBの順に調べる。

ユーザーの操作から駆動するプロセスを網羅的に計測するために、フロントエンドの処理からバックエンド、DBの順に計測します。知識があれば、LB,Gatewayなど、ネットワークが通過するサービスのメトリクスをチェックすることで、より確かな計測が可能になります。


フロントエンドからDevToolを用いて、NetworkタブやPerformanceタブを用いて、データ取得と描画にそれぞれ何秒かかっているのか計測する


Networkタブから、データ取得のリクエストに長時間かかっている様子を観測した。ふむふむ、こいつが悪そうだ!描画が悪い可能性は除外して、バックエンド側の調査へとうつる。

2. どんなにシンプルに見える処理でも網羅的に計測。

ボトルネックを見過ごさないように、ロジック層の端から端まで計測します。
特にDBへクエリを投げて結果を得る処理など、アプリ外部との通信処理は忘れずに計測してください。

バックエンドの計測は、ツールがなくてもログを仕込めば十分です。
ただし、あまりにもログ量が多くなりそうな場合は性能に影響するので考慮が必要です。


ぶん投げる

async process(
    ctx: ContextTypes,
    condition: MonthlyStatsRequest
  ): Promise<MonthlyStatsResponse> {
    console.time('search process');
    const results = await this.monthlyService.search(condition);
    console.timeEnd('search process');

    console.time('sort process');
    const sortedResults = results.sort((a, b) => a.value.compare(b.value));
    console.timeEnd('sort process');

    console.time('map process');
    const response = sortedResults.map(({ driverId, value }) => ({
      driverId: driverId.getValue(),
      value: value.toValues(),
    }));
    console.timeEnd('map process');

    return response;
  }

このように各処理の時間を計測できるように書き換えていき、monthlyService.search(condition)、a.value.compare(b.value)、value.toValues()といった処理の内部にも妥協せず計測し、searchまでは大丈夫そう、compareまでは大丈夫そう...と、ボトルネックである可能性から除外していきます。

3. ループ構造に目をつけ、1ループあたりの処理時間を測る。ループの因子が実環境だとどうなるか、想定して試算する。

大まかな処理時間を計測し、本番環境などで問題になりうるかどうかを精度高く”推測”します。

    const response = sortedResults.map(({ driverId, value }) => ({
      driverId: driverId.getValue(),
      value: value.toValues(),
    }));

計測すると、上記の1ループあたりの処理に5msかかっていた。
sortedResultsは本番環境では1万件にも及ぶと仮定すると、
5ms x 10,000 = 50秒かかる。犯人はこいつだ。

...といった具合に、フロントエンドからバックエンドと順に降りながら選択肢を除外しつつ、網羅的に計測していくことで、ボトルネックの可能性がある処理を見過ごさずに突き止めることができます。


実際に構造を解析して、ボトルネックを突き止めた後のご満悦レポート

まとめ

”確かな消去”法でボトルネックを突き止めるアプローチについて紹介しました。
難しいケースのボトルネックを突き止めるには幅広さと深さを兼ね備えた技術知見が求められることがあります。それでも、わかるところまで明らかにしていくことがとても重要です。
例えば、フロント、クラウドインフラ、バックエンドと順に追った結果、DBに投げたクエリが遅そうなことまではわかったけれど、肝心のSQLがなぜ遅いかがわからないとします。ですが、実はそこまでわかっただけでも取れる打ち手はいくつもあります。クエリだけ切り出してスペシャリストに頼ったり、クエリで実現しようとしているロジック自体を代替する手段を模索する、といった具合です。

パフォーマンス課題は「わからない」からこそ手が止まりがちですが、今回紹介したアプローチは少しでも原因と解決に近づける方法として有効です。諦めて眠って終わらず、今晩中に未知なる敵に立ち向かいましょう。

アセンドプロダクトチーム

Discussion