Slide 1

Slide 1 text

「最高のチューニング」をしないために Hack@DELTA v24.10 藤原俊一郎 @fujiwara

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

今日のお題 「わたし史上、最高のチューニング」 最高の定義とは……???

Slide 4

Slide 4 text

思い出のチューニング 2011年1月 カヤック入社 こえ部に配属 「こえ部」 音声専門のコミュニティサイト カヤックが開発、運営 2007年リリース 2014年サイバーエージェントに譲渡 2016年サービス終了

Slide 5

Slide 5 text

2011-01-21(金) 入社当日、手続きを終えてチームに行くと… 「半年ぐらい前から、急にサイトが重くなって落ちるんだけど原因が分からないので 調べてほしい」 一度発生すると30分〜1時間ぐらい落ちっぱなしになるらしい

Slide 6

Slide 6 text

2011-01-24(月) 障害の事象確認、調査 早速現象が発生したのでsshで入って調査 「アプリケーションサーバーがスラッシングしている…」 「MySQLのdisk ioがえらいことに…slow query logどこにあります?」 「見ても分からないからoffっちゃったんだよね」

Slide 7

Slide 7 text

サイトが落ちる原因 もはやうろおぼえ (当時書いたドキュメントもリポジトリもサービス譲渡時に引き渡し 済、チャットはSkypeだったのでログもない) アプリケーションサーバーでメモリが足りていない ピーク時に不足 → swap in/outが多発 MySQL で filesort するクエリが大量に発生 JOIN したテーブルの count だったはず お題 → 投稿(こえ) → コメントを数えるクエリ disk IO 多発 nginxでコネクション数が足りなくて詰まる 当時からnginxをバリバリ使っていたのは先進的だが

Slide 8

Slide 8 text

当時は「diskにアクセスしたら負け」 2011年当時借りていたレンタルサーバー(専有) CPU 2〜4コア メモリ 4〜8GB ストレージ SATA HDD シングル or RAID-1 (7200rpm IOPS 100以下ぐらい?) ストレージがとにかく遅い (今から見ると)

Slide 9

Slide 9 text

