$30 off During Our Annual Pro Sale. View Details »

Repositoryパターンを維持しながらN+1問題を起こさないようにする方法論について

Tomoya-Suzuki
October 03, 2021
1.1k

 Repositoryパターンを維持しながらN+1問題を起こさないようにする方法論について

PHP Conference 2021 の 25 min のセッションの資料です。

Tomoya-Suzuki

October 03, 2021
Tweet

Transcript

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

    View Slide

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

    View Slide

  3. 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/

    View Slide

  4. 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

    View Slide

  5. 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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  20. 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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  24. 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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  30. 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

    View Slide

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

    View Slide

  32. 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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide