ISUCON 本の読書メモ
調べる必要アリ
- gRPC
- ProtcolBuffer
- WebSocket
- リバースプロキシ
- ベンチマーカー
- SSH 接続
- ディレクティブ(文脈なんだっけ…)
- スロークエリ
- Cookie
- セッション
- k6
- http.file() は multipart リクエストか
- yum コマンド
- プライマリキーが UUID の場合のソート・探索方法
- IN 句を使用した N+1 問題の解消法
- sqlx
- echo
- TLS
- MySQL の設定のうち、再起動する必要があるもの・ないもの
- my.cnf
- mysqld.cnf
gRPC, ProtcolBuffer とは?
❓ プロトコル?
❓ WebSocket も改めて何?
リバースプロキシとは?
ベンチマーカーとは?
- 手動でリクエストを送るのではなく、機械的に自動でリクエストを送る処理をするもの
SSH 接続とは?
ディレクティブとは?
負荷試験
- ベンチマーカーで計測(ab コマンド)
- Web アプリのログで計測(nginx の combined 形式で json を指定し、alp で解析)
実運用時はベンチマーカーで負荷をかけられないので、Web アプリのログとも比較する
負荷試験ではベンチマーカーを使用
request_time
と upstream_response_time
をどっちも計測(p.61 図5)
比較ではレスポンスタイムを使用したが、負荷試験ではリクエスト数(スループット)を使用する
→ レイテンシが一定以下になれば、無視できるレスポンスタイムになるので、その場合はリクエスト数の方が重要になるため(pp.71~73)
スロークエリとは?
MySQL のスロークエリログ出力
long_query_time
を0にして、高速かつ大量に実行されているクエリも記録する
50ミリ秒でも、大量に実行されてたらボトルネック
mysqldumpslow で読んでる行数が分かる(p.78)
3行返すために10万行読んでるのは非効率
→ インデックスを貼る(インデックスとは❓)
ab コマンドが出力した Request per second とクエリの実行数(Count)から1リクエスト当たりの実行数が分かる
→ N+1 問題など
- ベンチマーカーの並列度を上げてみる
- Request per second が上がっているか
- Time per request が下がっているか
- サーバーの処理能力が上がっているか(CPU は top、メモリは free で確認できる)
Web アプリケーションのプロセス数は CPU のコア数の数倍(5倍など)にしてみる
シナリオありの負荷試験
ユーザーの振る舞いを模倣する
- リクエストを送信する
- レスポンスを解析する
- HTML 内のリンクを辿る、フォームを submit する
-
- から繰り返す
ISUCON では1分を負荷走行時間とする
本来はウォームアップの時間も含め、データを3点以上計測できる時間にする
k6
js でシナリオを書ける
ユーザーの行動を模倣するために、sleep
関数で時間を空ける
サーバーから送信された Set-Cookie
ヘッダを解釈し、後続のリクエストに自動で Cookie
ヘッダを付与する
→ ログインセッション維持のための記述は不要
check
関数でレスポンスの内容をチェック
リダイレクトは自動でされるので、ステータスコードは 302
などではなく 200
であることを確認
SharedArray で VUs(Virtual Users)で共用する読み取り専用のデータを保持できる(k6 初期化時に一度だけメモリに読み込まれ、書き込みできない)
k6 の結果も、nginx のアクセスログを alp で解析した結果と比較する
→ 呼び出し回数が多いもの、相対的にレスポンスタイムが遅いものを洗い出す
💡 5章以降で Web サービスを構成する各要素に踏み込んでチューニング
💡 逆に、1~4章は概論
実際の負荷試験では…
- 画像や CSS などの静的コンテンツの配信も考慮する
- アクセスログから、ユーザーの利用状況を把握する(想定されるユースケースでシナリオを構築する)
Cookie・セッションとは?
k6 の http.file() は multipart リクエスト?
❓ バイナリを Base64 でエンコードするかなどは Web アプリケーションの実装による?
RDBMS
- 一貫性
- 複数サーバーにデータを分散させるのが難しい
- バックアップやリードレプリカでスケーラビリティを確保
NoSQL
memcached
- KVS
- メモリ上でデータを扱うため高速
- データの一時保存
- RDBMS から読み出した変化しにくいデータを保持して、RDBMS への参照回数を抑えられる
Redis
- 幅広いデータ構造をサポート(List, Set など)
- 非同期処理のためのジョブキュー、ランキングなどで利用(RDBMS が苦手な領域)
- 複数サーバーにデータを分散配置して、可用性・スケーラビリティを実現
NewSQL
- RDBMS の特徴をもちながら、複数サーバーにデータを分散できる
- 一貫性
- SQL を利用する
- サーバーコストやレイテンシで劣ることもある
大規模なサービスを開発しない限り、メインは RDBMS で良い
ピンポイントで Redis などを活用
DB の負荷を測る
MySQL の SHOW PROCESSLIST コマンド
MySQL はマルチスレッドで動くので、どんなスレッドがいるか分かる
Sending data State は大量のデータを読み込んでいる可能性アリ
スロークエリログでクエリを解析(3章4節)
SET GLOBAL コマンドで、MySQL を再起動せずに変更できる
- 再起動すると元に戻る
- 永続化する場合は my.cnf に書くか、SET PERSIST を使用
pt-query-digest は mysqldumpslow より詳しく分析できる
https://github.com/kazeburo/query-digest で調査したいときだけスロークエリの出力方法を変更できる
pt-query-digest の結果
- 全体的な統計
- ランキング
- 各クエリの詳細
まずランキングに注目
負荷が大きかった順
- 実行回数は少ないが、1回当たりの負荷が大きいもの
- 1回当たりの負荷は小さいが、実行回数が大きいもの
Lock time が小さい
→ 他のスレッドの影響を受けている可能性はない
yum コマンドとは?
インデックス
インデックス=索引
特定のルールに沿って並べたもう一つの DB を作成し、高速で検索する
B ツリー
二分探索に適した構造
post_id
にインデックス post_id_idx
を作成
int 型で、インデックス内部は昇順で並んでいる
→ 二分探索で高速で検索できる
SQL クエリの先頭に EXPLAIN 句を付与すると、クエリの実行結果を得られる
❓『複合インデックス・並び替えにも使われるインデックス』(pp.135~140)分からない
-
post_id
で並び替えた後にcreated_at
で並び替えられているのはなぜ?(どちらで先に並び替えるかをどう指定している?) -
created_at
にインデックスを作成してUsing filesort
が解決したのはなぜ?-
created_at
でソートするようにクエリに含んでいたため?
-
- 降順インデックスを使用して
Backward index scan
が解決したのはなぜ?- クエリで
created_at
をDESC
でソートするように指定していたから? - 逆にクエリで
created_at
を昇順でソートするように指定していたら、Backward index scan
はそもそも発生していなかった?
- クエリで
プライマリインデックス
プライマリーキーに貼られるインデックス
1つのテーブルに1つのプライマリインデックス
複合インデックスでも作成可能
❓ プライマリーキーが UUID の場合はどのようにソート・探索する?
プライマリインデックス以外=セカンダリインデックス
クラスターインデックス構造
- プライマリインデックスが示す先にデータが含まれる
- セカンダリインデックスが示す先にプライマリーキーが含まれる
→ プライマリインデックスでの検索時にデータを読み取る回数を減らせる
→ どゆこと? - クラスターインデックス構造ではなく、単にプライマリインデックスを貼るだけで読み取り回数を減らせるのでは?
- あるいは、プライマリインデックスを貼った時点でセカンダリインデックスという概念が生まれて、クラスターインデックス構造になる?
セカンダリインデックスを使用した検索では、id
しか必要ない場合はプライマリインデックスまでアクセスする必要がない(Covering Index)
インデックスを貼る戦略
インデックスが増えるとオーバーヘッドが大きくなる
よって以下の方針でインデックスを貼る
- 頻繁に使使用するカラム(
id
など) - ORDER BY 句で使用するカラム(
created_at
など)
N+1
1回のクエリで得た結果 N 件に対して、関連情報を集めるため、追加で N 回以上のクエリを実行してしまう = N+1 問題
N+1 問題の見つけ方
- スロークエリログを取得して、実行数が多いクエリを見つける
- アプリケーションのソースコードでループ中にクエリを実行している箇所を探す
💡 つまり、アプリケーションのソースコードを修正する❗️
バッチのみで呼ばれて、実は速度に影響がない場合もある
N+1 の修正方法
キャッシュを使う
- キャッシュを保持するミドルウェアが必要
- DB の負荷を軽減できる
- 大規模サービスで使われる
あまり変化しないデータを memcached や Redis に保持しておく
- キャッシュを取得
- キャッシュがない場合、DB から取得
- キャッシュにデータを保持
1回目のアクセスではキャッシュが空なので N+1 が発生するが、2回目以降はキャッシュがあるのめ発生しない
💡 ただし、アプリケーションサーバーとキャッシュサーバーの通信は N 回以上行われるので、N+1 問題は残ってはいる
→ GetMulti
で一度の通信でまとめて取得
キャッシュが容易に消える環境や負荷の厳しい状況では別クエリでのプリロードを組み合わせる
別クエリでプリロードする
- ミドルウェア不要
- 複雑な SQL でナシ
- 実装コストも高くない
💡 IN 句を使用して DB からデータを取得
- プレースホルダ用のリストはなぜ作る?どう使う?
- IN 句の
("+strings.Join(placeholders, ",")+")"
の意味は?(p. 151)
JOIN 句を使った方法よりシンプルで、ミドルウェアも不要だが、十分に高速になる
→ 最適化の最初のステップとしてオススメ
→ 💡 イマイチ理解できてないので後で調べる
JOIN 句を使う
- 高速に動作
- DB の分割が必要になると、開発上のボトルネックになる
💡 今回は INNER JOIN のこと
- INNER JOIN
- 2つのテーブル間で共通のデータ "のみ" を取得する
- 左側(指定された最初のテーブル)のテーブルも、結合条件が一致する右側のテーブルの値がなければ表示されない
- LEFT JOIN
- 左側のテーブルの値は全て表示する
- 右側のテーブルの値は、結合条件が一致する場合は表示して、一致しない場合は NULL で埋める
sqlx
- Go 言語で DB を扱うときに使用
- JOIN したデータを構造体にマッピングするため、取得する際のカラム名を
user.実際のカラム名
としている(pp.153~154)
❓ DB 操作は sqlx、API 実装は echo?
FORCE INDEX・STRAIGHT_JOIN
💡 インデックス = KEY
当然、PRIMARY KEY もインデックス
💡 EXPLAIN 句を使用すると、クエリの実行結果を得られる(再掲)
FORCE INDEX 句を使用すると、使用するインデックスを指定できる
ORDER BY 狙いの posts_order_idx
を指定(pp. 155~157)
→ rows
が1234から30に減少
STRAIGHT_JOIN 句を使用すると、SQL クエリに書いた順序でテーブルを操作させられる(pp.157~158)
SELECT STRAIGHT_JOIN ... FROM "posts" p JOIN "users" u ON ...
LEFT JOIN では STRAIGHT_JOIN を使用できないので、JOIN_ORDER を使用する
DB に格納されるデータやオプティマイザの統計情報によって、使用されるインデックスは変化する❗️
元々使用されていたインデックスが突然使用されなくなるなど
↑ FORCE INDEX や STRAIGHT_JOIN でパフォーマンスチューニングする
必要なカラムだけを取得
SELECT *
を使用して、不必要な情報まで毎回取得している可能性アリ
画像(LONGBLOB)などの取得は通信コストが高いので注意❗️
カラムに INVISIBLE パラメータをつけると、SELECT *
したときの出力を抑制できる(後で調べる)
プリペアドステートメント
プリペアドステートメント
- 変数を埋め込める SQL クエリを発行し、DB でキャッシュしておく
- クライアントから変数のみを送ることで、実行効率が上がる
- SQL インジェクション対策にもなる
Web アプリケーションでは発行するクエリの種類が多く、キャッシュが有効活用されないこともある
→ プリペアドステートメントを準備する PREPARE クエリと、作ったステートメントを解放する CLOSE クエリが必要になる
→ 通信の回数が増え、かえって効率が悪くなる
go-sql-driver/mysql(Go の MySQL ドライバー)ではデフォルトで有効
→ ドライバーをインスタンス化するときに interpolateParams
を true にする(p.160)
DB の最大接続数・接続保持数
❓ TLS とは?
MaxOpenConns
- DB への最大コネクション数
- デフォルトは0(無限)
MaxIdleConns
- idle 状態の保持件数
- デフォルトは2
アプリケーションサーバーへの同時リクエスト数が増えるほど、DB への接続が増える
DB 側では最大の接続数があるので、それを超えると接続エラー
↑ MySQL では max_connections
で同時接続数を設定される
→ MySQL の再起動なしに動的に変更可能(my.cnf に追加するか、SET PERSIST)
❓ MySQL の再起動なしに動的に変更可能な設定と、再起動が必要な設定の違いは?
❓ my.cnf と mysqld.cnf の違いは?
MySQL はスレッドモデル
- 1つの接続に対して1つのスレッド
- スレッド当たりのメモリ使用量は比較的小さい
- しかし、小規模なリソースではメモリ不足に陥ることもある
- アプリケーション側の MaxOpenConns・MaxIdleConns を適切に設定する必要アリ
pp.162~164 のコラム後で読み返す
I/O 負荷が大きい場合は CPU 使用率のうち io-wait
が高くなる
io-wait
:CPU の処理上でデータの読み書きをしている時間
SSD などのストレージは、CPU やメモリに比べてデータの読み書きが遅い
よって、高速化のためには
- ストレージからの読み取り回数を減らしつつ、
- データを失わないよう安全性のバランスをとりつつ、
- ストレージへの書き込み回数を減らす
【読み取り】データサイズの確認
OS のディスクキャッシュで、一度ファイルから読み出したデータはメモリ上にキャッシュされる
→ 次回のアクセスから高速化
MySQL では、/var/lib/mysql
以下にデータがあり、*.ibd
ファイルのサイズを確認する
# *.ibd ファイルのサイズの合計を確認する
sudo ls -lh var/lib/mysql/isuconp
> total 1.4G
【読み取り】Buffer Pool
InnoDB Buffer Pool
- 読み込んだデータ・インデックスをメモリ上に確保する
- DB 専用に確保することで、高速なアクセスを実現
-
innodb_buffer_pool_size
でこの領域のサイズを設定- MySQL では 128MB デフォルト
- アプリケーションサーバーと共用ではなく、DB 専用のサーバーを用意する場合は、物理メモリの80%程度を割り当てる
OS によるディスクキャッシュと二重でメモリを確保しないよう、DB ファイルの読み書きをする際に 0_DIRECT
フラグを有効にする
↑ my.cnf の server
ブロックに追記する
❓ my.cnf に server
ブロックがあるということは、my.cnf はサーバーそのものの設定?
【書き込み(更新)】ストレージへの同期
データ更新時の処理がボトルネックの場合、スロークエリログで更新クエリが記録される
↑ pt-query-digest などで確認
fsync
- 一度コミットしたデータを失わないようにする工夫
- ディスクキャッシュ上に置いたデータをストレージデバイスに同期する
- ミリ秒単位の時間がかかる(サーバー内の処理としては遅い)
更新を高速化するには、fsync をやめて OS の非同期的なフラッシュ操作に任せる
→ 数秒・数十秒おきに実行されるため、電源が落ちたり OS がダウンしたりするとデータを失う
→ 更新の高速化と性能はデータを失うリスクを考慮する
MySQL では innodb_flush_log_at_trx_commit
で設定
- デフォルトは1
- コミットごとに更新データをログ(REDO ログ)に書き、ログをフラッシュ
- 0
- 更新データを1秒おきにログに書く
- フラッシュしない
- 2
- コミットのたびにログを書く
- 1秒ごとにフラッシュ
→ 0 or 2 では最大1秒間のデータを失う可能性があるが、パフォーマンスを優先できる
MySQL ではデフォルトで更新ログ(バイナリログ)が有効になっている
バイナリログ
- 複数のサーバーに非同期的にデータを複製する
- リードレプリカを作成する、レプリケーション・高可用性な構成に必要不可欠
→ ISUCON など、冗長化構成が不要な場合は無効化することでストレージへの書き込み処理を減らせる
disable-log-bin = 1
バイナリログが必要な場合、sync-binlog
の設定を変更することで I/O 処理を軽減できる
- デフォルトは1
- コミットごとに更新ログをフラッシュする
- 0
- フラッシュ命令をやめ、OS に委任する
- 1以上の数値
- コミットが更新ログに書き込まれた回数ごとにフラッシュする