【詳解システムパフォーマンス】システムのパフォーマンスチューニングができるようになるまでの日誌。
動機
エンジニアを目指し、大学3年の就職活動を通してあらゆるエンジニアから受けたフィードバック
それは
「ひろけい君はいろんな技術に手を出してて、興味領域が広いことは良いんだけど、なんかどれも中途半端なんだよね。。。」
横に長く勉強した結果、技術の名前と使い方は知ってるけど、それだけの人間になってしまった。
そこで次の目標は
「T型エンジニアになる」ということ。
何か一つ、これに関しては誰にも負けないという分野を確立することが今の自分に必要だったと思った。SREとして業務に活かせつつ、自分の伸ばしたい領域とかぶっているのはどんな技術かといえば、OSが思い浮かんだ。
半年後に初めてISUCONに参加するというイベントもあり、OSの観点からパフォーマンスチューニングができるようになるために、この書籍で学んでいきたいと思う。
お約束ごと
- 終わらせることではなく、内容を理解することを重視する。
- 一文単位でまとめない
- ページ数多いので終わらない
- 重要だと思った内容のメモや、セクションのまとめが自分の言葉でまとまっていれば良い
- 毎日やる
- 一日5ページでも良い。繰り返し思い出しながら、少しずつミルフィーユのように知識を積み上げていこう!
第1章 イントロダクション
システムパフォーマンス改善の目標は、レイテンシと計算コストを下げてエンドユーザーの体験を向上させること。
システムパフォーマンスにおけるフルスタックは、フロントバックエンドというアプリケーション側の意味とは違う。
- ハードウェア
- カーネル
- システムライブラリ
- アプリケーション
と、ハードウェアからアプリケーションまでの広い視点で使われる。
一般的なソフトウェアのスタック(システムパフォーマンスにおける担当範囲)
- ユーザーレベル
- アプリケーション
- Webサーバー
- アプリケーションサーバー
- DB
- コンパイラ
- システムライブラリ(アプリケーションが動作するために最低限必要なライブラリ)
- アプリケーション
- システムコール
- カーネルレベル
- カーネル
- スレッドスケジューラ
- ファイルシステム
- ネットワークスタック
- 仮想メモリ
- デバイスドライバ
- ハードウェアデバイス
- CPU
- NIC
- メモリ
- ディスク
- カーネル
コンパイラもシステムパフォーマンスの関心領域なのは驚き!
システムを構想してからリリースするまでにどんな作業がある?
リリース前
- 将来的に達成したいパフォーマンス上の目標を設定し、パフォーマンスモデルを作る
- プロトタイプのソフトウェアとハードウェアからパフォーマンス特定を掴む
- テスト環境でパフォーマンス分析を行う
- プロダクトの新バージョンのために非回帰テストを行う
- リリースのベンチマークテストを行う
- 本番環境でPoCテストを行う
リリース後
- 本番環境でパフォーマンスチューニングを行う
- 本番稼働しているソフトウェアをモニタリングする
- 本番環境でのパフォーマンス障害を分析する
- 本番環境でのパフォーマンス障害インシデント評価を行う
- 本番環境の分析を強化するためのパフォーマンスツールを開発する
PoCテストとは?
Proof of Concept(概念実証)の略。
新しい技術やアイデアが実現可能かどうかを検証するテスト。
リリースされたシステムが本番環境上で期待通りのパフォーマンスを発揮できるかをテストしたりする。
当たり前だが本番環境でパフォーマンス障害が発生した場合、開発段階では問題を検出、修正できなかったということになる。
できればハードウェアを選択したり、ソフトウェアを書いたりする前からパフォーマンスエンジニアリングをしたいところだが、どうしても問題が発生してからの対応になってしまいがち。
だが、もうそのタイミングでは遅く、アーキテクチャ上の決定に起因するパフォーマンス障害の修復は難化する。
カナリアテスト
クラウドの登場により、リリース前の作業を省略した概念検証テストを行うという新しいテクニックが生まれた。
例)
既存機能Aと、新しい機能A'がある
- 機能A'だけを提供するサーバーインスタンスを作成し、デプロイ。
- 元々あった機能Aから、A'を使うようにする
- 実際にA'を使ってみてテストする
物理的なハードウェアの準備が不要で迅速に性能や安定性を評価できるのがメリット。
新バージョンと旧バージョンを両方使うこともできるので、旧バージョンに戻す(ロールバックする)こともできる。
このように、一部のリクエストやユーザー(全体の1%にのみ公開とか)にだけ新機能をリリースし、本番環境でテストすることをカナリアテストという。
ブルーグリーンデプロイ
本番環境を2つ用意し、これらを交互に切り替えてリリースしていく手法。
ブルー: 現行で稼働しているサービス
グリーン: 新規リリースされるサービス
グリーン環境でのデプロイが完了し、テストが完了したらサービスの接続先をブルーからグリーンに切り替えることでリリースされる。
もしグリーン環境で不具合が発生したりした場合、接続先をブルー環境に切り替え、ロールバックすることができる。
このようにフェイルセーフ(失敗しても安全な状態を保てるよう)な方法を導入することで、あらかじめパフォーマンス分析をすることなく新ソフトウェアを本番環境でテストしてしまうことが多い。
カナリアテストは一部の機能に限定したものだが、サービス全体を切り替えてリリースしていこうというものがブルーグリーンデプロイと言える。
キャパシティプランニング
名前通り、設計中のシステムがどのくらいのキャパシティを持っていて、将来考え得る負荷に耐えられるためにどのくらいのリソースを持っているべきかを予測すること。
また、デプロイ後にリソースの監視をして将来的に発生し得る問題を予測する。
パフォーマンス分析の2つの視点
以下の二つの視点から見ることができる。
- ワークロード分析
- リソース分析
ワークロード分析はソフトウェアスタックのうち、アプリケーションの層から分析し、処理の負荷を軽減する視点。
リソース視点はソフトウェアスタックのうち、デバイスなどのハードウェアから分析していき、リソースを最適に分配していく視点。
ソフトウェアエンジニアリングの中でも、パフォーマンスは主観的な問題になることが多いんやな。
開発の分野においては、エラーは白黒はっきりとつく問題だ。エラーが発生していればバグや不具合があり、なければ問題ない。
一人のユーザーがパフォーマンスが「悪い」といっても、別のユーザーはパフォーマンスが「良い」ということもあるよね。
そういった主観の問題を客観的な問題にする方法がある。
それは、「目標を作る」こと。
目標平均応答時間は0.5sだ!1sかかっていたら問題!と具体的に達成したい目標を持つことで、エラーと同じように客観的な問題になる。
システムパフォーマンスの問題って複雑で難しい。。。
主観的な問題が客観的になったとしても、パフォーマンスの問題はとても複雑であることが多い。
まず、分析の自明な出発点がないことが難しい。
どこがボトルネックになっているのかがわからないまま、「DBが遅いのではないだろうか?」とか、「キャッシュに問題があるのではないだろうか?」と推測で問題を探すと、その推測が合っていて方向性が間違ってないかも分からなくなってしまう。迷宮入りだ。。。
システムを分割して分析すれば、サブシステム(小さな単位のシステム)のパフォーマンスは良好だが、構造が複雑故にパフォーマンスの問題が起こっている可能性もある。
問題を起こしたサブシステムが他のサブシステムの不具合の原因になっているような「障害の連鎖」が起こっている場合もある。
解決するには複雑な構造をときほぐし、どのように問題に関わっているのかを理解しなければならない。
ボトルネックが大きすぎて、一部のボトルネックを解消してもまた別の箇所でボトルネックが発生し、問題を問題を移しただけになってしまうこともある。
複雑なパフォーマンスの問題の解決には、マクロ(ホリスティック、全体を見るよう)な視点を持つことが大事。システム内部と外部のやりとりの両方を調査しなければならない。なので必要になる知識の幅が広く、一人では到底担当できないような職なんだな。
試しにKubernetesのIssuesを見てみると、performance
と検索するだけで155件ものパフォーマンス問題が解決していない状態だ。
大規模で複雑なシステムには発見されているけど、なかなか解決できないパフォーマンス問題が多くあるようだ。
ここから、パフォーマンス分析の難しさは、問題を見つけることではなく、問題の本質を見極めること、もっと重要な意味を持つ問題を見つけること!
可観測性って?
観察によってシステムを理解すること。
オブザーバビリティとも呼ぶ。
世の中には可観測性ツールなるものが存在し、Prometheusがその一例。
観察の方法はいくつかあり、
- カウンタ
- プロファイリング
- トレーシング
がある。
ベンチマークは観察に含まれないことに注意だ。
ベンチマークは環境で実験することによってシステムの状態を変えてしまうので、本番環境では使わないようにする。
可能な限り可観測性ツールを使用するようにするべきだ。
逆に、パフォーマンスを実験するために、検証環境やstaging環境ではベンチマークツールを使用した方が良い。
観察の方法についてみていくと。。。
カウンタ
OSや可観測性ツールは、自身の状態とアクティビティについてのデータを提供してくれている。
- オペレーション数
- バイト数
- レイテンシの計測値
- リソースの使用状況
- エラー率
- などなど。。。
これらのデータは、カウンタと呼ばれる場所に格納され、一部の取得対象は累計値で保存されることもある。
可観測性ツールは、これらのカウンタの値を任意のタイミングで呼び出し、時間による変化率や平均、割合などの統計量を計算する。
例として、vmstatを見てみよう。
$ vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
2 0 0 3102716 37036 607684 0 0 421 50 142 219 1 1 98 0 0
2 0 0 3102764 37036 607700 0 0 0 4 868 244 49 3 48 0 0
2 0 0 3103020 37036 607700 0 0 0 0 888 255 48 3 48 0 0
2 0 0 3103020 37036 607700 0 0 0 0 833 244 49 3 49 0 0
2 0 0 3103020 37036 607700 0 0 0 0 870 286 49 3 48 0 0
第一引数: 更新間隔を秒単位で指定
第二引数: 更新回数
コマンド結果を見てみると、
CPU使用率: 49 + 3 = 48%(us + sy)
指標
システムのパフォーマンスを評価するための統計量。
実際の企業のシステムでは、モニタリングエージェントを使って一定のインターバルで統計量を記録し、グラフに描いて時間の経過による推移をチェックしている。
モニタリングソフトウェアは、これらの指標からアラートを設定し、設定を上回ったり、下回ったりした際に通知を送ることもできる。
計測した統計量が、キャパシティプランニングに使用される。
- アプリケーションやカーネルからカウンタが取得される
- パフォーマンスツール、エージェントから統計量が取得される
- モニタリングUIから指標が取得され、
- イベント処理によってアラートが送信される。
カウンタ、統計量、指標はよく同じ意味で使われるので、そんなに厳格に区別する必要はない。
プロファイリング
システムやアプリケーションのパフォーマンスを詳細に分析する手法。
サンプリングツールという一定の感覚でシステムの状態を取得し(サンプリングし)、リソースの状況を把握する手法とも言える。
そうすることでプログラムの実行時にどのリソースがどれくらい使用されたのかを測定し、ボトルネックを特定する。
特によくプロファイリングの対象となるのはCPUで、一定の感覚でon_CPUのコードパスをサンプリングする手法。
コードパスとは
CPUがどの経路で実行されたのかを示す値。
どのプロセス、どのスレッドによって実行されたのかを確認できる。
CPUのプロファイルはフレームグラフにすると、とても見やすくなる。
フレームグラフを見れば、CPUの問題でけでなく、CPUに残された痕跡に基づいて他のタイプの問題さえ見つかることがある。
トレーシング
トレーシングは常にメトリクスを取得するわけではなく、イベントベースで記録される。
イベントの関連データを分析するために保存しておいたり、イベントの要約情報(カスタムサマリー)を作るために使用されたりする。
OSが提供するシステムコールやネットワークパケットには専用のトレーシングツールがある。
システムコール: trace(1)
ネットワークパケット: tcpdump(8)
そのほかにも様々なトレーシングツールがある。
トレーシング方法にも種類があり、以下の3つがある。
- 静的インストルメンテーション
- 動的インストルメンテーション
- BPF
静的インストルメンテーション
ソースコードにハードコーディングされたソフトウェアインストルメンテーションポイント。
ソフトウェアのコンパイル時に行われる。
Linux kernelには
- ディスクIO
- スケジューライベント
- システムコール
など、対象とするインストルメンテーションポイントが無数に用意されている。
特に、Linux kernelにおける静的インストルメンテーションポイントのことをトレースポイントと呼ぶ。
カーネル空間はトレースポイントが提供してくれているが、ユーザー空間のための静的インストルメンテーションポイントも存在し、USDT(User statically definedd tracing)と呼ばれている。
USDTはアプリケーション開発者がアプリケーションの中に直接埋め込むタイプのインストルメンテーションポイント。eBPFやBCCなどのツールを使ってUSDTを取得できるようになる。
動的インストルメンテーション
ソフトウェアの実行後、メモリ内の命令を書き換えてインストルメンテーションルーチンを挿入するという方法でインストルメンテーションポイントを作る。
デバッガの例がわかりやすい。
実行中のソフトウェアの任意の関数にブレイクポイントを挿入するのと似ている。
デバッガはブレークポイントがヒットすると実行フローを対話的なデバッガに渡すが、動的インストルメンテーションは、ランタイムを実行してからインストルメンテーションルーチンを挿入し、その後にターゲットとなるソフトウェアの実行を続ける。
こうすると、実行中の任意のソフトウェアからカスタムパフォーマンス統計を作れる。
これもeBPFを使えばインストルメンテーションルーチンの挿入、カスタムサマリーの作成を行ってくれる。
BPF
Linux kernel内で動作するトレース技術。
カーネル内で動作する仮想マシンを提供し、そこにユーザー定義のプログラムを実行できる。
そうすることで、特定の条件に合致したイベントのみをトレースすることができる。
これもまたeBPFで実装できるらしい。
結論、eBPFはトレーシングの覇権握ってる説。
「詳解システムパフォーマンス」におけるワークロードとは、一体なんなのか?
読んでるうちに度々目にする単語があったので、おさらい。
人によって解釈の差が大きい単語だと思ったので、本書で扱われている意味をまとめておく。
コンピュータやシステムにかかる負荷のこと。
この書籍で「ワークロード」という単語が出てきたら、「負荷」と読み替えた方がすっと頭に入ってくるかもしれない。
ベンチマークの種類
システムにワークロード(負荷)を与えて性能をテストするベンチマークには2種類の手法がある。
- マクロベンチマーク: クライアントからリクエストを投げるなどしてリアル(実際にユーザーがシステムを使用した時のよう)なワークロードを与える手法
- マイクロベンチマーク: CPU, ディスク, ネットワークなど、特定のコンポーネントにワークロードを与える手法
一般的にはマクロベンチマークのほうがデバッグ、反復、理解がしやすく安定している。
クラウドってなんなん?
クラウドコンピューティングの略。
自分たちが必要だっと思った時に必要な分だけの計算資源をデプロイすることができる手段のこと。
インスタンスと呼ばれる小さな仮想システムを増やし、それの上に自分たちが作ったアプリケーションをデプロイすることによって、簡単にスケーリングができるようになった。
これによって、簡単にキャパシティを増やせるようになったので、厳密なキャパシティプランニングの必要性が下がった!すごい!
Linuxパフォーマンス分析チェックリスト
パフォーマンス調査をする際に最初に見るべきチェックリスト
コマンド | チェック内容 |
---|---|
uptime |
負荷の平均。1分, 5分, 10分の平均の比較によって負荷が上がっているか下がっているのかが分かる |
dmesg -T | tail |
OOM(メモリ不足)などのエラー |
vmstat -SM 1 |
ランキューの長さ、スワップ、システム全体のCPU使用率などのシステムレベルでの統計量 |
mpstat -P ALL 1 |
CPU間のバランス。一つのCPUだけがビジー状態ならスレッドのスケーリングに問題がある |
pidstat 1 |
プロセスごとのCPUの使用状況。予想外にCPUを使用しているプロセスを明らかにするとともに、各プロセスのユーザー/システムCPU時間を見る |
iostat -sxz 1 |
IOPS、スループット、平均待ち時間、ビジー状態の割合など |
free -m |
ファイルシステムキャッシュを含むメモリの使用状況 |
sar -n DEV 1 |
ネットワークデバイスIO、パケットとスループット |
sar -n TCP,ETCP 1 |
TCP統計。接続のスピード、再送 |
top |
全体的なチェック |
2章 メソドロジ
目標
- 霊天使、使用率、飽和度などの基本的な指標を理解する
- ナノ秒までの計測時間の単位を感覚として理解する
- チューニングのトレードオフ、ターゲット、分析を中止すべきタイミングについて学ぶ
- ワークロードの問題かアーキテクチャの問題かを見分けられるようになる
- リソース分析か、アーキテクチャ分析かを考えられるようになる
- USEメソッド、ワークロードの特性の把握、レイテンシ分析、静的パフォーマンスチューニング、パフォーマンスマントラなどのさまざまなメソドロジの道筋をたどる
- 統計学と待ち行列理論の基礎を理解する
システムパフォーマンスにおける重要な単語まとめ
IOPS
Input Output Per Secondの略。
データ転送処理のスピードの指標。
ディスクIOについては、1秒あたりの読み書きの回数を示す。
応答時間
処理が完了するまでの時間。
結果の転送にかかった時間も含む。
使用率
コンピュータが提供するハードウェアリソースについて、単位時間あたりにアクティブに作業していた時間がどれだけかに基づいてリソースがどの程度忙しいのかを計測した値。
飽和度
キューイングされた処理のうち、完了していないものの割合。
レイテンシ
レイテンシは処理の完了までにかかった時間である。
だが、もう少し詳しく見ていこう。
レイテンシはスループットと並んで重要なパフォーマンス指標だよね。
厳密にいうとレイテンシにおける待ち時間の対象は処理の完了までにかかった時間というよりは、オペレーションが実行されるまでの時間である。
例えば、あるサーバーにhttpリクエストを送信する例を考えてみよう。
httpリクエストにおけるオペレーションは、データを転送せよというネットワークサービス要求。
このオペレーションに取り掛かる前に、システムはネットワークが開設されるのを待たなければならない。
それがオペレーションのレイテンシだ。応答時間というのは、レイテンシとオペレーションにかかった時間の合計である。
レイテンシは計測場所が異なっていて良いので、計測対象とともに表現されることがよくある。
Webサイトのロード時間を例にすると
- 名前解決レイテンシ
- TCP接続レイテンシ
- TCPデータ転送時間
の3つの時間から構成される。
TCP接続レイテンシは、通信の初期化(TCPハンドシェイク)を指す。
TCPハンドシェイクとは?
Webブラウザとサーバーのリクエストのやり取りをする前に、お互いの通信が安全かを確認する処理が発生している。その確認の通信がTCPハンドシェイクだ。
- クライアントがSYNchronizeパケットをサーバーへ送信
- サーバーがSYNchronizeを受信し、SYNchronize-ACKnowledgementを返送する
- クライアントがSYNchronize-ACKnowledgementを受信し、ACKnowledgeを返送する
- サーバーがACKnowledgeを受信し、TCPソケット接続が確立する
レイテンシという言葉自体はもっと広い意味で使われることもある。
例えば、クライアントからhttpリクエストを送信してブラウザの描画が完了するまでの時間を指すこともある。
だから、何を対象として処理時間を計測しているのかを明確にするために修飾語を入れて表現するべきだ。
例えば、TCP接続レイテンシとか、要求レイテンシとかね。
トレードオフ
システムには、3つの要素のうち2つを選び、1つを犠牲にするというトレードオフが存在する。
- ハイパフォーマンス
- 時間内
- 低コスト
多くのプロジェクトでは時間内であり、低コストであることが選ばれ、パフォーマンス面での修正は後回しになっている。
パフォーマンスチューニングで見られるトレードオフは、CPUかメモリを選ぶものだ。
メモリを使って計算結果をキャッシュすれば、CPUの使用率を下げられる。
しかし、データ圧縮のためにCPUを使って、メモリの使用を控えるという方法を使えば、メモリの使用率を下げることもできる。
チューニング対象と計測対象
システムのパフォーマンスチューニングをするとなると、いろんなターゲットが存在するよね。
システムコンポーネント | 対象の例 |
---|---|
アプリケーション | ロジック、要求キューのサイズ、DBクエリ |
データベース層 | テーブル設計、インデックス、バッファリング |
システムコール | ファイルの読み書き、同期、非同期IOフラグ |
ファイルシステム | レコードサイズ、キャッシュサイズ、ファイルシステムのパラメータ、ジャーナリング |
ストレージ | RAIDレベル、ディスクの数とタイプ、ストレージのパラメータ |
パフォーマンスのチューニングは、仕事が行われる場所からもっとも近いところでしたときに最も効果的になる。
Webアプリケーションを構築しているならアプリケーション自体のチューニングをすることが最も効果的になる。逆にシステムコールやOSレベルのチューニングをしてもあまり大きな改善は見られない。
しかし、アプリケーション層が観察の層として最も効果的なレイヤだとは必ずしも言えない。
遅いクエリはon-CPUで使った時間や実行したファイルシステム、ディスクIOから最もよくわかることが多い。
OSのパフォーマンス分析をすれば、OSレベルの問題だけでなくアプリケーションレベルの問題も見つけられることを覚えておこう。
ROIとは?
Return of Investmentの略。
投じた費用に対して、どれくらいの利益を上げられたかを示す指標。
on-CPUとは?
処理を実行している状態のCPUのこと。
複数コアを搭載したコンピュータならば、on-CPUとoff-CPUの状態が混在した状態も有り得る。
ボトルネックは負荷か、アーキテクチャか
アプリケーションのパフォーマンスは、土台となっているソフトウェア構成とハードウェアのアーキテクチャによって悪化することがある。
例えば、他のCPUはアイドル状態で使える状態なのに、一部のCPUがビジー状態になり、要求がキューイングされるシングルスレッドアプリケーションは、アーキテクチャに問題がある。
シングルスレッドというアーキテクチャがパフォーマンスの限界を生み出している。(なぜなら、スレッドはプロセスの中に生成され、プロセスに割り当てられたCPUリソースしか使えないので、プロセスに割り当てられたCPUリソースが一部のものだけになると、残りのCPUがアイドル状態になってしまうから)
一方、マルチスレッドアプリケーションで利用できるCPUが全てビジー状態になっても要求がキューイングされているような場合は、負荷の問題である。
この場合、パフォーマンスの限界を生み出しているのは使えるCPUの能力である。CPUが処理できる以上の負荷がかかっている状態だ。
使用率の定義
パフォーマンスを図る指標として、使用率というものがある。
- CPU
- メモリ
- ディスク
対象は色々ある。
使用率は2つの定義方法があり、時間ベースと能力ベースの2種類がある。
時間ベースの使用率の定義
サーバーまたはリソースがビジー状態だった時間の平均劇な割合
U = B / T
U: 使用率
B: 観察期間中ビジー状態だった時間の合計
T: 観察を行った時間
これはOSのパフォーマンスツール(iostatの%b)で簡単に得られる数値である。
能力ベースの使用率の定義
システムやコンポーネントなどは、ある水準のスループットを提供できる。
どのパフォーマンスレベルでも、システムやコンポーネントは、持っている能力の何らかの割合を使って動作している。この割合を使用率と呼ぶ。
この定義は、使用率が100%のディスクはそれ以上の要求を受け付けられないということを暗黙のうちに意味している。
時間ベースの定義では、使用率が100%だということは100%の時間でビジー状態だという意味でしかない。
つまり、一部のCPUがずっとビジー状態であれば、時間ベースの使用率は100%になってしまう。
100%のビジーは、100%の能力とは異なる。
飽和度
リソースが処理できる要求と比べて実際の要求数がどれくらい多いかを表現する値。
能力ベースの使用率が100%になり、これ以上の要求を処理できなくなって、キューイングが始まった時に始まる。
能力ベースの飽和度が100%を超えたあと、負荷に対して線形(y=ax)に飽和度が増えていく。
飽和状態は待ちのために時間が使われるので障害状態である。
キャッシュデータがどれくらい使われているか?
キャッシュの使用状態を示す単語をまとめる。
- ホットキャッシュ
- よく要求されるデータがセットされていて、キャッシュヒット率99%以上
- ウォームキャッシュ
- 役に立つデータも格納しているが、ホットというほどヒット率が高くない。
- コールドキャッシュ
- から、または不要なデータが格納されたキャッシュ。コールドキャッシュのヒット率は0。
キャッシュヒット率とパフォーマンスの関係
キャッシュヒット率は高ければ高いほどよい。
指数関数的にパフォーマンスが上がっていく。
例えば、キャッシュヒット率が10%から11%に上がった時と98%から99%に上がった時のパフォーマンスの差は、後者の方が圧倒的に大きい。
なぜかというと、98%から99%に上昇した場合、キャッシュミス率が2%から1%になるということは、今までのミス率が2分の1になるということ。
しかし、10%から11%に上がった時、キャッシュミス率が90%から89%になるということ。ミス率があまり変わらないので、さほど大きな差にはならない。
逆にいうと、キャッシュヒット率があまり高くないデータをキャッシングしても、パフォーマンスへの影響どころかキャッシュするために保存するメモリ領域が無駄になってしまうこともあり得るのか。
メソドロジ
この章のメインコンテンツ。
以降はメソドロジをメソッドと表現する。
街灯のアンチメソッド
このメソッドはまともなメソッドの欠如のこと。
とりあえず自分が知っているツールを使って明らかな問題があるかをチェックしてパフォーマンス分析とするもの。
この方法による分析は当たることも外れることもあり、問題の見過ごしが発生する。
たまたま知っているというだけのりゆで問題と無関係なツールやチューニングを見つけてきて試すので作業は遅くなる。
意味があるからではなく、他のツールの読み方も知らないのでtopコマンドを叩いて状況を見るのは、街灯のアンチメソッドの典型例だ。(ちなみに僕はめちゃくちゃ当てはまってた。。。)
ランダム変更アンチメソッド
実験によるアンチメソッド。
どこに問題があるかを適当に推測し、その問題が消えるまで適当に変更を加える。
- 変更する項目をランダムに選ぶ。(何かのカーネルパラメータとか)
- それをある方向に変更する。
- パフォーマンスを計測する(実行時間、演算時間、レイテンシ、スループットなど)
- 逆方向に変更する。
- パフォーマンスを計測する。
- ステップ3かステップ5の結果が最初のあたいよりも良いか、もし良くなっていたらその変更を残してステップ1に戻る。
とても時間がかかる上に無意味なチューニングを残してしまう危険がある。
このメソッドでバグを修正したとして、バージョンアップなどによってバグが解決された場合、無意味なチューニングが残ってしまうことになる。
また、本番環境の負荷が上がったときに正しく理解できてない変更がもっと大きな問題を引き起こし、変更を取り消さなければならなくなるかもしれない。
誰かのせいにするアンチメソッド
- 自分に責任のないシステムまたは環境のコンポーネントを見つけてくる。
- そのコンポーネントに問題があるという仮説を立てる。
- そのコンポーネントに対して責任を負うチームに問題を丸投げする。
- 仮説の誤りが証明されたら、1に戻る
このメソッドの問題は、問題の仮説を立てる際に根拠となるデータがないことから見分けられる。
このアンチメソッドの犠牲者にならないためには、告発者に対し、どのツールを実行し、出力をどのように解釈したかを示すスクリーンショットを提示するように要求し、誰か他の人からスクリーンショットと解釈に対するセカンドオピニオンをもらうと良い。
アドホックチェックリストメソッド
チューニングする際にあらかじめ決められたチェックリストを片っ端から実施していき、問題がないかを確認していくことでパフォーマンス問題を分析する。
このチェックリストは頻繁に改訂して新鮮さを保つ必要があるのが面倒くさい。
それと、パラメータの設定などの簡単にドキュメントできる基地の解決方法がある問題に偏りがちで、環境やソースコードなどのカスタムの修正には手が回らないことが多い。
問題の記述
めっちゃ重要!
パフォーマンス問題に対処するために行うルーチン作業。
問題を的確に把握するために以下の項目を聞いていく。
- パフォーマンスに問題があると思ったのはなぜか?
- 対象のシステムは良好なパフォーマンスで動いていたことがあったか?
- 最近の変化は何か、ソフトウェア、ハードウェア、負荷か?
- その問題はレイテンシか実行時間で表現できるか?
- その問題はあなただけの環境以外でも発生しているか?
- 問題が起きた環境はどうなっているのか、どのソフトウェア、ハードウェアを使っているのか、バージョンや構成はどうか?
新しい問題に立ち向かうときは最初のアプローチとしてこれを使うべき!!
科学的メソッド
以下の過程を経て道の事象について検証する。
- 問題
- 仮説
- 予測
- 検証
- 分析
問題はパフォーマンス障害の記述。
パフォーマンスが低くなっている原因について仮説を立てて
観察 or 実験による検証の方法を組み立て、それによって仮説に基づく予測を検証する。
集めた検証データを分析してプロセスを終えるか、新たな仮説を立てて2からやり直す。
例
- 問題
- 容量を小さいシステムに移行した後、アプリケーションのパフォーマンスが低下しました。
- 仮説
- 低パフォーマンスの原因はファイルシステムのキャッシュが小さくなったからではないか?
- 予測、検証
- 観察による検証の場合
- メモリを小さくしたシステムとしてないシステムを用意し、キャッシュミス率を計測してみよう。
- メモリを小さくしたシステムではキャッシュミス率が高くなるのではないか?
- 実験による検証の場合
- キャッシュサイズを増やしたらパフォーマンスが改善されるのではないか?
- 観察による検証の場合
- 分析
- 観察による検証結果の場合: キャッシュミス率が上がった!原因はファイルシステムのキャッシュが小さかったという仮説は合っていた!
例: 観察による検証
- 問題
- DBクエリが遅い
- 仮説
- 複数のテナントがディスクIOを実行していて、 DBとディスクIOを奪い合っている状態だからではないか?
- 予測
- クエリの過程でファイルシステムにおけるIOレイテンシが計測されるなら、クエリの遅さの原因はファイルシステムにあるだろう!
- 検証
- クエリレイテンシをの比率という形でデータベースシステムレイテンシをトレースすると、ファイルシステム待ちでは5%未満の時間しか使っていなかった。
- 分析
- このファイルシステムとディスクはクエリの速度低下の原因ではない。
原因解明には届かなかったが、検証対象から環境の中における大きなコンポーネントを除外できた。
解決できなかった、分析によって原因が解明できなかったので、新し仮説を立てていくことになる。
例: 実験による検証
- 問題
- ホストBからホストCの時と比べ、ホストAからホストCにHTTPリクエストを送った時の方が処理に時間がかかるのはなぜか?
- 仮説
- ホストAとホストBは別のデータセンターにあるから、処理時間が違うのではないか?
- 予測
- ホストAをホストBと同じデータセンターに移すと問題は解決するかもしれない!
- 検証
- ホストAを移して両方のリクエストの処理時間を計測する
- 分析
- リクエストの時間差がなくなった!仮説と整合しているので解決!
もし解決できなかった場合は、実験による変更を元に戻してから新しい仮説を検証することを忘れないでおきたい。
例: わざとパフォーマンスを低下させる実験による検証
- 問題
- ファイルシステムキャッシュのサイズを大きくしたらファイルシステムパフォーマンスが低下してしまった。。。
- 仮説
- 大きなキャッシュは格納するレコードが多くなっており、小さなキャッシュよりも大きなキャッシュを管理する時の方が計算能力が必要になるのではないか?
- 予測
- レコードサイズを徐々に小さくしていき、同じ量のデータを格納するために使うレコードの数を増やすと、パフォーマンスが徐々に低下していくのではないか?
- 検証
- 同じワークロードで段階的にレコードサイズを小さくして検証してみよう!
- 分析
- 結果をグラフにまとめると、予測と符合した結果になっている!キャッシュ管理ルーチンに対してドリルダウン分析を実行してみる。
意図的にパフォーマンスを低下させてシステムの理解を深める行為をネガティブテストと呼ぶ。
【リソース分析】USEメソッド
システムにおけるすべてのリソースについて
- 使用率: Utilization
- 飽和度: Saturation
- エラー: Errors
をチェックする手法。
主にリソース分析の視点で用いられる
リソース
物理サーバー上のすべての機能的なコンポーネントを指す。
使用率
基本的には時間ベースの使用率を測定する。
時間ベースの使用率となると、ビジー状態でも新しい要求を処理できることもあるが、新しい要求を処理できない度合いについては飽和度で明らかになる。
しかし、メモリなどの能力ベースでしか使用率を測ることができないリソースに対しては能力ベースの使用率をチェックする。
飽和度
処理できない要求を抱えている度合い。
プレッシャーという用語でも表現される。
エラー
エラーイベントの回数
なぜエラーも調査するのかというと、エラーもパフォーマンスを下げることがあるものの、そもそもエラーがすぐに修復可能ならば、パフォーマンス低下に気づかないからだ。
これらの指標をチェックしていくことで、システムのボトルネックになっていそうな問題を見つけ出す。
このメソッドで見つかった問題は別のメソッドで調査しても良い。
手順
一般には次の手順でチェックしていく。
- 調査対象のリソースのリストアップ
- 調査対象リソースの選択
- エラーがあるか?
- 飽和があるか?
- 使用率が高いか?
- もしエラーが発生している or 飽和状態になっている or 使用率が高い なら、検出されたことを精査して決定的な問題が特定されているかを調べる。
- すべてのリソースについてチェックし終わるまで3から繰り返していく
エラーを最初に調査する理由は、素早く簡単に解釈できるし、他の指標を調査する前にエラーを弾き出してしまえば時間を効率よく使える。
指標の表現
指標の表現の例をあげる。
- 使用率
- 1個のCPUが90%の使用率で実行されている。
- 飽和度
- CPUは、平均して長さが4のランキューを持っている。
- エラー
- このディスクドライブは、50回のエラーを起こしている。
使用率はインターバルを通じての割合で表現されるのだが、気をつける点がある。使用率のインターバルが長いと、一時的な使用率の上昇に気づけないということだ。
CPUの使用率は毎秒毎秒で極端に変化する可能性が大いにあるので、十分短いインターバルで調査する必要がある。
調査対象のリソースリスト
USEメソッドの最初のステップとして調査対象のリソースリストを作る必要がある。
できる限り完全な(すべての要素を網羅している)リストを作るべきなのだが、サーバーにおけるハードウェアリソースの汎用リストの例をあげる。
- CPU
- ソケット
- コア
- ハードウェアスレッド
- メモリ
- DRAM
- ネットワークインターフェース
- イーサネットポート
- インフィニバンド
- ストレージデバイス
- ディスク
- ストレージアダプタ
- アクセラレータ
- CPU
- TPU
- FPGA
- コントローラ
- ストレージ
- ネットワーク
- インターコネクト
- CPU
- メモリ
- IO
基本的にそれぞれのコンポーネントは単一のリソースタイプとして機能する。
例えば、メインメモリは容量、CPUは処理速度。それぞれのコンポーネントの役割に応じた性能指標がある。
ただ、ストレージ(容量に加えてIOの速度も重要)などのコンポーネントは複数の性能が重要視される。
パフォーマンスにおけるボトルネックになりそうなタイプはすべて調査しておきたい。
キャッシュなどの使用率が高いほどパフォーマンスが上がるコンポーネントに関してはチェックリストに載せなくて良い。
指標
基本的にはそれぞれのリソースについて使用率、飽和度、エラーを調査すべきだが、コンポーネントによってはやらなくても良い指標が存在したりする。
基本的なコンポーネントにおけるUSEメソッドの指標の例を挙げる。
リソース | 指標タイプ | 指標の説明 |
---|---|---|
CPU | 使用率 | CPUの使用率 |
飽和度 | ランキューの長さ | |
メモリ | 使用率 | 利用可能フリーメモリ |
飽和度 | スワッピング, OOM | |
ネットワークインターフェース | 使用率 | 受信スループット, 送信スループット |
ストレージデバイス | 使用率 | デバイスのビジー度 |
飽和度 | 待機キューの長さ | |
エラー | デバイスエラー |
ここで重要なのは、その時に取得できなかった指標のメモを取っておくことだ。
これは「存在は知っているけど、まだ確認していないもの」となり、のちのボトルネック特定に役立つ可能性がある。
指標の解釈方法
タイプごとに、こうなっていたら問題だという解釈方法がある。
指標 | 解釈方法 |
---|---|
使用率 | 使用率100%はボトルネックになっている兆候。60%を超えている場合も問題を示している場合がある。インターバル次第で使用率100%を示している可能性があるし、オペレーション中に他の要求は割り込みできないので処理速度に気づくレベルで遅くなっている。 |
飽和度 | 飽和度は0より大きければ、問題が起きている可能性がある。 |
エラー | エラー数が0より大きければ、精査すべきである。パフォーマンスが低い時にエラーが増える場合は特に精査すべき。 |
REDメソッド
マイクロサービスにおけるサービスを対象としてパフォーマンスを調査するメソッド。
ユーザーから見た健全性を監視するための3つの指標があり、それをすべてのサービスでチェックする必要がある。
- 要求率
- 1sあたりのサービス要求の数
- エラー
- 失敗した要求の数
- 処理時間
- 要求の処理が終わるまでの時間
マークロサービスアーキテクチャのダイアグラムを作り、個々のサービスに対してこれら3つの指標をモデリングすることが求められる。
メリットは手っ取り早く簡単に実施できて包括的なことだ。
要求率によって、パフォーマンス障害が負荷によるものか、アーキテクチャによるものかが判定できる。
要求率が安定しているのに処理時間が長くなっていたら、アーキテクチャの問題。
要求率が上がってから処理時間が長くなっているなら、負荷が高くなったと考えられる。
【ワークロード分析】ワークロードの特性の把握
システムにどんな負荷がかかっているのかを知ることで、システムパフォーマンス改善に貢献することができる。
具体的には以下のような特性を確認する必要がある。
- 誰が負荷をかけているのか
- プロセスID、ユーザーID、リモートIPアドレスは何か
- なぜ負荷がかかっているのか
- コードパス、スタックトレースはどうなっているか
- 負荷に特徴はあるか
- IOPS, スループット, 読み込みか書き込みか
- 負荷は時系列的にどのように変化しているか
- 毎日のパターンはあるか
すべての項目をチェックした例
WebサーバーがクライアントとなっているDBにパフォーマンス障害が起きた!
ここで、DBにリクエストを送っているのはWebサーバーだと思うだろう。
なぜならば、アーキテクチャがそうなっているからだ。
しかし実際にチェックしてみると、インターネット全体がDBに対して大量の負荷を掛けていたのだ。
なんと、DBはDoS攻撃を受けていたのだ!!
ワークロードの特性を知ると、問題が炙り出されて孵化の問題なのか、アーキテクチャの問題なのかを切り離して意考えることができるのでハッピーになれる。
【ワークロード分析】ドリルダウン分析
3つの段階を経ながら高レベルな問題から低レベルな問題へ焦点を絞っていくことで、問題の根本原因を見つけるためにソフトウェアスタックの深いところまで(必要ならハードウェアまで)掘り下げていく手法。
- モニタリング: 時系列的に高レベルの統計量を記録し、問題が起きているかもしれない時にそれを検出したり、アラートを発したりする。
- 特定: 問題が疑われる時にボトルネックになっている可能性のあるものを特定し、調査対象を関連性のありそうな一部のリソースや領域に絞り込む。
- 分析: 根本原因を明らかにして問題を定量化するために、特定したシステム領域をさらに解析する。
ドリルダウン分析の大半は分析の過程が占める。
疑いのある領域をさらに精査するためにトレーシングやプロファイリングツールをベースとして分析し、時にはカスタムツールを作ったり可能であればソースコードもチェックしたりする。
また、ドリルダウン分析では5回「なぜ?」を突き詰めることで原因がわかることが多いので、意識したい。
【ワークロード分析】レイテンシ分析
オペレーションが完了するまでにかかったレイテンシを細かいコンポーネントに分割することで最もレイテンシが高くなっている要素、根本原因を突き止め、定量化しようとするもの。
ソフトウェアスタックのレイヤを掘り下げていく点はドリルダウン分析と共通だ。
パフォーマンスマントラ
チューニングメソッドはいくつかの方向性がある。
- するな
- 不要な仕事、ワークロードを取り除く
- しても良いが2度とするな
- キャッシング
- 減らせ
- リフレッシュ、ポーリング、更新の頻度を下げるチューニング
- 先延ばししろ
- ライトバックキャッシング
- 見られていない時にしろ
- ピーク時間を外して実行するようなスケジューリング
- 同時並行にしろ
- シングルスレッドからマルチスレッドの切り替え
- 安上がりにしろ
- より高速なハードウェアの購入
モデリング
パフォーマンス分析において、計測したデータをモデリングすることで、負荷の上昇に伴いリソースをどのように増減していくのかを分析することができる。
負荷の上限によってリソース(CPUやメモリに加えてスレッドやプロセスなどのソフトウェアリソースも含めた)がどのように増減していて、どのくらいのパフォーマンス(主にスループット)を発揮できているのかを研究する分野があり、スケーラビリティ分析と呼ばれている。
つまり、モデリングはスケーラビリティ分析に非常に役立つ。
パフォーマンスの評価はモデリングに加えて2つのうちのどちらかの手段で行われる。
- 計測: 本番システムの観察
- シミュレーション: 実験的な検証
評価対象のシステムがすでに本番で稼働しているなら計測を、まだしていないならシミュレーションによって得たデータをモデリングしていくことでパフォーマンスの評価をしていく。
そこからスケーラビリティ分析をしていくと、リソースの制約のためにパフォーマンスがある一点で線形に伸びなくなっていく場合がある。
線形に伸びなくなった地点をニーポイントと呼び、これがある場合はスケーラビリティの欠如となり、パフォーマンス障害として調査していくことになる。
スケーラビリティの視覚的な究明
実験や観察によって十分なパフォーマンス指標のデータが集められたら、スケーリングパラメータ(サーバー台数やCPUのコア、スペック値など)と対応するパフォーマンス値(スループットや単位時間あたりのトランザクション数など)をプロットすると、パターンが現れてくることがある。
パターンにはある程度、「大体こんな傾向があったらこんな状態だ」というものが存在する。
線形
リソースをスケーリングすると、比例してパフォーマンスが上がる。
これは理想的な状態。
これがずっと続かず、早い段階で他のパターンになることがある。
競合
アーキテクチャの中で一部のコンポーネントが共有されていて、それらを同時並行で使えない場合、共有リソースに対する競合がスケーリングの効果を目減りさせてしまう。
形としては、だんだんと傾斜が緩やかになっていくのな形になっている。
コヒーレンス
サーバーの台数を増やす「スケールアウト」を実施したときに発生する現象。
台数を増やしてくと、サーバー上のすべての情報を同じにしておく必要がある。(コヒーレンシという)
そのために、一部のデータの変更をすべてのサーバーに伝播する必要がある。(プロパゲーション)
そうすると、伝播のために使う計算やネットワーク通信の量(オーバーヘッド)が台数増加によって発揮できたパフォーマンス増加の効果を相殺してしまうことがある。
結果、スケールアウトしてもオーバーヘッドの方が大きくなっていき、台数を増やせば増やすほどパフォーマンスはむしろ下がっていってしまうような形になっている。
ニーポイント
特定の位置にスケーラビリティプロファイルを変える要因があるとき。具体的に原因は特定できず、ケースバイケースで判断するしかない。
シーリング
バスやインターコネクトがスループットの上限に達するとか、ソフトウェアが設定した限界値に達するなどといった原因でどうしても越えられない限界に達する。
ある一点を境に全くパフォーマンスが上がらないようなy=bのような形式になる。
アムダールの法則
システムの並列化による性能向上の競合を予測するために使われる法則。
プログラムの並列化可能な部分と直列部分の比率に基づいて、理論上の最大速度向上を計算する。
公式はC(N) = N / (1 + α(N - 1))
N: プロセッサ数や負荷の大きさを示すパラメータ
α: 全体の処理のうち、直列部分の割合(0~1)
例: アプリケーションの95%の実行時間を8コアで並列実行すると、どのくらい早くなる?
N = 8
α = 0.05(0.95の割合で並列実行されるから)
8 / (1 + 0.05 * 7) ≒ 5.925
全体が直列で実行される場合と比べて約6倍の速さで処理できるようになる。
この場合、6倍まで早くできるとともに、ほとんどの処理を並列化しても6倍までしか性能を向上できないという限界の値も知ることができた。
待ち行列理論
待ち行列理論とは、キューを持つシステムの数学的な研究。
この待ち行列理論を用いることで
- キューの長さ
- 待機時間(レイテンシ)
- 時間ベースの使用率
を分析することができる。
ハードウェアソフトウェアともに、コンピュータで使われるあらゆるコンポーネントはキューイングシステムという形式で表現できる。
コンポーネントの中にキュー(待ち行列)とサービスセンター(コンポーネントの役割を担う箇所)が存在し、処理時間の全体は(待ち時間) + (サービス時間)で表現できる。
キューイングシステムは並列に処理を実行する複数のサービスセンターを持つことができる。
そして、キューイングシステムは以下の3つの要素によって分類することができる。
- 到着過程: キューに要求が到着する間隔の性質。ランダム、一定、ポアソン過程(指数関数的に増加するような分布)
- サービス時間分布: サービス時間が固定、指数分布、その他の分布などがある。
- サービスセンター数
これら3つの指標をケンドールの記述というもので表現できる。
A/S/m
A: 到着過程
S: サービス時間分布
m: サービスセンター数
特によく研究されている性質のキューイングシステムがある。
- M/M/1
- マルコフ過程(指数分布)の到着時間
- マルコフ過程のサービス時間
- サービスセンター一つ
- M/M/c
- サービスセンターが複数
- M/G/1
- マルコフ過程の到着時間
- 分布の詳細を問わないサービス時間
- サービスセンター一つ
- M/D/1
- マルコフ過程の到着時間
- 一定のサービス時間
- サービスセンター一つ
特に回転ハードディスクのパフォーマンスの研究では、一般にM/G/1が使われる。
M/D/1と60%の使用率
ワークロードをを一定時間で処理するディスクについて単純化して考えてよう。
この場合、ディスクの性質はM/D/1となる。
考えたい問題は、使用率が上がるとディスクの応答時間はどのように変わるかについてだ。
M/D/1の応答時間は次の式で計算できる。
r=s(2 - ρ) / 2(1 - ρ)
s: サービス時間
ρ: 使用率(0~1)
使用率を横軸、応答時間を縦軸とすると、指数関数的に応答時間が伸び、特に60%の地点で応答時間が急激に伸びている。
つまり、CPU使用率が60%を超えたあたりでパフォーマンスが急激に落ちる(具体的には待ち時間2倍)。
統計量
パフォーマンスの向上には、問題や改善の度合いを定量化することがとても重要だ。
パフォーマンス改善度の定量化
パフォーマンス問題の解決の度合いを定量化できると、解決する問題同士で比較したり優先順位をつけることができるようになる。
2つの手法がある。
観察で定量化
手順は以下の通り。
- 信頼できる指標を選ぶ
- 問題解決によるパフォーマンス向上の度合いを推定する。
例えば、アプリケーションが要求の処理に10m秒かかっているとする。
そのうち、9m秒がディスクの読み込みにかかっていた。
では、その要求をメモリにキャッシングするような構造にしよう。
メモリからの読み込みには10μ秒もかからない。
推定される改善度としては、10m秒から1.01m秒になるだろう。
これで、レイテンシが1/9に短縮される推定!
さて、他の箇所でレイテンシを1/9以上短縮する方法はあるだろうか?。。。
といった具合だ。
レイテンシはコンポーネント間の性能を直接比較できるので、定量化に非常に向いている。
実験で定量化
手順は
- fixする
- 信頼できる指標を使ってfixの前後のパフォーマンスを定量化する
例えば、アプリケーションのトランザクションの霊天使が平均10m秒になっている。
アプリケーションのスレッド数を増やし、キューイングではなく並行実行が増えるようにしてみよう!
結果、アプリケーションのトランザクションのレイテンシが2m秒になった!
改善度は10m秒から2m秒 → 1/5のレイテンシになった!
ベンチマークや科学的メソッドにおける実験的な検証と同様に、本番環境ではやらない方が良い。
平均
平均は、データセットを一つの値で代表させるもの。
平均にはいくつか種類があり、私たちが最も使う平均は算術平均というもの。
それぞれ平均の種類によって、特徴や得意とするデータの種類があるので、パフォーマンス問題や改善度の評価に使える。
幾何平均
すべての値をかけた積のn乗根。
以下の特徴がある。
- データがすべて同じでない限り、常に算術平均以下。
- 外れ値などの極端な値の影響を和らげる性質がある。
- 長期的な傾向の反映に適している。
- 0や負の値は使用できない。
長期的な傾向が反映されやすいところや、算術平均より悲観的な値が算出されることから、パフォーマンスの効率化率や成長比率などの長期的な比率データの扱いに適している。
調和平均
値の個数を値の逆数の総和で割ったもの。
以下の特徴がある。
- 小さい値の影響を受けやすい。
- 大きな値の影響を受けにくい。
- データがすべて同じでない限り、算術平均以下。
小さな値の影響を受けやすいところや、逆数との相性が良いことから、速度の平均に向いている。
標準偏差、パーセンタイル、中央値
データの代表値と一緒に覚えておきたい指標がある。
それは、データのばらつきだ。
標準偏差は、ばらつきを表すもので、値が大きければ大きいほど、ばらつきが大きいことになる。
99パーセンタイルは、データセットの値を小さい順に並べた時に、下位90%に含まれる最大値を示す。
レイテンシのパフォーマンスモデリングでは、それぞれもグループで最も遅いものの量を知るために99.9, 99, 90, 95パーセンタイルの値を使う。
これらはよくサービスのSLAとして指定されることもある。
50パーセンタイルは中央値と表現され、データの大部分が含まれるところを示せる。
多峰分布
システムパフォーマンスでは、データの分布が集中している箇所(峰)が複数になる時がある。
例えば、ある要求のレイテンシを計測し、ヒストグラムで可視化したとすると、その要求の峰は2つあった。
それはなぜか。
キャッシュヒットした要求の低いレイテンシと、キャッシュミスした要求の高いレイテンシが混在していたからだ。
そんな状態のデータセットで中央値や平均をパフォーマンスの指標にしていると、峰と峰の間という、実際のレイテンシとはかけ離れた値が算出されてしまう。
このように、データが多峰分布になっていることを見落とさないために、分布がどうなっているかを確認することは非常に大切だ。
練習問題
IOPSとは何か?
1秒あたりに何回のInput, Outputができているのかを示す指標。
コンテキストによって若干意味は違ってくる。
- ネットワークの場合: 1秒あたりにどのくらいのデータを転送できたのか
- ディスクの場合: 1秒あたりに何回読み書きができているか
使用率とは何か?
提供されているリソースのうち、どのくらいの量を使っているかを割合で示した指標。
時間ベースと能力ベースの表現方法がある。
時間ベースに関しては、単位時間あたり、どのくらいの時間で使われているのか(特にCPUについてはビジー状態の時間)。
時間ベースでの使用率における100%は必ずしもリソースを使い切っているわけではない。あくまでずっと使い続けているだけである。
能力ベースに関しては、提供される能力の何%を使用しているのかを示している。
この場合の100%はこれ以上リソースが使えない状態を指す。
飽和度とは何か
提供されているリソースの能力を100%使っても処理できておらず、キューイングされているワークロードの量。
プレッシャという言葉でも表現される。
レイテンシとは何か
処理の実行にかかった時間。
要求にかかった全体の待ち時間というよりは、特定のコンポーネントのオペレーションにかかった時間なので、実際に使うときはどの段階でのレイテンシか、もしくはどのコンポーネントのレイテンシかを表現することがとても重要だ。
架空の環境で使うメソドロジを5つ選てね、実施する順序で並べて、それぞれ選んだ理由もまとめてね。
- USEメソッド: まず問題を発見するために必要な作業だから。
- アドホックチェックリスト: 問題を発見するために必要だから。
- 問題の記述: 目に見える問題が発生した or USEメソッドによって発見した障害について解像度を高くする必要があるから。
- ワークロードの特性把握: 発生した障害の解像度を高くするため。
- 科学的 or ドリルダウン分析 or レイテンシ分析: 実際に問題解決のためにボトルネックを特定するために直接必要な作業だから。
唯一のパフォーマンス指標として平均レイテンシを使った時の問題点を教えて、99パーセンタイルを含めれば、その問題が解決できるかも教えてね。
- キャッシュミス or ヒットなど、何かの条件がきっかけで多峰分布になっている際、実際にはあまり怒っていないレイテンシが算出されてしまうから
99パーセンタイルを含めれば、外れ値の検出に役立つが、多峰分布の検出はできないので完全に解決はできない。
3章 オペレーティングシステム
システムのパフォーマンス分析では、OSとそのカーネルについての理解が必要不可欠である。
- システムコールはどうやって実行されているの?
- カーネルはどのようにCPUへスレッドをスケジューリングしているのか?
- 限られたメモリがパフォーマンスにどのように影響するのか?
- ファイルシステムはIOをどのように処理するのか?
頻繁に仮説を立てて検証していくことになる。
目標
- コンテキストスイッチ、スワッピング、ページング、プリエンプションなどのカーネルの用語を学ぶ。
- カーネルとシステムコールの役割を理解する。
- 割り込み、スケジューラ、仮想メモリ、IOスタックなどのカーネルの内部動作について生きた知識を身につける。
- Unixカーネルのパフォーマンス関連機能がLinuxにどのように追加されているかを理解する。
- 拡張BPFの基礎を理解する。
基本用語まとめ
- オペレーティングシステム: システムを起動し、プログラムを実行するためにシステムにインストールされているソフトウェアとファイル。OS自作の時に学んだように、OSはハードウェアとソフトウェア、ソフトウェアと人間を繋ぐインターフェースの役割を担ってくれている。
- カーネル: ハードウェアやCPUスケジューリングなどのシステムを管理するプログラム。ハードウェアに直接アクセスできるカーネルモードという特権的な権限を持っている。(https://zenn.dev/link/comments/0ccbb55df43ce4)
- プロセス: プログラムを実行するための単位。それぞれのプロセスのリソースは独立していて(https://zenn.dev/link/comments/a9ba14b3988f7e)、プログラムはこのプロセス上でユーザーモードで実行され、システムコールを介してカーネルモードにアクセスできる。
- スレッド: プロセスには一つ以上のスレッドが生成される。プロセスに割り当てられたハードウェアリソースが共有されているので、他のスレッドの情報にアクセスしたりできる。
- タスク: Linuxの実行可能なエンティティで、シングルスレッド、マルチスレッド、またはカーネルスレッドのいづれかである。
- BPFプログラム: BPF実行環境で動作するカーネルモードプログラム。
- メインメモリ: コンピュータの物理メモリ。
- 仮想メモリ: マルチタスク実行とオーバーサブスクリプションをサポートするようにメインメモリを抽象化したもの。実質的にメインメモリが無限になる。
- カーネル空間: カーネルのための仮想メモリアドレス空間。
- ユーザー空間: プロセスのための仮想メモリアドレス空間。
- ユーザーランド: ユーザーレベルプログラムやライブラリなど。
- コンテキストスイッチ: あるスレッドまたはプロセスから、別のスレッドまたはプロセスへの実行の切り替え。CPUは1秒間に何百回も計算をしているが、マウスが動いた時やキーボードが入力された時などにその計算対象を別のスレッドに切り替えることでコンピュータは動作している。
- モードスイッチ: カーネルモードとユーザーモードの切り替え。
- システムコール: ユーザープログラムが、デバイスのIOなどの特権的なオペレーションの実行をカーネルに要求するための明確に定義されたプロトコル。
- プロセッサ: 1個以上のCPU(コア)を納めている物理チップ。
- トラップ: 特権的な処理を要求するためにカーネルに送るシグナル。トラップの種類としてシステムコールやプロセッサ例外、割り込みがある。
- ハードウェアの割り込み: トラップの一種。
基礎知識
カーネル
カーネルはユーザー空間がハードウェアを呼び出すためのシステムコールを実行するソフトウェア。
Unix系のOSはCPUのスケジューリング、メモリ、ファイルシステム、ネットワークプロトコル、システムデ
バイスを管理するするモノリシックカーネルというカーネルモデルを持っている。
構造は下のレイヤから順に
- ハードウェア
- カーネル
- システムコール
- アプリケーション、システムライブラリ
となっている。
モノリシックカーネル以外のカーネルモデルも存在する。
- マイクロカーネル: カーネル自体はプロセス管理、メモリ管理、基本的な通信機能のみを持ち、ユーザーモードプログラムにその他の多くの機能を移管した小さなカーネルを使うような構造。(QNX, MINX)
- ユニカーネル: カーネルとアプリケーションのプログラムを一緒にコンパイルして一つのプログラムにする構造。(MirageOS, IncludeOS, HermitCore)
また、モノリシックカーネルとマイクロカーネルの両方の性質を併せ持ったハイブリッドカーネルというモデルを採用したOSも存在する。(Windows NT)
最近、Linuxはカーネルモデルを変更し、拡張BPFという新しいソフトウェアタイプを認めるようにした。
これにより、システムコールを介したユーザーモードアプリケーションの他にカーネルモードアプリケーションというBPFを別途用意し、ユーザーモードアプリケーションとカーネルモードアプリケーションの2種類をハードウェア上で実行するというアーキテクチャだ。
カーネルの実行
カーネルは一般に数百万業のコードから構成される大規模プログラムだ。基本的にはユーザーレベルプログラムがシステムコールを発行した時やデバイスが割り込みを送った時に実行される。
カーネルクロックやメモリ管理タスクなど、システム内で必要とされる作業のために、一部のカーネルスレッドは非同期に実行されるが、これらはCPUリソースをほとんど使用しない。
WebサービスをOS上で稼働させるときは、主としてカーネル上で実行される。
CPUを酷使するワークロードは、カーネルに割り込まれないように通常ユーザーモードで実行される。
カーネルは、こういったワークロードのパフォーマンスに影響を与えられないと考えたくなるが、実際には影響を与えてしまっているケースが多い。
よくあるのはCPUの競合だ。
他のスレッドがCPUリソースを必要としてきたとき、カーネルスケジューラはどちらを実行し、どちらを待たせるかを判断しなければならない。
カーネルは、スレッドをどのCPUで実行するかも選択するが、この時にプロセスにとってキャッシュが溜まっていてヒット可能性が高かったり、ループ処理などによって同じメモリに何度のアクセスしている方を選べばパフォーマンスを大きく上げられる。
カーネルモードとユーザーモード
カーネルはハードウェアへのフルアクセスと特権的な命令の実行を認めるカーネルモードという権限で実行される。
ユーザーモード(人間がコマンドを実行したり、自分で書いたコードを実行したり、アプリを操作したり、Webサーバーが要求を処理したり)でプログラムが実行されると、カーネルはプロセスを生成し、他のプロセスからデータアクセスされるのを防ぐ(リソースの独立)。
カーネルモードとユーザーモードは、特権リングと保護リングと呼ばれるアーキテクチャで実装されている。
階層構造の権限を持つことで、中心にあるハードウェアを障害から保護してくれる。
ちなみにカーネルモードでしか許可されていない命令をユーザーモードで実行すると例外が起き、その例外はカーネルで処理される。(permission deniedエラー生成のためにね。)
あとあと、ちなみに、カーネルモードで実行される命令の例としては、メモリ管理とIO操作とかプロセス管理が当てはまるよ。
ユーザーモードで実行されたプログラムはカーネルによってプロセスが生成される。
ユーザーモードで実行されたプログラムがカーネルモードによって実行する必要がある場合、システムコールによってカーネルにその処理を依頼する。
システムコール
システムコールについておさらいする。
ユーザープログラムはハードウェア上で実行されるためにカーネルへ要求を行う必要がある。
その要求のインターフェースをカーネルが提供しているのだが、そのインターフェースがシステムコールだ。
システムコールはカーネルに対してOSの基本的な機能の実行を要求する。
使えるシステムコールの数は数百命令だが、カーネルの単純性を保つためにその数をできるだけ少なくする努力がなされている。
ユーザーレベルのプログラム(ユーザーランド)では、これらを基礎としてシステムライブラリというより高度なインターフェースを作ることができる。
システムライブラリについておさらい
システムライブラリとは、アプリケーションが実行するために最低限必要なライブラリのことである。
OSから提供されていて、OSの種類によって異なるシステムライブラリが提供されている。
以下はUNIX系OSにおける例。
* libc: メモリ割り当てや文字列操作、入出力処理などの基本的なシステムコールをラップして関数にしたもの。
* libpthread: POSIXスレッドを扱うための関数で、マルチスレッドでプログラムを実行するときに必要になる。
* libm: 数学関数のライブラリ。三角関数や対数関数などの計算機能を提供
以下は覚えておきたい主要なシステムコールだ。
- read(2): バイトを読み出す
- write(2): バイトを書き込む
- open(2): ファイルを開く
- close(2): ファイルを閉じる
- fork(2): 新しいプロセスを作る
- clone(2): 新しいプロセスまたはスレッドを作る
- exec(): 新しいプログラムを実行する
- connect():ネットワークホストに接続する
- accept(): ネットワーク接続を受け入れる
- stat(): ファイルの統計量を取得する
- ioctl(): IOプロパティの他、雑多な機能属性を設定する
- mmap(): メモリアドレス空間ファイルにマッピングする
- brk(): ヒープポインタが指せる範囲を拡張する
- futex(2): 高度なユーザーモードミューテックス
システムコールの引数のようなものの意味って?
read(2)やwrite(2)における(2)の意味は、OSのマニュアルページのセクション番号を示している。
OSにはmanページという機能に関するマニュアルページを提供しており、標準で用意されているman
コマンドを使うことでマニュアルを読むことができる。
マニュアルにはセクションがあり、以下のような構成になっている
- セクション1: 一般的なコマンド
- セクション2: システムコール
- セクション3: ライブラリ関数
- セクション4: 特殊ファイル(デバイスファイルなど)
- セクション5: ファイルフォーマットと規約
- セクション6: ゲームとスクリーンセーバー
- セクション7: その他(プロトコル、ファイルシステムなど)
- セクション8: システム管理コマンドとデーモン
その中でも、readやwriteはシステムコールに分類されるので、セクション2に格納されている。
なので、セクション2のreadコマンドのマニュアルを読みたい時は
$ man 2 read
と実行することでターミナル上にマニュアルが表示されるので、読むことができる。
多くのシステムコールの目的は自明であるが、一般的な用法があまり自明とは言えないシステムコールも存在するので、簡単に説明する。
- ioctl(2): カーネルにさまざまな細かい処理を要求するために使われる。特に、目的がわかりやすいシステムコールには適していないシステム管理機能を実行するために使われる
- 例: コンピュータのシャットダウンやリセット、ネットワークインターフェースの設定など
- mmap(2): 実行可能ファイルやライブラリのアドレス空間にマッピングしたり、メモリマップトファイルを作るために使われる。システムコールの実行頻度を下げてパフォーマンスを上げるために、プロセスのワーキングメモリを確保するためにも使われることもある。
ヒープポインタとは?
まず、ヒープ領域について簡単に説明する。
ヒープ領域とはプログラムに割り当てられるメモリの領域のこと。
スタック領域とは違い確保、解放に順番がなく、プログラム側で自由に確保、解放の順序を決めることができる。
ヒープ領域はmalloc(2) (memory allocation)を使うことで動的に割り当てることができるので、プログラムのコンパイル時ではなく実行時に柔軟にメモリ領域を割り当てることができる。
ioctlは性質が曖昧なので、用途がわかりにくいものだ。
ioctlはLinuxのperf(1)ツールにて、パフォーマンスインストるメンテーションをコーディネートするための特権的な処理を実行するために使われる。
ファイルディスクリプタとは?
ファイルディスクリプタの前に、ファイルテーブルについて知っておく必要がある。
ファイルテーブルとは、カーネルが提供する記憶領域のことで開かれている開かれているファイルに関する情報を記憶してくれる。具体的に保存してくれている情報としては
- ファイルパス
- 開いているファイルの現在位置
- ファイルの状態(書き込み可能モードや読み取り専用モードなど)
などがある。
ファイルシステムによって、ファイルはコンピュータにおけるストレージ領域に保存される。
ストレージ領域に保存されているファイルが開かれると、開かれているファイルに関する情報(ファイルサイズや現在の操作位置、ファイルの状態など)は、ファイルテーブルのエントリという場所に保存される。
エントリの識別子は整数値で割り当てられ、新しいファイルが開かれるたびに固有で最小な整数値が割り振られる。
さて本題のファイルディスクリプタについて説明する。
ファイルの操作はカーネルが管理しているため、ユーザープログラムからファイルの読み書きを行うためにはシステムコールを介してカーネルに読み書きの要求をする必要がある。
その際なんのファイルに読み書きを行うのかを識別してくれる整数値が、ファイルディスクリプタだ。
具体的には
0: 標準入力
1: 標準出力
2: 標準エラー出力
3以降: 操作対象のファイル識別子
となっており、この整数値は、ファイルテーブルのエントリの識別子を示す。
ユーザープログラムの操作対象となるファイルテーブルのエントリの識別子。それがファイルディスクリプタ。
割り込み
割り込みとは、プロセッサの現在の処理に割り込んで処理してもらわなければならないようなイベントが発生したことをプロセッサに知らせるシグナルだ。
割り込みが発生した際、プロセッサは、まだカーネルモードでなければカーネルモードに切り替わり、現在のスレッドの情報を保存してからISR(Interrupt Service Routine: 割り込みサービスルーチン)を実行して割り込みイベントを処理する。
割り込みには大きく分けて2種類が存在し、
- ハードウェアによる非同期割り込み
- ソフトウェアによる同期割り込み
がある。
非同期割り込み
ハードウェアデバイスは、プロセッサにIRQ(Interrupt service ReQuest: 割り込みサービス要求)という要求を送ることができる。
IRQは、現在実行されているソフトウェアとは非同期にCPUに届く。
IRQの具体的な例を挙げると
- ディスクデバイスがディスクIOの完了を知らせる
- ハードウェアがエラーを知らせる
- ネットワークインターフェースがパケットの到着を知らせる
- キーボードやマウスなどの入力デバイスが入力を知らせる
DBがファイルシステムから読み出しを行う際の割り込みの例について考えてみる。
- DBが読み出し要求をし、ファイルシステムのフェッチを待つ。
- スケジューラはDBの処理を待っている間、他のスレッドにコンテキストスイッチし、他のアプリケーションの処理を実行する。
- しばらくしてディスクIOが完了するが、その時点ではCPUはすでに別の命令を行っている。
- IRQによる完了割り込みが発生する。
同期割り込み
同期割り込みは、ソフトウェアの命令によって生成され、3種類が存在する。
- トラップ: デバッグ時のブレークポイントなどの意図的な割り込みによって引き起こされる。トラップが発生すると、プロセスがカーネルモードに切り替わり、システムコールハンドラを起動し、カーネル内で必要な処理が実行された後にユーザーモードに戻る
- 例外: ゼロ除算などによって引き起こされる。通常の命令が中断されることを意味する。
- フォールト: 例外と違い、回復可能なエラーによる割り込み。無効なメモリへのアクセスによるページフォールトなど、メモリに関するイベントでよく使われる。
ページフォールトとは?
コンピュータ上の仮想メモリ管理において、プログラムがアクセスしようとしたメモリページが物理メモリ上に存在しない場合に発生するイベント。
プログラムがアクセスを試みたメモリが現在メモリにロードされておらず、ディスク上のスワップ領域やページファイルからそのメモリページをロードする必要があることを示している。
仮想メモリとは?
OSが物理メモリを仮想化してメモリの容量を拡張するための技術。
基本的に物理メモリがそのまま使われることはなく、カーネルの機能によって仮想メモリが常時提供されている。
メモリページとは?
コンピュータのメモリ管理において使用される固定サイズの連続したメモリブロック。仮想メモリシステムの基本的な単位として機能する。メモリページのサイズは4KBや8KBなど、CPUアーキテクチャやOSの設定によって変わってくる。
スワップ領域とは?
仮想メモリ管理において、物理メモリが不足すると、ディスク領域にメモリ領域を拡張させていくことができる。
その拡張したメモリ領域をスワップ領域という。
ページファイルとは?
スワップ領域によって占有されたストレージ領域のこと。
ページフォールトのプロセスとしては
- アクセス要求: プログラムが特定のメモリアドレスにアクセスを試みる
- ページテーブルの確認: CPUはページテーブルを参照し、要求されたメモリページが物理メモリに存在するかを確認する。
- ページフォールトの発生: 要求されたメモリページが物理メモリ上に存在しない場合、ページフォールトが発生。
- OSの介入: ページフォールトハンドラが呼び出され、OSがディスクから必要なメモリページを物理メモリにロードする。
- 実行の再開: メモリページがメモリにロードされたあと、プログラムの実行が再開される。
仮想メモリシステムの正常な動作の一部であり、必ずしもエラーを意味するものではない。
ただし、ページフォールトが頻発すると、ディスクIOが増加し、システムパフォーマンスが増加する恐れがある。
これはスラッシングと呼ばれる。
この場合、メモリの容量が足りないことが考えられる原因として挙げられる。
割り込みスレッド
ISRは、割り込まれたスレッドの影響を最小限に抑えるために、できる限り高速に実行されるように設計されている。
割り込みが少しまとまった仕事をしなければならない場合、特にロックでスレッドをブロックしなければならない場合、その仕事は、カーネルがスケジューリングすることによって割り込みスレッドで処理して良い。
割り込みスレッドの処理方法に関してはカーネルの種類によって異なる。
Linux kernelでは、デバイスドライバは二つの部分から構成されるものとしてモデリングできる。
上半分は割り込みを素早く処理し、後で下半分によって処理されるようにスケジューリングする。
上半分は新しい割り込みの到着を遅らせる割り込み禁止モードで実行されるので、割り込みを素早く処理することはとても重要なことだ。
上半分がのんびり自分の処理を実行していると、他のスレッドのレイテンシが高くなってしまう。
下半分はタスクを小さく分割して割り込み時間を小さくしたタスクレットか遅延処理を行うようにするワークキューにできる。
ワークキューはカーネルがスケジューリングできるスレッドで、必要に応じてスリープできる。
Linuxのネットワークドライバは、受信したパケットを知らせるIRQを処理するために上半分を使い、その上
半分がネットワークスタックにパケットをプッシュするために下半分を呼び出す形式をとっている。
下半分はsoftirq(ソフト割り込み、割り込みの処理を遅延させて実行させる形式)として実装されている。
割り込みのマスキング
カーネル内のコードパスには、安全に割り込めないものがある。
例えば、システムコール中にスピンロックを獲得するカーネルコードがそうだ。
割り込みもスピンロックを必要とする場合がある。
スピンロックとは?
まず、ロックについて理解する必要がある。
ロックとは、複数のスレッドが同時に動作する環境で、共有リソースへのアクセスを制御するための同期機構の一つ。
あるスレッドがリソースを使用する際にロックを獲得する。
ロックを獲得したスレッドのみがリソースにアクセスできる。
他のスレッドはロックが開放されるまでアクセスできない。
そして、スピンロックについてだ。
スピンロックはマルチスレッドプログラミングにおけるメカニズムの一つ。
他のスレッドの処理を待っているスレッドは、その場で処理がブロックされ、共有リソースのロックを獲得できるかを繰り返しチェックする。(繰り返しチェックする様子がスピンに例えられることから、スピンロックと名付けられた。)
(ロックの例は、ゴルーチンにおけるバッファリングされていないチャネルを扱った複数スレッドの生成時に発生するもの: https://zenn.dev/link/comments/9c3effbf19acb1)
ロックの獲得を待っている間、獲得チェックのためにスレッドはCPUを占有し続ける。これをビジーウェイトと呼ぶ。
システムコール中にロックを獲得した状態で割り込みスレッドがロックを獲得する必要が出た際にデッドロックが起きる場合がある。
そのような状況を避けるために、カーネルはCPUの割り込みマスクレジスタをセットして一時的に割り込みをマスク(遮断)できる。
マスクする(割り込みを無効にする)時間はできる限り短くしなければならない。さもなければ、他の割り込みによって目覚めるアプリケーションのタイムリーな実行に摂動を与えてしまう。
これは応答時間い厳格な要件が与えられているリアルタイムシステムでは重要な意味を持つ。
割り込みが無効になっている時間は、パフォーマンス分析の対象である。
優先度の高いイベントの中には無視してはならないものがあるので、それらはマスク不能割り込みにできる。
例えばLinuxでは、一定期間割り込みがないかどうかによってカーネルがロックアップした(カーネルが応答不能になり、システム全体が機能停止した状態)かどうかを推定するIPMI (Intelligent Platform Management Interface)ウォッチドッグを使える。
その場合、IPMIウォッチドッグはシステムをリブートさせるためのNMI割り込みを発行する。
ウォッチドッグとは?
システムの安定性を監視し、フリーズや異常な状態を検出して自動的に再起動などの対策を行う仕組み。
クロックとアイドル状態
クロックとは、コンピュータシステム内で一定の感覚で発生する電気的な信号のこと。
クロックには周波数というものが存在し、1秒あたりの振動回数を表し、ヘルツ(Hz)で表現される。
ハードウェアによるタイマーからの割り込みによって、一定間隔で実行されるclockルーチンは、Unixカーネルにおけるコアコンポーネントである。
クロックは歴史的に60/100/1000Hzのいずれかで実行されていた。そして毎回のクロック実行をティックと呼ぶ。
クロックによって
- システム時間の変更
- タイマーの発火とスレッドスケジューリングのためのタイムスライス(プロセスがCPUを利用できる時間の単位)の終了
- CPU統計のメンテナンス
- スケジューリングされたカーネルルーチンの実行
などが実行されている。
クロックには以下のようなパフォーマンス問題がある。
- ティックレイテンシ: 100Hzのクロックでは、次のティックが処理されるためにタイマーには10m未満のレイテンシが余分に加わる。この問題は、高分解能のリアルタイム割り込みを使って待たずにすぐ実行されるようにして解決された。
- ティックオーバーヘッド: ティックはCPUサイクルを消費するため、わずかながらアプリケーションに摂動を与え、OSジッターと呼ばれるものの一因になる。最近のプロセッサには、アイドル時に部品の電源を切る動的電力管理機能もあるが、クロックルーチンはこのアイドル時間にも割り込むため、無駄に電力を消費する恐れがある。
OSジッターとは?
ジッタとは、デジタル信号のタイミングにおける予期せぬ変動や不安定さを指す。
特にOSジッターとは、クロックなどの何かしらの処理が割り込んだり、バックグラウンドで実行されることで他の処理(アプリケーションの実行など)のレイテンシにばらつきが発生すること。
最近のカーネルはティックレスカーネルを作ることを目指して、クロックルーチンからオンデマンドの割り込みに多くの機能を移している。
こうすることで、プロセッサがクロックの割り込みによってオーバーヘッドが発生することを防ぎ、電力効率を上げている。
Linuのクロックルーチンはscheduler_tick()だが、CPUに負荷がない時にクロック呼び出しを省略する方法がある。
クロック自体は一般に250Hzで動作するが、NO_HZ機能によってクロック呼び出しを削減できる。
今は広くNO_HZが有効にされるようになっている。
アイドルスレッド
CPUが実行すべき仕事がない時は、カーネルは仕事を待つスレッドをスケジューリングする。(アイドルスレッド)
基本的には割り込みを受け取るまでループで新しい仕事があるかどうかをチェックするが、最近のLinuxではCPUの電源を切るhlt命令を呼び出せるようにして節電している。
プロセス
ユーザーレベルプログラムを実行するための環境で、以下で構成されている。
- メモリアドレス空間
- ファイルディスクリプタ
- スレッドスタック
- レジスタ
プロセスは、カーネルによってマルチタスク実行される。カーネルは一般に一つのシステムで数千ものプロセスを実行できる。
個々のプロセスにはPID(プロセスID)という一意な識別子が割り振られる。
プロセス内には1つ以上のスレッドが生成され、プロセス内のリソースを共有する。
スレッドは以下で構成されている。
- スタック
- レジスタ
- 命令ポインタ(プログラムカウンタとも呼ばれ、プログラム内で次にどこを実行するかを示した値)
Linuxでは、プロセスもスレッドもタスク(実行可能な最小単位のプログラム)である。
カーネルが最初に起動するPIDが1のinitで、これがユーザー空間のサービスを起動する。
Linuxでは、一般にSystemdを使ってサービスの依存関係を管理しながらサービスを起動している。
実際にコマンド叩いて確認してみた↓
プロセスの作成
プロセスはfork(2), clone(2)システムコールを使って作成される。
プロセスをゼロから生成せず、既存のプロセスを複製してプロセスIDを付与していくことでプロセスを作る。
続いてexec(2)コールを呼び出し、プログラムの実行を開始する。
forkやcloneはコピーオンライトという戦略を使ってパフォーマンスを上げられる。
前のアドレス空間の内容を全てコピーするのではなく、前のアドレス空間の参照手段だけを作る。
そして、どちらかのプロセスがメモリに変更を加えると、変更後の内容のために別のコピーを作る。
これでメモリのコピー作成を先延ばしにするか不要にできるので、メモリの使用量とCPUの使用率を下げてくれる。
スタック
スタックとはメモリ内の一次データ記憶領域で、後入れ先出し(Last In First Out)リストになっている。
スタックは、CPUのレジスタに入りきらない比較的重要度の低いデータを格納するために使われる。
関数が呼び出されると、スタックにはそれまでのコードパスで記憶していた
- ローカル変数
- 命令位置(リターンアドレス)
- 引数の値
などをスタックにpushし、呼び出した関数の引数の値やローカル変数などもスタックにpushされる。
呼び出した関数の処理が終了すると、子関数に関する情報はpopされ、返り値のみがスタックにpushされている。
子関数の返り値や呼び出し前の情報をスタックからpopして関数の返り値だけが取得できている状態になっている。
詳しくは「コンピュータシステムの理論と実装」第7, 8章あたりを読むと理解できる。
スタック上の関数実行関連のデータセットをスタックフレームと呼ぶ。
現在実行されている関数までのコールパスは、スレッドのスタック内のスタックフレームに格納されている。
これはリターンアドレスを解析すればわかるものだ。(これをスタックウォークと呼ぶ)
このコールパスは、スタックトレースと呼ばれる。
スタックは、あるコードがなぜ実行されているのかと言う問いに答えられるので、デバッグやパフォーマンス分析における貴重なツールとされている。
ユーザースタックとカーネルスタック
ユーザースタック: ユーザーモードで実行されるプログラムやプロセスが使用するスタック領域。
カーネルスタック: カーネルモードで実行される処理が使用するスタック領域。
プロセスがシステムコールなどによってユーザーモードからカーネルモードに切り替わる際、プロセスのコンテキストはユーザースタックからカーネルスタックに切り替わる。
ユーザーレベルプログラムとカーネルの間の保存情報が分離され、セキュリティと安定性が確保される。
要するに、ユーザープログラムとカーネルは別のスタックを使っているんだよってこと。
ファイルシステム
ファイルシステムとは、データをファイルとディレクトリによって組織し、POSIX標準(OSの標準的なインターフェースと環境を定義する規格。)を基礎としてデータに簡単にアクセスさせてくれるインターフェースのこと。
OSはルートレベルを先頭とするトップダウンのツリーとして構成されたファイル名のグローバルな名前空間を提供してくれる。
グローバルな名前空間って?
全てのファイルとディレクトリは、ルートディレクトリからの一意のパスで識別できる。
一つのファイルシステムの中に、重複したパスや、同じパスを指定したら複数のファイルが識別されることはないということ。
パスが一貫した方法でファイル、ディレクトリにアクセスすることを可能にしてくれている。
ファイルシステムは、ディレクトリに自分のツリーを付け加えるマウントによってツリーを結合する。
ディレクトリの接続点をマウントポイントとも呼ぶ。
なので、エンドユーザーは、土台のファイルシステムタイプを意識せず、ファイルの名前空間を透過的に移動できる。
ファイルシステムの内部構造や物理的な配置を意識することなく、ファイルのツリー構造を意識していれば、問題なくなるということだ。
ファイルシステムタイプとは?
ファイルシステムの種類のこと。
Linuxにおいては
- ext4: システムパーティション用(基本的にはこのタイプで管理する)
- XFS: 大容量データストレージ用
- NTFS: Windowsとのデータ交換用
- FAT32: USBメモリなどのポータブルデバイス用
一つのOSに複数のファイルシステムタイプが共存しているのが一般的で、異なるデバイスやOSとのデータ交換を可能にするために複数のファイルシステムタイプをサポートしている。
これは仕様によって固定されているわけではなく、コンテナを使ったり、OSのアップデートなどがきっかけでユーザーやベンダーによって動的に追加されることもある。
例えば、LinuxコンピュータにUSBデバイスを接続した時を考えてみると。。。
通常はext4で管理しているツリー構造の中に、XFSで管理しているファイル、またはディレクトリがどこかのマウントポイントにマウントされるという具合だ。
トップレベルのディレクトリは以下のディレクトリで構成されている
- etc: システム構成ファイルを格納する
- usr: システム供給のユーザーレベルプログラムやライブラリを格納する
- dev: デバイスノードを格納する
- var: システムログなどの変化していくファイルを格納する
- tmp: 一時ファイルを格納する
- ユーザーのホームディレクトリを格納するhome
などがある。
varやhome内は別のストレージデバイスで管理されていたり、独自のファイルシステムタイプを使用している可能性があるが、これらも別のコンポーネントと同じようにアクセスできる。
ほとんどのファイルシステムタイプはコンテンツを格納するためにストレージデバイスを使っているが、/procや/devのように、カーネルが動的に作成するファイルタイプもある。
VFS
VFS(Virtual File System)とはファイルシステムタイプを抽象化するためのカーネルインターフェース。
OSには複数のファイルタイプシステムが用意されていることが一般的だが、そのタイプの違いを吸収してくれるそうだ。
VFSインターフェースによって、カーネルに新しいファイルシステムを追加するのは簡単になった。
VFSはグローバルなファイル名の名前空間の提供元でもあり、ユーザープログラム、アプリケーション、さまざまなファイルシステムタイプに横断的にアクセスできるようになっている。
IOスタック
ユーザーレベルスタックからストレージデバイスまでのパスをIOスタックと呼んでいる。
ソフトウェアスタックのうちの一部(サブセット)でもある。
一般的には上から順に
- アプリケーション
- システムコール
- VFS
- ファイルシステム
- ブロックデバイスインターフェース
- ボリュームマネージャ
- ホストバスアダプタドライバ
- ディスクデバイス
となっている。
中には、システムコールからファイルシステムを介さずにブロックデバイスにアクセスするというケースも存在するが、それは管理ツールやデータベースが扱うことがある。
キャッシング
ディスクIOのレイテンシ高いから、メモリでデータをバッファリングすることでレイテンシ押さえていこうぜって技法。
レイヤごとに以下のようなキャッシングが存在する
- クライアントキャッシュ: Webブラウザキャッシュ(SWR)
- アプリケーションキャッシュ: []
- Webサーバーキャッシュ: Apacheキャッシュ
- キャッシングサーバー: memcached, redis
- データベースキャッシュ: MySQLバッファキャッシュ
- ディレクトリキャッシュ: Dcache
- ファイルメタデータキャッシュ: iノードキャッシュ
- OSバッファキャッシュ: バッファキャッシュ
- ファイルシステムプライマリキャッシュ: ページキャッシュ, ZFS ARC
- ファイルシステムセカンダリキャッシュ: ZFS L2ARC
- デバイスキャッシュ: ZFS vdev
- ブロックデバイスキャッシュ: バッファキャッシュ
- ディスクコントローラキャッシュ: RAIDカードキャッシュ
- ストレージアレイキャッシュ
- オンディスクキャッシュ
ネットワーキング
コンピュータがネットワークを通じて他のコンピュータと通信したり、Webサービスにアクセスしたりすることを可能にしているのは、OSがネットワークプロトコルスタックという機能を提供しているからだ。
このスタックは、TCP/IPスタックと呼ばれる。ユーザーレベルアプリケーションは、ソケットと呼ばれるプログラマブルなエンドポイントを通じてネットワークにアクセスする。
値とワークにアクセスするための物理デバイスは、ネットワークインターフェースであり、通常はネットワークインターフェースカードがその機能を提供する。
ネットワークインターフェースにIPアドレスを割り当てて、ネットワークと通信できるようにすることは、元々システム管理者が行なっていたが、DHCP(Dynamic Host Configuration Protocol)というTCP通信とOSの機能によって自動化されている。
デバイスドライバ
ハードウェアとOSを仲介してくれるソフトウェア。ハードウェアを開発しているベンダーから提供されることが多い。
機能としては、プリンターやグラフィックカード、ネットワークカードとOSの間の通信を可能にする。
VFSのように、さまざまな種類のデバイスの違いを吸収してくれるインターフェースとして機能してくれる。
OSの一部として捉えられがちだが、厳密には違う。
しかし、デバイスドライバはOSに合致したものを使う必要があるので、同じハードウェアでも異なるOS用に異なるドライバが必要。
メモリやCPU, ストレージに比べて、ボトルネックになるケースはあまりない。
カーネルプリエンプション
カーネルプリエンプションがOSによってサポートされていると、優先度の高いユーザーレベルプログラムがカーネルスレッドに割り込んで処理を実行できるようになる。
一般にLinuxでは、カーネルのコンフィグにおけるCONFIG_PREEMPT_VOLUNTARYDEボランタリーカーネルプリエンプションが有効にされている。
また、すべてのカーネルコードをプリエンプションできるようにするCONFIG_PREEMPT, プリエンプションを無効にしてレイテンシが高くなってもスループットを向上させるCONFIG_PREEMPT_NONEもある。
カーネル
Unix系OSのカーネルには様々なものがあり、例として
- Linuxカーネル
- Unix
- BSD
- Solaris
がある。
カーネル間の違いは
- サポートするファイルシステム
- システムコールインターフェース
- ネットワークスタックアーキテクチャ
- リアルタイムサポート
- CPU/ディスクIO/ネットワーキングのスケジューリングアルゴリズム
などがある。
試しにLinuxとその他のカーネルのバージョンとシステムコール数を比べてみると
- Unix version 7 : 48個
- Solaris: 142個
- BSD: 222個
- Linux 5.3.0-1010-aws: 493個
システムコール数からもわかるようにLinuxは複雑度が増してきているが、新しいシステムコールやその他のカーネルインターフェースを増やすことによってその複雑さをユーザー空間に曝け出している。
複雑度が増せば、学習、プログラミング、デバッグにより時間がかかるようになる。
練習問題
プロセス、スレッド、タスクの違いは何か
プロセスとスレッドの違いは、リソースを共有しているかの違い。
そして、スレッドはプロセス内に生成される。
プロセスはメモリ領域やリソースを独立して持っており、他のプロセスと共有されないが、スレッドはプロセス内のリソースを共有するので、同じデータにアクセスできたりする。
プログラムカウンタとスタックはスレッドごとに独立している。
モードスイッチとコンテキストスイッチとは何か
モードスイッチ: プロセスをカーネルモードで実行するか、ユーザーモードで実行するかの切り替え
コンテキストスイッチ: CPUがどのプロセスを実行するかの切り替え。割り込みなどによって発生する。
ページングとスワッピングの違いは何か
ページングはメモリのワードをある一定のまとまった単位で扱うことで、スワッピングはメモリで管理しきれなくなったデータをストレージ領域に拡張する形で管理すること。
ワークロードについて、IOバウンドとCPUバウンドの違いは何か
負荷の大きさと負荷の種類が違う。
IOバウンドは入力/出力を行う比較的軽いワークロードで、CPUバウンドは巨大な演算処理などの重いワークロード。
カーネルの役割は?
ハードウェアやCPUスケジューリングなどを管理するシステム。
カーネルモードというハードウェアに直接アクセスできる特権的なモードを持っていて、ソフトウェアへはシステムコールというインターフェースを通してユーザープログラムとハードウェアの橋渡しをしてくれる。
システムコールの役割を説明しなさい。
カーネルがユーザープログラムへ提供しているインターフェース。
このインターフェースを使うことで、ユーザープログラムがハードウェアにアクセスできる。
システムコールを介してユーザープログラムを実行することで、直接実行されることを防ぎ、予期せぬ不具合が発生することを防いでくれる。
VFSの役割とIOスタック内での位置付けを説明してね
VFSは複数のファイルシステムタイプを抽象化して一つのファイルシステムとして扱えるようにしたインターフェースの役割を持っている。
IOスタック内では、システムコールとファイルシステムをつなげる位置付けを持っている。
しかし例外として、DBや管理ツールなどは、VFSやファイルシステムを介さず直接ブロックデバイスへ命令を実行する。
スレッドがCPUを手放す理由を列挙してね
- ハードウェアによる非同期割り込み
- ソフトウェアによる同期割り込み
- プリエンプションによる優先度の高いスレッドの割り込み
- バッファリングされていない共有データへアクセスできずにロックされる
- ディスクへのIO操作待ち
- スリープによる明示的な手離し
- スケジューラによって割り当てられたCPU時間を迎えた
仮想メモリとデマンドページングの利点を説明してね
仮想メモリによって、物理メモリが抽象化されているので、スワッピングにより実質無限にメモリにデータを保存できる。
デマンドページングによって、データが必要になったときに物理メモリにページを割り当てるので、プログラムの起動が高速になり、アクセスされないページはロードされないため、メモリ使用量を節約できる。
可観測性ツール
OSは古くからシステムソフトウェアとハードウェアコンポーネントを観察するためのツールを提供してきたが、実際には隙間(観測できていないコンポーネント)がいくつもある。
Linuxの可観測性は、BPFベースのBCCとbpftraceをはじめとする動的トレーシングツールの成長によって大幅に改善されてきた。
biosnoopで解析できるようになったディスクIOなど、隙間になっていた部分が見えるようになってきた。
目標
- 静的パフォーマンスチューニングツールとクライシスツールを見分けられるようにする。
- ツールタイプとオーバーヘッドを理解する(カウンタ、プロファイリング、トレーシング)
- 可観測性データのソースについて学ぶ(/proc, /sys, トレースポイント, kprobe, uprobe, USDT, PMCなど)
- 統計量のアーカイブを作るsar(1)の設定方法を学ぶ
ここで詳解する可観測性ツールは、LinuxのUbuntuディストリビューションで使うことを想定している。
自身の使用したいOSでも同様のツールがあるので、調べてから使うようにしたい。
取り上げるツール
Linuxはソフトウェアスタックの個々のコンポーネントにおける可観測性ツールを提供している(詳細はp136参照)
- システムライブラリ: ltrace, gethostlatency
- システムコール: strace, opensnoop
- VFS: lsof, fatrace, filelife, pcstat
- ファイルシステム: ext4dist, ext4slower
- ボリュームマネージャ: mdflush
- ブロックデバイス: iostat, biosnoop, biolatency, biotop, blktrace
- ソケット: ss
- TCP/UDP: nstat, tcpfile, tcpretrans, udpconnect
- IP: nstat
- ネットデバイス: tcpdump
- スケジューラ: execsnoop, mpstat, profile, runqlen, offcputime, softiqs
- 仮想メモリ: vmstat, slabtop, free
- デバイスドライバ: hardirqs, criticalstat
静的パフォーマンスツール
可観測性ツールには、アクティブなワークロードを抱えたシステムではなく、休止状態のシステムを解析するタイプのものもある。静的パフォーマンスツールに該当するツールは以下の通り(p137参照)
静的パフォーマンスチューニングとは?
システムにワークロードを与えてパフォーマンスを解析することを特に動的パフォーマンスチューニングと呼ぶが、逆に、ワークロードを与えずにパフォーマンスをチューニングすることを指す。
具体的には
- このコンポーネントには意味があるのか
- 構成は、想定されるワークロードによって意味のあるものになっているか
- コンポーネントは、想定されるワークロードに最も合う状態に自動的に構成されるか
- コンポーネントがエラーを起こし、今は最適ではない状態で動作していないか
を確認する
ツールタイプ
可観測性ツールには、提供する可観測性がシステム全体化プロセス単位かと、カウンタベースかイベントベースかで分けると良い。
カウンタベースとイベントベース
まず、カウンタベースについてだ。
カウンタベースの情報とは、システムの状態や動作が定期的に数値で収集され、時系列データとして収集される。
例えば、
- CPU使用率
- メモリ使用量
- ネットワークパケット数
- エラー発生回数
などがある。カウンタによって収集された情報だな。
次にイベントベースについてだ。
イベントベースはシステムやアプリケーションで発生した特定の出来事を記録する。
発生時に初めて記録されるので、不定期だ。
- エラーメッセージ
- ユーザーのログイン、ログアウト
- システム起動、シャットダウン
- アプリケーションのデプロイ
カウンタベースは定期的に計測され、イベントベースは不定期に計測されるっていうのが違いだな。
システム全体 | プロセス単体 | |
---|---|---|
カウンタベース(固定カウンタ) | vmstat, mpstat, iostat, sar | ps top pamp |
イベントベース | tcpdump, perf, Ftrace, bpftrace | strace, gdb |
イベントベースツールは、プロファイラとトレーサーの二つに分類できる。
プロファイラはイベントについて一連のスナップショットを取ってアクティビティを観測し、ターゲットのおおよその姿を描く。
トレーサーは対象のイベントを全てインストルメンテーションし、例えばカスタマイズされたカウンタを生成するなどの処理を加えることもある。
固定カウンタ
カーネルは、システム統計を提供するために様々なカウンタを提供している。
通常のカウンタはイベントが発生するとインクリメントされる符号なし整数として実装されている。
例えば
- 受信したネットワークパケット数
- 発行したディスクIOの数
- 発生した割り込みの数
のカウンタがある。
これらは、モニタリングソフトウェアの指標として表示する。
カーネルは一般に2個の累積値カウンタを管理するという方法をとる。
- イベント数
- イベント処理にかかった時間の合計
を記録している。
この二つがあれば、イベントの数が直接わかるとともに、時間合計をイベント数で割ってイベント処理の平均時間を計測できる。これはレイテンシだ。
どちらも累積値なので、一定間隔で両方を読んで差分を計算すれば、1秒あたりのイベント数と平均レイテンシが得られる。
例: イベント数と平均レイテンシを得ることについて考えてみる。
T1~T2までの時間でのイベント数と平均レイテンシを得てみる。
T1-T2は計測する時間で、1sや5sに設定することが多い。
まずはイベント数について考えてみる。
- T1でのイベント合計数: N1
- T2におけるイベント合計数: N2
とすると、計測時間内に発生したイベント数を算出することができる。
ΔN = N2 - N1
次にイベント処理時間について考えてみる。
- T1でのイベント処理時間合計数: R1
- T2でのイベント処理時間合計数: R2
とすると、計測時間内に発生したイベントの合計処理時間を算出することができる。
ΔR = R2 - R1
計測時間は以下で表せる。
ΔT = T2 - T1
これらの算出した値を使うことで、以下が計算できる
- 1秒あたりのイベント数(スループット): ΔN / ΔT
- 平均のイベント処理時間(レイテンシ): ΔR / ΔN
システム全体
システム全体の状態を記録するカウンタの情報は以下のツールで確認できる。
- vmstat(8): 仮想メモリ、物理メモリの統計量
- mpstat(1): CPUごとの使用率
- iostat(1): ディスクごとのIO使用状況
- nstat(8): TCP/IPスタックの統計量
- sar(1): 様々な統計量
これらのツールの表示は一般にシステム上の全てのユーザーが見られる。
また、これらのツールが返す統計量は、一般にモニタリングソフトウェアによってグラフ化される。
多くのツールはオプションでインターバルと個数を指定できるという慣習的なルールに従っている。
例えば、vmstatコマンドを例にとると
vmstat 1 3
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
1 0 0 3353160 28548 405496 0 0 1549 142 217 422 2 2 95 1 0
0 0 0 3353160 28548 405672 0 0 0 0 256 208 0 0 100 0 0
0 0 0 3353160 28548 405672 0 0 0 1380 276 283 0 1 99 1 0
第一引数: インターバルに1sをセットし、1秒ごとにカウンタの値を表示。
第二引数: 表示個数に3をセットし、3行のみ表示させる。
プロセスごと
このタイプのツールはカーネルが個々のプロセスのためにメンテナンスしているカウンタを使っている。
Linuxにおけるツールとしては
- ps(1): プロセスのステータス、メモリやCPUの使用率などの様々な統計量を表示する。
- top(1): CPUの使用率、その他の統計量順に上位のプロセスを表示する。
- pmap(1): プロセスのメモリセグメントを利用統計付きでリストアップする.
これらのツールは、一般に/procファイルシステムから統計量を読み出している
【イベントベース】プロファイリング
プロファイリングは、ターゲットの挙動のサンプル、スナップショットを集めてターゲットの特徴を表す。
ターゲットは、CPUやメモリなどのハードウェアコンポーネントだ。
タイマーに基づいて命令ポインタやスタックトレースをサンプリング(収集)し、CPUサイクルを消費しているコードパスの特徴を浮かび上がらせる。
サンプルは、通常全てのCPUを通じて100Hzなどの決まった頻度で1分などの短い期間に収集される。
プロファイリングツールは、ターゲットのアクティビティとサンプリングが同じ歩調にならないように100Hzではなく99Hzを使うことが多い。
なぜサンプリングの頻度を少しずらすの?
サンプリングの頻度をターゲットのアクティビティと少しずらすのには、重要な理由がある。
それは、アクティビティとの同期を回避させることだ。
アクティビティとサンプリングの周期が同期してしまうと、周期的なイベントとサンプリングが被り、イベントに関するサンプルを取得できない可能性がある。
アクティビティとサンプリングのの同期を回避することにより、結果に偏りをなくすことができるのだ。
プロファイリングは、時間とは無関係にCPUのハードウェアキャッシュのキャッシュミスやバスのアクティビティなどのハードウェアイベントに基づいても良い。
プロファイラは、原因を作ったコードパスを示すこともできる。
これは、コードのメモリ消費を最適化するときに特に役立つ情報である。
固定カウンタとは異なり、プロファイリングは一般に必要な時に限り有効にされる。
それは収集のためにCPUオーバーヘッド、収納のためにストレージオーバーヘッドがかかるからだ。
オーバーヘッドの大きさは、ツールやインストルメンテーションするイベントの発生頻度によって変わる。
ちなみにインストルメンテーションとは、計測したいイベントのプログラムや実行ファイルに計測用のコードを挿入して、動作を追跡、測定する手法。
システム全体
システム全体を対象とするLinuxプロファイラには以下のものがある。
- perf(1): Linuxの標準プロファイラで、プロファイリングサブコマンドが含まれている。
- profile(8): BCCリポジトリに含まれているBPFベースのCPUプロファイラ。
プロセスごと
プロセス思考のプロファイラには次のものがある。
- gprof(1): GNUプロファイリングツール。コンパイルが追加したプロファイリング情報を分析する。
- cachegrind: valgrindツールキットに含まれている。ハードウェアキャッシュの使用状況をプロファイリングし、kcachegrindを使ってビジュアライズできる。
【イベントベース】トレーシング
トレーシングは発生したイベントを全てインストルメンテーションし、後で分析するためにイベントごとの詳細情報を格納したり、集計を生成したりすることができる。
プロファイリングと似ているが、サンプルだけではなく全てのイベントを収集、または調査することを目的とする。
トレーシングはプロファイリングよりもCPUとストレージのオーバーヘッドが大きくなるため、ターゲットの実行速度を遅らせることがある。
本番ワークロードにマイナスの影響を与える可能性があるので、そのことを頭に入れておく必要がある。
プロファイリングと同様にイベントベースのツールなので、必要な時にしか実行されない。
エラーやワーニングなどの発生頻度の低いイベントをログファイルに埋め込み、後で読み込めるようにするロギングは、デフォルトで有効にされる頻度の低いトレーシングと考えることができる。
ログにはシステムログが含まれる。
システム全体
システム全体でのトレーシングツールは、カーネルのトレーシング機能を使って、システムソフトウェアやハードウェアリソースのコンテキストでシステム全体のアクティビティを解析する。
Linuxのトレーサーには以下のようなものがある
- tcpdump(8): ネットワークパケットのトレーシング
- biosnoop(8): ブロックIOのトレーシング
- execsnoop(8): 新しいプロセスのトレーシング
- perf(1)
- Ftrace
- BCC
- bpftrace
プロセスごと
- strace(1): システムコールのトレーシング
- gdb(1): ソースレベルデバッガ
デバッガはイベントごとのデータを解析できるが、そのためにはターゲットの実行を停止、開始しなければならない。
オーバーヘッドが莫大になることがあるので、本番環境での使用には適していない。
モニタリング
カウンタベースやイベントベースとは異なり、モニタリングは後で必要になった時のために継続的に統計量を記録していく。
sar(1)
sar(1)はカウンタベースで、cronによってスケジューリングされた時刻に実行されるエージェントがシステム全体のカウンタの状態を記録していく。
デフォルトではsar(1)は、自分の統計アーカイブを読み出して、最近の履歴を表示する。
オプションのインターバルと個数を指定すれば、指定した頻度で現在のアクティビティを調べることができる。
エージェント
最近のモニタリングソフトウェアは、カーネルとアプリケーションの指標を記録してくれるエージェントというソフトウェアを実行する。
モニタリングアーキテクチャについてだが、基本的には指標をアーカイブするためのモニタリングデータベースサーバーとクライアントUIを提供するモニタリングWebサーバーが含まれている。
指標はエージェントからデータベースに送られ、さらにクライアントUIでグラフとして表示されたりダッシュボードにまとめられたりする。
可観測性ツールの情報ソース
Linuxが提供する可観測性ツールは、カーネルが提供する情報源からデータを取得して画面に表示している。
その情報源となるインターフェースには以下のようなものがある。
タイプ | ソース |
---|---|
プロセスごとのカウンタ | /proc |
システム全体のカウンタ | /proc, /sys |
デバイスの構成とカウンタ | /sys |
ハードウェアカウンタ | perf_event |
cgroup統計 | /sys/fs/cgroup |
プロセスごとのトレーシング | ptrace |
ネットワーク統計 | netlink |
ネットワークパケットキャプチャ | libpcap |
スレッドごとのレイテンシ指標 | 遅延アカウンティング |
システム全体のトレーシング | 関数のプロファイリング(Ftrace), トレースポイント, ソフトウェアイベント, kprobe, uprobe, perf_event |
cgroupとは?
Control Groupの略で、プロセスをグループ化してリソースを管理する機能。
Dockerコンテナへのリソース使用量制限などに使われる。
/proc
カーネル統計に対するファイルシステムだ。
プロセスやハードウェア、リソースやカーネルパラメータの情報を格納してくれるファイルシステムで、リアルタイムで情報が更新されている。
/procの中の各ディレクトリには対象プロセスのプロセスIDに基づく名前がつけられている。
実は/procはメモリ畳に存在するファイルシステムで、ストレージ上に保存はされない。
ファイルシステムなのに揮発性というのは驚きだ。
/procはほとんどが読み出し専用で、可観測性ツールのために統計量を提供している。
しかし、一部はプロセスやカーネルの動作をコントロールするために書き込み可能になっている。
なぜファイルシステムとしてインターフェースを提供しているの?
システム統計量を提供する手段としては、ファイルシステム以外に色々ある。
- 統計量を取得したいコンポーネントへ直接情報を取得させるシステムコールを用意する
- ストレージに情報を格納するデータベースを用意してそこにリアルタイムで統計量を保存する
しかしなぜ、ファイルシステムとして統計量を提供しているのだろうか?
それは以下のメリットがあるからだ
- open, read, close, cat, grepというシステムコールを使っても統計量を取得できる
- ファイルパーミッションを通じてユーザーレベルセキュリティも提供してくれる
- topやpsといった可観測性ツールが使えなくても、/procを見ればある程度プロセスのデバッグができる
- Unixの哲学である「すべてはファイルである」に則っている
- テキストベースで人間でも読みやすい
プロセスごとの統計量
/procにはプロセスごとの統計量も格納されている。
具体的には、/procディレクトリ下にプロセス用のディレクトリが作成され、プロセスIDがディレクトリ名として命名される。
プロセスに関する情報はプロセス用ディレクトリ内にファイルとして格納され、次のようなファイルが存在する。
- limits: 有効になっているリソース制限
- maps: メモリマップ
- sched: CPUスケジューラの様々な統計量
- schedstat: CPUの実行時間, レイテンシ, タイムスライス
- smaps: マッピングされているメモリ領域とその使用状況
- stat: CPUとメモリの全体的な使用状況を含むプロセスのステータスと統計量
- statm: ページ単位でのメモリの使用状況
- fd: ファイルディスクリプタシンボリックのディレクトリ
- cgroup: cgroupのメンバー権限
- task: タスクごとの統計量のディレクトリ
topコマンドでは、アクティブなプロセスに対して全てのディレクトリを開いて統計量を取得し、シェルから値を表示しているので、プロセスが多いシステムだとtopコマンドを実行するためにかかるオーバーヘッドが見てわかるほどになることがある。
特に、画面を表示するたびに全てのプロセスでこのシーケンスを繰り返すバージョンのtopでは顕著だ。
そんな時、topによって生成されたプロセスが最もCPUを消費しているプロセスとして自分自身を表示するような場合さえある。
システム全体の統計量
/procにはシステム全体の統計量も保持しており、/proc下にディレクトリとファイルが格納されている。
- cpuinfo: 全ての仮装CPU, モデル名, クロックスピード, キャッシュサイズなどの物理プロセッサ情報
- diskstats: 全てのディスクデバイスのディスクIO統計
- interrupts: CPUごとの割り込み数
- loadavg: 負荷の平均
- meminfo: システムメモリの使用状況
- net/dev: ネットサークインターフェースの統計量
- net/netstat: システム全体でのネットワーク統計
- net/tcp: アクティブなTCPソケットの情報
- pressure: PSI(システムリソースの競合による停滞時間の統計)ファイル
- schedstat: システム全体のCPUスケジューラ情報
- self: 現在のプロセスIDディレクトリへのシンボリックリンク
- stat: カーネルとシステムリソース統計のサマリーで、CPU, ディスク, ページング, スワップ, プロセスの情報が含まれる
CPU統計量の精度
/proc/statファイルはファイルシステム全体でのCPU使用率の情報を提供してくれている。
この情報はvmstat(8)やmpstat(1), sar(1)やモニタリングのエージェントなどによって使用されている。
この情報の精度は以下によって変わってくる。
- CPUのクロックティック(CPUの動作タイミングを制御する基本的な時間単位)の頻度
- カーネルの設定
- ハードウェアの特性
- システム負荷
デフォルトではクロックティック単位でCPU使用率を計測する。
CPUのクロック周波数が多いほど、より細かい精度でのCPU統計が更新されるため、精度が向上する。
(ちなみに、クロック周波数はカーネルの設定(CONFIG_HZ)によって設定することもできる)
解像度の高いカウンタを使ってより正確なIRQ(割り込み要求)時間を要求するオプションを指定すれば、パフォーマンスにわずかな影響が及ぶ精度を上げることができる。
具体的には、VIRT_CPU_ACCOUNTING_NATIVE, VIRT_CPU_ACCOUNTING_GENのIRQ_TIME_ACCOUNTINGを指定すれば良い。
正確なCPU使用率を得るためのアプローチとしては、MSRやPMCを使う方法もある。
- MSR(Model Specific Registers): 電力管理やパフォーマンス制御など、CPUの内部状態や動作を制御するためのレジスタ
- PMC(Performance Monitoring Counters): CPUのパフォーマンスを測定するための特殊なカウンタ
ファイルの内容
/procにどんな情報が格納されているかを調べる方法は、主に3つ。
- Linuxカーネルのドキュメントを読む(manコマンドでも表示できる)
- Linuxカーネルのソースコードを読む
- 提供されている統計量を使っているツールのソースコードを読む
/procに格納される統計量は、CONFIGによって依存しているものもある。
- schedstats: CONFIG_SCHED_STATS
- sched: CONFIG_SCHED_DEBUG
- pressure: CONFIG_:PSI
/sys
/procの他に、もう一つシステムの統計量を提供してくれるファイルシステムが存在する。
元々はデバイスドライバの統計量を提供するために設計されたものだが、いろいろな設計タイプを含むように拡張されてきた。
/sysファイルシステムは、一般に数万の統計量を格納する読み出し専用ファイルと、カーネルの状態を変更するための書き込み可能ファイルを持っている。
例えば、onlineという名前のファイルに0 or 1を書き込むと、それぞれCPUをオンライン、オフラインに切り替えることができる。
統計の読み出しと同様に、状態の設定は、ファイナリインターフェースではなく、コマンドラインでテキストを使って実行できる。
netlink
netlinkとは、カーネル空間とユーザー空間の通信に使用されるソケットファミリー(通信の取り決め)である。
ifconfigやipなどを使ってコンピュータのipアドレスを調べた経験はあるだろうか?
あのコマンドによって表示されるカーネル情報はnetlinkインターフェースを介して取得されている
netlinkを使う時には、AF_NETLINKアドレスファミリを指定してソケットをオープンし、一連のsend, secv呼び出しで要求を送り、バイナリデータ構造の情報を受け取る。
/procよりも複雑なインターフェースだけども、ファイルシステムを介してテキストとして保存するためのオーバーヘッドがなく、とても効率的だ。
カーネル情報がどこから取得されているかを調べるためには、straceコマンドを使ってシステムコールを確認するのが良い。
トレースポイント
トレースポイントは、Linuxカーネルに組み込まれた静的インストインストルメンテーションポイントのこと。
カーネルコードの論理的な位置(特定のイベント発生時)にハードコードされたインストルメンテーションポイントである。
具体的には、以下のようなイベントにポイントが組み込まれている。
- システムコール
- スケジューライベント
- ファイルシステムオペレーション
- ディスクIO開始、終了
トレースポイントは安定APIである。
安定APIとは?
長期にわたって互換性を維持し、予期せぬ変更によってクライアントのコードを壊さないことを保証するAPIのこと。
新しいバージョンのAPIは古いバージョンのクライアントコードでも動作する下位互換性があるという特徴がある。
トレースポイントはサマリー統計の域を超えてカーネルの動作を深く知るための高度なトレーシングツールを作る基礎となるもので、パフォーマンス分析の重要なリソースである。
トレースポイントの例
利用できるトレースポイントはperf, listコマンドで調べることができる。
$ sudo perf list tracepoint
List of pre-defined events (to be used in -e or -M):
~~~
block:block_rq_complete [Tracepoint event]
block:block_rq_error [Tracepoint event]
block:block_rq_insert [Tracepoint event]
block:block_rq_issue [Tracepoint event]
block:block_rq_merge [Tracepoint event]
block:block_rq_remap [Tracepoint event]
block:block_rq_requeue [Tracepoint event]
~~~
sched:sched_kthread_stop_ret [Tracepoint event]
sched:sched_kthread_work_execute_end [Tracepoint event]
sched:sched_kthread_work_execute_start [Tracepoint event]
sched:sched_kthread_work_queue_work [Tracepoint event]
sched:sched_migrate_task [Tracepoint event]
sched:sched_move_numa [Tracepoint event]
sched:sched_pi_setprio [Tracepoint event]
sched:sched_process_exec [Tracepoint event]
sched:sched_process_exit [Tracepoint event]
sched:sched_process_fork [Tracepoint event]
sched:sched_process_free [Tracepoint event]
sched:sched_process_hang [Tracepoint event]
sched:sched_process_wait [Tracepoint event]
sched:sched_skip_vma_numa [Tracepoint event]
sched:sched_stat_blocked [Tracepoint event]
sched:sched_stat_iowait [Tracepoint event]
sched:sched_stat_runtime [Tracepoint event]
sched:sched_stat_sleep [Tracepoint event]
sched:sched_stat_wait [Tracepoint event]
sched:sched_stick_numa [Tracepoint event]
sched:sched_swap_numa [Tracepoint event]
sched:sched_switch [Tracepoint event]
sched:sched_wait_task [Tracepoint event]
sched:sched_wake_idle_without_ipi [Tracepoint event]
sched:sched_wakeup [Tracepoint event]
sched:sched_wakeup_new [Tracepoint event]
sched:sched_waking [Tracepoint event]
scmi:scmi_fc_call [Tracepoint event]
scmi:scmi_msg_dump [Tracepoint event]
scmi:scmi_rx_done [Tracepoint event]
scmi:scmi_xfer_begin [Tracepoint event]
scmi:scmi_xfer_end [Tracepoint event]
scmi:scmi_xfer_response_wait [Tracepoint event]
scsi:scsi_dispatch_cmd_done [Tracepoint event]
scsi:scsi_dispatch_cmd_error [Tracepoint event]
scsi:scsi_dispatch_cmd_start [Tracepoint event]
scsi:scsi_dispatch_cmd_timeout [Tracepoint event]
scsi:scsi_eh_wakeup
~~~~
コンピュータ上に幾つのトレースポイントが設定されているのかを調べると
$ sudo perf list | grep Tracepoint | wc -l
1835
なんと1835個ものトレースポイントが設定されていた!
トレースポイントはイベントの発生時だけでなく、イベントのコンテキストデータも表示することができる。
- タイムスタンプ
- プロセス名、ID
- イベントの説明
- 引数
イベントによって変動する値は引数というフィールドに格納され、これは書式文字列によって生成される。
ここで注意しておきたいことがある。
トレースポイントは実際にはカーネルコードに配置されたトレーシング関数(トレーシングフック)である。
例えば、trace_sched_wakeup()というトレースポイントがあり、kernel/sched/core.c
にこの関数の呼び出しが含まれている。
このトレースポイントは、トレーサーでsched:sched_wakeupという名前を指定すればインストルメンテーションできるが、実際にはTRACE_EVENTマクロで定義されたトレースイベントである。
TRACE_EVENTマクロは、引数と定義、整形し、trace_sched_wakeup()のコードを自動生成し、tracefsとperf_event_open()インターフェースにトレースイベントを配置する。
トレースポイントの引数と書式文字列
個々のトレースポイントは、イベントのコンテキスト情報であるイベント引数を記述する書式文字列を持つ。
書式文字列の構造は、/sys/kernel/debug/tracing/events/{イベント名}/{トレースポイント名}/format
に格納されている。
試しにblock_rq_issue
の書式文字列を見てみると
$ sudo cat /sys/kernel/debug/tracing/events/block/block_rq_issue/format
name: block_rq_issue
ID: 1121
format:
field:unsigned short common_type; offset:0; size:2; signed:0;
field:unsigned char common_flags; offset:2; size:1; signed:0;
field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
field:int common_pid; offset:4; size:4; signed:1;
field:dev_t dev; offset:8; size:4; signed:0;
field:sector_t sector; offset:16; size:8; signed:0;
field:unsigned int nr_sector; offset:24; size:4; signed:0;
field:unsigned int bytes; offset:28; size:4; signed:0;
field:char rwbs[8]; offset:32; size:8; signed:0;
field:char comm[16]; offset:40; size:16; signed:0;
field:__data_loc char[] cmd; offset:56; size:4; signed:0;
print fmt: "%d,%d %s %u (%s) %llu + %u [%s]", ((unsigned int) ((REC->dev) >> 20)), ((unsigned int) ((REC->dev) & ((1U << 20) - 1))), REC->rwbs, REC->bytes, __get_str(cmd), (unsigned long long)REC->sector, REC->nr_sector, REC->comm
この出力のうち、"%d,%d %s %u (%s) %llu + %u [%s]"
が書式文字列であり、引数の形式を指定している。
kprobe, uprobe
トレースポイントを使ってシステム上のさまざまなイベントの情報をトレーシングできるようになったが、トレースポイントがないソフトウェアの実行をトレースしたい場合もあるだろう。
その時はkprobeインターフェースというものを使ってあらゆる関数、命令をトレースすることができるようになる。
カーネルのバージョンによって変更される可能性のあるカーネル関数と引数を表に出してしまうので、不安定なAPIだと考えられている
kprobeは本番稼働しているカーネルの動作についてほぼ無限の情報を引き出すための最後の手段なので重要である。
他のツールでは見えないパフォーマンス上の問題を観察、観測するためにkprobeは欠かせない。
トレースポイントとkprobeの比較をすると、以下のようになる。
項目 | kprobe | トレースポイント |
---|---|---|
タイプ | 動的 | 静的 |
イベントのおおよその数 | 5万種以上 | 千種以上 |
カーネルによるメンテナンス | なし | 必要 |
無効時のオーバーヘッド | なし | わずか |
安定API | ではない | である |
kprobeは、実行部分の部分と関数内の命令オフセットをトレースできる。
uprobeはkprobeと非常に似ているが、ユーザー空間に限定したイベントを対象としている。
USDT
USDTはトレースポイントのユーザー空間バージョンだ。
kprobeに対してトレースポイントがあるように、uprobeにはUSDTが存在する。
一部のアプリケーションやライブラリ、ミドルウェアはコードにUSDTを追加し、アプリケーションレベルイベントをトレースするための安定APIを提供している。
PostgreSQLにもUSDTは組み込まれている。
// postgresコンテナのIDをdocker psコマンドで探してexecする
$ docker exec -it <コンテナID> bash
// コンテナ内で以下のコマンドを実行
# bpftrace -l 'usdt:/usr/lib/postgresql/*/bin/postgres:*'
usdt:/usr/lib/postgresql/15/bin/postgres:postgresql:buffer__checkpoint__done
usdt:/usr/lib/postgresql/15/bin/postgres:postgresql:buffer__checkpoint__start
usdt:/usr/lib/postgresql/15/bin/postgres:postgresql:buffer__checkpoint__sync__start
usdt:/usr/lib/postgresql/15/bin/postgres:postgresql:buffer__flush__done
usdt:/usr/lib/postgresql/15/bin/postgres:postgresql:buffer__flush__start
usdt:/usr/lib/postgresql/15/bin/postgres:postgresql:buffer__read__done
usdt:/usr/lib/postgresql/15/bin/postgres:postgresql:buffer__read__start
usdt:/usr/lib/postgresql/15/bin/postgres:postgresql:buffer__sync__done
usdt:/usr/lib/postgresql/15/bin/postgres:postgresql:buffer__sync__start
usdt:/usr/lib/postgresql/15/bin/postgres:postgresql:buffer__sync__written
~~~~
// USDTの数は57個あった
# bpftrace -l 'usdt:/usr/lib/postgresql/*/bin/postgres:*' | wc -l
57
sar
sarは、System Activity Reporterの略で、Linuxシステムパフォーマンスを監視、分析するための重要なツールだ。
CPU使用率、メモリ使用量、ディスクIO、ネットワーク統計、ファイルシステムやVFS、仮想メモリやディスク、なんとファンさえ観測することができるのだ。
これからは、Ubuntuでsarを有効にしてシステム情報を取得する方法について紹介する。
まずはsarは初期で使えるようにはなっていない。
(ツール自体はインストールされているが、有効化する必要がある)
まずはsarを有効化する
# sudo vi /etc/default/sysstat
#
# Default settings for /etc/init.d/sysstat, /etc/cron.d/sysstat
# and /etc/cron.daily/sysstat files
#
# Should sadc collect system activity informations? Valid values
# are "true" and "false". Please do not put other values, they
# will be overwritten by debconf!
ENABLED="true"
ENABLEDをtrueにすることで、有効化することができる。
次に設定の変更を反映する。
# service sysstat restart
次に、sarが統計量をどのようなスケジュールで記録するのかを確認する。
cat /etc/cron.d/sysstat
~~~
# Activity reports every 10 minutes everyday
5-55/10 * * * * root command -v debian-sa1 > /dev/null && debian-sa1 1 1
~~~
5-55/10
という記述は、毎時5分から55分の間で10分間隔で記録を取るという意味である。
詳しい構文はドキュメントでまとまっているので、man sar
コマンドで確認してみると良いが、キリがないので今回は割愛。
それでは、sarによって記録されたデータをのぞいてみる。
まずはCPU統計量を診てみる。
-u
オプションで確認することができる。
# sar -u
Linux 6.8.0-45-generic (sysytem-performance) 09/29/24 _aarch64_ (4 CPU)
00:27:15 CPU %user %nice %system %iowait %steal %idle
00:30:08 all 0.63 0.36 0.52 0.04 0.00 98.45
00:40:10 all 0.05 0.00 0.14 0.01 0.00 99.80
00:50:11 all 0.05 0.00 0.13 0.01 0.00 99.81
01:00:11 all 0.05 0.00 0.13 0.01 0.00 99.81
01:10:11 all 0.05 0.00 0.13 0.01 0.00 99.81
01:20:11 all 0.02 0.00 0.10 0.01 0.00 99.87
01:30:11 all 0.02 0.00 0.09 0.01 0.00 99.88
01:40:11 all 0.01 0.00 0.08 0.01 0.00 99.89
01:50:11 all 0.01 0.00 0.08 0.01 0.00 99.90
02:00:11 all 0.01 0.00 0.08 0.01 0.00 99.89
02:10:11 all 0.01 0.00 0.09 0.01 0.00 99.89
02:20:11 all 0.02 0.00 0.09 0.01 0.00 99.89
02:30:08 all 0.01 0.00 0.08 0.01 0.00 99.89
02:40:07 all 0.03 0.00 0.11 0.02 0.00 99.84
Average: all 0.04 0.01 0.11 0.01 0.00 99.83
idle時間が99%あたりを維持しているので、CPUでは重い処理が実行されていないことがわかる。
そして、出力形式をセットすることで、いろいろなデータ形式でシステム統計量を出力することができる。
- json:
-j
- svg:
-g
- csv:
-d
トレーシングツール
Linuxのトレーシングツールは、今まで説明してきたイベントインターフェース(トレースポイント、kprobe, uprobe, USDT)を使って高度なパフォーマンス分析機能を提供している。
- perf
- Ftrace
- BPF
- ...
様々なトレーサーが存在するが、書籍の筆者はこれらのトレーサーを特定のシステムコンポーネントで使い分けている。
- CPU: perf
- カーネルコードの掘り下げ: Ftrace
- その他: BPF
可観測性ツールに対する観察
可観測性ツールとその基礎になる統計量は、ソフトウェアによって実装されている。そして、あらゆるソフトウェアにはバグの可能性がある。
同じことがソフトウェアを説明するドキュメントにも言える。
新しく触れる統計には健全な懐疑主義を持って臨み、その本当の意味は何か、本当に正しいのかどうかを問うようにすべきだ。
特に、これらの視点を持っておくと良い
- ツールと計測値が間違っている場合がある
- manページは必ずしも正しくない
- 得られる指標が不完全な場合がある
- 得られる指標のデザインに問題があ理、紛らわしい場合がある
- 指標収集コード(例えば、ツールの出力パーサー)にバグが含まれている場合がある
- 指標の処理(アルゴリズム/スプレッドシート)でも誤りが入る場合がある
練習問題
静的パフォーマンスツールの例をいくつか挙げなさい
- ip
- toutel
- numactl
- /proc/cpuinfo
- df
- sysctl
- /sys
プロファイリングとは何か
特定のイベントが発生した時、監視対象のコンポーネントの挙動を理解するプロセス。
一定期間内にターゲットのスナップショットを集めて(サンプリング)、ターゲットがどのような負荷を負っているのかを分析することができる。
プロファイラが100Hzではなく99Hzを使うのはなぜか
システムコンポーネントのクロック周波数と同期して、欲しいデータが取得できなくなってしまうのを防ぐため
トレーシングとは何か
イベントに関する全ての詳細情報を格納しておくこと
システムの動作や処理の流れを可視化したり、エラーの原因を追跡したりするのに使える。
静的インストルメンテーションとは何か
あらかじめカーネルのソースコードに分析や監視のためのコードを組み込む技術
動的インストルメンテーションが重要な理由を説明しなさい
静的インストルメンテーションでは検知できないイベントも検知できるようになるため
また、カーネルのオリジナルのソースコードを書き換えなくても監視ポイントを埋め込むことができるので本番環境での使用が比較的容易。
トレースポイントとkprobeの違いは何か
イベントベースか、カウンタベースで記録されているかの違い。
トレースポイントは常時システムの状態を記録して可観測性ツールのための情報を提供してくれていているが、kprobeは特定のイベントが発生した時にのみシステム情報を記録してくれる。
以下の作業で予想されるCPUのオーバーヘッドの度合いを説明しなさい
- ディスクのIOPSカウンタ
- トレーシングポイントかkprobeを使ったディスクIOごとのトレーシング
- トレーシングポイントかkprobeを使ったコンテキストスイッチごとのトレーシング
- トレーシングポイントかkprobeを使ったプロセスの起動ごとのトレーシング
- uprobeを使った libc malloc()呼び出しごとのトレーシング
- ディスクのIOPSカウンタ: カウンタによって提供される情報なので低い
- トレーシングポイントかkprobeを使ったディスクIOごとのトレーシング: イベントベースのベースの可観測性ツールなので、普通
- トレーシングポイントかkprobeを使ったコンテキストスイッチごとのトレーシング: コンテキストスイッチは頻繁に発生するイベントなので、高い
- トレーシングポイントかkprobeを使ったプロセスの起動ごとのトレーシング: プロセスの起動頻度はそんなに高くないため、普通
- uprobeを使った libc malloc()呼び出しごとのトレーシング: mallocの呼び出し頻度は高いので、オーバーヘッドは高くなる
パフォーマンス分析でPMCに価値がある理由を説明しなさい
CPU命令の処理効率、CPUキャッシュヒット率、インターコネクトの使用状況などはPMCでなければ取得できないシステム統計量だから。
可観測性ツールが与えられた時、どうすればそれを使っているインストルメンテーションソースを明らかにできるかを説明しなさい
- ドキュメントを読む
- 可観測性ツールやLinuxカーネルのソースコードを読む
アプリケーション
パフォーマンスを最も大きくチューニングできるのは、仕事を行っている場所に最も近いところ、つまりアプリケーションだ。
アプリケーションには、データベース、Webサーバー、アプリケーションサーバー、ロードバランサ、ファイルサーバーなどが含まれる。
以下の各章では、消費するリソースという観点から、アプリケーションにアプローチしていく。
アプリケーション内部の検討は、通常はアプリケーション開発者の領域であり、アプリケーション内部の状態を把握する(イントロスペクション)ためのサードパーティツールを用いることもある。
システムパフォーマンスの研究者にとって、アプリケーションの分析とはシステムリソースを最も最大限に活用するためのアプリケーションの構成、アプリケーションがシステムをどのように使っているかの特徴つけ、一般的な病理の分析などである。
目標
- パフォーマンスチューニングの目標を明らかにする
- マルチスレッドプログラミング、ハッシュテーブル、ノンブロッキングIOなどのパフォーマンスを引き上げるためのテクニックを頭にいれる
- ロックと同期プリミティブを理解する
- プログラミング言語の違いによる問題の違いを理解する
- スレッドの状態分析のメソドロ時の筋道をたどる
- CPUプロファイリングとoff-CPU分析を行う
- プロセスの実行のトレーシングなどのシステムコール分析を行う
- シンボルやスタックが失われることがあるというスタックトレースの罠を意識に刻み込む
アプリケーションの基礎知識
アプリケーションのパフォーマンスの問題に飛び込む前に、アプリケーションの役割、基本的な特徴、業界内でのエコシステムに親しんでおく必要がある。
これらを理解するとことでアプリケーションのアクティビティを理解するための枠組みが作られる。
また、よくあるパフォーマンス障害やチューニングについて学ぶ機会がつくられ、さらなる研究のための道筋が見えてくる。
まずは、次の問いに答えてみよう
- 機能: アプリケーションの役割とはなんだろうか?
- オペレーション: アプリケーションが処理する要件は何か。また、どのようなオペレーションを行うか。データベースサーバーはクエリを処理し、WebサーバーはHTTPリクエストを処理する。オペレーションは、負荷の計測やキャパシティプランニングのために速さ、頻度として計測できる。
- パフォーマンス要件: アプリケーションを実行する企業はSLO(Service Level Objectiveサービスレベル目標)を持っているか(例えば、99.9%の要求を100m秒未満のレイテンシで処理するなど)
- CPUモード: アプリケーションはユーザーレベルソフトウェアとして実装されているか、それともカーネルレベルソフトウェアとして実装されているか、ほとんどのアプリケーションはユーザーレベルソフトウェアで、一つ以上のプロセスとして実行されているが、NFS(Network File System)のようにカーネルサービスとして実装されるものもある。
- 構成、設定: アプリケーションはどのように設定、更新されているか、それはなぜか。この情報は、構成/設定ファイルを見たり、管理ツールを見たりすればわかる場合がある。バッファサイズ、キャッシュサイズ、並列処理、その他パフォーマンスに関するパラメータが変更されていないか確認しよう。
- ホスト: アプリケーションをホスティングしているものは何か、サーバーかクラウドインスタンスか。
- 指標: オペレーションの速さ、ペースなどのアプリケーションの指標が提供されているか。この情報は、バンドリングされているツールやサードパーティツール、API要求、オペレーションログの処理などによって明らかになることがある。
- ログ: アプリケーションはどのようなオペレーションログを作っているか。どのログを有効にできるか。ログからわかるレイテンシなどのパフォーマンス指標は何か。例えば、MySQLは、スロークエリログをサポートしており、特定の閾値よりも遅いクエリの詳しいパフォーマンス情報を提供してくれる。
- バージョン: アプリケーションは最新バージョンになっているか、最近のバージョンのリリースノートでパフォーマンスの向上、パフォーマンス問題などの修正が取り上げられているか。
- バグ: アプリケーションのバグデータベースがあるか。今使っているバージョンのアプリケーションが抱える「パフォーマンス」バグとは何か。今、パフォーマンス障害を抱えている場合は、それに似たものが以前に発生しているか、どのように調査されたか、他に何が関わっているかをバグデータベースでサーチしよう。
- ソースコード: アプリケーションはオープンソースか、そうであれば、プロファイラやトレーサーから明らかになったコードパスを精査すると、パフォーマンスの向上に繋げられる場合がある。パフォーマンスを向上させるためにアプリケーションコードを書き換えたり、公式バージョンに組み込んでもらえるように改良を提案したりできる。
- コミュニティ: そのアプリケーションには、パフォーマンスについてわかったことをシェアするコミュニティがあるか。コミュニティには、フォーラム、ブログ、IRC(Slack)チャンネル、ミートアップ、カンファレンスなどが含まれる。ミートアップとカンファレンスは、オンラインにスライドやビデオを投稿することが多いが、これらはその後何年も役にたつ参考資料になる。コミュニティの情報更新やニュースをシェアするコミュニティマネージャがいる場合もある。
- 書籍: アプリケーションやそのパフォーマンスについての本はあるか、それは良い本か(基準は著者がエキスパートか、実践的か、読者の時間を無駄にしないか、最新情報を反映しているかなど)。
- エキスパート: そのアプリケーションのパフォーマンスのエキスパートとして認められている人は誰か、その人の名前を覚えてけば、彼らが作った参考資料を見つけるために役立つ。
アプリケーションの内部構造を描いた機能ダイアグラムが入手できれば、とても役に立つ
パフォーマンスの目標
パフォーマンスの目標を設定すると、パフォーマンス分析の仕事の方向性が定まり、どうのような作業をするかを選択する上で役に立つ。
定性的になりやすいパフォーマンス問題を定量的に図ることも可能になってくる。
明確な目標がなければ、パフォーマンス分析は当てのない情報の探り出しに堕する危険がある。
アプリケーションのパフォーマンス目標を考える時は二つのことを考えるところから始めると良い
- アプリケーションが実行するオペレーションは何か
- パフォーマンスの目標は何か
目標は次のような項目になる。
- レイテンシ: アプリケーションの応答時間を短く、または一定にする。
- スループット: アプリケーションのオペレーションのベースやデータ転送速度を上げる。
- リソースの使用率: 与えられたワークロードに対して効率的にする。
- コスト: システム料金あたりのパフォーマンスを上げ、計算コストを下げる。
ビジネスやQoS(サービス品質)要件の指標を使って、これらの項目を例えば次のように定量化できれば良い
- アプリケーションの要求レイテンシは平均5m秒にする。
- 95%の要求は100m秒以下のレイテンシで処理されるようにする。
- 処理時間が1000m秒を超える要求はないようにする。
- 特定のサイズのサーバーでは、個々のサーバーのスループットは最大で毎秒10000要件以上にする。
- 毎秒10000要求を処理するための平均的なディスク使用量を50%未満にする。
目標を設定したら、目標達成を阻んでいる制約要因に手をつけられる。
レイテンシの場合ならディスクIOやネットワークIO、スループットの場合ならCPUの使用率かもしれない。この章と他の章の戦略を駆使すれば、このような要因を発見できるだろう。
スループットを目標にする場合は、すべての処理がパフォーマンスやコストという点で等しいわけではない。
目標がオペレーションスピードなら、そのオペレーションのタイプを指定するのも大切な場合がある。
目標は、予想または計測されたワークロードに基づく分布になる場合がある。
Apdex
一部の企業は、目標およびモニタリングする指標としてApdex(application performance index)を使っている。従来の方法よりも顧客エクスペリエンスをうまく定量化できる。
まず、顧客から見たイベントを3つの指標で分類する。
- 満足だったオペレーション
- 許容範囲に抑えられているオペレーション
- 不満なオペレーション
次に、式によってApdexを計算する。
Apdex = (満足 + (0.5 * 許容範囲) + (0 * 不満)) / 総オペレーション数
Apdexは0~1の値を取る
0は満足な顧客なし、1は顧客全体が満足した状態だ。
よく実行されるコードの最適化
稼働しているソフトウェアの内部はとても複雑だ。
ソースコードを見れば、アプリケーションは数万行、カーネルコードに至っては数十万行にも及ぶ。
アプリケーションのパフォオーマンスを効率よく向上させるには、ランダムに最適化すべき箇所を拾い上げていたのでは労多くて実りなしという状況になってしまうので、本番ワークロードで最もよく通るコードパスを見つけ出し、その部分を改善することが良い。
アプリケーションがCPUバウンドなら、頻繁にon-CPUになるコードパスを見つけ出し、
アプリケーションがIOバウンドなら、頻繁にIOにつながるコードパスを見つけ出せば良い。
そもそもCPUバウンド、IOバウンドって?
オペレーションの実行速度がCPUやディスクIOの処理能力によって制限される状態のこと
ボトルネックになってしまっている状態とも言える
そういった部分は、アプリケーションの分析とプロファイリングによって見つけることができる。
可観測性
パフォーマンスを最も大きく引き上げられるのは、不要な仕事を取り除いた時だ。
どのようなアプリケーションを作るかを選択する事例について考えてみよう。
アプリケーションAはBよりも10%高速なら、Aを選びたくなるだろう。
しかし、Aは不透明なのに対し、Bは可観測性ツールを豊富に取り揃えているので、長期的に見ればBを選んだ方が良い。
可観測性ツールを使えば、不要な仕事を見つけて取り除ける。
そして、ボトルネックや問題となっている今pーねんとをよく理解してチューニングできる。
充実した可観測性ツールから得られたパフォーマンス向上の効果から見れば、最初の10%のパフォーマンスの差は微々たるものに過ぎない場合がある。
同じことが言語やランタイムの選択にも当てはまり、新しい言語に比べ、JavaやCは成熟していて可観測性ツールも充実している。
アプリケーションのパフォーマンス向上のためのテクニック
IOサイズの選択
IOの実行に伴うコストには、以下のようなものがある。
- バッファの初期化
- システムコールの実行
- コンテキストスイッチ
- カーネルメタデータのアロケート
- プロセスの特権と制限のチェック
- アドレスからデバイスへのマッピング
- IOを実行するためのカーネル
- ドライバコードの実行
- メタデータとバッファの解放が含まれる
- などなど。。。
IOを実行するためにはらこれらの「初期化税」は、IOサイズが大きくても小さくても同じようにかかる。
効率を上げため、一回のIOで転送するデータが多ければ多いほど良い。
IOを一回行うごとにかかるコストについて考えれば、一回のiOで128KBのデータを送り込む方が、1KBのデータを128回転送するよりもずっと効率的だ。
特に回転ディスクでは、IO対象の場所を探す(シーク)時間がかかるので、一回のIOにかかるコストが高い。
ただ、アプリケーションが必要とする以上のIOサイズを選択してしまうと、無駄なデータ転送が発生し、IOサイズを指定しない場合よりもかえって遅くなってしまう。
IOサイズとしてアプリケーションが要求してくるサイズに近い最も小さな値を選べば、IOレイテンシは下がる。
キャッシング
コストの高いオペレーションをいつも必ず実行するのではなく、よく実行されるオペレーションの結果をローカルキャッシュに格納して後で使えるようにする。
キャッシュは読み出しのパフォーマンスを向上させるが、書き込みパフォーマンスの向上には、キャッシュストレージをバッファとして使うことが多い。
バッファリング
バッファに一時的なデータを保存し、一定のレベルまでIOサイズが大きくなってからデータ転送をすると、IOサイズを大きくすることができるので、スループットが向上することがある。
しかし、最初のバッファへの書き込みから一定のサイズのデータがバッファに書き込まれないとデータが転送されないという問題があるので、転送されるデータの量によってバッファサイズを調整する必要がある。
リングバッファとは?
リングバッファとは、循環型のデータ型である。
バッファの形状が輪っかになっていて、終端と先端が結合されたデータ構造をしている。(もちろん物理的にではなく、論理的に)
最も古いデータを最新のデータで上書きし、常に一定の過去データを保持する。
一定数のデータしかバッファリングしないので、オーバーフローの心配がないのが通常の配列型バッファと比べたメリット。
この手のバッファリングは動画や音楽再生サービスでよく使われている。
リングバッファ上に音楽の再生位置とデータの転送位置があり、再生位置が通り過ぎると、その場所に新しい音楽データが転送される。
そうすることで一時的に音楽データを保存でき、スムーズな音楽再生ができる。
ポーリング
監視対象のシステムやデバイスを定期的に監視する手法。
監視システムが一定間隔で監視対象に対して状態や性能情報を要求すると、対象のシステムが現在実行しているタスクを一時停止して情報を返送する。
ポーリング間隔の調整により、監視の精度と負荷のバランスを取ることができる。
ポーリング感覚が頻繁すぎるとシステムへの割り込みが頻繁に発生し、オーバーヘッドが大きくなってしまうので注意だ。
並行実行と並列処理
マルチプロセスやマルチスレッドのアーキテクチャを使うということは、カーネルが実行するプロセス、スレッドを決められるということ。
ということは、コンテキストスイッチのオーバーヘッドがかかる。
しかし、ユーザーモードアプリケーション側で独自のスケジューリングメカニズムを実装し、同じOSスレッドで異なる要求を実行できるようにする方法もある。
つまり、アプリケーション自体が処理の切り替えを管理してくれるので、OSから見ると一つのスレッドで動作しているが、アプリケーション内部では複数の処理を切り替えて実行している。
これによってカーネルによるコンテキストスイッチを回避してオーバーヘッドを低減できるようになる。
そのようなメカニズムは次のようなものがある。
ファイバー
軽量スレッドとも呼ばれ、ユーザーモードバージョンのスレッドである。
一つのユーザーレベルスレッドの中に個々のファイバーがスケジューリング可能なプログラムを表現する。
これらのファイバーはOSのスケジューラによって切り替えられることはなく、プログラマが明示的に制御を移す必要がある。
これはOSレベルのスレッドで同じことをする時よりもオーバーヘッドが軽くなる。
コルーチン
ユーザーモードアプリケーションがスケジューリングできるサブルーチンで、ファイバーよりも軽量であり、並行実行のためのメカニズムを提供する。
イベントベースの並行実行
プログラムが一連のイベントハンドラに分割されており、実行できるイベントハンドラはキューに基づいて決定される。
これらのどのメカニズムを使っても、IOに関してはカーネルが処理しなければならない。
IOの待機中に他の実行可能なプログラムを実行するので一般にOSスレッドのスレッドスイッチは不可避である。
また、並列処理のために複数のOSスレッドを使わなければならないので、それらを複数のCPUにまたがってスケジューリングできなければならない。
一部のランタイムは、軽量の並行実行のためにコルーチン、並列処理のために複数のOSスレッドを使っている。
Golangランタイムがそうで、OSスレッドのプールをを準備した上でgoroutineを使っている。
Golangのスケジューラはパフォーマンス向上のためにgoroutineがブロックを起こす呼び出しをするとブロックを起こしたスレッドの他のgoroutineが自動的にスレッドに移され実行される。
マルチスレッドプログラミングには広く使われているモデルが3つある。
- サービススレッドプール: あらかじめ一定数のスレッドを作成し、プールに保持しておく。クライアントリクエストが到着すると、プールから利用可能なスレッドを割り当てて処理する。リクエスト処理が完了すると、スレッドはプールに戻り、次のリクエストを待機する。
- CPUスレッドプール: CPUごとに一つのスレッドが作られる。
- 段階的イベント駆動型アーキテクチャ: アプリケーションの要求がステージに分解され、そのステージが一個以上のスレッドのプールによって処理される。
プールってなんなん?
近い将来使用する可能性が高いリソースを事前に用意し、一時的に保管しておくこと。
スレッドプール: 事前に作成したスレッドを再利用し、タスクを効率的に処理する。
コネクションプール: データベース接続などを事前に確率し、再利用する。
メモリプール: 頻繁に使用されるメモリブロックを事前に確保し、再利用する。
マルチスレッドプログラムはプロセスと同じアドレス空間を共有するので、コストの高いインターフェースを介さずに複数のスレッドが直接同じメモリを読み書きできる。しかし、複数のスレッドが同時に同じデータに向かって読み書きすると、データが壊れる可能性がある。そうならないように動機プリミティブが用意されている。
同期プリミティブ
同期プリミティブは複数のスレッドが同じデータにアクセスできないよう、メモリを読み書きできるスレッドにロックというものを持たせ、ロックを持っていないスレッドを停止したり、読み込み専用にしたりする技術だ。
トラフィックの流れを停止するので、待ち時間が発生するという特徴がある。
同期プリミティブには方式があり、広く使われているものが以下の4つ。
- ミューテックスロック: ロックを持つスレッドだけがCPUを使える。それ以外のスレッドはブロックされ、off-CPUで待ち続ける
- スピンロック: スピンロックを持っているスレッドは処理を実行できる。その他のスレッドはon-CPUだが、ロックが開放されていないかを繰り返し確認するようなループに入っている。ブロックされたスレッドがCPUを手放さず、ロック獲得後に数サイクルで処理を再開できるので、レイテンシの低いアクセスを提供できる。しかし、スレッドがスピンして待っている間にCPUリソースを無駄に使ってしまう。
- RWロック: 複数のリーダーのみを認めてライターを認めない or 一つのライターのみを認めてリーダーを一切認めないかのどちらかになるようにしてデータの完全性を保証する。
- セマフォ: 指定した数までのスレッドの並行処理を認めるか、一つのスレッドの実行だけを認めるかを選べる。
ノンブロッキングIO
Unixプロセスのライフサイクルでは、IOの実行中はプロセスはブロックされてスリープ状態になる。
しかし、この方式ではパフォーマンス上の問題が2つある。
- 個々のIOオペレーションは、スレッドをブロックして止めてしまう。そこで多数の並行実行されるIOをサポートするつもりなら、アプリケーションは多数のスレッドを作らなければならないが、そうするとスレッドの作成、破棄とスレッドの維持のために必要なスタックスペースのためにコストがかかる。
- 短時間で終了するIOを頻繁に行うと、頻繁にコンテキストスイッチを行うことによるオーバーヘッドのためにCPUリソースが多く使用され、アプリケーションのレイテンシが高くなってしまう。
ノンブロッキングモデルでは、現在実行されているスレッドをブロックせずにIOを非同期で実行するので、IO中でも他のオペレーションを実行することができる。
これはNode.jsの特徴でもある。
ノンブロッキングIO(非同期IO)を実現するメカニズムは、次のように複数ある。
- open(2): ファイルを開く際に実行されるシステムコール。O_NONBLOCKフラグを指定することでファイルディスクリプタがノンブロッキングモードで開かれ、後続の読み書き操作によるIOを待たずして次の処理を実行していく。
- io_submit(2)
- sendfile(2): あるファイルディスクリプタのデータを別のファイルディスクリプタにコピーする。
- io_uring_enter(2)
プログラミング言語
プログラミング言語の実行方式は大きく
- コンパイル言語
- インタプリタ言語
の二つに分類される。
それぞれメリット、デメリットが存在する。
コンパイル言語
実行する前にソースコードを機会が実行できるバイナリコードに変換(コンパイル)してコードを実行する形式の言語だ。
バイナリコードは実際にはファイル形式になっている。
コンパイル言語は、実行前に変換作業などをする必要がないので、一般的にパフォーマンスが高い。
コンパイル言語はコンパイル後のバイナリファイルが元となっているソースコードと対応付けされているので、実行時にどこでどんな処理をしているのかなどがトレースしやすい。よって、パフォーマンス分析がしやすい言語と言える。
コンパイラは、最適化という操作によってパフォーマンスを改善できる。
それを可能にしてくれる最適化器なのだが、実行が早くなる代わりに処理を追うことが難しくなり、パフォーマンス分析がしづらくなるという特徴がある。
なぜなら、「何の関数が実行しているのか」を表すフレームポインタをメモリから省略して実行しているので、今実行している処理がソースコードのどこなのかを知る方法がない。
インタープリタ言語
インタプリタ言語は実行時にプログラムを実際の動作に変換しながらプログラムを実行する。
この実行方式では、ソースコードを実行するためにCPUが実行可能な形式に変換する作業が必要になるので
オーバーヘッドがかかる。
(ちなみに、シェルスクリプトはインタプリタ言語の例だ)
そして、インタプリタ言語は実行時にプログラムの関数名が取得できず、パフォーマンス分析がしづらいという特徴がある。
そもそも、パフォーマンスを意識したソフトウェアを作る際、オーバーヘッドはかかるわ、分析はしづらいはでインタプリタ言語は論外である。
仮想マシン
仮想マシンは、ソースコードを仮想マシン上で実行できる形式にコンパイルする方式だ。
仮想マシンはプラットフォームに依存しないプログラミング環境を提供してくれるので、移植性が高い。
ただ、仮想マシンはかながという点では最も難しいタイプの言語でもある。
プログラムがCPUで実行されるまでにコンパイル、解釈のステージを複数潜り抜けるので、元のプログラムの情報がすぐに手に入らない。
ガベージコレクション
ガベージコレクションとは、プログラムが使用しなくなったメモリを自動的に検出し開放する機能だ。
この機能によってプログラミングを簡素化してくれるメリットがあるが、以下のようなデメリットもある。
- メモリ消費量の増加: アプリケーション側でメモリの利用方法を指定できないので、オブジェクトが自動的に開放可能だと判断されない場合にはメモリの消費量が増える可能性がある。
- CPUコスト: メモリが開放可能か断続的に判断するため、そのために使用するCPU使用率によって、アプリケーションが利用できるCPUリソース量が減る。
- レイテンシ外れ値: アプリケーションの実行中、ガベージコレクションのために処理を一時停止するケースがあり、その際にレイテンシが非常に高くなるケースがある。
メソドロジ
ロック分析
マルチスレッドアプリケーションでは、ロックがボトルネックになって並列処理やスケーラビリティに障害が発生する場合がある。
ロックの分析には2種類がある。
- 競合のチェック
- 長すぎるロック保持のチェック
ロックの名前とロックを保持するに至ったコードパスをはっきりさせることが重要。
ロックの分析はCPUプロファイリングだけで問題が解決できることもある。
スピンロックの場合、競合はCPU使用率に現れ、スタックトレースからのCPUプロファイリングによって簡単に見つかる。
静的パフォーマンスチューニング
静的パフォーマンスチューニングは、構成された環境の問題点を明らかにしてくれる。アプリケーションの場合、静的構成における次の項目をチェックする。
- どのバージョンのアプリケーションを使っているのか
- 既知のパフォーマンス問題は何か
- 構成がデフォルトとは違ったり、チューニングが異なる場合、理由は何か
- アプリケーションはオブジェクトのキャッシュを使うか、サイズはどれくらいか
- アプリケーションは並行実行されているか、並行実行はどのように構成されているか(例えばスレッドプールのサイズとか)
- アプリケーションは特別なモードで実行されているか(デバッグモードが有効になっていないか、リリースビルドではなくデバッグビルドになっていないか)
- アプリケーションは何のライブラリを使っているか、ライブラリのバージョンはどうなっているか
- アプリケーションはどのメモリアロケータを使っているか
- アプリケーションはヒープのために大きなページを使うように構成されているか
- アプリケーションはコンパイルされているか、コンパイラのバージョンはいくつか。コンパイラオプションの最適化はどうなっているか、64ビットか
CPU
CPUはコンピュータが動作するために必要な計算資源の中核をなすコンポーネントだ。
CPUは様々なレベルでの視点をもつことができる。
- 高レベル: システム全体でのCPUの使用状況をモニタリングしたり、プロセスやスレッドごとの使用状況を解析したりすることができる。
- 低レベル: アプリケーションとカーネルのコードパスをプロファイリング、解析したり、割り込みのCPU使用状況を解析したりすることができる。
- 最低レベル: CPUの命令実行やサイクルの振る舞いを解析でき、タスクがCPUを使える順番を待つためのスケジューラレイテンシなどの振る舞いも調べられる。
この章の目標
- CPUのモデルとコンセプトを理解する
- CPUハードウェアの内部構造を頭にいれる
- CPUスケジューラの内部構造を頭にいれる
- CPU分析の様々なメソドロジの筋道をたどる
- ロードアベレージとPSIを解釈できるようになる
- システム全体とCPUの使用の特徴を明らかにする
- スケジューラレイテンシの問題を見つけて定量化する
- CPUサイクル分析で効率の悪い部分を明らかにする
- プロファイラとCPUフレームグラフでCPUの使用状況を明らかにする
- CPUを消費するソフトIRQとハードIRQを明らかにする
- 割り込みCPUフレームグラフやその他のCPU指標のビジュアライゼーションを解釈する
- CPUの様々なパラメータを意識に刻み込む
用語
この章で使う用語まとめ
- プロセッサ: CPUでも、特に物理的なボードに特定して表現したいときに使う。
- コア: CPUの中でも、特に演算をしてくれる回路を特定して表現したい時に使う。一つのCPUに複数のコアが搭載されたものもあり、それをマルチコアプロセッサと呼ぶ。
- ハードウェアスレッド: 一つのコアの上で複数のスレッドを擬似的に並列実行してくれるCPUアーキテクチャ。各スレッドは独立したCPUインスタンスである。このスケーリングアプローチを同時マルチスレッディングと呼ぶ。
- CPU命令: CPUの命令セットに含まれる1個のCPUオペレーション。演算やメモリIO, 制御ロジックのための命令がある。
- 論理CPU: 仮想プロセッサとも呼ばれる。OSのCPUインスタンス。プロセッサがハードウェアスレッド、コア、シングルコアプロセッサのどれかで論理CPUを実装する。
- スケジューラ: 実行のためにスレッドにCPUの計算資源を与えるカーネルサブシステム(これはOSの機能)
- ランキュー: CPUが与えられるのを待っている実行可能スレッドのキュー。最近のカーネルは、実行可能スレッドを格納するために他のデータ構造を使っているが、それでもランキュートと言う言葉が使われることが多い。
モデル
CPUの構成
CPUの物理的な構成は、以下の通り
- プロセッサがある
- プロセッサの中にコアがある(マルチコアプロセッサの場合は複数のコアがある)
- コアの中にハードウェアスレッドがある
このCPUをOSからの視点について考えてみる。
1コアの中に2つのハードウェアスレッドを持つコアが4つ搭載されているマルチコアプロセッサCPUの場合、ハードウェアスレッドは一つの論理CPUとして扱うことができるので、OSからしたら2*4=8個のCPUが搭載されているように見ることができる。
CPUのランキュー
OSがCPUに処理を要求する際、二つの状態が存在する。
- On-CPU: CPUによって実行中のスレッド
- 実行待ち: カーネルのスケジューラによってランキュー上にキューイングされているスレッド
実行可能状態でランキューにキューイングされているソフトウェアスレッド数は、CPUの飽和度を示す重要なパフォーマンス指標。
CPUランキューでの待ち時間は、ランキューレイテンシやディスパッチャキューレイテンシ、スケジューラレイテンシと呼ばれることがある。
マルチプロセッサシステムでは、カーネルは一般に個々のCPUにランキューを提供している。(全てのCPUに負荷分散する形ではない)なので、スレッドを同じランキューにキューイングするように努力する。
と言うことは、スレッドはCPUキャッシュにデータがキャッシングされているCPUで実行され続ける可能性が高い。
このようなキャッシュは温まっていると表現され、同じスレッドで実行しようとすることをCPUアフィニティと呼ぶ。
コンセプト
クロックスピード
クロックとは、CPUを動かすためのデジタル信号である。
個々のCPU命令は、実行するためにクロックサイクルを必要とする。
そしてCPUは特定のクロックスピードで実行される。
例えば4GHzのCPUは1秒間に40億回のクロックが起きる。
クロック周波数が上がれば、消費電力と温度が上がる代わりに高速になり、下がれば低消費電力になる。
このクロック周波数はOSからの要求によって上下することもできれば、プロセッサ自身の判断で動的に帰ることもできる。
例えば、カーネルのアイドルスレッドは節電のためにクロック周波数を下げるようにCPUに要求できる。
CPUのクロック周波数が大きければ大きいほどパフォーマンスに優れていると思われがちだが、そうではない。
重要なのは高いクロック周波数で何が実行されるかだ。
メモリアクセス待ちで待機状態になっていれば、クロック周波数が高くても何も計算は進まないので電力を無駄に使っていることになる。
命令
CPUは、命令セットと呼ばれる、どんな命令が来たら、どんな01の命令をCPUにするのかという命令コードから選ばれた命令を実行する。
命令の実行には以下のようなものがある。
- 命令のフェッチ
- 命令のデコード
- 命令の実行
- メモリアクセス
- レジスタへの書き戻し
これらはそれぞれ機能ユニットと呼ばれる部品によって処理される。
多くの命令はCPU上のレジスタでオペレーションを行い、メモリアクセスを必要としない。
各ステップは少なくとも1クロックサイクルを必要とし、ものによっては複数クロックサイクルを必要とするものもある。特にメモリアクセスは遅く、メインメモリの読み書きには数十サイクルもかかる。
そしてその間、命令の実行はストール(待機)している。(そしてストール中のCPUサイクルをストールサイクルと呼ぶ)。
なので、メモリアクセスを減らすためにCPUキャッシングが重要になってくる。
命令パイプライン
命令パイプラインとは、CPUが命令を並列に実行するCPUアーキテクチャのこと。
例えば、先ほどの5つの命令を実行する際、すべての命令は独立した機能ユニットで実行される。
そして、すべての命令を実行完了するまでに最低でも5サイクルかかる。
1つの機能ユニットが5つのステップをすべて実行しているのだ。そして残り4つの機能ユニットはアイドル状態。
そこですべての命令を別々の機能ユニットで並列実行してしまえば、スループットは大幅に向上するのではないか?
命令パイプラインを使えば、パイプライン内ですべての命令を並列実行できる。
複数の機能ユニットに同時にアクティブにすることができ、パイプライン内で別々の命令を処理できる。
分岐予測
最近のプロセッサは前の方の命令がストールしている間に後の命令を終わらせることができる。しかし、命令に条件分岐があった際は難しい。後の命令が変わってくるからだ。
そこで、プロセッサは分岐予測という機能を実装していることが多い。
テストの結果を当て推量し、その結果に基づいて実行されるはずの命令の実行を始めてしまうという機能だ。
しかし、分岐予測が外れてしまった場合は後の命令をやり直さなくてはならなくなるため、パフォーマンスが下がる。
分岐予測の精度を上げるために、プログラマはコードにヒントを入れるようにしている。
例えば、Linuxカーネルであれば、likely、unlikelyなどだ。
命令幅
命令幅とは、CPUが1サイクルあたりに実行できる命令の数だ。
CPUは同じタイプの複数の機能ユニットを組み込めば、クロックサイクル毎にもっと多くの命令を先に進める。
このようなCPUアーキテクチャをスーパースカラーと呼ぶ。
最近のプロセッサは幅3〜幅4であることが多い。
IPC, PCI
IPC(instructions per cycle)は、1サイクルあたりに実行できる命令数。(≒スループット)
高いほど性能が良いとされる。
CPUがどこでクロックサイクルを使っているのかを説明するに使われる
CPI(cycle per instructions)は、1命令の完了に必要なクロックサイクルの数だ。(≒レイテンシ)
低いほど性能が良いとされる。
どちらもCPUの処理速度の指標としてよく使われる。
IPCが低いということは、CPUがストール待ちになっている可能性がある。
一般的にはメモリアクセス待ちが原因。
メモリアクセス待ちが原因になっているなら、より高速なメモリを搭載するようにしたり、メモリの局所性(同じアドレスに複数回アクセスするようにする特性)を改善したり、メモリIOの量を減らしたりするとパフォーマンスが改善する。
IPCが低いCPUを発見した際、CPUのクロックスピードを上げてもパフォーマンスが上がらないケースがある。クロックスピードを上げても、ストールサイクルが増えるだけでパフォーマンスは上がらない。ボトルネックになっているのはCPUではなくメモリIO速度なのだ。
IPCを最低どれくらいの値を上回っていて欲しいかは、パフォーマンス目標であるSLIやSLOに定めると良いなと思った。
使用率
一定期間中にCPUがビジー状態になっていた割合を使用率と呼ぶ。
ビジー状態とは、ユーザーレベルのアプリケーションスレッドやその他のカーネルスレッドがアイドルスレッド(CPUに実行可能な命令がない時のスレッド)に割り込み処理をしている時間。
CPUの使用率が高いのは必ずしも問題ではなく、システムがしっかりと仕事している証拠かもしれない。
リソースを使い切っている状態であれば、投資利益率が高いと考える人もいる。
逆にCPU使用率が高くないシステムというのは、無駄にリソースにお金を払っているという見方もできる。
ディスクやメモリなどの他のコンポーネントとは異なり、カーネルが優先度やプリエンプション、タイムシェアリングをサポートしているので、パフォーマンスは必ずしも下がるとは言い切れない。
CPU使用率の測定値はストールサイクルも含むため、IPCのようにCPUがメモリIO待ちで使用率が上がってしまうことがある。
使用率が高いからといって、ボトルネックがCPUにあるとは限らないのだ。
ユーザー時間/カーネル時間
CPUがユーザーモードで実行されている時間をユーザー時間。
カーネルモードで実行されている時間をカーネル時間。
カーネル時間にはシステムコール中、カーネルスレッド実行、割り込み処理の時間が含まれる。
CPUバウンドのアプリケーションは、ほとんどの時間をユーザーレベルスレッドの時間に費やし、ユーザー:カーネルの実行時間比は99:1になる。
IOバウンドのアプリケーションは、IOを実行するシステムコールの割合が高くなる。
例えば、ネットワークIOを実行するWebサーバーは、ユーザーカーネル比は70:30程度になる。
飽和
使用率100%のCPUは飽和しており、スレッドがon-CPUになるのを待つスケジューラレイテンシが発生する。
そして全体のパフォーマンスは落ちる。
すケジューラレイテンシとは、CPUランキュー、その他スレッド管理のために使われる構造の中でスレッドが待機のために費やす時間のこと。
ただ、OSのプリエンプションによって優先度の高い要求は割り込みされるので、CPUの使用率が高くてもあまり問題視する必要はない。
優先度の逆転
優先度の低いスレッドが優先度の高いスレッドをブロックすると、優先度の逆転が起きる。
こうなると、優先度の高い要求のパフォーマンスが下がる。
この問題は優先度の継承で解決できる。
- スレッドAはモニタリング目的のもので、優先度は低い。メモリの使用率を確認するために本番DBのメモリアドレス空間のロックを獲得する。
- ここでログの圧縮を行うルーチンタスク、スレッドBが実行を開始する。(優先度はスレッドAより高い)
- 両方のスレッドを実行するにはCPUの能力が足りないので、スレッドBがスレッドAにプリエンプションをかけて実行を開始する。
- スレッドCは本番DB用のスレッドで、高い優先度を持つが、IO待ちでスリープしている。待っているIOが完了し、スレッドCは実行可能状態になる。
- スレッドCがスレッドBにプリエンプションをかけて実行するが、スレッドAが保持しているアドレス空間ロックによってCPUを手放す。
- スケジューラは、次に優先度の高いスレッドBを実行する。
- スレッドBが実行されている間、優先度の高いスレッドCは、実質的に優先度の低いスレッドBにブロックされている。優先度の逆転だ。
- 優先度の継承により、スレッドAにはスレッドCと同じ優先度が継承され、スレッドAはスレッドBにプリエンプションをかけてロックを開放するまで実行する。すると、スレッドCが実行できるようになる。
IntelのCPUにはパフォーマンスを司る二つのステートが定義されており、PとCステートが存在する。
- Pステート(processor performance state): CPUのクロックスピードを温度によって動的に変化させている。(熱ければクロック周波数下げて、問題なければ最大クロック周波数になる)
- Cステート: 消費電力を制御するためのステート。どうやらCPUが消費電力を抑えている状態だと、「CPUが眠っている状態」と言われるらしい。
キャッシュのエントリはキャッシュに格納されているデータの単位。
CPUのプロファイリングには2種類あって
- タイマーベースのプロファイリング
- 関数のトレーシング
がある。
ほとんどの環境で使われているのはタイマーベースの方。
一定時間CPUで実行された命令をサンプリングする。サンプリング頻度をCPUのクロック周波数とずらしてサンプリングの偏りをなくしていく。
CPUのプロファイリングについて、具体的な方法はあるのかな?
CPU全体の命令をプロファイリングするんじゃなくて、ツールとか使って特定のプロセスの命令だけをプロファイリングする方法とかもあるっぽい。
以下の記事ではベンチマークを合わせて使うことで、ワークロード発生時のリソース使用率を調べることについても書いてあったりした
やはりCPUのプロファイリングでもフレームグラフ出力は重要そうだな
理由は数百万行とあるようなソースコードのプロファイリング結果のスタックトレースを1行1行読んでいくのは非現実的なわけで、要約した結果が欲しい時にフレームグラフが役に立つのか。
フレームグラフの読み方
フレームグラフからパフォーマンスを上げられる部分を見つけるための方法
- 上から下に見て、高原を探す。高原は多くのサンプルで一つの関数が使われている箇所。そこを高速化することでコスパよくパフォーマンス上げられる
- 下から上に見て、コードの階層構造を理解する。下で実行されている命令で高速化できるところも見ていく。例えばシステムコールが発生している命令を組み込みコマンドを使ってシステムコールが起きないようにするとか。
- より丁寧に下から上に見る、同じような形状の山が複数存在するときは原因が共通である可能性がある。
そもそもフレームグラフの意味が理解できない人は、こちらのページでニュアンスをつかむとわかりやすいかも