Slide 1

Slide 1 text

Kaigi on Rails 2 0 2 4   都市伝説バスターズ!! 「Webアプリのボトルネックは   DBだから 言 語の性能は関係ない」 Daisuke Aritomo (osyoyu)

Slide 2

Slide 2 text

最初にお詫びいたします • 勢いでタイトルを考えてしまいました • ちょっと後悔しています • 「 言 語の性能」だけの差の話ではありませんでした • Web server (Puma など) の話もあります • しかし「 言 語の性能」の話もあります • どうぞよろしく

Slide 3

Slide 3 text

@osyoyu.descriptions [ "Daisuke Aritomo (osyoyu; おしょうゆ)", "株式会社スマートバンク勤務", "Organizer of 東京Ruby会議12", "..." ]

Slide 4

Slide 4 text

@osyoyu.descriptions @tokyork 1 2 2 0 2 5 / 1 / 1 8 !!! チケット発売中! 3000円! スピーカー募集中!!! (内容はあとから編集可能) [ "Daisuke Aritomo (osyoyu; おしょうゆ)", "株式会社スマートバンク勤務", "Organizer of 東京Ruby会議12", "..." ]

Slide 5

Slide 5 text

@osyoyu.descriptions [ "Daisuke Aritomo (osyoyu; おしょうゆ)", "株式会社スマートバンク勤務", "Organizer of 東京Ruby会議12", "..." ] inspect したときに全部並べてほしいと 思っている

Slide 6

Slide 6 text

Webアプリのボトルネックは DB (SQL) だから 言 語の性能は関係ない

Slide 7

Slide 7 text

これは B/ 4 3 API の Rails アプリの New Relic です

Slide 8

Slide 8 text

Webアプリケーションは何に時間を使っているか? I/O (MySQL) Rubyの処理 (CPU) • B/ 4 3 のバックエンドサーバーの 場合、およそ 65-70% の時間を SQLの実 行 待ち (を含む I/O = Input/Output) で過ごしている • New Relic や Datadog で簡単に 見 られるので、皆さんのアプリ も確かめてみよう

Slide 9

Slide 9 text

つまりこう • I/O (Input/Output) •SQLの実 行 待ち • 外部API通信 • CPUを使う計算処理 • HTMLやJSONの 生 成 • SQLの組み 立 て ・ 結果 行 のパース • リクエストヘッダのパース • など • つまりだいたい I/O 待ち

Slide 10

Slide 10 text

だからこそ、SQLの実 行 時間を改善するのが重要 いろいろあって、どれも効果が 高 い • 適切なインデックスの適 用 • explain を読む • N + 1 クエリの解消 • includes, joins, eager_load, ... • limit の設定 • トランザクションやロックの適切な運 用 • Batch insert の活 用

Slide 11

Slide 11 text

Webアプリのボトルネックは DB (SQL) だから 言 語の性能は関係ない

Slide 12

Slide 12 text

Webアプリのボトルネックは DB (SQL) だから 言 語の性能は関係ない 逆に 言 えば、 それ以外の部分 = Ruby で処理する部分は あんまり関係ない?

Slide 13

Slide 13 text

Rails (Puma) の設計は I/O が多い仮定に 立 脚している • Puma は Rails の裏側で働いている Rack server のこと • 実際に HTTP request を受信して、Rails に つないでくれているいいヤツ • hogelog さんのワークショップに 行 くと仕組み がわかるかもしれない [要出典]

Slide 14

Slide 14 text

req req req thread req thread thread req worker (ruby process) queue per-worker queue req req … … … … req thread t worker … …

Slide 15

Slide 15 text

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 ばかり してるので問題ない

Slide 16

Slide 16 text

ここまでのおさらい • プログラムの処理は "CPUを使う計算処理" と "I/O" に 大 別できる • CPU = いわゆる Ruby コード, I/O = DB などの待ち • 一 般的なWebアプリでは 60-80% ぐらいが I/O • Ruby の web server (Puma etc) は ↑ を前提に設計されている

Slide 17

Slide 17 text

Webアプリのボトルネックは DB (SQL) だから 言 語の性能は関係ない

Slide 18

Slide 18 text

いろいろな都市伝説 • Ruby は遅い • いや、Ruby は意外と遅くない • 遅いのは GC だ • いや GVL が良くない • 遅いけど Web アプリを作る上ではそんなに関係ない

Slide 19

Slide 19 text

プログラミング 言 語の速度差は、歴然として "ある" // 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

Slide 20

Slide 20 text

他のベンチマークでも Ruby は上位にはいない The Computer Language Benchmarks Game • いろいろなベンチマークを集めた コレクションがある • 二 分 木 、permutation、マンデルブロ、 正規表現、…… • どれを取ってもRubyは C, Rust, Goの10+倍遅い • ぐぬぬ

Slide 21

Slide 21 text

なぜ問題にならないのか? 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 の例

Slide 22

Slide 22 text

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 ばかり してるので問題ない

Slide 23

