Closed31

ISUCON 本の読書メモ

ピン留めされたアイテム
どぎーどぎー

調べる必要アリ

  • gRPC
  • ProtcolBuffer
  • WebSocket
  • リバースプロキシ
  • ベンチマーカー
  • SSH 接続
  • ディレクティブ(文脈なんだっけ…)
  • スロークエリ
  • Cookie
  • セッション
  • k6
    • http.file() は multipart リクエストか
  • yum コマンド
  • プライマリキーが UUID の場合のソート・探索方法
  • IN 句を使用した N+1 問題の解消法
  • sqlx
  • echo
  • TLS
  • MySQL の設定のうち、再起動する必要があるもの・ないもの
    • my.cnf
    • mysqld.cnf
どぎーどぎー

ベンチマーカーとは?

  • 手動でリクエストを送るのではなく、機械的に自動でリクエストを送る処理をするもの
どぎーどぎー

負荷試験

  • ベンチマーカーで計測(ab コマンド)
  • Web アプリのログで計測(nginx の combined 形式で json を指定し、alp で解析)

実運用時はベンチマーカーで負荷をかけられないので、Web アプリのログとも比較する
負荷試験ではベンチマーカーを使用

request_timeupstream_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倍など)にしてみる

どぎーどぎー

シナリオありの負荷試験

ユーザーの振る舞いを模倣する

  1. リクエストを送信する
  2. レスポンスを解析する
  3. HTML 内のリンクを辿る、フォームを submit する
    1. から繰り返す

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 などの静的コンテンツの配信も考慮する
  • アクセスログから、ユーザーの利用状況を把握する(想定されるユースケースでシナリオを構築する)
どぎーどぎー

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. 全体的な統計
  2. ランキング
  3. 各クエリの詳細

まずランキングに注目
負荷が大きかった順

  • 実行回数は少ないが、1回当たりの負荷が大きいもの
  • 1回当たりの負荷は小さいが、実行回数が大きいもの

Lock time が小さい
→ 他のスレッドの影響を受けている可能性はない

どぎーどぎー

インデックス

インデックス=索引
特定のルールに沿って並べたもう一つの 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_atDESC でソートするように指定していたから?
    • 逆にクエリで 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 問題の見つけ方

  1. スロークエリログを取得して、実行数が多いクエリを見つける
  2. アプリケーションのソースコードでループ中にクエリを実行している箇所を探す

💡 つまり、アプリケーションのソースコードを修正する❗️

バッチのみで呼ばれて、実は速度に影響がない場合もある

どぎーどぎー

N+1 の修正方法

キャッシュを使う

  • キャッシュを保持するミドルウェアが必要
  • DB の負荷を軽減できる
  • 大規模サービスで使われる

あまり変化しないデータを memcached や Redis に保持しておく

  1. キャッシュを取得
  2. キャッシュがない場合、DB から取得
  3. キャッシュにデータを保持

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 やメモリに比べてデータの読み書きが遅い

よって、高速化のためには

  1. ストレージからの読み取り回数を減らしつつ、
  2. データを失わないよう安全性のバランスをとりつつ、
  3. ストレージへの書き込み回数を減らす

【読み取り】データサイズの確認

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以上の数値
    • コミットが更新ログに書き込まれた回数ごとにフラッシュする
このスクラップは7日前にクローズされました