グダグダなパフォーマンス改善からの学び

に公開

はじめに

こんにちは、ブルーモ証券株式会社で主にGoのサーバーサイドの実装をしている葛西です。

先日、APIの高速化に着手したのですがかなりグダグダになってしまい逆に学びが多かったので他の方が同じ過ちを繰り返さないために、また自分への戒めとしようと思い記事を書きました。

課題の概要

ブルーモのアプリでは、下記のようにユーザーの資産状況をグラフ表示する機能があり日毎の資産状況をグラフで見ることができます。
以前からこのグラフの描画速度が遅いという課題があったのでバックエンド側でこのグラフの元となる履歴取得APIのパフォーマンス改善をするタスクに取り掛かりました。
目標値は「本番環境でのレスポンス時間の99パーセンタイルを1/5にする」でした。

※テスト環境の画面です

資産総額グラフ

実際のタスクの流れ

N+1の解消

まず私はこのタスクの着手開始時点で履歴取得処理のN+1解消を実施しました。以前から履歴取得処理がN+1になってしまっているのではないかと薄々気づいたのと大体データ取得のボトルネックはDB周りだと思い込んでいたので特に調査もせずそこが原因だとしてその解消を始めました。

N+1解消後にテスト環境で履歴データが多いユーザーを対象に計測をしたところ目標値に全く届いておらず、また修正前の計測を行なっていなかったので修正による効果の測定もできませんでした。

Traceログの追加

次にボトルネックを突き止めるために各処理にTraceログを仕込み、それぞれの処理での処理時間を計測することにしました。
ただしTraceログでの計測では各関数の処理時間しか分析できず、メモリ消費量やCPU使用量が計測できずに原因の深掘りができませんでした。

Traceログの分析とO(N²)の解消

最初に述べたようにパフォーマンス問題はほとんどDBがボトルネックであるという思い込みがあったのでTraceログを見ている時も当初はDB周りの処理時間ばかり気にしていました。
しかしさらに詳しくTraceログを追っていった結果、ボトルネックとなっていたのはDB処理ではなくアプリケーションロジックでの評価額計算部分だと判明しました。
各銘柄に対して全体に対する評価額の割合を計算する処理があったのですが、その計算のたびに全体の評価額を計算していたため保有銘柄数をNとして計算量のオーダーがO(N²)となっていました。
最終的には全体の評価額の計算をメモ化することによって計算量を削減し、結果として3倍以上レイテンシが改善されました。

pprofの分析とGCのチューニング

O(N²)の解消後にチームメンバーからGoのプロファイリングツールであるpprofが既に導入されていることを後で教えてもらったので計測と分析を行ったところ、処理時間の約半分がガベージコレクション(GC)に費やされていることがわかりました。
下記がpprofによる計測結果なのですが、大量のデータを処理する過程で頻繁なメモリアロケーションとGCが発生しているようでした(右半分がGCによる処理時間)。

pprof_改善前

対策としてGCの頻度を減らすためにGo1.19から導入されたGOMEMLIMIT環境変数を利用するようにしました。
Go1.18まではGCは使用メモリ量が一定割合以上増えると発生するようになっておりその頻度をGOGCという環境変数で制御していましたが、Go1.19からはGOMEMLIMITによって上限の使用メモリ量を指定することでGCの発生タイミングを制御できるようになっていたのでこちらを利用しました。
参考:Go1.19から始めるGCのチューニング方法

このチューニングにより、GCの頻度が減少しレイテンシが改善されました(右1/5程度がGCによる処理時間)。
またこのチューニングを行ったことによりAPIのレスポンスだけではなく他のバッチ系の処理も高速化されたため恩恵が大きかったです。

pprof_改善後

最終結果

これらの改善の結果、テスト環境での履歴データが多いユーザーでのパフォーマンスも改善され、目標としていた「本番環境でのレスポンス時間の99パーセンタイルを1/5にする」を達成することができました。

教訓のまとめ

今回のタスクを通じた教訓は以下の通りです。

  1. パフォーマンス改善はまず計測から始める
    • 改善前と改善後の計測結果を比較することで、効果を正確に評価できる
    • 比較をしやすくするためになるべく同じ環境・条件での計測をすること
  2. 適切な計測ツールを使用する
    • メモリ消費量やCPU使用量も計測できるような計測ツールを使う
  3. データ分析をして改善点を洗い出す
    • 問題の原因について先入観を持たず、データからボトルネックを分析して影響が大きい箇所から改善を行う
    • DB以外がボトルネックになることも普通にある
  4. GCの影響も考慮する
    • メモリ使用量の多いアプリケーションではGCが大きなオーバーヘッドになり得る
    • Go x Kubernetesの環境ではGOMEMLIMIT によるチューニングが効果的
    • Goでのメモリ使用量やCPU処理時間の計測はpprofを使えば良さそう

また弊社のようなスタートアップ企業では改善速度の重要度を高く設定しているため、計測が重要だとわかってはいるものの計測の環境が整っていないことを理由として計測をスキップしてしまうことも多々あります。そのような状況に直面した場合は将来のリターンやランニングコストなどを加味した上で計測環境を整えることを検討することも重要だと今回のタスクを通じて感じました。

終わりに

今回の高速化タスクでは

  • 計測をしないでとりあえず気になるところを改善する
  • 雑な計測する
  • DBがボトルネックだと思い込む

などの失敗により無駄な時間を割いてしまいましたが目標としていたパフォーマンス改善は実現でき、また結果として失敗からの学びも大きかったです。
今後は今回の学びを活かし生産性向上に繋げるととに、このような日頃のタスクにおける小さい失敗についても学びを共有して知見を貯めることを意識して強いエンジニア組織を作っていきたいと思います。

We are hiring!

ブルーモでは次世代の金融システムを作る仲間を募集しています!
エンジニア、デザイナー、PdM、事業開発などさまざまポジションで募集をしているので興味がある方は下記の採用ページを覗いてみてください!

https://careers.bloomo.co.jp/

ブルーモ証券 テックブログ

Discussion