Slide 23 text

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 • 各年の優勝 言 語を 見 ていきましょう (本当に問題になってない?)

Slide 24

Slide 24 text

実験で確かめる

Slide 25

Slide 25 text

「 言 語の性能が関係ない」か、実験を通して確かめる • 現実的な Web アプリを複数 言 語で実装するのは 大 変なので、 それをモデリングした簡単なプログラムを考える • I/O (SQL) の時間と、CPU を使う時間を調節しつつ ab (Apache Bench) でリクエストをたくさん投げて パフォーマンス傾向を測定

Slide 26

Slide 26 text

改めましておことわり • Kaigi on Rails なので、Web の話をしたくて…… • そうすると Web サーバー (HTTP をさばくやつ) が関係してきて…… • 言 語の性能の話だけではなくなってしまって…… • Ruby (Puma) vs Go (net/http) という構図になりました

Slide 27

Slide 27 text

実験 • 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

Slide 28

Slide 28 text

実験の条件 (1) • Ruby: Puma の上で動く Rack アプリ (Sinatra や Rails ではない) • Go: net/http で実装 • GOMAXPROCS= 1 0

Slide 29

Slide 29 text

実験の条件 (2) • ab (Apache Bench) のパラメーター • -n 1 0 0 0 (1000リクエスト) • -c (並列数) • 1, 10, 20, 50 でそれぞれ試 行 • リクエストの中 身 はからっぽ

Slide 30

Slide 30 text

io-only • Go と Ruby とでほぼ同じ性能特性を 見 せている! • 並列リクエスト数を 大 きくしても、どちらもしっかりスケールしている

Slide 31

Slide 31 text

cpu-only • Go に 大 差をつけられてしまった • 理由は後ほど説明します • Go ってすごいですね

Slide 32

Slide 32 text

realistic (だいたい 30% cpu + 7 0 % io) • 差は縮まった。これどう評価するか?

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

cpu req req thread io thread thread io worker (ruby process) queue per-worker queue req req … … … … req thread t worker … … Ruby Puma

Slide 35

Slide 35 text

cpu req req goroutine go process queue … … cpu cpu goroutine goroutine … … … … Go net/http

Slide 36

Slide 36 text

実験2: スケーラビリティ • それぞれの 言 語はどこまでスケールするか? を確かめたい • 実験1では最 大 並列数を50に抑えていたが • いけるところまでいってみる

Slide 37

Slide 37 text

結果2: スケーラビリティ 今回は realistic 設定だけです • Go のほうがだいぶ伸びていく • すごい • Puma が 600 で打ち 止 まる のに対し Go は 1200 まで伸 びた

Slide 38

Slide 38 text

スケーラビリティに差が出る • 同じ 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倍!

Slide 39

Slide 39 text

Go のコンポーネントは 同じ rps の Ruby コンポーネントより 実際にタスク数が少ない

Slide 40

Slide 40 text

Goはすごい

Slide 41

Slide 41 text

Webアプリのボトルネックは DB (SQL) だから 言 語の性能は関係ない → busted! (たぶん……)

Slide 42

Slide 42 text

わかったこと • 仮にリクエストが 100% I/O なら Ruby と Go の間の差は 小 さい • しかし、現実的な仮定をおくと、差はどんどん開いてゆく • リクエスト中の CPU 比 率が増えれば増えるほど、I/O 比 率が減れば減るほど Ruby は不利になっていく

Slide 43

Slide 43 text

ところで……

Slide 44

Slide 44 text

このスライド、覚えてますか • DB で使う時間を減らすことが最重要と主張していた • これって…… I/O の 比 率を減らしているのでは!!!??? • Ruby 不利の状況にわざわざ持ち込んでいる!!!!!!??????

Slide 45

Slide 45 text

よくチューニングされた Rails アプリほど、CPU 比 率が 高 い • もはや N+ 1 クエリをガンガン書いたほうがいいのでは? • Ruby 有利にもっていくぞ!!! • もちろんそんなことはない • 1 request あたりの時間を 1/N にしても、さばけるリクエスト数は N 倍にはならないが • 1 /N になること 自 体には当然意味がある

Slide 46

Slide 46 text

とはいえ、I/O 比 率を意識したチューニングはすべき

Slide 47

Slide 47 text

cpu req req thread io thread thread io worker (ruby process) queue per-worker queue req req … … … … req thread t worker … … なんスレッドにするべき?

Slide 48

Slide 48 text

rails/rails# 5 0 4 5 0 "Set a new default for the Puma thread count"

Slide 49

Slide 49 text

- 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% もあることはそうない(という議論)

Slide 50

Slide 50 text

Puma のスレッド数は 大 いに影響がある • 先ほどのベンチマークのオマケ • いろいろな状況で puma の ベンチマークを 見 てみた結果

Slide 51

Slide 51 text

なぜスレッド数が多すぎると良くないか? 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

Slide 52

Slide 52 text

なぜスレッド数が多すぎると良くないか? 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 待ち

