Share
Embed
Repositoryパターンを維持しながらN+1問題を起こさないようにする方法論について
PHP Conference 2021 の 25 min のセッションの資料です。
Tomoya-Suzuki
October 03, 2021
Transcript
38 slides
-
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クラウド レスポンス速度について 6 TTFB(Time To First Byte) • サーバから最初のバイトがブラウザに到達するまでの時間
• 要するに、以下の要素の合計 ◦ ネットワーク的距離 ▪ ユーザの近くにサーバを置く ◦ サーバにおける処理時間 ▪ PHPのコードの改善 -
Copyright© M&Aクラウド レスポンス速度について 8 ビジネス視点 • ユーザに提供するのは速度だけではない • 使いやすい機能を開発する時間も大事 •
サイト速度は大事だが、工数をかけすぎてはいけない • 速度改善で今後の機能提供がしづらくなるのも NG -
Copyright© M&Aクラウド 今回の例 11 内部的な既存実装の前提① UserRepository を通して User Entity の
List を取得 Infra/UserRepository@getList() UserRepository@getList() UserQueryService Infra Repository Implementation Domain Repository Interface Application Layer -
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(): Watch UserQueryService ① User Entity List取得 ② foreach で Watch Entity を1個ずつN回取得 Domain Repository Interface Infra/UserRepository@getList() Infra Repository Implementation WatchRepository@get() -
Copyright© M&Aクラウド 今回の例 - 愚直にやったパターン 16 N+1問題の何が悪いのか? • SQL実行が1個 0.02sで完了するとしても
• 300回直列で実行したら6秒かかる • TTFB ◦ サーバ処理 ▪ SQL実行 => ここだけで6秒 ▪ その他PHP処理 ◦ ネットワーク通信 -
Copyright© M&Aクラウド 改善方法 17 改修方法案 1. キャッシュ 2. ページネーション 3.
Command Query Separation 4. Hash Map Attachment法(New!) -
Copyright© M&Aクラウド 1. キャッシュ 19 DBの前段に memcached などキャッシュを差し込む UserRepository@getList() WatchRepository@get()
UserQueryService ① User Entity List 取得 ② foreach で Watch Entity を1個ずつN回取得 Domain Repository Interface Infra/UserRepository@getList() Infra Repository Implementation WatchRepository@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/2d1c74c3253e9c3b0562 CURLでWEBサイトのパフォーマンス測定 , https://sites.google.com/site/kanta01web/techmemo-2/curldewebsaitonopafomansuceding -
Copyright© M&Aクラウド 2. ページネーション 22 • N + 1のNを減らしてしまおうという発想 •
1ページに表示する件数をN=100件までなど制限する • メリット ◦ TTFBの改善効果「中」 & 設計への影響「中」 ◦ ブラウザレンダリングの軽量化に効果が高い(本発表趣旨からややズレる) • デメリット: ◦ 仕様として一覧性が重要なケースだと使いづらい ◦ Ctrl + F でのページ内検索が使えなくなる ◦ 検索機能を追加したり工数がかかることも -
Copyright© M&Aクラウド 3. Command Query Separation 24 Infra/UserRepository@getList() UserRepository@getList() UserQueryService
UserPort@getDTOList() Infra Domain Application Layer UserAdapter@getDTOList() 最初からDTOの形で取得 Infra層で join などしてDTOに直接入れる 注記: 本発表の本題からズレるので、 CQSとCQRSについての詳細議論はしません。 • こちらなど参照: https://qiita.com/hirodragon/items/6281df80661401f48731 -
Copyright© M&Aクラウド 3. Command Query Separation 25 • メリット ◦
TTFBへの効果「大」 ◦ N+1の解決策にはなっている • デメリット ◦ 設計への影響「大」。既存の設計と食い違う ◦ 取得が Repository と Port&Adapter で2箇所に散らばる • コードを書くときの辛さ ◦ 今までのEntityを使ったコードを使いまわせないので大変 ◦ フィールドの多いDTOを組み上げる処理を書くだけで日が暮れる • コードを拡張するときの辛さ ◦ DBのフィールドを変更したときに忘れずに 2箇所直さないといけないので大変 -
Copyright© M&Aクラウド 3. Command Query Separation 26 • すでにRepositoryパターンが入っているアプリケーションにおいてという前提で、できれば導 入しない方が望ましい
• 弊社では過去に一部導入を試してみたが、辛いのでそれ以降はどうしても必要なパターン以 外は導入しないようにしている -
Copyright© M&Aクラウド 基本に立ち返る 27 設計の目的ってなんだっけ? • 目的 ◦ 機能を追加したり削除するのが簡単に行えるようにすること •
誰のため? ◦ ユーザのため: より良い機能を素早く提供する ◦ 開発者のため: 機能を追加するのに辛い気持ちにならないこと -
Copyright© M&Aクラウド 基本に立ち返る 28 具体的にどういう手法だったら良い? • 一つの意図の改修をするのに処理が散らばっていないこと ◦ いわゆる「凝集度が高い」状態 •
速度改善の目的を達成するために不必要な複雑性を持ち込みたくない ◦ 既存の設計をできるだけ変えないことで保守コストを抑えたい ◦ 簡単に導入できて高い効果を得たい -
Copyright© M&Aクラウド 4. Hash Map Attachment法 30 何が問題だったんだっけ? UserRepository@getList() WatchRepository@get():
Watch UserQueryService ① User Entity List 取得 ② foreach で Watch Entity を1個ずつN回取得 Domain Application Layer -
Copyright© M&Aクラウド 4. Hash Map Attachment法 31 UserRepository@getList() WatchRepository@getList(): WatchList
UserQueryService ① User Entity List取得 ② Watch List を1回だけ取得 Domain get が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=1 Hash Map を使おう -
Copyright© M&Aクラウド 終わりに 35 Hash 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