目次
はじめに
弊社ではECCUBE4系をベースに構築したネットスーパープラットフォームである「マルクト」を2021年の6月にOPENしました。
OPEN当初は「対象配送エリア:1エリア、お買い物できる店舗:1店舗」と小さなサービスでしたが、その後徐々に配送エリア・店舗数ともに拡大を進め、サイト全体で扱うデータ数の増加・機能追加によるシステムの複雑さが増していきます。
サービス開始半年後からパフォーマンス問題が顕在化し、社内でも重要課題として認識され始めました。
ユーザーにスムーズな買い物体験を提供するべく、早急に改善が必要であるとし、パフォーマンス改善のプロジェクトをスタートしました。
「マルクト」はなぜ遅かったのか
① 別システムの転用による構造上の課題
マルクトの前身サービスとして、当社ではECCUBE4系をカスタマイズしたRackerというネットスーパーのプラットフォームサービスを提供していました。
同じネットスーパーということもあり、マルクトでもRackerの機能の多くを利用できるため、マルクトはこのRackerのシステム基盤に、約2カ月の軽微開発を加えて初期リリースを迎えました。
しかし、Rackerのコンセプトは店舗毎にカートが分かれるという構造である一方、マルクトはエリア内の店舗を横断して商品を購入できるというコンセプトになっており、機能的には共通しつつ構造としては大きく違いがありました。
もちろんこの構造の違いを見過ごしていた訳ではありませんが、新規事業であるという性質上、まずは最低限の投資でOPENすることを優先し、マルクトのビジネス構造に合わせた抜本的なリファクタリングが後回しになったことで、冗長な処理が生じていました。
② 描画判定の複雑さ(エリア/店舗/ユーザー)に起因したコスト増
2023年6月現在、マルクトは3エリア(都内11区)17店舗からの注文が可能となり、商品マスタに登録されているデータも8万点を超えています。
マルクトはユーザーの近所のお店から「まとめて買って」「まとめて届く」のが特徴のネットスーパーです。
ユーザーがお届け先の郵便番号を設定することで、注文可能なエリアを絞り込み、エリアに紐づく店舗の商品であれば、複数の店舗の商品を横断的に閲覧・注文が可能になります。
また、マルクトはモール型のネットスーパーであるため、エリア内の全商品が見ることができるページに加えて、特定の店舗内の商品から探すことができる店舗ページが存在します。
そのため、システム的にはどのような情報を出すにも、エリア・店舗といった絞り込みの条件が加わる構成になっています。
そのほか、販促施策として打っているキャンペーン※により、エリア・店舗軸に加えてユーザー毎に表示要素も変わってくるため、複数のデータを加味して情報の出しわけや計算を行わなければなりません。
先の構造上の課題に加え、サービス単体としての複雑さも加わり、処理コストの大きなシステムになりました。
※「前週購入実績のあるユーザーはポイント付与率がUP」
高速化を実現するために何を考える?
① 改善にはビジネス都合の考慮も必要
開発者の方は、複雑なシステムや負債のあるシステムの開発に関わっていると、“綺麗に”リファクタリングしたいと考えがちではないでしょうか。
私自身、システム構成がシンプルかつ綺麗に設計されていれば、パフォーマンス問題は発生しないのでは、、、と思うことがしばしばあります。
しかしながら、ビジネス観点では、“より素早く” “より大きな”効果を生み出すことが求められます。
すでに稼働しているシステムや多くの機能が育っているシステムにおいて、構成を練り直すというのは、サイト全ての機能に影響する可能性があり、多くの機能にデグレリスクが発生します。
全体をリファクタリングするとなると、それなりの工数やデータ移行なども必要になるかもしれません。かと言って、多少のリファクタリングでは効果が薄い場合も多いです。
費用や工数に余裕があり、より確実にパフォーマンスが改善される見込みがあるのであればそれも1つの手ですが、そのような余裕はないケースが多いのではないでしょうか。
ユーザーからしてみれば、コードやデータ構造といったシステム的な綺麗さは意識することはありません。
サイトがスムーズに動き、サービスが使いやすくあればメリットを感じることができますし、サービスの成長に寄与します。
あまりに力技の開発を行えば、その後の改修コスト/期間に跳ね返ります。
ビジネス面を考えて方針を検討する場合には、“システムを綺麗にする”だけでなく、“迅速かつ確実に速度を改善する”ことを念頭にしたアプローチ方法を検討することも大事だと考えます。
② 調査で効果の大きい対策を見極める
システムが複雑になればなるほど、その原因が多岐に渡ることが多いです。
そのすべてをひとつずつ解消していくアプローチもありますが、前述の通りビジネス面からは“より素早く” “より大きな”効果を生み出すことを求められるため、効果の大きな箇所を優先して対応していくことも重要です。
そのためにはまず“最も速度的にネックになっている処理はどこなのか”、もしくは“サイトユーザーにとって一番ストレスを感じる部分はどこなのか”を把握します。
マルクトに限らず一般的なECサイトの場合、ユーザーの利用頻度が高い(=回遊が多い)のは、TOP/商品一覧/商品詳細になります。
一方で、カート以降の注文系のページは、1回の注文で1度しか通らないことが多いです。
そのため、マルクトの速度改善プロジェクトでは、カート以前を最優先課題とし、カート以降の処理は速度改善の対象から外すことになりました。
(カート以降はお金に関わることもあり、デグレを懸念したこともありますが、、、)
③ システムの特徴を理解する
マルクトはEC-CUBEというOSSを拡張したサービスであり、EC-CUBEはPHPのフレームワークであるSymfonyで構築されています。
SymfonyではDBアクセスにDoctrineというORMを利用しています。
ORMはSQLを書くことなくDBに対して操作ができ、アプリケーションのModelとマッピングもしてくれるので、実装初心者でも比較的扱いやすいというメリットがあります。
一方、あまり意識をせずに実装を進めていくと、簡単に「N+1問題」が発生してしまいます。
また、ECCUBEには「ブロック管理」という機能を持っており、CMS的に簡単にブロックのレイアウトを変更することが可能です。
「マルクト」は品揃え豊かで賑わいを演出するために、一般的なECサイトより多くのコンテンツを表示していますが、このブロック機能があるために、ブロックごとにリクエストを行わなければならず、同じような処理に対しても冗長なリクエストとそれに伴う冗長なクエリ発行でパフォーマンスが悪化していました。
このようにシステム特徴を理解しておくと、原因調査や方針策定に有効な場合も多く、紋切型の解決策を持つのではなく、システムを知った上で解決策を考えることも重要だと考えます。
3段階で高速化を実現する
改善フェーズ① DBクエリチューニング
開発者の方が「パフォーマンス改善」ときくと、「クエリチューニング」を思い浮かべる方も多いのではないでしょうか。
事実「特定のクエリが突出して時間を要しており、それが原因でサイト全体のパフォーマンスが低下する」というケースは、システムにおいてよくある事象だと思います。
マルクトにおいても、まず初めはそのような観点で調査を行いました。
しかしながら、特定のブロック表示やリクエストに時間がかかっているようには見えず、SQLのProcessListやオブザーバブルツールを使って見ても著しく時間のかかっているクエリはありませんでした。
そこで、SymfonyのProfilerを使用してTOPページのアクセス時に発行されているクエリを見てみると、 “リクエストのたびに似たようなクエリが何本も飛んでしまっている”ということが掴めました。
対処法として思いついたのは、ブロックの内部ロジックを1つずつ追っていくことで無駄のある処理をリファクタリングしていくことでした。
しかしながら、この方法はある程度の時間を要する上、効果を見積もり辛い部分もあり、ビジネス的に最適とは言い難い対応策です。
そこで、不確実性の高い対処方法に時間をかけることはできないと判断し、各リクエストの処理をリファクタリングする対応は見送ることになりました。
改善フェーズ② ページ静的化
これまでの調査により、冗長な処理があることは掴めていたものの、時間と確実性の観点でリクエストの細かい改善は取りやめました。
そこで、次に考えたのは、TOPページで使用しているブロックをできるだけ静的化して、ページを開いた際のリクエストを減らすことです。
もともと
ページアクセス
↓
ページに配置されている各ブロックを表示するためのAPIが実行される
↓
各ブロックAPIの中でいくつものクエリが実行される
↓
ページを表示するためにかなりの数のクエリが実行される
という構成になっており、ページの表示がかなり待たされる印象がありました。
各ブロックで表示したい情報は定時バッチでDBアクセスし、html形式に変換しておくことで、ページのアクセス時にはDBアクセスを伴わずに情報を表示することができます。
静的化対応の結果、5〜6秒かかっていたTOPページへのアクセスは、平均1〜1.5秒で表示されるように改善されました。
[対処1.静的化]
メリット:細かいロジックの調査・修正を伴わずに、冗長な処理でユーザーを待たせなくてよくなる。
デメリット:静的化対応をした部分はデータが即時反映ではなくなる。
改善フェーズ③-1 外部検索ツールの導入
先の静的化により、ブロック表示は早くなったものの、商品検索など静的化できない要素に関しては、依然として課題が残りました。
ECサイトにおいて「欲しい商品をスムーズに検索できない」という状態はユーザーの購買意欲の低下に繋がります。
一般的なECサイトでも、検索はネックになりやすいですが、マルクトの場合は、さらに「エリア」「店舗」という概念もあるため、ユーザーが登録している郵便番号に応じて表示する商品やコンテンツの出し分けが必要になります。
先の静的化したブロックと同様に、検索処理においても「エリア」「店舗」の情報を取得して判定する箇所が無限にあり、処理本数が膨大になっていました。
内部でのクエリ改善やElastic searchなどの利用も検討しましたが、検索キーワードに応じた表示順の最適化や、あいまい検索対応ECサイトに最適化した機能を標準で持っていることから外部の検索ツールを導入することに決定いたしました。
[対処2:検索ツール導入]
メリット:検索ロジックの実装をツール側に転嫁できるため、複雑さによる速度の心配がない。自分達で実装するよりも楽に検索の精度を高められる。
デメリット:ツール導入費用、毎月の利用料金がかかる。
改善フェーズ③-2 SPA化
SPA(Single Page Application)とは、従来のWebページがページ遷移のたびに要素の全体をサーバーから取得していたのに対し、必要な要素のみ更新することで、高速なページ遷移が実現できるアプリケーション構成です。
TOPページを含め、検索を伴わないページに関しては、フェーズ2の静的化で大きく速度改善を実現しました。また、検索ツールの導入により、検索処理においてもある程度の改善を見込める状態となりました。
しかし、更なるユーザーの体感速度の向上と、今後の改修コストも考慮した際に、SPAによって要素をコンポーネント化してしまった方が、パフォーマンス的にも改修コスト的にも優位である、と判断し、TOPページと商品一覧・商品詳細ページをSPA化することになりました。
SPA化対応の結果、ほぼ0秒台でページの遷移が可能になりました。
[対処3:SPA化]
メリット:ページ遷移をせず回遊が可能になることで、カート追加までの動線がよりスムーズになる。UI変更に迅速に対応しやすくなる。
デメリット:ECCUBE標準で提供されているCMSの機能が使えなくなる。
改善フェーズ③-3 APIのパフォーマンス改善(N+1問題の解消)
少し余談になりますが、SPA化をした後、一部のAPIの実行に時間がかかってしまい、以前より表示が遅く感じる事象が発生してしまいました。
調査をしたところ、「静的化」の対応時に目をつぶっていた各APIの中の処理に時間がかかっているということがわかりました。先にも触れた “N+1問題”です。
先ほどフェーズ①では、正攻法のリファクタリングは避けましたが、さすがにここまで特定されてくるとリファクタリングでの対応の方が早く効果的であることが明確になり、最後は結局リファクタリングを行いました。
いろいろな経緯はあると思いますが、やはり最後は基本的な部分に落ちてくるので、エンジニアとしては開発時点でN+1問題が起きないように考慮したいですね!
[対処4:N+1問題の解消]
メリット:システムとして正攻法で速度改善ができる。(=技術的負債を残さない)
デメリット:調査工数がかかる場合がある。ロジック修正によるデグレリスクがある。
最後に
いかがでしたでしょうか。このように「マルクト」では、 1年以上かけて複数のアプローチを試し、段階的にパフォーマンス問題の対処にあたってきました。
「マルクト」の事例が、パフォーマンス改善に悩む開発者の方の参考になれば幸いです。
Diezonでは、引き続き”より良い買い物体験”をユーザーに提供するべく、サービスの品質向上に向かって開発を進めてまいります。