Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Railsパフォーマンス・チューニング入門

 Railsパフォーマンス・チューニング入門

Kaigi on Railsでの発表資料です。

オンライン開催ということで比較的広い層が参加するだろうと思い、比較的初学者向けかつ実践的な題材としてパフォーマンスチューニングの話をしました。

kokuyouwind

October 03, 2020
Tweet

More Decks by kokuyouwind

Other Decks in Programming

Transcript

  1. Rails パフォーマンス・チューニング ⼊⾨ 弥⽣株式会社 黒曜 (@kokuyouwind)

  2. $ whoami 黒曜 @kokuyouwind Misoca → 弥⽣株式会社 (We're Hiring!) ⼀応Rails

    エンジニア 最近はAWS とかDocker 周りを 弄っていることが多い
  3. Rails は便利!

  4. 🤔 「どうして」便利なんだろう…… ?

  5. 「いい感じ」に書ける!

  6. 「いい感じ」に書ける! ⼈間の「意図」を 表現しやすい ↓

  7. 例えば…… Event.find_by(name: 'Kaigi on Rails') .sessions .find_or_create_by(speaker: 'kokuyou .update(start_time: '10:50')

    1 2 3 4
  8. 例えば…… イベントの中から「Kaigi on Rails 」を探して、 発表者がkokuyouwind のセッションを⾒つける。 ( 存在しない場合は作る) そして、セッション開始時刻を10:50

    に更新する。 Event.find_by(name: 'Kaigi on Rails') .sessions .find_or_create_by(speaker: 'kokuyou .update(start_time: '10:50') 1 2 3 4
  9. ⼈間の「意図」は 表現しやすい

  10. 実際の挙動は? どんなクエリが、合計何回実⾏される? イベントやセッションがたくさんあっても⼤丈夫? Event.find_by(name: 'Kaigi on Rails') .sessions .find_or_create_by(speaker: 'kokuyou

    .update(start_time: '10:50') 1 2 3 4
  11. 細かい「挙動」は 把握しづらいことがある

  12. 細かい挙動が把握しづらいと…… それ⾃体は悪いことではない 低レベルの挙動は抽象化されていたほうが、 ⾼レベルの意図を理解しやすい 気をつけないと、効率の悪い処理になる場合がある DB 処理は ActiveRecord -> SQL

    -> 実⾏計画と 2 段階に翻訳されるので、より把握しづらい パフォーマンス悪化や、最悪応答不能になることも
  13. パフォーマンスの計測・改善をしよう!

  14. アジェンダ パフォーマンスの計測 DB 処理のチューニング CPU 処理のチューニング ケーススタディ まとめ

  15. アジェンダ パフォーマンスの計測 DB 処理のチューニング CPU 処理のチューニング ケーススタディ まとめ

  16. パフォーマンスを改善するには 問題がどこにあるか 分析する必要がある

  17. パフォーマンスに影響を与える要素 CPU フロントエンド サーバサイド ⼊出⼒ データベース ファイル ネットワーク

  18. パフォーマンスに影響を与える要素 CPU フロントエンド サーバサイド ⼊出⼒ データベース ← だいたいここが問題 ファイル ネットワーク

  19. APM (Application Monitoring Management) https://www.skylight.io/support/skylight-guides

  20. APM ツールでわかること どのエンドポイントが重いか https://www.skylight.io/support/skylight-guides

  21. APM ツールでわかること どの処理やクエリに時間がかかっているか https://www.skylight.io/support/skylight-guides

  22. とりあえず好きなAPM ツールを 導⼊するのがオススメ ( 定期的に⾒よう!)

  23. アジェンダ パフォーマンスの計測 DB 処理のチューニング CPU 処理のチューニング ケーススタディ まとめ

  24. 重いクエリの要因は⾊々あるが、 特に重くなりやすい3 つを取り上げる

  25. 重いクエリ三銃⼠ N+1 FULL SCAN Filesort

  26. MySQL の気持ちになって 考えてみよう ※ MySQL 以外を使ってる⼈は「XXX( 任意のRDBMS) の気持ちになって」と読み替えてください

  27. FULL SCAN( テーブルフルスキャン) N+1 FULL SCAN Filesort

  28. FULL SCAN の例 id speaker start_time end_time 1 tenderlove 10:10

    10:40 2 kokuyouwind 10:50 11:10 3 toshimaru 11:10 11:30 4 lulalala 11:30 11:40 5 beta_chelsea 12:40 12:50 6 makicamel 12:50 13:10 sessions
  29. FULL SCAN の例 ジョーカー(joker1007) さんの セッション開始時刻はいつ? SELECT start_time FROM sessions

    WHERE speaker = "joker1007"; 1 2 3 Sessions.find_by(speaker: 'joker10 .pluck(:start_time) 1 2
  30. FULL SCAN の例 id speaker start_time end_time 1 tenderlove 10:10

    10:40 2 kokuyouwind 10:50 11:10 3 toshimaru 11:10 11:30 4 lulalala 11:30 11:40 5 beta_chelsea 12:40 12:50 6 makicamel 12:50 13:10 発表者名を順に全部⾒る( テーブルフルスキャン)
  31. 100 万件あったら 100 万件全部読む( かもしれない) 死

  32. インデックスをつけよう

  33. インデックスのイメージ 索引 speaker id b beta_chelsea 5 f fukajun 11

    j joker1007 17 k koic 16 kokuyouwind 2 l lulalala 4 index(speaker on sessions)
  34. インデックスのイメージ 索引 speaker id b beta_chelsea 5 f fukajun 11

    j joker1007 17 k koic 16 kokuyouwind 2 l lulalala 4 j から始まるspeaker を⼀発で⾒つける
  35. インデックスのイメージ 索引 speaker id j joker1007 17 ID からレコードを⾒つけてstart_time を⾒つける

    id speaker start_time end_time 16 koic 16:20 16:40 17 joker1007 16:40 17:00 18 a_matsuda 17:10 17:40
  36. FULL SCAN しなくなった

  37. Filesort N+1 FULL SCAN Filesort

  38. Filesort の例 id event_id speaker start_time end_time 1 1 tenderlove

    11:45 12:10 2 2 tenderlove 10:10 10:40 3 2 koic 16:20 16:40 4 1 koic 14:00 14:25 5 2 kokuyouwind 10:50 11:10 id name 1 RubyKaigi Takeout 2020 2 Kaigi on Rails events sessions
  39. Filesort の例 Kaigi on Rails のセッションを 開始時刻順で教えて? SELECT * FROM

    events WHERE name = 'Kaigi on Rails'; SELECT * FROM sessions WHERE event_id = 2 ORDER BY start_time ASC; 1 2 3 4 5 6 Events.find_by(name: 'Kaigi on Rai .sessions.order(:start_time) 1 2
  40. Filesort の例 索引 event_id id 1 1 1 1 4

    2 2 2 2 3 2 5 id name 1 RubyKaigi Takeout 2020 2 Kaigi on Rails events index (event_id on sessions)
  41. Filesort の例 id event_id speaker start_time end_time 2 2 tenderlove

    10:10 10:40 3 2 koic 16:20 16:40 5 2 kokuyouwind 10:50 11:10 sessions index (event_id on sessions) 索引 event_id id 2 2 2 2 3 2 5 ↑ 順に並んでいない!
  42. Filesort の例 id speaker start_time end_time 2 tenderlove 10:10 10:40

    3 koic 16:20 16:40 5 kokuyouwind 10:50 11:10 id speaker start_time end_time 2 tenderlove 10:10 10:40 5 kokuyouwind 10:50 11:10 3 koic 16:20 16:40 ⾒つけたレコードをstart_time 順に メモリ上で並べ替える! (Filesort)
  43. LIMIT で件数を制限しても、 全件(100 万件かも) を読み込んで 並び替えないと返せない 死

  44. 処理順に合わせて 複合インデックスをつけよう

  45. 複合インデックスの例 索引 event_id start_time id (1, 11) 1 11:45 1

    (1, 14) 1 14:00 4 (2, 10) 2 10:10 2 2 10:50 5 (2, 16) 2 16:20 3 index (event_id, start_time on sessions) ↑ event_id とstart_time を 組み合わせた索引
  46. 複合インデックスの例 索引 event_id start_time id (2, 10) 2 10:10 2

    2 10:50 5 (2, 16) 2 16:20 3 index (event_id, start_time on sessions) id speaker start_time end_time 2 tenderlove 10:10 10:40 5 kokuyouwind 10:50 11:10 3 koic 16:20 16:40 sessions start_time でソート済みの状態で取れる!
  47. filesort しなくなった

  48. 複合インデックス( 悪い例) 索引 start_time event_id id (10, 2) 10:10 2

    1 10:50 2 4 (11, 1) 11:45 1 2 (14, 1) 14:00 1 5 (16, 2) 16:20 2 3 index (start_time, event_id on sessions) ↑ start_time が先だと、 event_id=2 を索引から探せない!
  49. 複合インデックス( 悪い例) 索引 start_time speaker id (10:10, t) 10:10 tenderlove

    1 (10:50, k) 10:50 kokuyouwind 2 (11:10, t) 11:10 toshimaru 3 (11:30, l) 11:30 lulalala 4 index (start_time, speaker on sessions) start_time だけで昇順に並ぶため、speaker はソートされない 必要ならam/pm 区分カラムなどを作る必要がある Sessions.where(start_time: '0:00'..'12 .order_by(:speaker) 1 2
  50. N+1 クエリ N+1 FULL SCAN Filesort

  51. N+1 クエリの例 イベントごとに、イベント名と セッションの発表者を表示して? SELECT * FROM events; SELECT *

    FROM sessions WHERE event_id SELECT * FROM sessions WHERE event_id 1 2 3 4 Events.each do |event| p event.name event.sessions.each { p _1.speake end 1 2 3 4
  52. N+1 クエリの例 イベントが100 個あると… SELECT * FROM events; -- =>

    100 個のイベント SELECT * FROM sessions WHERE event_id = 1 SELECT * FROM sessions WHERE event_id = 2 SELECT * FROM sessions WHERE event_id = 3 -- ... SELECT * FROM sessions WHERE event_id = 9 SELECT * FROM sessions WHERE event_id = 1 1 2 3 4 5 6 7 8 9
  53. SQL クエリを繰り返し⼤量に発⾏する 死… ぬほどではないけど めっちゃ重い

  54. includes をつかおう

  55. includes の例 SELECT * FROM events; -- => 100 個のイベント

    SELECT * FROM sessions WHERE event_id IN (1, 2, . 1 2 3 4 Events.includes(:sessions).each do |e p event.name event.sessions.each { p _1.speaker end 1 2 3 4 クエリ2 回で完了!
  56. N+1 クエリしなくなった

  57. 問題の⾒極め⽅ APM などで時間のかかっているクエリを特定する

  58. 問題の⾒極め⽅ 1 クエリで時間がかかっている場合、EXPLAIN を⾒る type に "ALL" や "index" がいたらFULL

    SCAN type がref などで、key が使われてればOK !
  59. 問題の⾒極め⽅ extras に "Using filesort" がいたらFilesort "Using filesort" が消えればOK !

    1 クエリで時間がかかっている場合、EXPLAIN を⾒る
  60. 問題の⾒極め⽅ APM で同じクエリが何回も流れていたらN+1 クエリを疑う NewRelic は 呼び出し回数を教えてくれる https://docs.newrelic.com/docs/apm/transactions/transaction-traces/transaction-traces-database-queries-page Skylight は

    マークを付けてくれる https://www.skylight.io/support/performance-tips#repeated-queries
  61. アジェンダ パフォーマンスの計測 DB 処理のチューニング CPU 処理のチューニング ケーススタディ まとめ

  62. CPU 処理のチューニング 「単独で重い処理」はそんなに多くない 軽い処理でも繰り返し回数が多いと重くなる 以下のコードはA, B がそれぞれ1,000 件の配列だと member? 内の⽐較処理を1,000,000

    回呼び出す ( ⽐較処理が1 ナノ秒の処理でも1 秒かかる) A.filter { B.member?(_1 1
  63. 対策1: データ構造とアルゴリズムの⾒直し Array は全件探索になりやすいデータ構造 「キーから値を探す」ならHash 「共通部分や差分を取る」ならSet RDB やRedis などのミドルウェア側で処理する⼿も アルゴリズムを⾒直すことで効率が良くなる可能性

    ⼀般的なアルゴリズムを調べる ループを早く打ち切れるように処理順を変える
  64. 対策2: メモ化・キャッシュを利⽤する 同じ処理が何度も⾏われる場合に効果的 リクエストごとで⼗分ならメモ化 リクエストを跨いで保持したいならRails キャッシュ 根本的解決ではないため注意が必要 初回処理時は重い( 必要ならキャッシュを温める) 古いキャッシュがバグを起こすこともあるので

    キャッシュキーの選定には熟慮が必要
  65. アジェンダ パフォーマンスの計測 DB 処理のチューニング CPU 処理のチューニング ケーススタディ まとめ

  66. ケース1: 関連⽂書の取得 請求書から、関連⽂書 ( 変換した・された⾒積書・納品書) を取る際に N+1 が発⽣していた

  67. ケース1: 関連⽂書の取得 任意の2 要素に関連を持たせるため、 クラス名とID から⾃⼒でLookup していた FromType FromID ToType

    ToID Estimate 1 Invoice 1 Invoice 1 DeliverySlip 1 Invoice 1 DeliverySlip 2 def converted_docs DocumentConversion .find_by(from_type: 'Invoice', fro .map do |doc| doc.to_type.constantize.find(doc end end 1 2 3 4 5 6 7
  68. ケース1: 関連⽂書の取得 ポリモーフィック関連付けに書き換え、 includes を指定できるようにした class Invoice has_many :document_conversions, as:

    :source_docu has_many :converted_delivery_slips, through: :document_conversions, source: :converted_document, source_type: 'DeliverySlip' end # usage Invoice.all.includes(:converted_delivery_slips) 1 2 3 4 5 6 7 8 9 10
  69. ケース2: PDF 変換 gem を更新したら PDF ⽣成処理が急に重くなった ブログ記事: https://tech.misoca.jp/entry/2020/06/12/110000

  70. ケース2: PDF 変換 stackprof を使って調査した結果、gem 内から CompareWithRange#cover? が⼤量に呼ばれていた

  71. ケース2: PDF 変換 def group_original_code_points_by_bit(os2) Hash.new { |h, k| h[k]

    = [] }.tap do |result| os2.file.cmap.unicode.first.code_map.each_key do |co # === ↓2 重ループ内で cover? を呼んでいる!!! === range = UNICODE_RANGES.find { |r| r.cover?(code_po # ... 1 2 3 4 5 6 7 8 https://github.com/prawnpdf/ttfunk/blob/1.6.2.1/lib/ttfunk/table/os2.rb#L273-L275
  72. ケース2: PDF 変換 アルゴリズムを変えて、 cover? の呼び出しを減らすPull Request を送った ( 未マージ)

    https://github.com/prawnpdf/ttfunk/pull/83
  73. アジェンダ パフォーマンスの計測 DB 処理のチューニング CPU 処理のチューニング ケーススタディ まとめ

  74. まとめ パフォーマンス改善には、まず計測から とりあえずAPM ツールを⼊れて、定期的に⾒よう 重いDB クエリはEXPLAIN してインデックスを貼ろう FULL SCAN やfilesort

    は重いので倒そう 複合インデックスは効き⽅を想像して貼ろう N+1 クエリが発⽣しないようincludes しよう CPU 処理の問題は、プロファイラで根本原因を調査しよう データ構造やアルゴリズムを⾒直せないか考えよう
  75. None