Slide 1

Slide 1 text

そのpreloadは必要? 見過ごされたpreloadが技術的負債として爆発した日 Shuta Mugikura (mugi) Kaigi on Rails 2025 2025/9/26

Slide 2

Slide 2 text

自己紹介 1 麦倉 柊太(Shuta Mugikura) / mugi ⚫ X: @mugi_1359 ⚫ GitHub: @mugitti9 ⚫ Kaigi on Rails オーガナイザー ⚫ 株式会社ギフティ

Slide 3

Slide 3 text

自己紹介 2 株式会社ギフティ ⚫ eギフトプラットフォーム ⚫ eギフトの発券・流通・販売まで一気通貫で提供

Slide 4

Slide 4 text

障害の概要 3 ある日の夜… 突然500番台エラーを返しすぎていることを知らせる通知がなる

Slide 5

Slide 5 text

障害の概要 4 ある日の夜… OOM GETのAPI

Slide 6

Slide 6 text

障害の概要 5 ある日の夜… OOM GETのAPI サーバのメモリは 1GBもあるはず

Slide 7

Slide 7 text

障害の概要 6 ある日の夜… 400MB

Slide 8

Slide 8 text

障害の概要 7 ある日の夜… 400MB grandchilrenは不要

Slide 9

Slide 9 text

障害の概要 8 ある日の夜… grandchilrenは不要 400MB ↓ 80%削減

Slide 10

Slide 10 text

9 preloadのメモリコスト

Slide 11

Slide 11 text

preloadのメモリコスト 10 Article ID: 1 Article ID: 2 Article ID: 3 Article.preload(comments: :likes)

Slide 12

Slide 12 text

preloadのメモリコスト 11 Article ID: 1 Article ID: 2 Article ID: 3 SELECT * FROM comments WHERE article_id IN (1, 2, 3) Article.pluck(:id) SELECT * FROM likes WHERE comment_id IN (1, 2, 3, ...)

Slide 13

Slide 13 text

preloadのメモリコスト 12 Article ID: 1 Comment ID: 1 Comment ID: 2 Comment ID: 3 Article ID: 2 Comment ID: 4 Comment ID: 5 Comment ID: 6 Article ID: 3 Comment ID: 7 Comment ID: 8 Comment ID: 9 Like ID: 1 Like ID: 2 Like ID: 3 Like ID: 4 Like ID: 5 Like ID: 6 Like ID: 7 Like ID: 8 Like ID: 9 Like ID: 10 Like ID: 11 Like ID: 12 Like ID: 13 Like ID: 14 Like ID: 15 Like ID: 16 Like ID: 17 Like ID: 18

Slide 14

Slide 14 text

preloadのメモリコスト 13 Article ID: 1 Article ID: 2 Article ID: 10 ・ ・ ・ Comment ID: 1 Comment ID: 2 Comment ID: 3 Comment ID: 100 ・ ・ ・ Like ID: 1 Like ID: 2 Like ID: 3 Like ID: 100 ・ ・ ・

Slide 15

Slide 15 text

preloadのメモリコスト 14 Article ID: 1 Article ID: 2 Article ID: 10 ・ ・ ・ Comment ID: 1 Comment ID: 2 Comment ID: 3 Comment ID: 100 ・ ・ ・ Like ID: 1 Like ID: 2 Like ID: 3 Like ID: 100 ・ ・ ・ 約100MB

Slide 16

Slide 16 text

preloadのメモリコスト 15 Article ID: 1 Article ID: 2 Article ID: 10 ・ ・ ・ Comment ID: 1 Comment ID: 2 Comment ID: 3 Comment ID: 100 ・ ・ ・ Like ID: 1 Like ID: 2 Like ID: 3 Like ID: 100 ・ ・ ・ もしこれが 使われないpreloadだったら?

Slide 17

Slide 17 text

障害時に何が起きていたか 16 content_tag_groups.preload( content_tag: { contents: { content_publish_permission: { content_registrations: :available_content_selections } } } )

Slide 18

Slide 18 text

