Slide 1

Slide 1 text

Copyright© M&Aクラウド Repositoryパターンを維持しながら N+1問題を起こさないようにする方法論 PHP Conference 2021 @yamotuki

Slide 2

Slide 2 text

Copyright© M&Aクラウド 今日の話題: レスポンス速度と設計の話 2

Slide 3

Slide 3 text

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/

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

Copyright© M&Aクラウド レスポンス速度について 6 TTFB(Time To First Byte) ● サーバから最初のバイトがブラウザに到達するまでの時間 ● 要するに、以下の要素の合計 ○ ネットワーク的距離 ■ ユーザの近くにサーバを置く ○ サーバにおける処理時間 ■ PHPのコードの改善

Slide 7

Slide 7 text

Copyright© M&Aクラウド レスポンス速度について 7 極限まで早くしたいか? エンジニアとしては追い求めたい

Slide 8

Slide 8 text

Copyright© M&Aクラウド レスポンス速度について 8 ビジネス視点 ● ユーザに提供するのは速度だけではない ● 使いやすい機能を開発する時間も大事 ● サイト速度は大事だが、工数をかけすぎてはいけない ● 速度改善で今後の機能提供がしづらくなるのも NG

Slide 9

Slide 9 text

Copyright© M&Aクラウド 今日使う例について 9

Slide 10

Slide 10 text

Copyright© M&Aクラウド 今回の例 10 管理画面における一覧の例 ユーザ情報だけの一覧

Slide 11

Slide 11 text

Copyright© M&Aクラウド 今回の例 11 内部的な既存実装の前提① UserRepository を通して User Entity の List を取得 Infra/UserRepository@getList() UserRepository@getList() UserQueryService Infra Repository Implementation Domain Repository Interface Application Layer

Slide 12

Slide 12 text

Copyright© M&Aクラウド 今回の例 12 こういう管理画面を表示させたい もともと時計の列は無くて、今回機能追加したい

Slide 13

Slide 13 text

Copyright© M&Aクラウド 作りたい Data Transfer Object(以下DTO) 13

Slide 14

Slide 14 text

Copyright© M&Aクラウド 今回の例 14 内部的な既存実装の前提② ● Watch も独立した Entity として存在する ● Watch の中に所有者情報として一つの User Id ● 簡略化のための前提 ○ 所有者は1人とする ○ 1人1個とする

Slide 15

Slide 15 text

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()

Slide 16

Slide 16 text

Copyright© M&Aクラウド 今回の例 - 愚直にやったパターン 16 N+1問題の何が悪いのか? ● SQL実行が1個 0.02sで完了するとしても ● 300回直列で実行したら6秒かかる ● TTFB ○ サーバ処理 ■ SQL実行 => ここだけで6秒 ■ その他PHP処理 ○ ネットワーク通信

Slide 17

Slide 17 text

Copyright© M&Aクラウド 改善方法 17 改修方法案 1. キャッシュ 2. ページネーション 3. Command Query Separation 4. Hash Map Attachment法(New!)

Slide 18

Slide 18 text

Copyright© M&Aクラウド 改善案1. キャッシュを使う 18

Slide 19

Slide 19 text

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() 設計への影響: 低

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

Copyright© M&Aクラウド 改善案2. ページネーションを入れる 21

Slide 22

Slide 22 text

Copyright© M&Aクラウド 2. ページネーション 22 ● N + 1のNを減らしてしまおうという発想 ● 1ページに表示する件数をN=100件までなど制限する ● メリット ○ TTFBの改善効果「中」 & 設計への影響「中」 ○ ブラウザレンダリングの軽量化に効果が高い(本発表趣旨からややズレる) ● デメリット: ○ 仕様として一覧性が重要なケースだと使いづらい ○ Ctrl + F でのページ内検索が使えなくなる ○ 検索機能を追加したり工数がかかることも

Slide 23

Slide 23 text

Copyright© M&Aクラウド 改善案3. Command Query Separation 23

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

Copyright© M&Aクラウド 3. Command Query Separation 25 ● メリット ○ TTFBへの効果「大」 ○ N+1の解決策にはなっている ● デメリット ○ 設計への影響「大」。既存の設計と食い違う ○ 取得が Repository と Port&Adapter で2箇所に散らばる ● コードを書くときの辛さ ○ 今までのEntityを使ったコードを使いまわせないので大変 ○ フィールドの多いDTOを組み上げる処理を書くだけで日が暮れる ● コードを拡張するときの辛さ ○ DBのフィールドを変更したときに忘れずに 2箇所直さないといけないので大変

Slide 26

Slide 26 text

Copyright© M&Aクラウド 3. Command Query Separation 26 ● すでにRepositoryパターンが入っているアプリケーションにおいてという前提で、できれば導 入しない方が望ましい ● 弊社では過去に一部導入を試してみたが、辛いのでそれ以降はどうしても必要なパターン以 外は導入しないようにしている

Slide 27

Slide 27 text

Copyright© M&Aクラウド 基本に立ち返る 27 設計の目的ってなんだっけ? ● 目的 ○ 機能を追加したり削除するのが簡単に行えるようにすること ● 誰のため? ○ ユーザのため: より良い機能を素早く提供する ○ 開発者のため: 機能を追加するのに辛い気持ちにならないこと

Slide 28

Slide 28 text

Copyright© M&Aクラウド 基本に立ち返る 28 具体的にどういう手法だったら良い? ● 一つの意図の改修をするのに処理が散らばっていないこと ○ いわゆる「凝集度が高い」状態 ● 速度改善の目的を達成するために不必要な複雑性を持ち込みたくない ○ 既存の設計をできるだけ変えないことで保守コストを抑えたい ○ 簡単に導入できて高い効果を得たい

Slide 29

Slide 29 text

Copyright© M&Aクラウド 改善案4: Hash Map Attachment 法 (独自命名) 29

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

Copyright© M&Aクラウド 4. Hash Map Attachment法 31 UserRepository@getList() WatchRepository@getList(): WatchList UserQueryService ① User Entity List取得 ② Watch List を1回だけ取得 Domain get がN回走るからよくないので、予め getList で取得しちゃおう

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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 を使おう

Slide 34

Slide 34 text

Copyright© M&Aクラウド コード例 34

Slide 35

Slide 35 text

Copyright© M&Aクラウド 終わりに 35 Hash Map Attachment法という命名について ● Hash Map を元のリストにくっつける(Attachment)する ● 社内の啓蒙のために私がつけた名前なのでググっても他の記事は出ません ○ 詳細はこちら https://tech.macloud.jp/entry/2021/03/24/154426 ● 近い発想は Laravel Eloquent の eager loading (with関数)で使われてます ● 適用レイヤが違うので便宜的に違う名前をつけました

Slide 36

Slide 36 text

Copyright© M&Aクラウド 終わりに 36 ● アプリケーションをキメラ化させない ○ 銀の弾丸はない ○ 今あるアプリケーションや状況にあわせた、 ROIの高い設計を導入しましょう ● Hash Map Attachment 法も全てのアプリケーションで推奨するものではありません ● 設計の目的を忘れてはならない ○ ユーザのため ○ 開発者のため 振り返り

Slide 37

Slide 37 text

Copyright© M&Aクラウド 37 ● 採用してます ○ https://www.wantedly.com/projects/513494 ● Twitter ○ https://twitter.com/yamotuki

Slide 38

Slide 38 text

Copyright© M&Aクラウド 38 フィードバックお願いします! https://joind.in/event/php-conference-japan-2021