PHP Conference 2021 の 25 min のセッションの資料です。
Copyright© M&AクラウドRepositoryパターンを維持しながらN+1問題を起こさないようにする方法論PHP Conference 2021@yamotuki
View Slide
Copyright© M&Aクラウド今日の話題: レスポンス速度と設計の話2
Copyright© M&Aクラウド 3レスポンス速度について重いサイトはユーザーが離れる● 直帰率の上昇○ 「2秒以内に読み込まれるページの平均直帰率は9%」○ 「5秒以内に読み込まれるページの直帰率は38%に急上昇」● コンバージョン低下やSEOへの影響など他多数「Website Load Time Statistics: Why Speed Matters in 2021」, https://www.websitebuilderexpert.com/building-websites/website-load-time-statistics/
Copyright© M&Aクラウドレスポンス速度について4速度に関する指標● TTFB● StartRender● Visual Complete● Speed Index● onLoad● Fully Loaded「Here’s What We Learned About Page Speed」, https://backlinko.com/page-speed-stats
Copyright© M&Aクラウドレスポンス速度について5速度に関する指標● TTFB => 今回はこれを見るところ● StartRender● Visual Complete● Speed Index● onLoad● Fully Loaded「Here’s What We Learned About Page Speed」, https://backlinko.com/page-speed-stats
Copyright© M&Aクラウドレスポンス速度について6TTFB(Time To First Byte)● サーバから最初のバイトがブラウザに到達するまでの時間● 要するに、以下の要素の合計○ ネットワーク的距離■ ユーザの近くにサーバを置く○ サーバにおける処理時間■ PHPのコードの改善
Copyright© M&Aクラウドレスポンス速度について7極限まで早くしたいか?エンジニアとしては追い求めたい
Copyright© M&Aクラウドレスポンス速度について8ビジネス視点● ユーザに提供するのは速度だけではない● 使いやすい機能を開発する時間も大事● サイト速度は大事だが、工数をかけすぎてはいけない● 速度改善で今後の機能提供がしづらくなるのもNG
Copyright© M&Aクラウド今日使う例について9
Copyright© M&Aクラウド今回の例10管理画面における一覧の例ユーザ情報だけの一覧
Copyright© M&Aクラウド今回の例11内部的な既存実装の前提①UserRepository を通して User Entity の List を取得Infra/UserRepository@getList()UserRepository@getList()UserQueryServiceInfra Repository ImplementationDomain Repository InterfaceApplication Layer
Copyright© M&Aクラウド今回の例12こういう管理画面を表示させたいもともと時計の列は無くて、今回機能追加したい
Copyright© M&Aクラウド作りたい Data Transfer Object(以下DTO)13
Copyright© M&Aクラウド今回の例14内部的な既存実装の前提②● Watch も独立した Entity として存在する● Watch の中に所有者情報として一つの User Id● 簡略化のための前提○ 所有者は1人とする○ 1人1個とする
Copyright© M&Aクラウド今回の例 - 愚直にやったパターン15● Repositoryは対象のEntityまたはそのリストしか返せないという制約があるとする● DTOを作るのに foreach を回すので N+1 回のSQL発行UserRepository@getList() WatchRepository@get(): WatchUserQueryService① User Entity List取得② foreach で Watch Entity を1個ずつN回取得Domain Repository InterfaceInfra/UserRepository@getList()Infra Repository ImplementationWatchRepository@get()
Copyright© M&Aクラウド今回の例 - 愚直にやったパターン16N+1問題の何が悪いのか?● SQL実行が1個 0.02sで完了するとしても● 300回直列で実行したら6秒かかる● TTFB○ サーバ処理■ SQL実行 => ここだけで6秒■ その他PHP処理○ ネットワーク通信
Copyright© M&Aクラウド改善方法17改修方法案1. キャッシュ2. ページネーション3. Command Query Separation4. Hash Map Attachment法(New!)
Copyright© M&Aクラウド改善案1. キャッシュを使う18
Copyright© M&Aクラウド1. キャッシュ19DBの前段に memcached などキャッシュを差し込むUserRepository@getList() WatchRepository@get()UserQueryService① User Entity List 取得② foreach で Watch Entity を1個ずつN回取得Domain Repository InterfaceInfra/UserRepository@getList()Infra Repository ImplementationWatchRepository@get()CachedWatchRepository@get()設計への影響: 低
Copyright© M&Aクラウド1. キャッシュ20● 実はあんまり解決になってないかも(SQL実行が重いケースを除く)● AWS でのあるネットワーク条件下での例○ backサーバからmemcachedに接続速度を計測● time_total は 0.016s● これが300回直列で叩かれるとそれだけで 4.8s になる● キャッシュもDBもネットワーク上に存在するなら、いくらmemcachedやredisが早くても無意味curl でDBやキャッシュに接続 , https://qiita.com/yamotuki/items/2d1c74c3253e9c3b0562CURLでWEBサイトのパフォーマンス測定 , https://sites.google.com/site/kanta01web/techmemo-2/curldewebsaitonopafomansuceding
Copyright© M&Aクラウド改善案2. ページネーションを入れる21
Copyright© M&Aクラウド2. ページネーション22● N + 1のNを減らしてしまおうという発想● 1ページに表示する件数をN=100件までなど制限する● メリット○ TTFBの改善効果「中」 & 設計への影響「中」○ ブラウザレンダリングの軽量化に効果が高い(本発表趣旨からややズレる)● デメリット:○ 仕様として一覧性が重要なケースだと使いづらい○ Ctrl + F でのページ内検索が使えなくなる○ 検索機能を追加したり工数がかかることも
Copyright© M&Aクラウド改善案3. Command Query Separation23
Copyright© M&Aクラウド3. Command Query Separation24Infra/UserRepository@getList()UserRepository@getList()UserQueryService UserPort@getDTOList()InfraDomainApplication LayerUserAdapter@getDTOList()最初からDTOの形で取得Infra層で join などしてDTOに直接入れる注記: 本発表の本題からズレるので、CQSとCQRSについての詳細議論はしません。● こちらなど参照: https://qiita.com/hirodragon/items/6281df80661401f48731
Copyright© M&Aクラウド3. Command Query Separation25● メリット○ TTFBへの効果「大」○ N+1の解決策にはなっている● デメリット○ 設計への影響「大」。既存の設計と食い違う○ 取得が Repository と Port&Adapter で2箇所に散らばる● コードを書くときの辛さ○ 今までのEntityを使ったコードを使いまわせないので大変○ フィールドの多いDTOを組み上げる処理を書くだけで日が暮れる● コードを拡張するときの辛さ○ DBのフィールドを変更したときに忘れずに2箇所直さないといけないので大変
Copyright© M&Aクラウド3. Command Query Separation26● すでにRepositoryパターンが入っているアプリケーションにおいてという前提で、できれば導入しない方が望ましい● 弊社では過去に一部導入を試してみたが、辛いのでそれ以降はどうしても必要なパターン以外は導入しないようにしている
Copyright© M&Aクラウド基本に立ち返る27設計の目的ってなんだっけ?● 目的○ 機能を追加したり削除するのが簡単に行えるようにすること● 誰のため?○ ユーザのため: より良い機能を素早く提供する○ 開発者のため: 機能を追加するのに辛い気持ちにならないこと
Copyright© M&Aクラウド基本に立ち返る28具体的にどういう手法だったら良い?● 一つの意図の改修をするのに処理が散らばっていないこと○ いわゆる「凝集度が高い」状態● 速度改善の目的を達成するために不必要な複雑性を持ち込みたくない○ 既存の設計をできるだけ変えないことで保守コストを抑えたい○ 簡単に導入できて高い効果を得たい
Copyright© M&Aクラウド改善案4: Hash Map Attachment 法(独自命名)29
Copyright© M&Aクラウド4. Hash Map Attachment法30何が問題だったんだっけ?UserRepository@getList() WatchRepository@get(): WatchUserQueryService① User Entity List 取得 ② foreach で Watch Entity を1個ずつN回取得DomainApplication Layer
Copyright© M&Aクラウド4. Hash Map Attachment法31UserRepository@getList() WatchRepository@getList(): WatchListUserQueryService① User Entity List取得② Watch List を1回だけ取得Domainget がN回走るからよくないので、予め getList で取得しちゃおう
Copyright© M&Aクラウド4. Hash Map Attachment法32● 新たに出る問題点○ Watchリスト内を array_search(O(n))で探すとする○ User List のN回のループの中で、array_searchをやるので、O(n^2)になる○ 仮に N+1 問題を解決できたとしても以下の計算量■ 1000件の2重ループは 10^6● (多分)ここら辺までは1秒以内に収まることも多い■ 10000件とかは現実的な応答速度に収まるか未知これだけでいいのか?「プログラミングコンテスト攻略のためのアルゴリズムとデータ構造」,https://www.amazon.co.jp/dp/B00U5MVXZO/ref=dp-kindle-redirect?_encoding=UTF8&btkr=1&asin=B00U5MVXZO&revisionId=&format=4&depth=1
Copyright© M&Aクラウド4. Hash Map Attachment法33● Hash Map からの get は O(1)で高速● あらかじめ Watch List を Hash Map に変換(PHPでは key value array)○ key が User Id○ value が Watch Entity● User List ループ(O(n))で対応する Watch を Hash Map 経由で取得(O(1))○ ループ回してもO(n)の計算量になる「みんなのデータ構造」, https://www.amazon.co.jp/gp/product/4908686068/ref=ppx_yo_dt_b_search_asin_title?ie=UTF8&psc=1Hash Map を使おう
Copyright© M&Aクラウドコード例34
Copyright© M&Aクラウド終わりに35Hash Map Attachment法という命名について● Hash Map を元のリストにくっつける(Attachment)する● 社内の啓蒙のために私がつけた名前なのでググっても他の記事は出ません○ 詳細はこちら https://tech.macloud.jp/entry/2021/03/24/154426● 近い発想は Laravel Eloquent の eager loading (with関数)で使われてます● 適用レイヤが違うので便宜的に違う名前をつけました
Copyright© M&Aクラウド終わりに36● アプリケーションをキメラ化させない○ 銀の弾丸はない○ 今あるアプリケーションや状況にあわせた、ROIの高い設計を導入しましょう● Hash Map Attachment 法も全てのアプリケーションで推奨するものではありません● 設計の目的を忘れてはならない○ ユーザのため○ 開発者のため振り返り
Copyright© M&Aクラウド 37● 採用してます○ https://www.wantedly.com/projects/513494● Twitter○ https://twitter.com/yamotuki
Copyright© M&Aクラウド 38フィードバックお願いします!https://joind.in/event/php-conference-japan-2021