Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Repositoryパターンを維持しながらN+1問題を起こさないようにする方法論について
Search
Tomoya-Suzuki
October 03, 2021
2
1.5k
Repositoryパターンを維持しながらN+1問題を起こさないようにする方法論について
PHP Conference 2021 の 25 min のセッションの資料です。
Tomoya-Suzuki
October 03, 2021
Tweet
Share
More Decks by Tomoya-Suzuki
See All by Tomoya-Suzuki
安易に前職同僚飲み会に行ったら 売り上げのほぼないスタートアップに入社してた話
yamotuki
0
1.1k
プログラマ三大美徳を実現するデプロイフローを目指して
yamotuki
0
1k
再コンパイル不要._core_dump_さえ吐ければ_gdb_デバッグできます.pdf
yamotuki
0
490
PHPでleetCodeのeasyレベル100問ノック
yamotuki
0
2k
Featured
See All Featured
Writing Fast Ruby
sferik
626
60k
Docker and Python
trallard
40
3k
The Myth of the Modular Monolith - Day 2 Keynote - Rails World 2024
eileencodes
12
1.5k
StorybookのUI Testing Handbookを読んだ
zakiyama
26
5.1k
Designing with Data
zakiwarfel
98
5.1k
Reflections from 52 weeks, 52 projects
jeffersonlam
346
20k
Raft: Consensus for Rubyists
vanstee
136
6.6k
Sharpening the Axe: The Primacy of Toolmaking
bcantrill
37
1.7k
CoffeeScript is Beautiful & I Never Want to Write Plain JavaScript Again
sstephenson
159
15k
Web development in the modern age
philhawksworth
205
10k
Navigating Team Friction
lara
183
14k
What's new in Ruby 2.0
geeforr
341
31k
Transcript
Copyright© M&Aクラウド Repositoryパターンを維持しながら N+1問題を起こさないようにする方法論 PHP Conference 2021 @yamotuki
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クラウド レスポンス速度について 6 TTFB(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() UserQueryService Infra Repository Implementation Domain Repository Interface Application 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(): 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. キャッシュを使う 18
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. ページネーションを入れる 21
Copyright© M&Aクラウド 2. ページネーション 22 • N + 1のNを減らしてしまおうという発想 •
1ページに表示する件数をN=100件までなど制限する • メリット ◦ TTFBの改善効果「中」 & 設計への影響「中」 ◦ ブラウザレンダリングの軽量化に効果が高い(本発表趣旨からややズレる) • デメリット: ◦ 仕様として一覧性が重要なケースだと使いづらい ◦ Ctrl + F でのページ内検索が使えなくなる ◦ 検索機能を追加したり工数がかかることも
Copyright© M&Aクラウド 改善案3. Command Query Separation 23
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 法 (独自命名) 29
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クラウド コード例 34
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
Copyright© M&Aクラウド 38 フィードバックお願いします! https://joind.in/event/php-conference-japan-2021