障害時に何が起きていたか 17 children.sizeをするためpreload Parent parent.children_countを持つ 設計の方針変更 (例: counter_cacheの導入) parent = Parent.preload(:children).first parent.children.each { p it.do_some } scopeの追加 (例: activeのscopeの絞り込みを追加) parent = Parent.preload(:children).first parent.children.active.each { p it. do_some } Parent Children

Slide 19

Slide 19 text

障害時に何が起きていたか 18 content_tag_groups.preload( content_tag: { contents: { content_publish_permission: { content_registrations: :available_content_selections } } } )

Slide 20

Slide 20 text

障害時に何が起きていたか 19 content_tag_groups.preload(:content_tag)

Slide 21

Slide 21 text

障害時に何が起きていたか 20 content_tag_groups.preload( content_tag: { contents: { content_publish_permission: { content_registrations: :available_content_selections } } } ) content_tag_groups.preload(:content_tag) 約 400 MB ↓ 80%削減

Slide 22

Slide 22 text

障害時に何が起きていたか ⚫ 不要なpreloadが維持される ⚫ プロダクトの成長によってデータが増えていく ⚫ preloadをした際に作成されるActiveRecordインスタンスが増加 Out Of Memory が発生 21

Slide 23

Slide 23 text

22 なぜ未然に防げなかったのか 障害後にどのような対応を行ったか

Slide 24

Slide 24 text

なぜ未然に防げなかったのか ⚫ メモリ監視やアラートが弱く、兆候を早期に検知できなかった ⚫ preload のメモリ負荷についての認識がチーム内で不十分だった 23

Slide 25

Slide 25 text

なぜ未然に防げなかったのか ⚫ メモリ監視やアラートが弱く、兆候を早期に検知できなかった ⚫ preload のメモリ負荷についての認識がチーム内で不十分だった 24

Slide 26

Slide 26 text

なぜ未然に防げなかったのか 25

Slide 27

Slide 27 text

なぜ未然に防げなかったのか 26 意識的に見る必要がある ↓ 誰も負債が積み重なることに気付けない

Slide 28

Slide 28 text

なぜ未然に防げなかったのか 27

Slide 29

Slide 29 text

なぜ未然に防げなかったのか 28 ⚫ メモリ監視やアラートが弱く、兆候を早期に検知できなかった ⚫ preload のメモリ負荷についての認識がチーム内で不十分だった

Slide 30

Slide 30 text

なぜ未然に防げなかったのか 29 preloadは「N+1 を防ぐための便利なSolution」 とりあえず preload しておけば安心

Slide 31

Slide 31 text

なぜ未然に防げなかったのか 30 AVOID eager loading detected Parent => [:children] Remove from your query: .includes([:children])

Slide 32

Slide 32 text

なぜ未然に防げなかったのか 31 Model / Service / Viewなど Controllerから叩く部分の変更 それを利用する側のController, APIを確認

Slide 33

Slide 33 text

なぜ未然に防げなかったのか 32 Article HasManyAssociation (Comments) Comment Comment Comment target

Slide 34

Slide 34 text

33 まとめ

Slide 35

Slide 35 text

まとめ 34 ⚫ 不要なpreloadの蓄積によってサービスの可用性に関わるな障害へ ⚫ メモリのメトリクスが不足していた ⚫ preloadに対するリスクを認識できていなかった

Slide 36

Slide 36 text

まとめ 35 ⚫ 些細な負債の積み重ねが大きな障害へとつながる ⚫ 不要なpreloadという些細な問題から、プロダクトの成長によって、その 負債が蓄積し、全社的な障害へとつながった ⚫ Railsにおいても銀の弾丸は存在しない ⚫ 常にトレードオフが存在し、これを入れたら全てが解決するというような 手法は存在しない ⚫ 例えばpreloadの場合は、N+1を防げる一方で、不要なものを放置すると 多大なメモリ利用等につながる

Slide 37

Slide 37 text

まとめ 36 ⚫ 持続可能なRailsを維持するためには、ドメインの変化に合わせて 絶え間なく修復し続けることが必要 ⚫ 今回はそれを怠ったため、大規模な障害を引き起こしてしまった

Slide 38

Slide 38 text

まとめ 37

Slide 39

Slide 39 text

まとめ 38 そのpreloadは(今も)必要?

Slide 40

Slide 40 text

39 Thank you