こちらの記事はコドモン Advent Calendar 2022の10日目の記事です。
こんにちは、登降園・請求管理機能チーム所属の海部です。
コドモンの登降園管理機能には、各施設が園児全員の1か月分の登降園データをCSV形式で出力する機能があります。1か月分のデータをすべてエクスポートできる便利な機能です。一方、対象園児数が当初想定していたよりも遥かに多いユースケースでは処理時間が課題となっていました。
今回は技術的調査により上記課題を解消し、登降園データ出力の高速化に成功したリアルな過程を紹介します。
解決までの過程 その1
Xdebug Profiling と Webgrind によるパフォーマンス解析
まずは該当機能のプロファイリングを実施して、処理に時間がかかる原因を調べてみました。
PHPコードでしたので、XdebugのProfiling機能を使用しプロファイリング結果を取得しました。 結果の可視化のため、Webgrindというシンプルな結果可視化ツールを用いました。
原因
手元の開発環境で調査した結果、内部で使用されているある関数が全体の10%を占めていることがわかりました。手元のデータが少ない環境ですら10%を占めていましたので、この関数を使用している部分が怪しいと見て調査しました。
関数の使われ方を詳しく見てみると、ある園児のデータを取得するために、30日分の園児すべての登降園データが入った配列内で線形探索していることが判明しました。
園児数Nとしたとき、園児すべての登降園データ数は30×Nとなるため、処理全体として30×N2のループが回ります。これはN=1000のとき、約107程度のループが回る計算となり、この処理が原因と判断しました。
ボトルネックの解決
各園児の情報を取得するのに線形探索を行っていることが原因でしたので、各園児のidをkeyとする連想配列を作成し、対象園児30日分のデータを振り分ける対応を取りました。
連想配列による検索はO(1)ですので、元の処理で検索にかかっていたO(30×N)の処理がO(1)になり、全体のループとしてもO(30×N2)がO(N)となりました。
結果
課題となっていたデータ量で試した所、処理時間が1/30程度となりました🎉
この解決によりパフォーマンスは問題ないかと思われたのですが、取得するデータ種別が多い場合に、別のパフォーマンス問題が発生することが発覚しました。
解決までの過程 その2
原因調査
課題となった状況をまずステージング環境で再現しました。
余分なループによる高負荷が原因と考え「その1」と似た修正を加えたものの、処理時間はそこまで変わらなかったため、別のアプローチが必要と考えました。
原因と解決
ログを仕込んだりしながら改めてコードを眺めると、DBへの取得処理が相当数あることに気が付きました。情報を取得する関数においてある一人の情報を取得する際SELECT文はそこまで多く投げられないものの、多くの人数に対しスケールするような設計となっていなかったのです。
そこで、多少姑息な解決ではあるものの、DBへのアクセスを一度で済むような専用関数を切り出す判断をしました。既存関数ごとリファクタする選択肢もありましたが、他の複数箇所で使われていたことを鑑みた結果でした。既存関数の挙動担保のための必要テスト工数を考えた上で苦渋の決断をしました。
結果、対象園児数が当初想定していたよりも遥かに多いユースケースでも問題なく使える時間でダウンロードできるようになりました🎉
まとめ
今回はパフォーマンス課題に対してのアプローチの一例を紹介しました。
まず、すべてのコードを最初から最後まで眺めるのではなく、プロファイリングツールを使用して被疑箇所を明確にしました。
その上で被疑箇所の計算量を考えて、課題となっているスケールに耐えられるかを検討しました。今回は計算量を調査した上でパフォーマンス改善を行うという手段を取りましたが、場合によっては並列化やマシンパワーで対応するのも手かもしれません。
また、DBなどのI/Oが絡む処理は必要最低限にすることが有効であることも改めて実感しました。DBからデータを取得するために用意された関数は、場合によってはスケーラビリティが考慮されていない可能性があり、課題となるスケールに耐えれるかを検討することの重要性を認識した一例でした。
今後もこういった課題を解決していくことで、施設や保護者といったユーザーの方々に快適にご利用いただけるようにパフォーマンスを向上していきます。
コドモンの開発チームのTwitterを始めました! アドベントカレンダーの新着記事も毎日ツイートしていくので、ぜひフォローしてください😊