diskにアクセスするとなぜ負けてしまうのか(当時) アプリケーションサーバーでメモリが足りなくなるとswapが発生して遅い swap out = メモリに載りきらない分をdiskに書き出す swap in = diskから読んでメモリに戻す diskが遅いため、ほとんどの処理時間をswap処理に費やしてしまう MySQL の filesort が発生すると遅い クエリ結果がメモリに載りきらない場合、ファイルに書いてからソート diskが遅いため(以下略 雪だるま式に待たされるリクエストが増加 nginxのコネクションが埋まって接続不能になる

Slide 10

Slide 10 text

具体的なチューニング アプリケーションサーバーのプロセス数を減らす swap が発生しない数まで下げる slow query logを出す 関連テーブルの count 結果を更新時にカラムに保存する いわゆる「非正規化」 関連テーブルのINSERT時に +1, DELETE時に -1 一度メンテナンスを入れてデータを埋めてindexを足して再開 covering index にできたので filesort 撲滅 nginxの設定 worker_connections 1536 → 4096 keepalive_timeout 65 → 5

Slide 11

Slide 11 text

タイムライン 2011-01-21(金) 入社 2011-01-25(火) 障害の原因特定 2011-01-26(水) アプリケーションサーバーとnginxの対処 2011-01-31(月) メンテを入れてDBの非正規化で根本解決

Slide 12

Slide 12 text

思い出と教訓 2011当時はマシンのスケールアップもスケールアウトも容易ではなかった その場にあるリソースでなんとかしない限りいつまでも解決できない お金で殴れない ISUCONの初回も同年 固定スペック、台数での競技(今も)

Slide 13

Slide 13 text

2011 → 2024 ストレージがそもそも速い(SSDで数万IOPSが普通、当時のHDDは100 IOPS/1本程度) メモリから溢れても即終了にならない クラウドならスケールアップが容易 64コア/512GBとか128コア/1TBとか… 当時のこえ部の問題は、今ならお金を払えばすぐに解決できる アプリケーションサーバーのメモリ = スケールアップ MySQL の filesort = 高速なストレージ、スケールアップ nginx のコネクション数 = マネージドなLBに任せる でも「お金を掛けなくても解決できた」問題でもある

Slide 14

Slide 14 text

お金だけで解決しているといつか行き詰まる スケールアップで解決できる問題はいっぱいあるが、どこかで限界が来る (CPU数、メモリ、IOPSは無限には増やせない) リソースを増やしてもリニアにスケールするとは限らない (特にロック関係のボトルネックがある場合) お金で殴る = 根本対処までの時間を稼ぐための手段 お金で稼いだ時間で原因を見つけて対処するのが大事

Slide 15

Slide 15 text

よかったこと 既にレスポンスタイムが計測、可視化されていた 効果を数値で確認しながら改善を進めることができた 「計測ができていた」 計測ができていないと無駄にお金を払うことになる(かもしれない)

Slide 16

Slide 16 text

よかったこと(2) (入社直後に大きな問題を即解決したので社内での評判が爆上がりした)

Slide 17

Slide 17 text

ボトルネックを心で理解した話

Slide 18

Slide 18 text

Lobi ゲームユーザー向けのコミュニティ サービス 2010年 ナカマップとしてリリース 2013年 Lobiに改名 オンプレミスの仮想化基盤(KVM) アプリケーションサーバーは比較的 容易にスケール可能 hostは24vCPU、台数も余裕あり データベースには ioDrive 当時最強の フラッシュメモリストレージ

Slide 19

Slide 19 text

事件発生 2013年6月某日 ある日のピーク時刻(23時頃)にアプリケーションのレイテンシが急激に悪化 全てのサーバーの CPU, メモリ, IO などは完全に余裕がある 数十分すると回復する 毎日発生するけどモニタリングしても全く理由が分からない slow query log もちゃんと取っているけど何もでてこない

Slide 20

Slide 20 text

何が遅いのか切り分ける アプリケーションからアクセスする 相手 = MySQL, KyotoTycoon PerlのDevel::KYTProfを仕込んで観察 (分散Traceの先駆け的なライブラリ) useするだけで各種外部通信に掛かっ た時間をログに出してくれる 任意のコードにhookを設定可能

Slide 21

Slide 21 text

結果 DBのクエリが遅くなっている 通常時 2,3ms で終わるクエリが 20〜30ms になる つまりDBとクライアントの間に何かあるはず 丹念にグラフを見る ……なんかこのネットワーク転送量、頭打ってるな…?

Slide 22

Slide 22 text

# ethtool eth1 Settings for eth1: Supported ports: [ TP ] Supported link modes: 10baseT/Half 10baseT/Full 100baseT/Half 100baseT/Full 1000baseT/Full Supports auto-negotiation: Yes Advertised link modes: 10baseT/Half 10baseT/Full 100baseT/Half 100baseT/Full 1000baseT/Full Advertised pause frame use: No Advertised auto-negotiation: Yes Speed: 100Mb/s Duplex: Full Port: Twisted Pair PHYAD: 1 Transceiver: internal Auto-negotiation: on MDI-X: on Supports Wake-on: pumbag Wake-on: g Current message level: 0x00000001 (1) Link detected: yes

Slide 23

Slide 23 text

# ethtool eth1 Settings for eth1: Supported ports: [ TP ] Supported link modes: 10baseT/Half 10baseT/Full 100baseT/Half 100baseT/Full 1000baseT/Full Supports auto-negotiation: Yes Advertised link modes: 10baseT/Half 10baseT/Full 100baseT/Half 100baseT/Full 1000baseT/Full Advertised pause frame use: No Advertised auto-negotiation: Yes Speed: 100Mb/s <======================== ???? Duplex: Full Port: Twisted Pair PHYAD: 1 Transceiver: internal Auto-negotiation: on MDI-X: on Supports Wake-on: pumbag Wake-on: g Current message level: 0x00000001 (1) Link detected: yes

Slide 24

Slide 24 text

100Mb/s !!? # ethtool eth1 Settings for eth1: Speed: 100Mb/s 1GbpsのNIC/スイッチなのになぜか100Mbpsでリンクアップしていた (多分オートネゴシエーションがおかしかった) ネットワークが100Mbpsで頭を打った結果 MySQLはクエリを2msで完了して結果を送信する slow logはクエリが完了したタイミングで書かれる(ので出ない) クライアントは結果を受信するのに20ms掛かってしまう(ので詰まる)

Slide 25

Slide 25 text

画に描いたようなボトルネック 1Gbpsに設定変更してあっさり解決 https://ja.wikipedia.org/wiki/ボトルネック より引用

Slide 26

Slide 26 text

対策 NICの設定 # /etc/sysconfig/network-scripts/ifcfg-eth1 ETHTOOL_OPTS="speed 1000 duplex full autoneg on" Zabbixの監視テンプレートで ethtool ethX | grep Speed の結果を取得する設定

Slide 27

Slide 27 text

教訓 1カ所のボトルネックがシステム全体の性能を制約する お金で殴ったつもりでも1カ所のボトルネックが全てを破滅させる

Slide 28

Slide 28 text

昔話をしました 昔は本番で問題解決のためのチューニングをいっぱいしてきた ここ数年はあんまりしてない気がする なぜか?

Slide 29

Slide 29 text

ちゃんと負荷試験をするようになった システムにリクエストを送信して負荷を掛け、 想定の/必要な性能が出ているかどうかを確認するテスト リリース前や新機能追加、大規模改修時にやることが多い カヤックの例 2014年ごろまで: ほぼしていない 2015年ごろから: やりはじめた 2018年ごろから: 本番の想定スケール限界までやる 2015年にはオンプレ廃止、全部クラウドなのでやりやすくなった

Slide 30

Slide 30 text

負荷試験をやる/やらない 負荷試験は結構大変 開発の最終盤にならないとできない(ことが多い) 準備が大変 問題が出たからといってリリースを遅らせられるかというと…(体制による) とはいえ やる = リリース後に発生する性能問題の9割(体感)は把握できる やらない = やっていたら把握できていたはずの問題が、リリース後に全部出る やらないと事故確率が大幅に上がると思ってよい

Slide 31

Slide 31 text

https://speakerdeck.com/fujiwara3/k6niyorufu-he-shi-yan-ru-men-karashi-jian-made

Slide 32

Slide 32 text

負荷試験をする「前」に考えること ゴールを決める レイテンシ(レスポンスタイム)が目標? どのレスポンスを何秒で返せればOKなのか 平均値? 中央値? P99? max? そのレイテンシが保てる最大の並列アクセス数は? スループット(単位時間内に処理できるリクエスト数)が目標? レイテンシ要件はない? レイテンシ1secで1,000並列 レイテンシ10secで10,000並列(どちらも1,000req/sec) あるサーバーリソースでのxxの最大化が目標?

Slide 33

Slide 33 text

負荷試験をする「前」に考えること その試験で測りたいものを言語化する レイテンシ スループット サーバーリソースの利用率・利用量(コストも含む) 運用中に実際に起きるどのような状況で、これらの数値目標を達成するのか 【具体的な例】 状況 : Push通知から○○分以内に○○人のユーザーがアプリを起動する シナリオ : アプリが起動してホーム画面に遷移するときに発生するAPIアクセス 目標 : 平均レイテンシ250msec、P99 2sec、サーバーエラーなし 詳しくは ISUCON本 1章へ

Slide 34

Slide 34 text

負荷試験をする「前」にやること 負荷をかける対象のサーバーリソースは(できるだけ)固定する →「AutoScaling」的なものを止めておく 負荷試験には 「変数が多い」 変数の例: シナリオ(測定対象URL)、リクエストの送信レート、並列度… 固定できないものは、せめてそのサイズ(キャパシティ)を同時に記録しておく 固定した上で、目標に達しない場合は自分で変数(スケール)を変えて試験する

Slide 35

Slide 35 text

負荷試験をする「前」にやること サーバー側のメトリクスを本番同様に取得して、ダッシュボードを作る 最低でも以下を一目で見られる状態に 各種リソース使用率(CPU, Memoryなど) HTTP スループット(req/sec)、レイテンシ、statusコード別 データベースへのクエリ数(など) アプリケーションサーバー固有の情報 同時リクエスト処理数(worker数) job queueがあるならそのメトリクス ネットワークトラフィック

Slide 36

Slide 36 text

結果を評価する 1. 負荷テスト側の出力とサーバー側のメトリクスに齟齬がないか →メトリクスの取得がおかしい可能性が高いので見直す 2. エラーは起きてないか →エラーが目標/想定より多い場合は性能を評価する意味がない 原因を調査する 3. レイテンシは目標に達しているか →これ以上並列度を増やすのはたいてい無意味 ボトルネックの調査をする 4. スループットは目標に達しているか or →並列度を2倍にして変化を見る 無闇に変数を変えて試さない

Slide 37

Slide 37 text

クライアント側で起きる問題の頻出例 ネットワーク帯域が上限を打っている 1Gbpsの回線で1MBのレスポンスを取得→(たったの)125req/sec(=125MB/sec) HTTP Keep-Aliveが無効になっている 3 way handshake のオーバーヘッドが大きい Goで自作した場合にやりがち: http.Response.Body を全て読み切らない →次のリクエスト時は新規のTCP接続になる (ISUCONの作問者も最初はだいたい全員やらかします) ファイルディスクリプタを使い果たしている ulimit -n (max open files)がデフォルトの1024 ローカルポート(エフェメラルポート)が枯渇している 大量/高速に接続切断を繰り返すと使い果たす ISUCON本 9章をどうぞ

Slide 38

Slide 38 text

最近負荷試験していて最初に爆発するところ 身の回りでは最初にRedis(シングル構成)が限界になることが多い Redisは速い という思い込み 確かに単純なクエリは速いが使うコマンドによってだいぶ違う CPUコアを増やしてもスケールしづらい Redis5まではほぼ性能が上がらない 6以降はマルチスレッド化が進んで伸びるようになってきたけど Redis以外のコンポーネントが容易に性能が上げられるようになった クラウドでスケールアウト、スケールアップが用意になった 相対的にスケールしづらいミドルウェアになってしまった

Slide 39

Slide 39 text

困ったらRedis ClusterにすればOK? OKだけど意外と大変 クライアント(ライブラリ)が対応してないとClusterは使えない Clusterでは発行できない処理(複数nodeにまたがった集合演算など)がある アプリケーションでキーの設計や発行するクエリの考慮が必須 開発中にClusterを考慮していないアプリケーションを リリース直前にCluster対応するのはかなり大変……

Slide 40

Slide 40 text

教訓 シングルのRedisはあなたが思ってるよりだいぶ前に限界が来る スケールしたかったら最初からClusterで開発しておいてね (Redis Clusterを手元やCI上げるのが大変という問題はある)

Slide 41

Slide 41 text

負荷試験もした、リリースして最初も乗り切って安定している 長生きしている = 既知の問題が致命的だったらそもそも生きていない 大きすぎる問題はすべて解決済み コードを変更したりアクセスが急増したりしない限り性能問題は出ない! と思いますよね

Slide 42

Slide 42 text

リリース1年後に爆発 リリース後の試練に耐えて安定してきたプロダクト(今はもう10年選手) 複数のインデックスがあるテーブルではMySQLのオプティマイザが使うインデックス を決定する(実行計画) 突然、使って欲しくない方を使うようになって「死」

Slide 43

Slide 43 text

解決法 FORCE INDEX MySQLに特定のインデックスを使うように強制する

Slide 44

Slide 44 text

教訓 誰も何もしていないのに突然死ぬことはある ( 「データが増えた」→「実行計画が変わった」ので何も変わってないわけではない)

Slide 45

Slide 45 text

おまけ: リリース10年を超えた某サービスの今日 レスポンスタイムのp99がここ数日で急上昇 To be continued...