Slide 53

Slide 53 text

じゃあ、うちも RAILS_MAX_THREADS= 3 にしますか…… デフォルト変わったしな…… • 今 5 なら 3 にしても良いかも • でも、せっかくなら "仮説" をもって "検証" したが楽しいかも

Slide 54

Slide 54 text

やりかた: 自 分のアプリの I/O 比 率を知る • New Relic や Datadog を導 入 して いればすぐわかる • この例では CPU 時間は約 30% • ということはやはり 3 threads (100/30 = 3.33) ぐらいが良い I/O (MySQL) Rubyの処理 (CPU) I/O (外部リクエスト)

Slide 55

Slide 55 text

I/O 比 率: 傾向と対策 • DB だけで I/O 比 率が 50% を超えることはそうない • 超えていたらスレッド数を 2 にする前に N+ 1 などをつぶした 方 が良い • しかし、激重外部リクエストが中 心 のアプリならあり得る

Slide 56

Slide 56 text

変更したら計測する • スレッド数を変えると、外部から 見 た request time が変わるはず • GVL 待ちの時間が短縮されるはず • ALB なら TargetResponseTime

Slide 57

Slide 57 text

Tuning Performance for Deployment https://guides.rubyonrails.org/tuning_performance_for_deployment.html

Slide 58

Slide 58 text

やりかた2: CPU を使う処理を減らす • 「ボトルネック」ではないからこそ、 見 逃されがちだが…… • プロファイラを 入 れてみると、削れそうな場所が 面白 いほど出てくる

Slide 59

Slide 59 text

そこでプロファイラ 自己 紹介は→→→→→ • 別になにかが特別遅いなーと思わないときでも、 趣味としてプロファイラの出 力 を眺めると いろいろな出会いがある

Slide 60

Slide 60 text

プロファイラでの、いろいろな出会い……

Slide 61

Slide 61 text

JSON.parse

Slide 62

Slide 62 text

JSON.parse • これはリクエストスコープでの出会い • チリツモでめちゃくちゃ CPU 使ってる!!! • Oj に置き換えるとびっくりするほど速くなる

Slide 63

Slide 63 text

mysql 2

Slide 64

Slide 64 text

mysql 2 • これもリクエストスコープでの出会い • libmysqlclient も意外と CPU 使ってる • Trilogy に置き換えたら 10% ぐらい速くなった

Slide 65

Slide 65 text

Rack::Session

Slide 66

Slide 66 text

Rack::Session • これは APM ではなく puma にプロファイラを 入 れてみて初めて気づいた • Cookie から Session をデシリアライズする処理が 全体の 5% ぐらいある! • もういっそ C で書き直してしまいたい

Slide 67

Slide 67 text

プロファイルの対象を広げたり狭めたりするとよい リクエスト (APM) puma, unicorn (pf 2 , vernier, stackprof) CRuby (perf, pf 2 ) OS (perf, PCM) 問題 問題 問題 問題 • 問題はいろいろな レイヤに遍在する • 各レイヤに合った 方 法 でプロファイルする ことで、 見 えたり 見 え なかったりする • 全部 見 てみるとお得 問題

Slide 68

Slide 68 text

さらにもうひとつのやりかた 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 普通にこの時間を直接計測して 最 小 化できればいいのでは? たったひとつの…… というわけではない

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

大 閑話休題でした

Slide 71

Slide 71 text

言 語の話ではなく Web サーバーの話に 見 えたが • Ruby のランタイムで Go net/http のようなサーバーのモデルは 必ずしも容易に実装できない • 並列実 行 できる Goroutine (coroutine) の概念は Ruby では浸透していない • Fiber (coroutine) はあるが、並列に実 行 はされない • そういう意味では、やはり 言 語の制約を受けていると 言 える

Slide 72

Slide 72 text

しかし……

Slide 73

Slide 73 text

それでも Ruby と 生 きていきたい

Slide 74

Slide 74 text

Ruby が好きだから、Ruby で速いプログラムを書きたい • 言 語ランタイムの特性によって、できることやできないこと、 達成できないパフォーマンスがある • でも Ruby も進化を続けている • 自 分も Ruby で速いプログラムを書くためにできることはやりたい

Slide 75

Slide 75 text

最近は M:N Threads に興味があります • Ruby 3 . 3 から Go に近しいスレッドモデルが導 入 された • Puma とはまったく違うモデルになりそう • 実 用 的に使ってみたい • Kaigi on Rails に来てやるぞ!!! の気持ちが 高 まっている

Slide 76

Slide 76 text

で皆さんをお待ちしています • @tokyork 1 2 フォローして! • チケット売ってます! • Kaigi on Rails で 高 まったプロポーザル、 超待ってます! 11/5 まで!

Slide 77

Slide 77 text

Kaigi on Rails 2 0 2 4   都市伝説バスターズ!! 「Webアプリのボトルネックは   DBだから 言 語の性能は関係ない」 Daisuke Aritomo (osyoyu)