読者です 読者をやめる 読者になる 読者になる

tom__bo’s Blog

情報系学生がプログラミングしたり、ボルダリングしたりボルダリングしたことを書くブログ。もはやダイアリー

Systems Performance 読んでいく (5章 その1)

Book Linux

Systems Performance: Enterprise and Cloudを読んでいく。

前回の続き。
5章前半部分。

5. Applications

パフォーマンスは作業を実行するところ、つまりアプリケーションをチューニングすることが最良である。
これにはデータベース、ウェブサーバ、アプリケーションサーバ、ロードバランサ、ファイルサーバなどが含まれる。
この章以降ではアプリケーションが消費するリソース、つまりCPU、 メモリ、ファイルシステム、ディスク、ネットワークといった観点からアプリケーションを見ていく。
その前に、この章ではアプリケーションレベルについて述べる。

アプリケーション、特に複数のコンポーネントを含む分散環境にあるアプリケーションは非常に複雑になり得る。
これらは通常、サードパーティのツールも含めてアプリケーションデベロッパの管轄である。
システムアドミニストレータを含むこれらの人が、コンフィギュレーション(以降、構成)を含むシステムパフォーマンスについて学ぶことは、システムリソースをより効率よく使うことにとても有効である。

5.1 Application Basics

アプリケーションのパフォーマンスに飛び込む前にアプリケーションの役割に習熟する必要がある。
これは基本的な特徴付けで、業界のエコシステムでもある。
これはあなたがアプリケーションの活動を理解するコンテキストを形作ってくれる。
更に、一般的なパフォーマンス問題とその解決策を学ぶ機会となり、さらなる学習のための手法となるだろう。
こういったコンテキストを学ぶために以下の質問に答えてみよう

  • Function(機能)
    • アプリケーションの役割は何か
    • DB?Webサーバ?ロードバランサ?
  • Operation(動作)
    • アプいケーションはどんなリクエストを捌くか。
    • DBはクエリを、WebサーバはHTTPリクエストをさばく
  • CPU model
    • アプリケーションはカーネルレベルで動作するのかゆーざれべるなのか。
    • 大抵はユーザレベルだが、NFSのようにカーネルレベルのものもある。
  • Configuration(構成)
    • どのように構成されているのか。なぜか。
    • これらはconfファイルか管理ツールで見つかるだろう
    • パフォーマンスに関連する設定が変更されていたら注意する
  • Metrics
    • どういったメトリクスが提供されているか
  • Logs
    • アプリケーションはどういったログを出しているか
    • パフォーマンスに関連するログをだせるか
  • Version
    • 最新のバージョンを使っているか
  • Bugs
    • バグはあるか、パフォーマンスに関連するものはあるか
  • Community
    • パフォーマンスに関することを共有しているコミュニティはあるか
  • Books
    • パフォーマンスに関する本はあるか
  • Experts
    • そのアプリケーションに関するスペシャリストはいるか

5.1.1 Objectives

パフォーマンス改善におけるゴールはどういった対処をするかの指針になってくれる。
明確なゴールを決めない作業は"fishing expedition"になってしまう。

アプリケーションパフォーマンスに関するゴールであれば以下の様なものになるだろう - Latency : アプリケーションのレスポンスタイム - Throughput : アプリケーションの稼働率やデータの転送率 - Resource utilization : 負荷に対する仕事率

これらを定量化出来ると、なお良いだろう。例えば - リクエストに対する平均レイテンシを5msにする - 95%のリクエストのレイテンシを100ms以下にする - 応答に1000ms以上かかるリクエストをなくす - 1秒辺り10000リクエストのアプリケーションで、ディスク利用率を50%以下にする

一度ゴールを決めてしまえば、後はそれに対して作業をし始められる。
問題の特定と解決はこれ以降の章の内容が手助けしてくれるだろう。

スループットに対する目標はパフォーマンスやコストの観点による作業と同じではない。
もし目標が稼働率ベースならそのアプリケーションがどういったタイプか見極める必要がある。

5.1.2 Optimize the Common Case

ソフトウェアの内部は非常に複雑になっており、様々なコードパスが存在する。
これらをランダムに探検するのは膨大な労力が必要で、かつ得られるものはそれほど無いだろう。

改善への有効な方法は、もっともよく使われているコードパスから手を付けることだろう。
CPUバウンドなアプリケーションであればCPUを最も使っている部分を、IOバウンドであればIOをよく発生させている部分から手を付けよう。
これらは以降の章でカバーされているプロファイリングやスタックトレースによって理解することが出来る。

