Slide 1

Slide 1 text

ISUCONに強くなるかもしれない日々の過ごしかた ISUCON常勝軍団の頭の中〜メンバー集めから解き方の秘密まで〜 2024-11-14 藤原俊一郎 @fujiwara

Slide 2

Slide 2 text

自己紹介 @fujiwara (X, GitHub, Bluesky) 面白法人カヤック SREチーム ISUCON 優勝4回 / 運営(出題)4 回 github.com/kayac/ecspresso github.com/fujiwara/lambroll

Slide 3

Slide 3 text

「ISUCONの練習方法を教えてください」 「ISUCONの練習」には2種類ある 1. 「ISUCONという競技に慣れるための練習」 2. そもそも「パフォーマンスが高い」ものを作るための練習

Slide 4

Slide 4 text

「ISUCONという競技に慣れるための練習」 競技のルールを理解する 競技をスムーズに実施できるようになる 1. インスタンスを起動してsshで入る 2. GitHubにprivate repoを作ってコードをpush 3. コードを編集 / commit / デプロイ(再起動) 4. ベンチマークを実行して結果をまとめる …などなど 参加したことがない人は1回はやってください (当日まごつくと単に時間がもったいない)

Slide 5

Slide 5 text

そもそも「パフォーマンスが高い」ものを作れるようになる これを 「ISUCONの過去問でやる」 のは効率が悪い 練習、年に何時間やりますか…? 普段の日々の過ごしかたで強くなるほうがずっと時間を使える

Slide 6

Slide 6 text

全プログラマーが知るべきレイテンシー数 nano sec L1キャッシュ参照 0.5 分岐予測失敗 5 L2キャッシュ参照 7 Mutexのロックとアンロック 25 メインメモリー参照 100 Zippy[Snappy]による1KBの圧縮 3,000 1Gbpsネットワーク越しに2KBを送信 20,000 メモリーから連続した1MBの領域の読み出し 250,000 同一データセンター内におけるラウンドトリップ 500,000 0.5 msec ディスクシーク 10,000,000 10 msec ディスクから連続した1MBの領域の読み出し 20,000,000 20 msec パケットをカリフォルニア→オランダ→カリフォルニアと送る 150,000,000 150 msec http://norvig.com/21-days.html#answers

Slide 7

Slide 7 text

実際のISUCON/Webアプリケーションのチューニングでは CPU内(L1キャッシュにヒットするとか)まではあまり気にしない 他のところがもっと圧倒的に遅いことが多い ある処理がCPUとメモリで完結するか 例: プロセス内のメモリキャッシュ ローカルディスクを読み書きするか (最近は少ないですね) データセンター内の別サーバーにアクセスするか 例: DBへのクエリ (0.5 ms〜∞) 地理的に離れた場所にアクセスするか(その距離は?) 例: 外部APIアクセス (10 ms〜∞) 処理単位でこれらのレイテンシを常に意識するのが大事

Slide 8

Slide 8 text

ISUCONの場合 # ISUCON13 ruby実装 get '/api/tag' do tag_models = db_transaction do |tx| tx.query('SELECT * FROM tags') end json( tags: tag_models.map { |tag_model| { id: tag_model.fetch(:id), name: tag_model.fetch(:name), } }, ) end 「DBにSQLを発行して結果をJSONにしている」のが分かりやすい 実際に発行しているSQLも書いてある

Slide 9

Slide 9 text

実際のWebアプリケーションの場合…… @items = current_user.items .is_active .preload(:tags) .page(page).per(per) 見た目ではおそらく…… DBで items テーブルをクエリしていそう tags テーブルにもクエリしてそう(preload) でも本当にそうなのかはもっとよく調べないと分からない Redisやメモリにcacheしているかもしれない 外部APIにリクエストを発行しているかもしれない(!!?)

Slide 10

Slide 10 text

ISUCONの問題は「養殖物」 あえてパフォーマンスに問題があるように意図的に作られたもの アプリケーション自体の層は薄いのでコードを読み切れる コードは全て読まれる前提で、問題があるところを直してもらいたい これだけで練習すると特定の力しか付かない

Slide 11

Slide 11 text

「天然物」を相手にして力を養う 誰も「遅くしよう」と思って作ってはいない(けど遅い) アプリケーションが重厚長大(特に歴史が長いと) コードは簡単に読み切れる分量ではない 機能の実装や改修時に「ここでは実際に何が起きる?」を考える/検証する癖を付ける

Slide 12

Slide 12 text

マイクロベンチマークを手癖にする 例:「Goでsliceに要素を追加する場合、先にキャパシティを確保したほうが速い」 func AppendFromEmpty(n int) { var s []int // sliceを宣言するだけ for i := 0; i < n; i++ { s = append(s, i) } } func AppendFromPreallocated(n int) { s := make([]int, 0, n) // capacityをn個分確保したslice for i := 0; i < n; i++ { s = append(s, i) } }

Slide 13

Slide 13 text

Go標準のtestingモジュールでベンチマークができる import "testing" func BenchmarkAppendFromEmpty(b *testing.B) { for i := 0; i < b.N; i++ { AppendFromEmpty(10000) } } func BenchmarkAppendFromPreallocated(b *testing.B) { for i := 0; i < b.N; i++ { AppendFromPreallocated(10000) } } $ go test -bench . -benchmem (他の言語でも同じようなものがあります)

Slide 14

Slide 14 text

$ go test -bench . -benchmem goos: linux goarch: amd64 pkg: example.com/bench cpu: AMD Ryzen 5 3400G with Radeon Vega Graphics BenchmarkAppendFromEmpty-8 9975 121229 ns/op 357627 B/op 19 allocs/op BenchmarkAppendFromPreallocated-8 43009 27803 ns/op 81920 B/op 1 allocs/op 事前にcapacityを確保する=メモリのアロケートが減る 4倍速い、ことがわかる

Slide 15

Slide 15 text

折に触れて「これはどれぐらい時間が掛かる?」を意識して検証する ISUCONに近いところの例だと… MySQLでprimary keyで1行引くだけのクエリ MySQLで100万行を読んで1行だけ返すクエリ Redisのget/set... 使っている言語のHTTPクライアントでリクエストを送受信 使っているフレームワークでHello Worldを返すだけのアプリ https://www.techempower.com/benchmarks/ 1MBのJSONをencode/decode

Slide 16

Slide 16 text

早すぎる最適化は「悪」 「ボトルネック以外を改善しても(あまり)意味がない」のは間違いない 細か過ぎるチューニングを無闇に入れてもスコアは上がらない 「推測するな計測せよ」 でもどこから計測すれば…? 「ここでこんな時間掛かるのおかしいよね」 の当て感を身につける そのためには 「書いた処理のレイテンシを常に意識する」 「気になった処理のマイクロベンチマークを手癖にする」 のが役に立ちます

Slide 17

Slide 17 text

実例: ISUCON 11 優勝の分岐点 Zipを生成してダウンロードさせる機能 初期実装は zip 外部コマンド呼び出し → Go の archive/zip で作成するように fujiwara組: zip.Store (非圧縮)を指定してCPUコスト削減 NaruseJun: 圧縮(deflate)したZipを生成 ← 非圧縮だったら逆転していたらしい