プロセスを目で見る
目標
OSの教科書で読んだことを実際に自分の手で作ってみます。ゾンビ、孤児、仮想メモリ、 開いているファイルなど、すべてこのPodの中で数行あれば作れます。
ここで学ぶことは、あとで障害を調べるときにそのまま役立ちます。「ディスクが空かない」、 「メモリをなぜこんなに使っているのか」、「Podが終了しない」への答えは、すべてここにあります。
見る場所
Linuxはカーネルの状態をファイルとして見せてくれます。
ps -eo pid,ppid,stat,comm # 프로세스 목록
ls /proc/<PID>/task # 그 프로세스의 스레드들
ls -l /proc/<PID>/fd # 열어 둔 파일들
cat /proc/<PID>/status # 메모리를 포함한 상태 전부
/procは本物のディスクではなく、カーネルが作ってくれる画面です。
バックグラウンドで起動するとき
Pythonの出力が見えないときは、flush=Trueを付け忘れたことが原因です。
print(os.getpid(), flush=True)
ステップ
- プロセスの系譜 →
01-tree.txt - スレッド →
02-threads.txt - ゾンビ →
03-zombie.txt - 孤児とPID 1 →
04-orphan.txt - VSZとRSS →
05-memory.txt - 削除したファイル →
06-deleted.txt - TERMとKILL →
07-signal.txt - まとめ →
08-notes.md
参考
ステップ5とステップ6では、数値を両方とも残す必要があります。前後の比較が、そのステップのすべてだからです。
誰が誰を生んだのか
今のPodで動いているプロセスの親子関係を取り出し、01-tree.txtに残してください。自分のシェルのPIDとその親も、あわせて書きます。
ps -eo pid,ppid,stat,commを使います。自分のシェルのPIDはecho $$、その親はps -o ppid= -p $$で確認できます。
すべてのプロセスには親がいます。親が子をforkで作り、子が終わると親がwaitで結果を刈り取ります。この2つの文が、次の3つのステップのすべてです。
スレッドはどこにあるのか
スレッドを4つ作るプログラムを起動し、psではプロセスが1つに見えますが、実際には複数あることを02-threads.txtに示してください。
Pythonならthreading.Threadを4つ作れば十分です。print(os.getpid(), flush=True)でPIDを出力してください(flush=Trueがないと出力が見えません)。
確認はls /proc/<PID>/task | wc -lで行います。メインの1つとスレッド4つで、5と表示されます。Linuxでスレッドは、メモリを共有するタスクにすぎず、カーネルから見るとプロセスと大きくは変わりません。
ゾンビを作る
子が先に終了したのに、親が刈り取らない状態を作り、psでZと表示されることを03-zombie.txtに残してください。
os.fork()で子を作り、子はos._exit(0)、親はwait()を呼ばずに眠っていれば十分です。
ゾンビはメモリを使いません。終了したという事実と終了コードだけが残っている場所です。ただしPIDは占有するので、溜まると新しいプロセスを作れなくなります。
親が先に死ぬと
親が子より先に死ぬ状況を作り、その子のPPIDが何に変わるかを04-orphan.txtに残してください。このPodのPID 1が何であるかも、あわせて残します。
ps -o ppid= -p <자식PID>で確認します(プレースホルダーは子のPIDです)。親を失ったプロセスはPID 1に引き継がれます(リペアレンティング)。
続けてps -o comm= -p 1を見てください。コンテナのPID 1は、たいていinitではなくアプリケーション自身です。initでなければ、引き継いだゾンビを刈り取りません。これがコンテナでゾンビが溜まる理由であり、--initやtiniを使う理由です。
予約したメモリと実際に使ったメモリ
300MBを予約だけして、そのうち80MBだけを実際に触るプログラムを起動し、VSZとRSSがどう違うかを05-memory.txtに残してください。
mmap.mmap(-1, 300*1024*1024)で予約し、触る前と後のps -o vsz=,rss= -p <PID>を両方残してください。
bytearray(300*1024*1024)は使えません。Pythonが0で埋める際に、すでにすべて触れてしまうからです。
VSZはアドレスを確保した量、RSSは実際に物理メモリが割り当てられた量です。300MBを予約しても、RSSは10MBにもなりません。
削除したのに容量が戻らない
64MBのファイルを作り、開いたまま削除してから、dfで容量が戻らないことを示し、プロセスを終了すると戻るところまで06-deleted.txtに残してください。
dd if=/dev/zero of=big.bin bs=1M count=64で作成し、Pythonでopen()した後にos.remove()して眠らせれば十分です。
証拠はここにあります。ls -l /proc/<PID>/fd/にbig.bin (deleted)として残ります。
ファイル名を消すことと、データを消すことは別です。最後の参照(名前でも開いているfdでも)がなくなって初めて、ブロックが返却されます。ログを削除したのにディスクが空かない事故の原因は、まさにこれです。
TERMとKILLの違い
SIGTERMを受け取って処理はするものの、終了はしないプログラムを作り、TERMでは生き残りKILLでは死ぬことを07-signal.txtに残してください。
signal.signal(signal.SIGTERM, 핸들러)を使います(プレースホルダーはハンドラー関数です)。確認はkill -TERM <PID>のあとにkill -0 <PID>(生きていれば成功)、そのあとにkill -KILL <PID>です。
TERMはお願いで、KILLは通告です。TERMはプログラムが受け取って後始末をする機会を与え、KILLはカーネルがそのまま消し去ります。そのため後始末ができません。Kubernetesが終了するときにまずTERMを送り、terminationGracePeriodSecondsだけ待ってからKILLする理由がこれです。
3つのことをまとめる
08-notes.mdに3行以上書いてください。ゾンビとは何でなぜ生まれるのか、VSZとRSSの違い、削除したファイルの容量がいつ戻るのか、の3点です。
本文に좀비、RSS、참조が含まれている必要があります(韓国語の2つの語は、それぞれ「ゾンビ」と「参照」を意味します)。