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

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

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

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

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

kokuyouwind

October 03, 2020
Tweet

More Decks by kokuyouwind

Other Decks in Programming

Transcript

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

    View Slide

  2. $ whoami
    黒曜
    @kokuyouwind
    Misoca →
    弥⽣株式会社 (We're Hiring!)
    ⼀応Rails
    エンジニア
    最近はAWS
    とかDocker
    周りを
    弄っていることが多い

    View Slide

  3. Rails
    は便利!

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  9. ⼈間の「意図」は
    表現しやすい

    View Slide

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

    View Slide

  11. 細かい「挙動」は
    把握しづらいことがある

    View Slide

  12. 細かい挙動が把握しづらいと……
    それ⾃体は悪いことではない
    低レベルの挙動は抽象化されていたほうが、
    ⾼レベルの意図を理解しやすい
    気をつけないと、効率の悪い処理になる場合がある
    DB
    処理は ActiveRecord -> SQL ->
    実⾏計画と
    2
    段階に翻訳されるので、より把握しづらい
    パフォーマンス悪化や、最悪応答不能になることも

    View Slide

  13. パフォーマンスの計測・改善をしよう!

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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
    発表者名を順に全部⾒る(
    テーブルフルスキャン)

    View Slide

  31. 100
    万件あったら
    100
    万件全部読む(
    かもしれない)

    View Slide

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

    View Slide

  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)

    View Slide

  34. インデックスのイメージ
    索引 speaker id
    b beta_chelsea 5
    f fukajun 11
    j joker1007 17
    k koic 16
    kokuyouwind 2
    l lulalala 4
    j
    から始まるspeaker
    を⼀発で⾒つける

    View Slide

  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

    View Slide

  36. FULL SCAN
    しなくなった

    View Slide

  37. Filesort
    N+1
    FULL
    SCAN
    Filesort

    View Slide

  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

    View Slide

  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

    View Slide

  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)

    View Slide

  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

    順に並んでいない!

    View Slide

  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)

    View Slide

  43. LIMIT
    で件数を制限しても、
    全件(100
    万件かも)
    を読み込んで
    並び替えないと返せない

    View Slide

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

    View Slide

  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

    組み合わせた索引

    View Slide

  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
    でソート済みの状態で取れる!

    View Slide

  47. filesort
    しなくなった

    View Slide

  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
    を索引から探せない!

    View Slide

  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

    View Slide

  50. N+1
    クエリ
    N+1
    FULL
    SCAN
    Filesort

    View Slide

  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

    View Slide

  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

    View Slide

  53. SQL
    クエリを繰り返し⼤量に発⾏する
    死…
    ぬほどではないけど
    めっちゃ重い

    View Slide

  54. includes
    をつかおう

    View Slide

  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
    回で完了!

    View Slide

  56. N+1
    クエリしなくなった

    View Slide

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

    View Slide

  58. 問題の⾒極め⽅
    1
    クエリで時間がかかっている場合、EXPLAIN
    を⾒る
    type
    に "ALL"
    や "index"
    がいたらFULL SCAN
    type
    がref
    などで、key
    が使われてればOK

    View Slide

  59. 問題の⾒極め⽅
    extras
    に "Using filesort"
    がいたらFilesort
    "Using filesort"
    が消えればOK

    1
    クエリで時間がかかっている場合、EXPLAIN
    を⾒る

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

  63. 対策1:
    データ構造とアルゴリズムの⾒直し
    Array
    は全件探索になりやすいデータ構造
    「キーから値を探す」ならHash
    「共通部分や差分を取る」ならSet
    RDB
    やRedis
    などのミドルウェア側で処理する⼿も
    アルゴリズムを⾒直すことで効率が良くなる可能性
    ⼀般的なアルゴリズムを調べる
    ループを早く打ち切れるように処理順を変える

    View Slide

  64. 対策2:
    メモ化・キャッシュを利⽤する
    同じ処理が何度も⾏われる場合に効果的
    リクエストごとで⼗分ならメモ化
    リクエストを跨いで保持したいならRails
    キャッシュ
    根本的解決ではないため注意が必要
    初回処理時は重い(
    必要ならキャッシュを温める)
    古いキャッシュがバグを起こすこともあるので
    キャッシュキーの選定には熟慮が必要

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  69. ケース2: PDF
    変換
    gem
    を更新したら
    PDF
    ⽣成処理が急に重くなった
    ブログ記事: https://tech.misoca.jp/entry/2020/06/12/110000

    View Slide

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

    View Slide

  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

    View Slide

  72. ケース2: PDF
    変換
    アルゴリズムを変えて、
    cover?
    の呼び出しを減らすPull Request
    を送った
    (
    未マージ)
    https://github.com/prawnpdf/ttfunk/pull/83

    View Slide

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

    View Slide

  74. まとめ
    パフォーマンス改善には、まず計測から
    とりあえずAPM
    ツールを⼊れて、定期的に⾒よう
    重いDB
    クエリはEXPLAIN
    してインデックスを貼ろう
    FULL SCAN
    やfilesort
    は重いので倒そう
    複合インデックスは効き⽅を想像して貼ろう
    N+1
    クエリが発⽣しないようincludes
    しよう
    CPU
    処理の問題は、プロファイラで根本原因を調査しよう
    データ構造やアルゴリズムを⾒直せないか考えよう

    View Slide

  75. View Slide