「[24時間365日]サーバー/インフラを支える技術」を読んだ
読んだのでメモ。
1章 冗長化・負荷分散の基本
コールドスタンバイ -> 予備機を使わずに、現用機に障害が発生したら切り変えること(e.g. ルーター) ホットスタンバイ -> 予備機も稼働させておくことで現用機と同じ状態を保つこと(e.g. Webサーバー) フェイルオーバー -> 現用機に障害が発生した時に自動的に処理を予備機に引き継ぐこと
Webサーバーのフェイルオーバーは2段階。
- ヘルスチェック
- 仮想IPの引き継ぎ
1. ヘルスチェック
IP引き継ぎの機会(障害の発生)を検出する。
- ICMP監視(レイヤ3)
- ping
- マシンに電源が入っていることをチェックするだけなので、webサーバーが落ちているかはわからない。
- ポート監視(レイヤ4)
- TCPで接続
- Webサーバーが稼働しているかチェックするだけなので、サービスのエラーはわからない。
- サービス監視(レイヤ7)
- HTTPリクエスト
- サービスのエラーはわかるが、他と比べると負荷が高い。
2. 仮想IPの引き継ぎ
- ただIPを別のサーバーに割り当てても通信はルーターから届かない
4章 性能向上、チューニング。Linux単一ホストの負荷の見極め
負荷 -> 複数のタスク(プロセス)がリソースを奪い合う結果生じる待ち時間
ボトルネック見極めの基本的な流れ
多くの場合Webアプリケーションにおいては 負荷分散 = ディスクI/Oの軽減
- キャッシュを効かせることが鍵
AppサーバーはCPUバウンドであることが多い
- 基本的にDBサーバーから受け取ったデータを加工してクライアントサイドに送るだけだから
DBサーバーはI/Oバウンドであることが多い
- 多くのデータをディスクからフェッチするから
topなどで出る3つのロードアベレージの数字はそれぞれ直近1分、5分、15分の間で待ち状態にあったタスクの単位時間あたりの数
- プロセススケジューラがプロセスディスクリプタの情報を元にタスクの優先順位を決める
- ディスクリプタの正体は
task_struct
という構造体 https://github.com/torvalds/linux/blob/61a09258f2e5b48ad0605131cae9a33ce4d01a9d/include/linux/sched.h#L629 - ディスクリプタに記録されるプロセスの状態:
- TAS_RUNNING ... 実行可能 or 実行中。CPUリソースが余っていなくて待たされている
- TASK_INTERRUPTIBLE... 割り込み可能。ユーザーの入力待ちなどの予測のできない復帰
- TASK_UNINTERRUPTIBLE ... 割り込み不可能。ディスクの入出待ちなどの短時間の復帰
- TASK_SLEEP
- TASK_ZOMBIE ... 子プロセスがexitして親プロセスにkillされるのを待っている状態
- ディスクリプタの正体は
- プロセススケジューラがプロセスディスクリプタの情報を元にタスクの優先順位を決める
ロードアベレージのカーネルコードを見る
- run queueに存在するTASK_RUNNINGとTASK_UNINTERRUPTIBLEの2つをカウントしている
- ロードアベレージにカウントされるのはCPU or I/O待ちで実行されていないタスク
long calc_load_fold_active(struct rq *this_rq, long adjust) { long nr_active, delta = 0; nr_active = this_rq->nr_running - adjust; nr_active += (long)this_rq->nr_uninterruptible; if (nr_active != this_rq->calc_load_active) { delta = nr_active - this_rq->calc_load_active; this_rq->calc_load_active = nr_active; } return delta; }
CPU使用率とI/O待ち率
- マルチCPU(シングルコアを仮想的に複数のcpuに見せることが可能)
- cpu負荷は分散できるがI/O負荷は分散できない
- e.g) 2cpuだとして、全体のI/O負荷20%、cpu1のI/O負荷0%、cpu2のI/O負荷40%がありえる
- CPU使用率、I/O待ち率はCPUごとに見ていく必要がある
- e.g) 2cpuだとして、全体のI/O負荷20%、cpu1のI/O負荷0%、cpu2のI/O負荷40%がありえる
- cpu負荷は分散できるがI/O負荷は分散できない
CPU使用時間などを記録する構造体。sarが表示する項目が全部入っている:
enum cpu_usage_stat {
CPUTIME_USER,
CPUTIME_NICE,
CPUTIME_SYSTEM,
CPUTIME_SOFTIRQ,
CPUTIME_IRQ,
CPUTIME_IDLE,
CPUTIME_IOWAIT,
CPUTIME_STEAL,
CPUTIME_GUEST,
CPUTIME_GUEST_NICE,
NR_STATS,
};
cpu使用率のカーネルコードを見る
タイマ割り込みの度にaccount_process_tickを呼ぶ
void update_process_times(int user_tick) { struct task_struct *p = current; /* Note: this timer irq context must be accounted for as well. */ account_process_tick(p, user_tick); run_local_timers(); rcu_sched_clock_irq(user_tick); ...
user_tickが正であればaccount_user_timeを、そうでなければaccount_system_timeもしくはaccount_idle_timeを呼ぶ
void account_process_tick(struct task_struct *p, int user_tick) { u64 cputime, steal; if (vtime_accounting_enabled_this_cpu()) return; if (sched_clock_irqtime) { irqtime_account_process_tick(p, user_tick, 1); return; } cputime = TICK_NSEC; steal = steal_account_process_time(ULONG_MAX); if (steal >= cputime) return; cputime -= steal; if (user_tick) account_user_time(p, cputime); else if ((p != this_rq()->idle) || (irq_count() != HARDIRQ_OFFSET)) account_system_time(p, HARDIRQ_OFFSET, cputime); else account_idle_time(cputime); }
各関数はtask_struct構造体の対応するメンバに時間を足して記録する
void account_user_time(struct task_struct *p, u64 cputime) { int index; /* Add user time to process. */ p->utime += cputime; /* <- ここ account_group_user_time(p, cputime); index = (task_nice(p) > 0) ? CPUTIME_NICE : CPUTIME_USER; /* Add user time to cpustat. */ task_group_account_field(p, index, cputime); /* Account for user time used */ acct_account_cputime(p); }
void account_system_time(struct task_struct *p, int hardirq_offset, u64 cputime) { int index; if ((p->flags & PF_VCPU) && (irq_count() - hardirq_offset == 0)) { account_guest_time(p, cputime); return; } if (hardirq_count() - hardirq_offset) index = CPUTIME_IRQ; else if (in_serving_softirq()) index = CPUTIME_SOFTIRQ; else index = CPUTIME_SYSTEM; account_system_index_time(p, cputime, index); } void account_system_index_time(struct task_struct *p, u64 cputime, enum cpu_usage_stat index) { /* Add system time to process. */ p->stime += cputime; /* <- ここ account_group_system_time(p, cputime); /* Add system time to cpustat. */ task_group_account_field(p, index, cputime); /* Account for system time used */ acct_account_cputime(p); }
void account_idle_time(u64 cputime) { u64 *cpustat = kcpustat_this_cpu->cpustat; struct rq *rq = this_rq(); if (atomic_read(&rq->nr_iowait) > 0) cpustat[CPUTIME_IOWAIT] += cputime; else cpustat[CPUTIME_IDLE] += cputime; }
psとスレッド
- 別名LWP(Light Weight Process)
- カーネル内では"タスク"としてプロセスと同じ扱いを受ける
- ps -Lで見れる
- ps aux -L | wc -l と数字が一致するので、sar -qのplist-sz(システムに存在するプロセス数)にはスレッドの数も含まれている雰囲気
ps, sar, vmstatの使い方
- ps
- TIMEはCPU使用時間のことであり、プロセスが起動してからの時間ではない
- sar
- sadcというバックグラウンドで動く、カーネルからレポートを収集して保存するプログラムがある
- 時間毎のOSの状態が見れるので、プログラムなどを入れ替えた直後に使ったりする
/var/log/sa
に過去の情報がある
- vmstat
その他
- Linuxは可能な限り空いているメモリをキャッシュに回そうとする
- DBサーバー再起動->flushでキャッシュが空になる->データ全体に読み込みをかけることでキャッシュ構築->本番環境に戻す