Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

都市伝説バスターズ「WebアプリのボトルネックはDBだから言語の性能は関係ない」 - Kaig...

osyoyu
October 26, 2024

都市伝説バスターズ「WebアプリのボトルネックはDBだから言語の性能は関係ない」 - Kaigi on Rails 2024

osyoyu

October 26, 2024
Tweet

More Decks by osyoyu

Other Decks in Technology

Transcript

  1. Kaigi on Rails 2 0 2 4   都市伝説バスターズ!! 「Webアプリのボトルネックは

      DBだから 言 語の性能は関係ない」 Daisuke Aritomo (osyoyu)
  2. @osyoyu.descriptions @tokyork 1 2 2 0 2 5 / 1

    / 1 8 !!! チケット発売中! 3000円! スピーカー募集中!!! (内容はあとから編集可能) [ "Daisuke Aritomo (osyoyu; おしょうゆ)", "株式会社スマートバンク勤務", "Organizer of 東京Ruby会議12", "..." ]
  3. Webアプリケーションは何に時間を使っているか? I/O (MySQL) Rubyの処理 (CPU) • B/ 4 3 のバックエンドサーバーの

    場合、およそ 65-70% の時間を SQLの実 行 待ち (を含む I/O = Input/Output) で過ごしている • New Relic や Datadog で簡単に 見 られるので、皆さんのアプリ も確かめてみよう
  4. つまりこう • I/O (Input/Output) •SQLの実 行 待ち • 外部API通信 •

    CPUを使う計算処理 • HTMLやJSONの 生 成 • SQLの組み 立 て ・ 結果 行 のパース • リクエストヘッダのパース • など • つまりだいたい I/O 待ち
  5. だからこそ、SQLの実 行 時間を改善するのが重要 いろいろあって、どれも効果が 高 い • 適切なインデックスの適 用 •

    explain を読む • N + 1 クエリの解消 • includes, joins, eager_load, ... • limit の設定 • トランザクションやロックの適切な運 用 • Batch insert の活 用
  6. Rails (Puma) の設計は I/O が多い仮定に 立 脚している • Puma は

    Rails の裏側で働いている Rack server のこと • 実際に HTTP request を受信して、Rails に つないでくれているいいヤツ • hogelog さんのワークショップに 行 くと仕組み がわかるかもしれない [要出典]
  7. req req req thread req thread thread req worker (ruby

    process) queue per-worker queue req req … … … … req thread t worker … …
  8. cpu req req thread io thread thread io worker (ruby

    process) queue per-worker queue req req … … … … req thread t worker … … Ruby の制約上 (GVL)、同時にCPUを 使った処理ができるのは 1 thread だけだが web app ではどうせみんな I/O ばかり してるので問題ない
  9. ここまでのおさらい • プログラムの処理は "CPUを使う計算処理" と "I/O" に 大 別できる •

    CPU = いわゆる Ruby コード, I/O = DB などの待ち • 一 般的なWebアプリでは 60-80% ぐらいが I/O • Ruby の web server (Puma etc) は ↑ を前提に設計されている
  10. いろいろな都市伝説 • Ruby は遅い • いや、Ruby は意外と遅くない • 遅いのは GC

    だ • いや GVL が良くない • 遅いけど Web アプリを作る上ではそんなに関係ない
  11. プログラミング 言 語の速度差は、歴然として "ある" // Go func tarai(x int, y

    int, z int) int { if x <= y { return y } else { return tarai( tarai(x-1, y, z), tarai(y-1, z, x), tarai(z-1, x, y)) } } # Ruby def tarai(x, y, z) if x <= y y else tarai( tarai(x-1, y, z), tarai(y-1, z, x), tarai(z-1, x, y)) end end " 竹 内関数" の速度 比 較をすると、 Ruby は Go の 5-15 倍ぐらい遅い ※ 再帰はRubyに不利な問題設定ではある Y Z [ 3VCZ (P    T T    T T    ෼ T
  12. 他のベンチマークでも Ruby は上位にはいない The Computer Language Benchmarks Game • いろいろなベンチマークを集めた

    コレクションがある • 二 分 木 、permutation、マンデルブロ、 正規表現、…… • どれを取ってもRubyは C, Rust, Goの10+倍遅い • ぐぬぬ
  13. なぜ問題にならないのか? I/O や外部のプログラムの結果待ちにかかる時間は プログラミング 言 語の性能に (ほぼ) 依存しない (ネットワーク通信, DBMS

    を待つ時間, etc...) # Ruby db.query("select * from users limit 1;") // Go db.ExecContext(ctx, "select * from users limit 1;") RubyでもGoでも、ほぼ同じ時間で終わる I/O の例
  14. cpu req req thread io thread thread io worker (ruby

    process) queue per-worker queue req req … … … … req thread t worker … … CPU を使う処理の性能差は それなりにあるが (I/O の性能差は 小 さいし) web app ではどうせみんな I/O ばかり してるので問題ない
  15. ISUCON 1 3 ( 2 0 2 3 ) Go

    ISUCON 1 2 ( 2 0 2 2 ) Go ISUCON 1 1 ( 2 0 2 1 ) Go ISUCON 1 0 ( 2 0 2 0 ) Go ISUCON 9 ( 2 0 1 9 ) Ruby ISUCON 8 ( 2 0 1 8 ) Go ISUCON 7 ( 2 0 1 7 ) Go ISUCON 6 ( 2 0 1 6 ) Go ISUCON 5 ( 2 0 1 5 ) Perl ISUCON 4 ( 2 0 1 4 ) Perl ISUCON 3 ( 2 0 1 3 ) Perl ISUCON 2 ( 2 0 1 2 ) Perl ISUCON ( 2 0 1 1 ) Perl • ISUCON の提供 言 語 • Go, Ruby, Python, Perl, PHP, Node.js • 各年の優勝 言 語を 見 ていきましょう (本当に問題になってない?)
  16. 「 言 語の性能が関係ない」か、実験を通して確かめる • 現実的な Web アプリを複数 言 語で実装するのは 大

    変なので、 それをモデリングした簡単なプログラムを考える • I/O (SQL) の時間と、CPU を使う時間を調節しつつ ab (Apache Bench) でリクエストをたくさん投げて パフォーマンス傾向を測定
  17. 改めましておことわり • Kaigi on Rails なので、Web の話をしたくて…… • そうすると Web

    サーバー (HTTP をさばくやつ) が関係してきて…… • 言 語の性能の話だけではなくなってしまって…… • Ruby (Puma) vs Go (net/http) という構図になりました
  18. 実験 • 3種類のHTTPサーバーを Ruby と Go とで実装して ab (Apache Bench)

    で負荷をかけて みる • Requests per Second (rps) と レスポンスタイムを観察 # realistic class HelloWorld def call(env) burn_cpu db.query "SELECT sleep(0.05);" end end # cpu-only class HelloWorld def call(env) db.query "SELECT sleep(0.05);" end end # io-only class HelloWorld def call(env) db.query "SELECT sleep(0.05);" end end
  19. 実験の条件 (1) • Ruby: Puma の上で動く Rack アプリ (Sinatra や

    Rails ではない) • Go: net/http で実装 • GOMAXPROCS= 1 0
  20. 実験の条件 (2) • ab (Apache Bench) のパラメーター • -n 1

    0 0 0 (1000リクエスト) • -c (並列数) • 1, 10, 20, 50 でそれぞれ試 行 • リクエストの中 身 はからっぽ
  21. io-only • Go と Ruby とでほぼ同じ性能特性を 見 せている! • 並列リクエスト数を

    大 きくしても、どちらもしっかりスケールしている
  22. realistic (だいたい 30% cpu + 7 0 % io) •

    差は縮まった。これどう評価するか?
  23. なぜ Go は cpu-only, realistic で良い成績を 見 せたのか • Ruby

    と Go の並列特性はまったく異なる • Ruby では同時に CPU を使えるのは 1 Thread のみだが • Go ではその制約は緩い (GOMAXPROCS まで) • 通常は GOMAXPROCS = コア数 • なのでコアを使いきれる • realistic の評価 • Ruby は Go のちょうど 70% ぐらいの性能のところ? • 3 0 % cpu + 7 0 % io なら妥当?
  24. cpu req req thread io thread thread io worker (ruby

    process) queue per-worker queue req req … … … … req thread t worker … … Ruby Puma
  25. cpu req req goroutine go process queue … … cpu

    cpu goroutine goroutine … … … … Go net/http
  26. スケーラビリティに差が出る • 同じ 1200 rps をさばくとき • Go では 1

    processes で済むのに対して • Ruby では最低 3 processes 必要になる • メモリ使 用 量で 見 ると2-3倍程度だった (Go 1 7 MB vs Ruby 5 1 MB) • ここは Copy-on-Write が効いてますね • ので、たとえば ECS tasks ベースなら3-10倍ぐらい必要になる • Ruby で3-10倍のタスク数を起動すれば、Go と同性能を出せる • わりと実感とも合う。……でもそれってインフラコスト10倍!
  27. わかったこと • 仮にリクエストが 100% I/O なら Ruby と Go の間の差は

    小 さい • しかし、現実的な仮定をおくと、差はどんどん開いてゆく • リクエスト中の CPU 比 率が増えれば増えるほど、I/O 比 率が減れば減るほど Ruby は不利になっていく
  28. よくチューニングされた Rails アプリほど、CPU 比 率が 高 い • もはや N+

    1 クエリをガンガン書いたほうがいいのでは? • Ruby 有利にもっていくぞ!!! • もちろんそんなことはない • 1 request あたりの時間を 1/N にしても、さばけるリクエスト数は N 倍にはならないが • 1 /N になること 自 体には当然意味がある
  29. cpu req req thread io thread thread io worker (ruby

    process) queue per-worker queue req req … … … … req thread t worker … … なんスレッドにするべき?
  30. rails/rails# 5 0 4 5 0 "Set a new default

    for the Puma thread count"
  31. - max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } + threads_count =

    ENV.fetch("RAILS_MAX_THREADS") { 3 } Rails のデフォルトの Puma スレッド数が変更された • rails new したときのスレッド数の設定が 5 → 3 になった • およそチューニングされていない Rails アプリでも、 I/O 比 率が 80% もあることはそうない(という議論)
  32. なぜスレッド数が多すぎると良くないか? GVL = CPUを使 用 する権利 I/O (SQL, Network, ...)

    CPU (AR, Session, ...) I/O CPU CPU GVL wait I/O CPU GVL wait CPU I/O I/O ← Request 1 → ← Request 2 → ← Request 3 → I/O CPU CPU Idle I/O I/O CPU Thread 1 Thread 2 Thread 3
  33. なぜスレッド数が多すぎると良くないか? GVL = CPUを使 用 する権利 I/O (SQL, Network, ...)

    CPU (AR, Session, ...) I/O CPU CPU GVL wait I/O CPU GVL wait CPU I/O I/O ← Request 1 → ← Request 2 → ← Request 3 → I/O CPU CPU Idle I/O I/O CPU Thread 1 Thread 2 Thread 3 ここに "マジで何もしてない時間" がある posts = Posts.where(...) # posts.map { ... } I/O が済み、進みたいが GVL 待ち
  34. じゃあ、うちも RAILS_MAX_THREADS= 3 にしますか…… デフォルト変わったしな…… • 今 5 なら 3

    にしても良いかも • でも、せっかくなら "仮説" をもって "検証" したが楽しいかも
  35. やりかた: 自 分のアプリの I/O 比 率を知る • New Relic や

    Datadog を導 入 して いればすぐわかる • この例では CPU 時間は約 30% • ということはやはり 3 threads (100/30 = 3.33) ぐらいが良い I/O (MySQL) Rubyの処理 (CPU) I/O (外部リクエスト)
  36. I/O 比 率: 傾向と対策 • DB だけで I/O 比 率が

    50% を超えることはそうない • 超えていたらスレッド数を 2 にする前に N+ 1 などをつぶした 方 が良い • しかし、激重外部リクエストが中 心 のアプリならあり得る
  37. Rack::Session • これは APM ではなく puma にプロファイラを 入 れてみて初めて気づいた •

    Cookie から Session をデシリアライズする処理が 全体の 5% ぐらいある! • もういっそ C で書き直してしまいたい
  38. プロファイルの対象を広げたり狭めたりするとよい リクエスト (APM) puma, unicorn (pf 2 , vernier, stackprof)

    CRuby (perf, pf 2 ) OS (perf, PCM) 問題 問題 問題 問題 • 問題はいろいろな レイヤに遍在する • 各レイヤに合った 方 法 でプロファイルする ことで、 見 えたり 見 え なかったりする • 全部 見 てみるとお得 問題
  39. さらにもうひとつのやりかた I/O (SQL, Network, ...) CPU (AR, Session, ...) I/O

    CPU CPU GVL wait I/O CPU GVL wait CPU I/O I/O ← Request 1 → ← Request 2 → ← Request 3 → I/O CPU CPU Idle I/O I/O CPU Thread 1 Thread 2 Thread 3 普通にこの時間を直接計測して 最 小 化できればいいのでは? たったひとつの…… というわけではない
  40. AutoPumaTuner というのを作って発表したかった • 最近の Ruby では GVL に関係する イベントを取れるので、それで auto-

    tune できるかと思った • 実際に autotune するためには 適切なベンチマーカーの設計が必要で パッケージングしづらい • Help wanted! before_fork do PumaTuner.run end --- puma srv tp 001: cpu: 65.1% io: 33.7% gc: 1.1% gvlwait: 2.2% Recommended config: puma -t 1:3
  41. 言 語の話ではなく Web サーバーの話に 見 えたが • Ruby のランタイムで Go

    net/http のようなサーバーのモデルは 必ずしも容易に実装できない • 並列実 行 できる Goroutine (coroutine) の概念は Ruby では浸透していない • Fiber (coroutine) はあるが、並列に実 行 はされない • そういう意味では、やはり 言 語の制約を受けていると 言 える
  42. 最近は M:N Threads に興味があります • Ruby 3 . 3 から

    Go に近しいスレッドモデルが導 入 された • Puma とはまったく違うモデルになりそう • 実 用 的に使ってみたい • Kaigi on Rails に来てやるぞ!!! の気持ちが 高 まっている
  43. で皆さんをお待ちしています • @tokyork 1 2 フォローして! • チケット売ってます! • Kaigi

    on Rails で 高 まったプロポーザル、 超待ってます! 11/5 まで!
  44. Kaigi on Rails 2 0 2 4   都市伝説バスターズ!! 「Webアプリのボトルネックは

      DBだから 言 語の性能は関係ない」 Daisuke Aritomo (osyoyu)