5.1.3 Observability

この本で何度も説明しているように、OSにおいてパフォーマンスが勝つのは常に不要な処理を削除した時である。
これはアプリケーションでも同様である。
この事実はパフォーマンスの観点からシステムを選択するときにも見られるが、処理が多少遅かったとしても、きちんとしたロギングやパフォーマンス監視の機構のあるシステムを選ぶことが長期的には優っていることもある。

5.1.4 Big O Notation

Big O記法は一般的なコンピュータサイエンスの科目で習うものであり、アルゴリズムの複雑度を測り、データセットのスケールに対してモデリングするものである。
これらを意識することはシステムを開発する上で効果的である。

(表5.1 Big O記法とサンプルのアルゴリズム
(Big O記法とオーダーによる実行時間の比較図)

5.2 Application Performance Techniques

この章ではアプリケーションのパフォーマンス改善において一般的なテクニックを紹介する。

5.2.1 Selecting an I/O Size

I/Oに関連するパフォーマンスコストは以下の様なものが含まれる。
バッファの初期化、システムコールの発行、コンテキストスイッチカーネルメタデータの配置、プロセスの権限の確認と制限、デバイスとアドレスのマッピングカーネルとデバイスコードの実行、そして最後にメタデータやバッファの解放。
I/Oが大きかろうと少なかろうと"初期化"という税金を毎回払っている。

I/Oのサイズを大きくすることはアプリケーションのスループットを改善する一般的な戦略である。
大抵の場合、128KBの一回のI/Oを発行するほうが、1KBの128回のI/Oを行うより効率が良い。
一方で、巨大なI/Oサイズが裏目に出ることもある。
データベースが8KBのランダムアクセスを行う時、128KBのI/Oでは120KBが無駄な転送になってしまう。
また、不必要なほど巨大なI/Oはキャッシュサイズも無駄に埋めてしまうことになる。

5.2.2 Caching

OSはファイルへのreadやメモリ配置のパフォーマンスを向上するためにキャッシュを利用していて、アプリケーションもしばしば同じ理由でキャッシュを利用する。
重い処理を何度も実行する代わりに、一度実行したものを将来また使うことを見越してローカルにキャッシュしておくのである。
一般的なタスクは、どのデータをキャッシュするか決定し、(またはキャッシュを有効化し)そのサイズを適切に設定することである。

最も肝心なことは統一性を保つことと、古くなったキャッシュデータを返さないようにすることである。
readの性能はキャッシュによって向上し、writeの性能はこの後のバッファリングによって向上する。

5.2.3 Buffering

writeのパフォーマンスを向上するために、データを次のレベルのストレージに送る前にバッファリングすることがある。
これはI/Oサイズとその処理の効率を向上する。
writeの発生の仕方にもよるが、バッファリングをすることによってレイテンシが増えることもある。

5.2.4 Polling

ポーリングはシステムが何かのイベントが起こるのをループでチェックして確認する方法である。
これには基本的に以下の問題がある

  • 繰り返しのチェックによるCPUのオーバーヘッドが大きい
  • エベントが起きた時から次にポーリングによるチェックが行われるまでのレイテンシが大きい

ポーリングがパフォーマンス上の問題になっているアプリケーションでは、イベントをlistenしてイベントの発火をすぐに適用するように変更する必要がある。

[poll() System Call]

ファイルディスクリプタの変化を監視するpoll()システムコールがある。
これはポーリングと似たような機能を提供するが、実際はイベントベースになっているので、ポーリングと同じ問題を心配する必要なない。
しかし、poll()はファイルディスクリプタに関連するアプリケーションをarrayからO(n)で探索するので、O(1)で探索するepoll()を使うほうが望ましい。

5.2.5 Concurrency and Parallelism

Unixを含むタイムシェアリングシステムでは、プログラムの並行性を提供している。
並行性とは複数の実行可能なプログラムを読み込み実行を開始できることである。
これらの実行される時間は重複する一方で、必ずしもCPU上で同時に実行されていなければならないということではない。
マルチスレッドやマルチプロセスによって異なるアプリケーション同士の並行性や同じアプリケーション内の異なる関数での並行性がある。
また、他のアプローチとして、イベントベースの並行性もある。
これによって、イベント駆動で実行する関数を切り替えることができる。
例えば、Node.jsがこの方法をとっている。
イベントベースによって並行性を提供できるが、これは徐々にボトルネックになるであろうシングルスレッドやシングルプロセスによって実現されている。

マルチプロセッサの利点を活かすために、アプリケーションは同時に複数のCPUで実行するべきである。
これが、アプリケーションがマルチプロセスやマルチスレッドになることによって達成される、並列性である。
この理由は6章CPUでされる。

CPUのスループットの増加の他に、マルチスレッド・マルチプロセスはI/Oの並行実行を可能にする。

マルチスレッドプログラミングでは、同じアドレス空間を共有しているため、スレッドは同じメモリ空間を読みだしたり、書き込んだりすることが可能である。
一方で、完全性のために、同期プリミティブを使って、同時に読み出しや書き込みが発生してもデータが壊れないようにする必要がある。
これらはパフォーマンスを向上するためにハッシュテーブルを使って行われる。

[Synchronization Primitives]

同期プリミティブは、道路における信号機のようにメモリへのアクセスを管理する。
よく使われるのは以下の3つである。

  • Mutex(MUTually EXclusive) locks
  • Spin locks
  • RW locks

Mutex lockは"adaptive mutex locks"としてライブラリかカーネルに実装されている。
adaptive mutex lockは、他のCPUで実行されている場合はspinロックを行い、そうでない場合やspinロックのスレッシュホールドに達した場合は、単純なmutexロックを行うというものである。
Adaptive mutex locksはCPUリソースな無駄な消費をせずに低レイテンシを実現するために、Solarisによって長年使われてきた。
Linuxでは"adaptive spinning mutexes"として2009年から実装されている。

[Hash Tables]

ハッシュテーブルは巨大なデータ構造に対して、ロックを最適な数だけ取得するために使われる。
ハッシュテーブルを使わない場合の巨大なデータ構造に対するロックのアプローチは以下の2つが考えられる。

  • 1つのグローバルロック
    • この方法はシンプルだが、複数のアクセスがロックの部分で直列化され、パフォーマンスを大きく損なう
  • データ構造ごとのロック
    • データ構造ごとにロックを使うので、必要な部分だけでロックのチェックが行われる
    • しかし、ロック機構が増えることによる、ストレージのオーバーヘッド、ロック機構を作ったり壊したりすることによるCPUのオーバヘッドが増えることになる

ハッシュテーブルによるロックは、競合が少ないと予想される場合の、上記の中間に当たる解決法である。
まず、一定数のロック機構を作る。それに対して、ハッシュアルゴリズムによって、どのロック機構がどのデータ構造に使用されているかを選択する。
これによって、ロック機構をデータ構造ごとに構築、破壊するコストをさけることができる。

図5.2にハッシュテーブルの例を示す。

この例ではハッシュのキーが重複した場合の例も示している。
キーが重複した場合は、キーに対してデータチェインを作って順に格納するようにしている。
このハッシュチェインは重複するデータが増えると重大なパフォーマンス低下につながる。そのため、ハッシュ関数とテーブルサイズはハッシュチェインを短くするように選択する必要がある。

理想的には、ハッシュのキーサイズは並列数の最大化のためにCPUの数と同じかそれ以上になることが好ましい。

5.2.6 Non-Blocking I/O

3章で図示したUnix process life cycleではプロセスはI/Oによってブロックされsleep状態に入るとしていたがこれは以下のような点で問題がある。

  • 並行なI/OではそれぞれのI/Oがスレッドやプロセスを消費し、大量のスレッドを作らなければならなくなる。
  • 頻繁な短い期間のI/Oが発生する場合、コンテキストスイッチのオーバーヘッドがレイテンシの増加を招く

"non-blocking I/O"モデルではI/Oを非同期で行い、現状のスレッドをブロックせずに実行できる。 (非同期I/Oとの区別があいまいになっていそう)

5.2.7 Processor Binding

NUMA環境に対して、プロセスやスレッドを1つのCPUに縛って実行することはI/Oの観点から非常に有効である。
これによってメモリの局所性が上がり、メモリのI/Oのパフォーマンスが上がることでアプリケーション全体のパフォーマンスが上がる。
このトピックについては7章メモリで紹介される。

一方で、仮想化環境ではCPU Bindingはリスクになる。
仮想化した環境の負荷に偏りがある場合、割り当てたCPUの上限を超えないように注意する必要がある。

5.3 Programming Languages

プログラミング言語は、コンパイルや翻訳をされ、仮想マシーンで実行されるものが多いだろう。
多くのプログラミング言語ではその特徴にパフォーマンス最適化を挙げているが、これは厳密に言うと言語そのものの特徴ではなく、その言語の実行環境が最適化しているという特徴である。
例えばJava Hot Spot VMソフトウェアは動的にパフォーマンスを改善するためにjust-in-timeコンパイラを含んでいる。

インタラプターと言語の仮想マシンはそれぞれ異なるレベルのパフォーマンス監視サポートを提供する。
パフォーマンス分析者にとってこれらのツールを使うことで素早く問題点を解決できることがある。

以降の節ではプログラミング言語のタイプごとにパフォーマンスの特徴について説明する。 言語のより詳しい特徴についてはプログラミング言語の本を参照するように。

5.3.1 Compiled Languages

コンパイルは実行よりまえにプログラムを受けて、マシン命令を実行可能なバイナリファイルにする。
バイナリファイルあh再び困憊することなくいつでも実行が可能である。 コンパイルされたコードは一般的にはさらなる変換をせずにCPUで実行が出来、ハイパフォーマンスである。

コンパイルされる言語のパフォーマンス分析は一方向に行われる。というのは大抵の場合実行されるマシーンコードはオリジナルのプログラムと近い形でマッピングされているからである。 コンパイルによって、関数名やオブジェクトがシンボルテーブルに変換される。
そのため、プロファイリングやCPU実行のトレーシングは直接プログラムの名前にマッピングされており、プログラムの実行を分析できるようになっている。
スタックトレースや含んでいるアドレスの数も同時にマッピングされているので、関数を呼び出している祖先のコードパスも知ることができる。

[Compiler Optimization]

gccコンパイラは0から3の最適化レベルを持っており、それぞれによって適用されるコンパイルオプションが異なる。 最適化を強めることによってパフォーマンスを向上できる場合もあるが、これによってのちのパフォーマンス分析ができなくなることもあり、このトレードオフを検討する必要がある。

5.3.2 Interpreted Languages

インタープリンタ言語は実行時にプログラムの翻訳が実行される。
そのため、実行にはオーバヘッドが加わることになるので、パフォーマンスの観点では不利だが、そもそもインタープリンタ言語は実装の用意さやロジックのデバッグのしやすさから選択されることが多い。
これらはインタープリンタの実装にも依存するが、たいていインタープリンタのでき具合のせいでパフォーマンスの分析は難しいことが多い。
しかし、実際にパフォーマンスにシビアな環境ではいんたプリンタ言語は使われないということもある。

5.3.3 Virtual Machines

言語のバーチャルマシン(プロセスバーチャルマシンとも呼ばれる、以降VM)はコンピュータをシミュレーションするソフトウェアである。
JavaErlangは一般的にプラットフォームに依存しないVMによって実行される。
こうした言語はVMの命令セット(バイトコード)にコンパイルされ、VMによって実行される。

バイトコードはアプリケーション言語からコンパイルして生成され、VMによってマシン語に翻訳される。
Java HotSpot VMではJITコンパイラを備えており、これはバイトコードを前もってマシン語に翻訳する。 こうした機能はアプリケーションコードの移植性だけでなく、パフォーマンスにも利点をもたらしている。

VMによって実行される言語あhは特に観測が難しい。
場合によっては、CPU上で実行されている命令は複数回のステージにわけられてコンパイルや翻訳がなされており、さらに元のコードの情報が読める状態ではないことが多い。
これらのパフォーマンス分析はDtraceを使ったサードパーティ製のものかVMの提供するパフォーマンス監視ツールを使うことになる。

5.3.4 Garbage Collection

いくつかの言語では自動でメモリの管理をしてくれる、これらでは非同期に行われるガーベッジコレクションにより、確保したメモリを明確に開放する必要なない。
これによってコードを書きやすくなる一方で、いくつかの欠点も存在する。

  • Memory Growth
    • アプリケーションの利用するメモリをコントロールしにくい
    • 開放して良いオブジェクトがわかりづらいとメモリが肥大化することになる
  • CPU cost
    • GCはメモリ上のオブジェクトの探索を際限なく行う必要があるそのためのCPUコストを消費することになる
    • アプリケーションが使用するメモリ量が多くなるほど、GCによるCPUの消費量も多くなる
  • Latency outliers
    • GCによって実行が止まるアプリケーションでは、レイテンシを増加させることになる。

GCは一般的にパフォーマンスチューニングの対象となる。
たとえばJava VMではおおくのGCタイプやGCのためのスレッド数などを指定するパラメータが存在する。
もし、これらが効果的ではなければ、問題はアプリケーションがゴミを作りすぎているか、参照をリークさせてしまっている可能性がある。 これらはデベロッパーの問題である。