【webパフォーマンスチューニング】OSの基礎知識とチューニング
Linuxではコードを書き換えずともカーネルの挙動を書き換える機能として、カーネルパラメータが存在する。
このカーネルパラメータをいじることで、大体のパフォーマンスチューニングにおけるユースケースには対応できるんだ〜。
カーネルって何なん?
osの中核となる部分で、メモリ, CPU, 周辺機器からの入出力などのハードウェアを抽象化し、ハードウェアとソフトウェアの架け橋となる部分。
Linuxを学ぶ
OSとしてのコアの機能をLinux kernelというソフトウェアが担っている。
OS上で動作するアプリケーションはシステムコールという命令でLinux kernelの機能を利用できる。
他のOSだったら、どんなカーネルが用意されているの?
- Windows: windowsカーネル
- macos: XNUカーネル(X is not unix)
カーネルでハードウェアとのやり取りが抽象化されてるおかげで、ソフトウェア側が下の階層を意識しなくても利用できるようになっているのね。
straceコマンドによって、コマンドを実行する際に利用しているシステムコールを確認できる
$ strace ls
execve("/usr/bin/ls", ["ls"], 0xffffd42d1040 /* 25 vars */) = 0
brk(NULL) = 0xaaaad247b000
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xffff905d6000
faccessat(AT_FDCWD, "/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
newfstatat(3, "", {st_mode=S_IFREG|0644, st_size=21291, ...}, AT_EMPTY_PATH) = 0
mmap(NULL, 21291, PROT_READ, MAP_PRIVATE, 3, 0) = 0xffff905d0000
close(3) = 0
openat(AT_FDCWD, "/lib/aarch64-linux-gnu/libselinux.so.1", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0\267\0\1\0\0\0\0\0\0\0\0\0\0\0"..., 832) = 832
newfstatat(3, "", {st_mode=S_IFREG|0644, st_size=161936, ...}, AT_EMPTY_PATH) = 0
mmap(NULL, 300400, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xffff90557000
mmap(0xffff90560000, 234864, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0) = 0xffff90560000
munmap(0xffff90557000, 36864) = 0
munmap(0xffff9059a000, 25968) = 0
mprotect(0xffff90586000, 65536, PROT_NONE) = 0
mmap(0xffff90596000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x26000) = 0xffff90596000
mmap(0xffff90598000, 5488, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xffff90598000
プロセス
OSにはプログラムを実装する単位としてプロセスというものがある。
OSにはプロセスをゼロから作る機能はなく、親プロセスから子プロセスを作成する機能が提供されている。
プロセスにはIDが振られ、1から始まり、新しく作成されるごとにインクリメントされていく。
Linuxにおける一番親となるプロセスは何なのか?
Linuxにおける市場親のプロセスは、PID=1のプロセス。
確認してみると
$ ps -p 1
PID TTY TIME CMD
1 ? 00:00:01 systemd
systemdというコマンドが一番最初に実行され、プロセスが作られているそうだ。
ちなみにMacOSで試してみると
$ ps -p 1
PID TTY TIME CMD
1 ?? 351:59.00 /sbin/launchd
launchdというコマンドが実行されているそうだ。
sshでlinuxへ接続した状態でpsコマンドを実行すると、以下のようなプロセスが起動している
root 778 0.0 0.1 15156 7984 ? Ss 06:25 0:00 sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups
root 1332 0.0 0.2 18388 9640 ? Ss 06:27 0:00 \_ sshd: hirokei [priv]
hirokei 1390 0.0 0.1 18520 6660 ? S 06:27 0:01 \_ sshd: hirokei@pts/0
hirokei 1391 0.0 0.1 9444 6100 pts/0 Ss 06:27 0:00 \_ bash --rcfile /dev/fd/63
hirokei 19103 0.0 0.0 9968 2868 pts/0 R+ 07:13 0:00 \_ ps axufww
表示が木構造になっていることから、PID: 778から子プロセスPID: 19103が生成されている。
sshとかしたときも、プロセスって作られてたんだ。。。!
このように、親プロセスが子プロセスを生成することをforkと呼ぶ。
ちなみに、pstreeでも親子関係確認できる
$ pstree
systemd─┬─ModemManager───2*[{ModemManager}]
├─agetty
├─containerd───8*[{containerd}]
├─cron
├─dbus-daemon
├─dockerd───9*[{dockerd}]
├─irqbalance───{irqbalance}
├─login───bash
├─multipathd───6*[{multipathd}]
├─networkd-dispat
├─node_exporter───4*[{node_exporter}]
├─packagekitd───2*[{packagekitd}]
├─polkitd───2*[{polkitd}]
├─rsyslogd───3*[{rsyslogd}]
├─snapd───10*[{snapd}]
├─sshd───sshd───sshd───bash───pstree
├─systemd───(sd-pam)
├─systemd-journal
├─systemd-logind
├─systemd-network
├─systemd-resolve
├─systemd-timesyn───{systemd-timesyn}
├─systemd-udevd
├─udisksd───4*[{udisksd}]
└─unattended-upgr───{unattended-upgr}
psコマンドのオプションに指定していた、axufwwとは何なのか?
プロセスのリストを詳細に表示するために使用されるpsコマンドのオプションの組み合わせである。
それぞれ見ていくと
a
全てのユーザーのプロセスを表示する
デフォルトでは自分(psコマンドを叩いたユーザー)のプロセスが表示される。
x
ターミナルに関連づけられていないプロセスも含めて表示する。
バックグラウンドで実行されているプロセスも含める
u
プロセスの詳細情報をユーザーフォーマットで表示する。
ユーザーフォーマットの具体的なフィールドは以下の通り
- USER: プロセスを所有しているユーザー名。
- PID: プロセスID。
- %CPU: プロセスが使用しているCPUリソースの割合。
- %MEM: プロセスが使用している物理メモリの割合。
- VSZ: 仮想メモリサイズ(プロセスが使用している仮想メモリの量、キロバイト単位)。
- RSS: 実際の物理メモリの使用量(Resident Set Size、キロバイト単位)。
- TTY: プロセスが関連付けられている端末。デーモンやバックグラウンドプロセスには?が表示されます。
- STAT: プロセスの状態。例えば、以下のような文字が表示されます。
- R: 実行中(Running)
- S: 待機中(Sleeping)
- D: ディスク(I/O待ち中)
- Z: ゾンビプロセス(Zombie)
- T: 停止中(Stopped)
- I: アイドル(Idle)
- START: プロセスが開始された時間または日付。
- TIME: プロセスが使用した累積CPU時間。
- COMMAND: プロセスが実行しているコマンドの名前と引数。
f
フォーマットを階層的に表示する
w
コマンド結果をフル出力
デフォルトでは、ある程度まで長くなるとカットされる。
子プロセスを生成する理由
プロセスによって分割されるリソースというのは、物理的にリソースが確保されるわけではなく、
- 仮想CPU
- 仮想メモリ
と呼ばれる
Linux kernel上で仮想化されたリソースを利用している。
物理的な割り当てをするケースもある
これと対比して、仮想ではなく物理的にリソースを使用する際には
- 実CPU
- 実メモリ
と表現される
仮想化されたリソースを使用することで、ユーザ空間のプロセスは物理的なリソースについて気にすることなく効率的にリソースを使用できる。
子プロセスを生成する理由は、独立したリソース空間を用意できるから。
そもそもなぜ独立したリソース空間を用意するの?
リソース空間が独立していると、いくつかメリットがあるから。
- 一つのプロセスがクラッシュしても、別のプロセスに影響を与えないので、障害の局所化ができる
- 他のプロセスにアクセスできないので、並列での実行が可能になる
他にもいろいろあるがキリがないので、ここでは割愛
C10K問題とは?
人生で一度は耳にする、C10K問題。
これは一体何なのだろうか?
クライアントの台数が約1万台に達すると、サーバーのハードウェア性能に余裕があるにも関わらず、レスポンス性能が大きく下がる現象のこと。
主にApacheで発生する。
どうして性能が大きく下がってしまうのかというと、Apacheの駆動方式に原因がある。
Apacheは新しいクライアントが接続される度に新しいプロセスが生成される。
すると、クライアントの数だけプロセスが増えるという現象が起きる。
しかし、サーバー(OS)が生成できるプロセス数には限界があり、その限界に到達するとプロセスを新規生成することができなくなってしまう。
すると、今起動しているプロセスが終わらないと、新しい接続が待つ必要が出てくるため、レスポンスが遅くなる。
ちなみに、プロセスとスレッドの関係性などはこちらでまとめてる
Linuxのネットワーク
ネットワークにおいて重要な性能指標。
それは、以下の2つ。
- スループット: 同時に処理できる量
- レイテンシ: ネットワーク通信を処理するためにかかった時間
HTTPリクエストを受け取った時の挙動
HTTP通信を最終的に処理するのはWebサーバー(req, resをやりとりするプロセス)
NICから受け取ったそのままの情報はHTTPリクエストとして認識されておらず、パケットという単位で処理を行っている。
NICってなんぞ?
ネットワークインターフェースカードの略
ネットワーク内にてコンピュータ間で有線、無線での通信するためのハードウェア
- サーバーがクライアントからHTTPリクエストを受信する
- サーバーに搭載されたNICが光信号などで情報を受け取る
- 受け取った信号を電気信号に変換され、ドライバを通してカーネル空間でパケットとして処理される
- 最終的にユーザ空間のWebサーバーまでたどり着く
パケットごとに情報のやり取りをするってどういうこと?情報が断片的なら、そもそもWebサーバーは情報を処理できないのでは?
Webサーバーに到達する時点では、クライアントから送信されたリクエストの情報は完全なものになっているよ!
NICにパケットとして渡ってきた情報はネットワークのプロトコルに則って完全な情報に直される。
具体的にどんなプロトコルで、何の層でどんな変換がされるのかについては割愛。(OSI参照モデル、TCP/IPモデルを参照)
パケット処理の効率化
Linux kernelはネットワークの通信を早く行えるように色々な改良がなされてきた。
その一例として、RSS(Receive Side Scaling)がある。
「Webサービスを提供する」というのは、「ネットワーク上にWebアプリケーションを公開する」と言い換えられる。
ユーザーとリクエストのやり取りをするホストは、どのタイミングでパケットが受信されるのかを事前に予測することはできない。
そこで、LinuxはパケットをNICから受け取った段階でCPUに対して「即座にパケットを処理せよ」という割り込み命令を実行する。これをInterruptと呼ぶ。
Interrupt(割り込み命令)には以下の種類がある。
- ハードウェア割り込み: パケット割り込みやキーボードの入力など、即時処理が必要なもの
- ソフト割り込み: 割り込みの中でも遅れて実行するもの
パケットを受信したことによる割り込みを分割すると、受信したタイミングで発生するものにハードウェア割り込み、プロトコルを解釈する処理にソフト割り込みを採用している。
複数のCPUコアを搭載している場合でもパケットの受け取りに使用したコアとその後の解釈に利用するコアは同一にする設計がなされている。
なぜかというと、CPUにはコア毎に処理を行うためのキャッシュ機構が搭載されており、同じコアを利用することでキャッシュを効率的に利用できることが大きな理由。
パケット一つあたりの処理はそんなに負荷の高い処理ではありませんが、時代が進むにつれ処理するパケットの数の増えてきた。
求められるパケットの増加に対応してNICの高速化は進んでいるが、パケットの処理を行うCPU自体の高速化はNICのそれに追いつけなかった。
1コアでパケットを処理するには限界が出てきたのだ。
そういった背景から、ソフトウェアであるLinux kernelとハードウェアであるNICの両方で多くの発明がなされている。
Linux kernelの改善
割り込み命令の数を減らしつつ、遅延の少ない形でパケットを処理できるようにLinux NAPIという仕組みが導入された。
NICの改善
RSSという技術が導入された。
パケットを受け取るパケットキューというものをCPUコア数増やすことで、パケットの処理をCPUのコア数分に分散できるようになり、マルチコアなコンピュータに対応できるようになってきた。
パケットを受け取る側のキャパシティを大きくすることから、Receive Side Scalingと呼ばれた。
1つのCPUは、複数の命令を完全に同時進行で行うことはできない。そのため、複数のプロセスを動作させるために超高速で処理するプロセスを切り替えて擬似的に同時進行しているように見せるというアプローチをしている。これを実現するためにコンテキストスイッチという概念を導入している。プロセスの切り替えにかかる時間をコンテキストスイッチコストと呼ぶ。
プロセスのリソース割り当ては誰の仕事?それをシステムを起動させた人が制限をかけることはできるの?
ユーザー側から指定しない場合は、基本的にLinuxのスケジューラが仕様として用意されているポリシーによってリソースを割り当ててくれる。(https://wiki.linuxfoundation.org/realtime/documentation/technical_basics/sched_policy_prio/start)
どのプロセスにどのくらいのリソースを割り当てるのかは、基本的に公平に(全てのプロセスに同じ量の)リソースが割り当てられるが、nice値という値によってリソース割り当ての優先度が変わることもある。
デフォルトのnice値は0で、値が低いほど優先度が高い。これは我々ユーザー側からniceコマンドを使って操作することができる。
システムを起動させたユーザーも明示的にリソース割当量をセットすることができる。
以下のコマンドにて、各リソースの使用量を明示的にセットできる。
- ulimit: 自分が起動したシェル上のプロセスの割当量を変更できる。
- メモリ使用量の最大値
- CPU時間の最大値
- ユーザーあたりのプロセス数
- などなど。。。
- 詳しくはhttps://www.ibm.com/docs/ja/aix/7.1?topic=u-ulimit-command
- taskset: 特定のプロセスを特定のCPUにバインド(割り当て)することができる。
taskset -c 0,2 ./my_program arg1 arg2
- my_programをCPU 0, 2で実行する
ディスクIO
コンピュータは情報を記憶するためにメモリを使い、計算結果を保持したりどんな命令をするかを保持したりしている。
しかし、中には大量のデータや大容量のデータを扱う必要も出てきたため、メモリだけで情報の記憶をすることは難しくなってきた。
そこで、メモリほど高速に読み書きはできないけど、安価に大容量のデータを保存することができるストレージというものが登場した。
磁気ディスク上へ物理的に情報を書き込んでいくHDD(Hard Disk Drive)と、電圧を保持できる浮遊ゲートというもを使って電源を切っても情報を記憶できる回路を用いたSSD(Solid State Drive)という2つの方式のストレージが存在する。
情報へのアクセス方法の違い
ストレージの読み書き方法には以下の2つの方法がある
- シーケンシャルリード、シーケンシャルライト: 保存領域の連続したアドレス空間から順番に読み取り、書き込みが行われる
- ランダムリード、ランダムライト: 非連続のアドレス空間から読み取られるパターン。
ストレージの種類
ストレージの方式として2種類が存在している。
- ファイルシステム: ディレクトリがあり、その中にファイルが存在するという木構造を持っている
- オブジェクトストレージ: ファイルにIDが渡され、そのIDをもとに読み書きするストレージ。
ストレージの性能
以下の3つに大きく分けられる
- スループット: 一定時間において読み書きを終えたファイルサイズ
- レイテンシ: 一度ストレージへ読み書きしたときにかかった時間
- IOPS(Input Output Per Second): 1秒間あたりに読み書きができた回数
100IOPSの場合、1秒間あたりに100個のファイルを開いて読むことができる。
スループットとIOPSは単位時間あたりに発揮できたパフォーマンスという意味では同じでは?
確かに、単位時間あたりにどのくらいの処理をこなせたかという意味では共通だが、ファイルサイズとファイル数という点が違う点だ。
スループットは、単位時間あたりにどのくらいの大きさのファイルサイズを読み書きできたのか。
IOPSは、単位時間あたりにどのくらいの数のファイルを読み書きできたのか。
IOPS: 100, スループット: 1MG/secの性能を持つストレージでファイルの読み書きをする場合について考えてみる。
1MGのファイルを100個扱う場合を考えてみると、1MB * 100 = 100MGだ。1sあたり100ファイルの読み書きができて、かつ1sあたりに1MGのファイルを読み書きできるので、1sに1MGのファイルの読み書きを100件こなすことができる。
一方、ファイルサイズをもっと小さくしてその文数を増やし、1KBのファイルを100000個扱う場合について考えてみる。
スループットとしては1MGなのでかなり余裕があるが、IOPSが100件なので、100000件行うためには1000s(=約15分)もかかってしまうことになる。
つまり、テキストや画像などの小さなファイルを大量に扱うWebサーバーにはIOPSが優れたストレージを使うべきで、動画などの大きなサイズのファイルを扱う場合はスループットが優れたストレージを使うべきである
ストレージの性能を見てみる。
fioの詳しいオプションについては割愛。
$ fio -filename=./fioReport -direct=1 -rw=read -bs=4k -size=2G -runtime=10 -group_reporting -name=fileIO
fileIO: (g=0): rw=read, bs=(R) 4096B-4096B, (W) 4096B-4096B, (T) 4096B-4096B, ioengine=psync, iodepth=1
fio-3.28
Starting 1 process
fileIO: Laying out IO file (1 file / 2048MiB)
Jobs: 1 (f=1): [R(1)][100.0%][r=40.6MiB/s][r=10.4k IOPS][eta 00m:00s]
fileIO: (groupid=0, jobs=1): err= 0: pid=48455: Tue Aug 6 01:58:47 2024
read: IOPS=10.0k, BW=39.2MiB/s (41.1MB/s)(392MiB/10001msec)
clat (usec): min=66, max=21460, avg=99.04, stdev=180.46
lat (usec): min=66, max=21460, avg=99.12, stdev=180.47
clat percentiles (usec):
| 1.00th=[ 68], 5.00th=[ 70], 10.00th=[ 71], 20.00th=[ 87],
| 30.00th=[ 90], 40.00th=[ 91], 50.00th=[ 93], 60.00th=[ 94],
| 70.00th=[ 98], 80.00th=[ 101], 90.00th=[ 105], 95.00th=[ 137],
| 99.00th=[ 176], 99.50th=[ 249], 99.90th=[ 1020], 99.95th=[ 2180],
| 99.99th=[ 8586]
bw ( KiB/s): min=34936, max=45272, per=100.00%, avg=40455.37, stdev=2899.00, samples=19
iops : min= 8734, max=11318, avg=10113.58, stdev=724.72, samples=19
lat (usec) : 100=78.78%, 250=20.72%, 500=0.30%, 750=0.07%, 1000=0.03%
lat (msec) : 2=0.04%, 4=0.04%, 10=0.01%, 20=0.01%, 50=0.01%
cpu : usr=2.26%, sys=10.51%, ctx=100255, majf=0, minf=27
IO depths : 1=100.0%, 2=0.0%, 4=0.0%, 8=0.0%, 16=0.0%, 32=0.0%, >=64=0.0%
submit : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
complete : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
issued rwts: total=100252,0,0,0 short=0,0,0,0 dropped=0,0,0,0
latency : target=0, window=0, percentile=100.00%, depth=1
Run status group 0 (all jobs):
READ: bw=39.2MiB/s (41.1MB/s), 39.2MiB/s-39.2MiB/s (41.1MB/s-41.1MB/s), io=392MiB (411MB), run=10001-10001msec
Disk stats (read/write):
dm-0: ios=98787/31, merge=0/0, ticks=8936/0, in_queue=8936, util=99.02%, aggrios=100253/13, aggrmerge=0/18, aggrticks=9101/10, aggrin_queue=9113, aggrutil=98.95%
vda: ios=100253/13, merge=0/18, ticks=9101/10, in_queue=9113, util=98.95%
IOPS
iops : min= 8734, max=11318, avg=10113.58, stdev=724.72, samples=19
スループット
bw ( KiB/s): min=34936, max=45272, per=100.00%, avg=40455.37, stdev=2899.00, samples=19
レイテンシ(clat)
fileIO: (groupid=0, jobs=1): err= 0: pid=48455: Tue Aug 6 01:58:47 2024
read: IOPS=10.0k, BW=39.2MiB/s (41.1MB/s)(392MiB/10001msec)
clat (usec): min=66, max=21460, avg=99.04, stdev=180.46
lat (usec): min=66, max=21460, avg=99.12, stdev=180.47
clat percentiles (usec):
| 1.00th=[ 68], 5.00th=[ 70], 10.00th=[ 71], 20.00th=[ 87],
| 30.00th=[ 90], 40.00th=[ 91], 50.00th=[ 93], 60.00th=[ 94],
| 70.00th=[ 98], 80.00th=[ 101], 90.00th=[ 105], 95.00th=[ 137],
| 99.00th=[ 176], 99.50th=[ 249], 99.90th=[ 1020], 99.95th=[ 2180],
| 99.99th=[ 8586]
ディスクマウント
物理的、もしくはネットワーク経由で接続されたブロックストレージをWebアプリケーションからファイルシステムとして読み書きできるようにするために、ディスクへマウントという処理をする必要がある。
接続されたブロックストレージをブロックデバイスと呼ぶ。
現在、どのディレクトリにどのブロックデバイスがマウントされているのかを、lsblk, dfコマンドで確認できる。
$ lsblk
loop0 7:0 0 59.8M 1 loop /snap/core20/2267
loop1 7:1 0 59.8M 1 loop /snap/core20/2321
loop3 7:3 0 77.4M 1 loop /snap/lxd/28384
loop4 7:4 0 33.7M 1 loop /snap/snapd/21467
loop5 7:5 0 33.7M 1 loop /snap/snapd/21761
loop6 7:6 0 77.4M 1 loop /snap/lxd/29353
sr0 11:0 1 1024M 0 rom
vda 252:0 0 64G 0 disk
├─vda1 252:1 0 1G 0 part /boot/efi
├─vda2 252:2 0 2G 0 part /boot
└─vda3 252:3 0 60.9G 0 part
└─ubuntu--vg-ubuntu--lv 253:0 0 30.5G 0 lvm /
vdaというブロックデバイスはルートにマウントされている。(/dev/は全てのルートにつける)
$ lsblk /dev/vda
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
vda 252:0 0 64G 0 disk
├─vda1 252:1 0 1G 0 part /boot/efi
├─vda2 252:2 0 2G 0 part /boot
└─vda3 252:3 0 60.9G 0 part
└─ubuntu--vg-ubuntu--lv 253:0 0 30.5G 0 lvm /
dfコマンドによって、ルートディスクは/dev/mapper/ubuntu--vg-ubuntu--lv
というブロックデバイスがext4というファイルシステムでマウントされていることがわかった。
df -hT /
Filesystem Type Size Used Avail Use% Mounted on
/dev/mapper/ubuntu--vg-ubuntu--lv ext4 30G 12G 18G 40% /
今回はMocOS上にlinuxのVMを起動していたので、linuxのブロックデバイスが検出された。
マウントする際には、ディスクをどのように扱うかについてもオプションで設定できる。
まずは現在の設定を確認してみる。
$ mount | grep "/dev/vda"
/dev/vda2 on /boot type ext4 (rw,relatime)
mountコマンドを使うと、wr(read, writeの読み書き)とrealtime(ファイルが最後にアクセスされた時間の保存タイミングの制御オプション)のオプションが有効化されている。
CPU使用率
CPUとは、コンピュータ上で実際に計算を行う部品。
コンピュータ上で何かしらの操作をした時は、ほぼ全てCPUで演算処置が実行される。
パフォーマンスをチューニングしたりWebアプリケーションを運用する際は、CPU使用率に特に注意すべき。
CPU使用率とは、コンピュータが持つ計算リソースをどの程度の割合利用しているのかを指す。
実際にCPU使用率を確認してみる。
CPUの負荷情報を得るためには、topコマンドを実行してみる。
特に、搭載されているCPUが複数のコアを提供している場合には-1を引数として渡してあげる。
それぞれのコアが同じような負荷で稼働しているのか、それとも一部のコアに処理が集中している状態なのかを把握する上でとても重要になってくる
$ top -1
top - 22:07:02 up 1 day, 3:52, 2 users, load average: 1.06, 1.06, 1.01
Tasks: 124 total, 1 running, 123 sleeping, 0 stopped, 0 zombie
%Cpu0 : 0.3 us, 0.0 sy, 0.0 ni, 99.7 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st %Cpu1 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu2 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st %Cpu3 : 0.0 us, 0.3 sy, 0.0 ni, 99.7 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
MiB Mem : 3911.2 total, 1667.4 free, 274.4 used, 1969.4 buff/cache
MiB Swap: 3911.0 total, 3911.0 free, 0.0 used. 3445.0 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
47931 root rt 0 224124 25664 7388 S 0.3 0.6 0:05.95 multipathd
47937 root 20 0 1859872 37028 26208 S 0.3 0.9 1:09.76 containerd
47981 systemd+ 20 0 25228 12532 8296 S 0.3 0.3 0:39.65 systemd-resolve
51953 root 20 0 0 0 0 I 0.3 0.0 0:00.94 kworker/3:0-mld
51992 root 20 0 0 0 0 I 0.3 0.0 0:00.29 kworker/u8:2-events_power_efficient
1 root 20 0 101148 11128 7644 S 0.0 0.3 0:16.56 systemd
2 root 20 0 0 0 0 S 0.0 0.0 0:00.48 kthreadd
3 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 rcu_gp
4 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 rcu_par_gp
5 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 slub_flushwq
6 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 netns
8 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 kworker/0:0H-events_highpri
10 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 mm_percpu_wq
11 root 20 0 0 0 0 S 0.0 0.0 0:00.00 rcu_tasks_rude_
12 root 20 0 0 0 0 S 0.0 0.0 0:00.00 rcu_tasks_trace
13 root 20 0 0 0 0 S 0.0 0.0 0:01.72 ksoftirqd/0
14 root 20 0 0 0 0 I 0.0 0.0 0:59.82 rcu_sched
15 root rt 0 0 0 0 S 0.0 0.0 0:03.87 migration/0
16 root -51 0 0 0 0 S 0.0 0.0 0:00.00 idle_inject/0
18 root 20 0 0 0 0 S 0.0 0.0 0:00.00 cpuhp/0
19 root 20 0 0 0 0 S 0.0 0.0 0:00.00 cpuhp/1
20 root -51 0 0 0 0 S 0.0 0.0 0:00.00 idle_inject/1
21 root rt 0 0 0 0 S 0.0 0.0 0:02.07 migration/1
22 root 20 0 0 0 0 S 0.0 0.0 0:02.28 ksoftirqd/1
24 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 kworker/1:0H-events_highpri
25 root 20 0 0 0 0 S 0.0 0.0 0:00.00 cpuhp/2
コマンドの結果を見ていこう。
今回のコマンド結果では、%Cpu0~3まで表示されているので、4コアを搭載したCPUが使われていることになる。
それぞれのCPUでは、以下のような要素が表示されている。
- us(ユーザ空間のプロセス)
- sy(カーネル空間のプロセス)
- ni(Nice値が変更されたプロセス)
- id(利用されていないCPU)
- wa(IO処理を待っているプロセス)
- hi(ハードウェア割り込みプロセス)
- si(ソフト割り込みプロセス)
- st(ハイパーバイザ)
特に重要だと判断したus, sy, niのみまとめる。
us (User): ユーザ空間におけるCPU使用率
システムコールを利用するLinuxOS上のアプリケーションが動作する部分である、ユーザ空間におけるCPU使用率。
Webアプリケーションが動作するような環境であれば、まさに実装しデプロイされているWebアプリケーションが多くのCPUを利用している場合に上昇する値。
sy (System): カーネル空間におけるCPU使用率
Linux kernel内の処理であるカーネル空間におけるCPU使用率を指す。
- プロセスのforkが多く発生している環境
- コンテキストスイッチを行っている時間が長くなっている環境
においてはカーネル空間の処理が大きくなるため、syの値が上昇するんやな。
Webアプリケーションを運用する場面においては、WebアプリケーションやミドルウェアがCPUの支援処理を利用する際に大きく上昇する。
カーネル空間って何なん?
前提として、OSには2つのメモリ空間がある。
- カーネル空間
- ユーザ空間
カーネル空間っていうのは、システム全体を管理し、ハードウェアとの直接的なやり取りを担当してくれる。
役割としては
- メモリ管理: メモリの割り当てと解放
- プロセス管理: プロセスのスケジューリング、コンテキストスイッチ
- デバイス管理: ハードウェアとの通信
- ファイルシステム管理: ファイルの読み書き
- ネットワーク管理: クライアントから受信したパケットからHTTPリクエストを生成、TLS暗号化してクライアントにレスポンスを送信
ユーザ空間っているのは、一般的なアプリケーションやプロセスが実行される空間。
ユーザ空間はシステムコールを通じてカーネルとやり取りし、カーネルが提供するサービスを通じてハードウェアとのやり取りができる。
(ハードウェアとのやり取りが必要になり)システムコールが発生する例としては、ファイルの読み書きやネットワークの接続、またはHTTPリクエストの送受信などである。
ni(Nice): Nice値が変更されたプロセスのCPU使用率
Nice値というのは、プロセスがどのくらいの優先度でCPUに計算して欲しいかを表すパラメータ。
低いほど(最低-20)優先度が高く、高いほど(最高19)優先度が低い。
仮に、topコマンドにおけるプロセスの2番目に表示されたcontainerdプロセスのnice値を変えてみる。
デフォルトでは0だが、-15に変えてみる
これで、containerdのプロセスの優先度をあげることができた。
$ sudo renice -n -15 -p 47937
~~~
47937 root 35 -15 1859872 36916 26208 S 0.3 0.9 1:19.12 containerd
~~~
wa(Wait): IO処理を待っているプロセスのCPU使用率
マルチスレッドでの処理を行わない場合にプロセスがIO処理を行っていると、そのプロセスはIO処理が終わるまでのかの処理を行うことができない。
waはプロセスの中でも、ディスクなどのIO処理を待っているプロセスのCPU利用率。
IO待ち状態って一体何なん?
CPUではなく、ディスクの読み書きやネットワークの送受信についての入力、出力操作をプロセスが待っている状態のこと。
IO処理がブロッキング、非ブロッキングかによって処理の流れが異なってくる。
ブロッキング操作: IO待ち状態のプロセスを一旦CPUを破棄する。その間に他のプロセスを実行するようにしてくれる。
非ブロッキング操作: IO待ちの状態であってもCPUを放棄せず、プロセスの処理を続ける。
CPU使用率は低い方が良いのか?
CPU使用率が高い状態とは、それ以上のCPU負荷が捌ききれない状態のことを指す。
逆にCPU使用率が低い状態とは、CPUにもっと負荷をかけても良い状態を指す。
CPU使用率が低い状態の方が好ましいと思われるかもしれないが、実はそうでもない。
CPU使用率が低い状態とは言い換えれば、余分なインフラ費用を払っている状態とも言える。
なので、CPU使用率にある程度余裕を持った状態でパフォーマンスに影響が出ていない(キャパシティが需要と釣り合っている)かつ、それ以上需要が上昇したら、自動的にCPUのスペックやサーバーの台数を増やすなどして対応できるような状態にしておくのが理想だと思った。
Linuxカーネルパラメータ
OSという観点からのパフォーマンスチューニングにて、Linuxのコアのコードを書き換えることなく、挙動を変えることができる。
それは、カーネルパラメータという設定値によってコントロールできる。
Linuxには無数のカーネルパラメータが用意されているので、Webサービスを提供する際に利用するパラメータの例を一つまとめる。
net.core.somaxconn
まとめてどのくらいのネットワークの受信を行えるかを決定する数値。
Linuxがパケットを受け取る際には、listen(2)というシステムコールを用いて通信が開始される。
無事に接続が完了すれば、accept(2)を使って通信が開始される。
この時、accept(2)接続待ちとなっている接続要求のキューから1つ取り出して接続を行う。
このキューのことをbacklogと呼び、net.core.somaxconnは、このbacklogにてどのくらいの量を格納できるのかを設定するカーネルパラメータ。
backlogが触れた場合、Linux kernelは新たに接続を行うことができないと判断してパケットを破棄してしまう。そのため、同時に大量のリクエストを多く受信するような環境においてはリソースに余裕があったとしてもコネクションが生成されずに性能が落ちてしまう。
システムコールって何なん?
ユーザープロセスがOSのカーネルに対してサービスを要求するためのインターフェース。
ユーザープロセスが直接ハードウェアやカーネルの機能にアクセスすることは許されていないため、システムコールを介してアクセスされる。
役割
- ハードウェアへのアクセス: ファイルシステムやディスク、ネットワーク周り
- システムリソースの管理: メモリやプロセスの管理
- 通信と同期: プロセス間の通信
種類
ファイル操作
- open(): ファイルを開く
- read(): ファイルからデータを読み取る
- rwite(): ファイルにデータを書き込む
- close(): ファイルを閉じる
プロセス管理
- fork(): 新しいプロセスを作成する
- exec(): プロセスの実行ファイルを置き換える
- wait(): 子プロセスの終了を待つ
- exit(): プロセスを修了する
メモリ管理
- brk(): データセグメントの末尾を変更する
- mmap(): メモリ領域をマップする
デバイス管理
- ioctl(): デバイスの制御操作をする
- select(): IOの状態を監視する
通信
- pipe(): パイプを作成する
- socket(): ソケットを作成する
- connect(): ソケットに接続する
- send(): データを送信する
- recv(): データを受信する
sysctlコマンドによって、net.core.somaxconnの設定値を確認できる。
$ sysctl net.core.somaxconn
net.core.somaxconn = 4096
デフォルトでは4096となっている。
これを倍の8192書き換えてみる。
一時的に設定値を変える場合はsysctlコマンドを実行して書き換え、永続的に書き換えるには/etc/sysctl.confファイルを書き換える。
$ vi /etc/sysctl.conf
// 最終行に以下を追加
net.core.somaxconn = 8192
// 設定を追加
$ sudo sysctl -p
net.core.somaxconn = 8192
// 設定値が更新されている
$ sysctl net.core.somaxconn
net.core.somaxconn = 8192