CUBE SUGAR CONTAINER

技術系のこと書きます。

stress コマンドを使ってマシンに負荷をかける

stress(1) を使うと、Unix 系 OS で動作しているホストの CPU やメモリ、ディスクに簡単に負荷をかけられる。 今回は使い方や動作などを一通り見ていく。

使った環境は次のとおり。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 22.04 LTS
Release:    22.04
Codename:   jammy
$ uname -srm
Linux 5.15.0-40-generic aarch64
$ stress --version
stress 1.0.5
$ dstat --version | head -n 1
pcp-dstat 5.3.6
$ taskset --version
taskset from util-linux 2.37.2

もくじ

下準備

あらかじめ必要なパッケージをインストールしておく。 肝心の stress(1) は stress パッケージで入る。

$ sudo apt-get -y install stress pcp util-linux

CPU に負荷をかける

まずは CPU に負荷をかける方法から。 今回は環境として CPU が 4 コアの仮想マシンを使っている。

$ cat /proc/cpuinfo | grep processor | wc -l
4

CPU に負荷をかけるときは stress コマンドと共に -c オプションを使う。 引数として、負荷をかけるのに使うワーカーのプロセス数を指定する。

$ stress -c 1

ワーカーはランダムな数値に対する平方根を無限ループで計算することで CPU に負荷をかける。 基本的にプロセス数 = 負荷をかけるコア数と考えれば良い。

dstat(1) で確認すると、-c 1 なら 4 コア環境において user 時間を 25% (1/4) 消費している。

$ dstat -c
----total-usage----
usr sys idl wai stl
 25   0  75   0   0
 25   0  75   0   0
 25   0  75   0   0
...

試しにワーカー数を 2 に増やしてみよう。

$ stress -c 2

すると、次のように消費する user 時間が 50% (2/4) に増えた。

$ dstat -c
----total-usage----
usr sys idl wai stl
 50   0  50   0   0
 51   0  50   0   0
 50   0  50   0   0
...

特定の CPU コアを指定して負荷をかけたい場合は taskset(1) と組み合わせるのが良い。 以下では stress(1) が 0 番コアで動作するように割り当てている。

$ taskset -c 0 stress -c 1

dstat(1) で確認すると、たしかに cpu0 で user 時間を消費していることが確認できる。

$ dstat -c -C 0,1,2,3
-----cpu0-usage----------cpu1-usage----------cpu2-usage----------cpu3-usage----
usr sys idl wai stl:usr sys idl wai stl:usr sys idl wai stl:usr sys idl wai stl
100   0   0   0   0:  0   0 100   0   0:  0   0 100   0   0:  0   0 101   0   0
100   0   0   0   0:  0   0  99   0   0:  0   0 100   0   0:  0   0 100   0   0
100   0   0   0   0:  0   0 100   0   0:  0   0 100   0   0:  0   0 100   0   0
...

次は試しに 1 番と 2 番コアに割り当ててみよう。

$ taskset -c 1,2 stress -c 2

次のとおり、ちゃんと cpu1 と cpu2 の user 時間を消費している。

$ dstat -c -C 0,1,2,3
-----cpu0-usage----------cpu1-usage----------cpu2-usage----------cpu3-usage----
usr sys idl wai stl:usr sys idl wai stl:usr sys idl wai stl:usr sys idl wai stl
  0   0 100   0   0:100   0   0   0   0:100   0   0   0   0:  0   0 101   0   0
  0   0 100   0   0:100   0   0   0   0:100   0   0   0   0:  0   0 100   0   0
  0   0 100   0   0:100   0   0   0   0:100   0   0   0   0:  0   0 100   0   0

メモリに負荷をかける

続いてはメモリに負荷をかけてみよう。

メモリに負荷をかけるときはオプションとして -m を指定する。 引数は CPU のときと同じで実際の処理をするワーカープロセスの数になる。 --vm-bytes は確保するメモリのサイズを指定している。 また、--vm-hang 0 はメモリを確保したまま処理を止めるオプションになっている。 -v はワーカープロセスの動作を詳細にログに残すため。

$ stress -m 1 --vm-bytes 512M --vm-hang 0 -v
stress: info: [23977] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd
stress: dbug: [23977] using backoff sleep of 3000us
stress: dbug: [23977] --> hogvm worker 1 [23978] forked
stress: dbug: [23978] allocating 536870912 bytes ...
stress: dbug: [23978] touching bytes in strides of 4096 bytes ...
stress: dbug: [23978] sleeping forever with allocated memory

上記のログを見ると、ワーカープロセスが 512MB のメモリを確保した上で、そのメモリにアクセスしている。

ps(1) で確認すると、ワーカーのプロセスの VSZ (仮想メモリ) と RSS (物理メモリ) が増加している。

$ ps -C stress -o command,pid,vsz,rss
COMMAND                         PID    VSZ   RSS
stress -m 1 --vm-bytes 512M   23977   3704  1316
stress -m 1 --vm-bytes 512M   23978 527996 525276
$ pstree -p $(pgrep stress | head -n 1)
stress(23977)───stress(23978)

--vm-hang 0 を指定しないと、どのような挙動になるだろうか。

$ stress -m 1 --vm-bytes 512M

プロセスのメモリを見ると、VSZ はそのままで RSS が定期的に増えたり減ったりするはず。

$ watch -n 1 ps -C stress -o command,pid,vsz,rss

なぜこのような挙動になるかは -v オプションをつけて実行すると理解できる。 一瞬でログが流れるので head(1) を使って先頭だけ確認する。

$ stress -m 1 --vm-bytes 512M -v | head -n 15
stress: info: [23993] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd
stress: dbug: [23993] using backoff sleep of 3000us
stress: dbug: [23993] --> hogvm worker 1 [23995] forked
stress: dbug: [23995] allocating 536870912 bytes ...
stress: dbug: [23995] touching bytes in strides of 4096 bytes ...
stress: dbug: [23995] freed 536870912 bytes
stress: dbug: [23995] allocating 536870912 bytes ...
stress: dbug: [23995] touching bytes in strides of 4096 bytes ...
stress: dbug: [23995] freed 536870912 bytes
stress: dbug: [23995] allocating 536870912 bytes ...
stress: dbug: [23995] touching bytes in strides of 4096 bytes ...
stress: dbug: [23995] freed 536870912 bytes
stress: dbug: [23995] allocating 536870912 bytes ...
stress: dbug: [23995] touching bytes in strides of 4096 bytes ...
stress: dbug: [23995] freed 536870912 bytes
stress: FAIL: [23993] (416) <-- worker 23995 got signal 13
stress: WARN: [23993] (418) now reaping child worker processes
stress: FAIL: [23993] (452) failed run completed in 1s

上記から、メモリを確保してアクセスしたらすぐに開放するのをずっと繰り返すことが分かる。

ちなみに --vm-hang はメモリをアクセスした後に開放するまでの時間を制御できる。 たとえば引数に 2 を指定すると、アクセス後に 2 秒待ってから開放する挙動になる。

$ stress -m 1 --vm-bytes 512M --vm-hang 2 -v | head -n 15
stress: info: [24077] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd
stress: dbug: [24077] using backoff sleep of 3000us
stress: dbug: [24077] --> hogvm worker 1 [24079] forked
stress: dbug: [24079] allocating 536870912 bytes ...
stress: dbug: [24079] touching bytes in strides of 4096 bytes ...
stress: dbug: [24079] sleeping for 2s with allocated memory
stress: dbug: [24079] freed 536870912 bytes
stress: dbug: [24079] allocating 536870912 bytes ...
stress: dbug: [24079] touching bytes in strides of 4096 bytes ...
stress: dbug: [24079] sleeping for 2s with allocated memory
stress: dbug: [24079] freed 536870912 bytes
stress: dbug: [24079] allocating 536870912 bytes ...
stress: dbug: [24079] touching bytes in strides of 4096 bytes ...
stress: dbug: [24079] sleeping for 2s with allocated memory
stress: dbug: [24079] freed 536870912 bytes
stress: FAIL: [24077] (416) <-- worker 24079 got signal 13
stress: WARN: [24077] (418) now reaping child worker processes
stress: FAIL: [24077] (452) failed run completed in 7s

なお、-m の引数を増やしたときは、各ワーカープロセスごとにメモリの確保・アクセス・開放をすることになる。

$ stress -m 2 --vm-bytes 512M
$ pstree -p $(pgrep stress | head -n 1)
stress(21021)─┬─stress(21022)
              └─stress(21023)
$ ps -C stress -o command,pid,vsz,rss
COMMAND                         PID    VSZ   RSS
stress -m 2 --vm-bytes 512M   21021   3704  1376
stress -m 2 --vm-bytes 512M   21022 527996 273084
stress -m 2 --vm-bytes 512M   21023 527996 383700

また、--vm-keep を指定すると、メモリを開放しなくなる。 つまり、一度確保したメモリにアクセスすることだけを何度も繰り返す。 たとえば --vm-hang と組み合わせると、インターバルを入れながらメモリにアクセスする。

$ stress -m 1 --vm-keep --vm-hang 2 -v | head -n 10
stress: info: [27420] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd
stress: dbug: [27420] using backoff sleep of 3000us
stress: dbug: [27420] --> hogvm worker 1 [27422] forked
stress: dbug: [27422] allocating 268435456 bytes ...
stress: dbug: [27422] touching bytes in strides of 4096 bytes ...
stress: dbug: [27422] sleeping for 2s with allocated memory
stress: dbug: [27422] touching bytes in strides of 4096 bytes ...
stress: dbug: [27422] sleeping for 2s with allocated memory
stress: dbug: [27422] touching bytes in strides of 4096 bytes ...
stress: dbug: [27422] sleeping for 2s with allocated memory
stress: FAIL: [27420] (416) <-- worker 27422 got signal 13
stress: WARN: [27420] (418) now reaping child worker processes
stress: FAIL: [27420] (452) failed run completed in 6s

ちなみに、上記で「メモリにアクセスする」というのは char の 'Z' を各アドレスに書き込むことを指している。

ディスクに負荷をかける

続いてはディスクに負荷をかける方法について。

ディスクに負荷をかけるときは -d オプションを指定する。 引数はこれまでと同じでワーカープロセスの数になる。

$ stress -d 1

dstat(1) で確認すると、次のように確かにディスクに書き込みが生じている。

$ dstat -d -D sda,total
--dsk/sda----dsk/total-
 read  writ: read  writ
   0   822M:   0   822M
   0  1710M:   0  1710M
   0   651M:   0   651M
...

stress(1) の実装としてはカレントワーキングディレクトリ以下に mkstemp(3) を使ってファイルを作ることで実現している。 そのため負荷をかけたい特定のディスクがあるならマウントしたパスで実行する必要がある。 上記はカレントワーキングディレクトリが sda デバイス上にあることを示す。

挙動は -v オプションをつけて実行すると分かりやすい。 実行していても作業中のファイルが見えないのは、ファイルを開いた直後に unlink(2) しているため。

$ stress -d 1 -v
stress: info: [22636] dispatching hogs: 0 cpu, 0 io, 0 vm, 1 hdd
stress: dbug: [22636] using backoff sleep of 3000us
stress: dbug: [22636] --> hoghdd worker 1 [22637] forked
stress: dbug: [22637] seeding 1048575 byte buffer with random data
stress: dbug: [22637] opened ./stress.H9UjIh for writing 1073741824 bytes
stress: dbug: [22637] unlinking ./stress.H9UjIh
stress: dbug: [22637] fast writing to ./stress.H9UjIh
stress: dbug: [22637] slow writing to ./stress.H9UjIh
stress: dbug: [22637] closing ./stress.H9UjIh after 1073741824 bytes
stress: dbug: [22637] opened ./stress.0X3Feo for writing 1073741824 bytes
stress: dbug: [22637] unlinking ./stress.0X3Feo
stress: dbug: [22637] fast writing to ./stress.0X3Feo
stress: dbug: [22637] slow writing to ./stress.0X3Feo
stress: dbug: [22637] closing ./stress.0X3Feo after 1073741824 bytes
stress: dbug: [22637] opened ./stress.ErRA1c for writing 1073741824 bytes
stress: dbug: [22637] unlinking ./stress.ErRA1c
stress: dbug: [22637] fast writing to ./stress.ErRA1c
stress: dbug: [22637] slow writing to ./stress.ErRA1c
stress: dbug: [22637] closing ./stress.ErRA1c after 1073741824 bytes

作られるファイルサイズはデフォルトで 1GB になっている。 ファイルサイズは --hdd-bytes オプションで指定できる。 指定したファイルサイズが小さいと、短いスパンで作業用のファイルが作られたり消えたりする。

$ stress -d 1 --hdd-bytes 1M -v
stress: info: [22663] dispatching hogs: 0 cpu, 0 io, 0 vm, 1 hdd
stress: dbug: [22663] using backoff sleep of 3000us
stress: dbug: [22663] --> hoghdd worker 1 [22665] forked
stress: dbug: [22665] seeding 1048575 byte buffer with random data
stress: dbug: [22665] opened ./stress.r7I03v for writing 1048576 bytes
stress: dbug: [22665] unlinking ./stress.r7I03v
stress: dbug: [22665] fast writing to ./stress.r7I03v
stress: dbug: [22665] slow writing to ./stress.r7I03v
stress: dbug: [22665] closing ./stress.r7I03v after 1048576 bytes
stress: dbug: [22665] opened ./stress.lN80Ka for writing 1048576 bytes
stress: dbug: [22665] unlinking ./stress.lN80Ka
stress: dbug: [22665] fast writing to ./stress.lN80Ka
stress: dbug: [22665] slow writing to ./stress.lN80Ka
stress: dbug: [22665] closing ./stress.lN80Ka after 1048576 bytes
...

また、これは直接的なディスクへの負荷ではないけど、関連するオプションとして -i がある。 これは sync(2) を発行しまくるワーカープロセスを作るためのオプション。 sync(2) は、キャッシュされた書き込みを永続ストレージに同期するためのシステムコール。

$ stress -i 1
stress: info: [22656] dispatching hogs: 0 cpu, 1 io, 0 vm, 0 hdd

上記を実行すると無限ループで sync(2) を実行しまくるワーカープロセスが生成される。 ユースケースとしては -d オプションと組み合わせて使う感じなのかな。

まとめ

今回は stress(1) を使って Unix 系 OS で動作するホストに様々な負荷をかける方法について見てみた。

参考

github.com

いつの間にか MLflow Tracking Server が Artifact のプロキシに対応していた

以前の MLflow Tracking Server では、アーティファクトを保存する場所については URI としてクライアントに伝えるだけだった。 クライアントは、サーバから教えてもらった URI に自分でつなぎにいく。 この形では、アクセスするためのクレデンシャルがそれぞれのクライアントで必要になるなど、利便性にやや欠ける面があった。

そんな折、どうやら MLflow v1.24 から Tracking Server がアーティファクトをストレージとの間でプロキシする機能が追加されたらしい。 ドキュメントにも、通信のシナリオとして以前は存在しなかった 5 と 6 が追加されている。

www.mlflow.org

今回は、上記の追加された機能を試してみよう。 なお、通信のシナリオでいうと 5 に該当する。

使った環境は次のとおり。

$ sw_vers
ProductName:    macOS
ProductVersion: 12.4
BuildVersion:   21F79
$ uname -srm                     
Darwin 21.5.0 arm64
$ python -V
Python 3.9.13
$ pip list | grep -i mlflow
mlflow                        1.27.0

もくじ

下準備

下準備として、あらかじめ必要なパッケージ類をインストールする。 今回は Artifact を保存するバックエンドに S3 互換のオブジェクトストレージを使いたい。 そのために、OSS の S3 互換オブジェクトストレージの実装である MinIO をインストールしておく。 また、S3 を操作するために AWS CLI も入れておこう。

$ brew install minio awscli

そして、肝心の MLflow と、バックエンドに S3 互換のオブジェクトストレージを使うために boto3 も入れておく。

$ pip install mlflow boto3

インストールが終わったら MinIO のサーバを起動する。

$ mkdir -p /tmp/minio
$ minio server /tmp/minio

続いて、MinIO にアクセスできることを確認する。 デフォルトのクレデンシャルを使ってバケットの一覧を確認する。 この時点では特に何も表示されなければ大丈夫。

$ export AWS_ACCESS_KEY_ID=minioadmin
$ export AWS_SECRET_ACCESS_KEY=minioadmin
$ aws --endpoint-url http://localhost:9000 s3 ls

MLflow Tracking Server が使うためのバケットを作成する。 ここでは mlflow-example という名前にした。

$ aws s3 --endpoint-url http://localhost:9000 mb s3://mlflow-example
make_bucket: mlflow-example

プロキシが有効な MLflow Tracking Server を起動する

以上で準備が終わった。 次に Artifact のプロキシを有効にした MLflow Tracking Server を起動する。 そのためには mlflow server サブコマンドを実行するときに --serve-artifacts オプションをつける。 また、同時にアーティファクトを保存するバックエンドを --artifacts-destination で指定する。 バックエンドが S3 互換ストレージであれば、エンドポイントやクレデンシャルの情報を環境変数で渡せる。

$ export MLFLOW_S3_ENDPOINT_URL=http://127.0.0.1:9000
$ export AWS_ACCESS_KEY_ID=minioadmin
$ export AWS_SECRET_ACCESS_KEY=minioadmin
$ mlflow server \
    --backend-store-uri sqlite:///tracking.db \
    --artifacts-destination s3://mlflow-example/artifacts \
    --serve-artifacts

このように、Tracking Server がストレージとの間でアーティファクトをプロキシするパターンでは、クレデンシャルなどの情報がサーバ側に集約される。

プロキシが有効な MLflow Tracking Server を利用する

MLflow Tracking Server が起動したら、それを使ってみよう。

いくつかのやり方があるけど、ここでは環境変数 MLFLOW_TRACKING_URI を使って MLflow Tracking Server の URI を指定する。

$ export MLFLOW_TRACKING_URI=http://127.0.0.1:5000

そして Python のインタプリタを起動しよう。

$ python

mlflow パッケージをインポートする。

>>> import mlflow

mlflow.get_tracking_uri() 関数で、Tracking URI が先ほど環境変数で指定したものになっていることを確認する。

>>> mlflow.get_tracking_uri()
'http://127.0.0.1:5000'

また、mlflow.get_artifact_uri() で、Artifact URI が mlflow-artifacts: から始まっていることを確認する。 こうなっていれば MLflow Tracking Server が Artifact をプロキシしてくれることを示している。 この形では、クライアント側にアーティファクトを保存するためのクレデンシャルは必要ない。

>>> mlflow.get_artifact_uri()
'mlflow-artifacts:/0/b91a91c68cbf4928b3069c6222479f03/artifacts'

試しに適当なテキストファイルをアーティファクトとして記録してみよう。

>>> with tempfile.TemporaryDirectory() as d:
...    filename = 'test-artifact'
...    artifact_path = pathlib.Path(d) / filename
...    with open(artifact_path, 'w') as fp:
...        print('Hello, World!', file=fp)
...    mlflow.log_artifact(artifact_path)
... 

実行できたら mlflow.end_run() で実験を完了する。

>>> mlflow.end_run()

記録したアーティファクトが MinIO のストレージに記録されているか確認しよう。

$ aws --endpoint-url http://localhost:9000 s3 ls --recursive s3://mlflow-example
2022-07-03 17:41:01         14 artifacts/0/b91a91c68cbf4928b3069c6222479f03/artifacts/test-artifact

たしかに、なにかファイルができている。

中身を表示してみよう。

$ aws --endpoint-url http://localhost:9000 s3 cp s3://mlflow-example/artifacts/0/b91a91c68cbf4928b3069c6222479f03/artifacts/test-artifact -
Hello, World!

ちゃんと、先ほど記録したメッセージが表示された。 どうやら、ちゃんとプロキシが機能しているようだ。

まとめ

今回は MLflow v1.24 で追加された Tracking Server のアーティファクトをプロキシする機能を試してみた。 このパターンでは、クライアント側にストレージにアクセスするためのクレデンシャルが必要なくなる。 また、クライアントからストレージに直接疎通がないような構成でも問題がない。 ただし、逆に言えばクレデンシャルなしでクライアントがストレージにアクセスできてしまう点にはアクセスコントロールの観点で注意が必要となる。

Linux の IPC Namespace について

Linux のコンテナ仮想化を実現する機能の一つに Namespace がある。 Namespace はプロセスが動作する際のリソースをカーネルの中で隔離 (分離) する仕組み。 Namespace は隔離する対象のリソースによって色々とある。

man7.org

今回は、その中でも IPC (Inter Process Communication) に関するリソースを隔離する仕組みの IPC Namespace について扱う。 ここでいう IPC には、たとえば SystemV IPC と POSIX IPC がある。 今回は、unshare(1) と unshare(2) を使って SystemV IPC に関するリソースが Namespace によって隔離される様子を観察してみる。

使った環境は次のとおり。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 22.04 LTS
Release:    22.04
Codename:   jammy
$ uname -srm
Linux 5.18.0-051800-generic aarch64
$ unshare --version
unshare from util-linux 2.37.2
$ gcc --version
gcc (Ubuntu 11.2.0-19ubuntu1) 11.2.0
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

もくじ

下準備

あらかじめ必要なパッケージをインストールしておく。

$ sudo apt-get update
$ sudo apt-get install \
    util-linux \
    build-essential \
    python3-sysv-ipc

SystemV IPC について

今回は IPC Namespace の動作確認のために SystemV IPC を使う。 SystemV IPC は、その名のとおり UNIX System V で導入された IPC の仕組み。 SystemV IPC はメッセージキュー、共有メモリ、セマフォという 3 種類の機能を提供している。 操作するためには msgget(2) や msgsnd(2) といったシステムコールを使う。 詳細は man 7 sysvipc を参照する。

man7.org

ただし、今回は SystemV IPC 自体を詳しく解説したいわけではない。 そこで、操作には util-linux に含まれる ipcs(1) と Python ラッパーの sysv-ipc を使う。

たとえば ipcs(1) をオプションなしで実行すると、SystemV IPC に関するリソースの利用状況がわかる。 初期状態では、特に何も作られていない。

$ ipcs

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages    

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status      

------ Semaphore Arrays --------
key        semid      owner      perms      nsems     

ここで、試しにメッセージキューを作ってみよう。 まずは、Python のインタプリタを起動する。

$ python3

sysv_ipc パッケージをインポートして、0x100 というキーでメッセージキューを作る。

>>> import sysv_ipc
>>> q = sysv_ipc.MessageQueue(key=0x100, flags=sysv_ipc.IPC_CREAT, mode=0o644)

別のターミナルから ipcs(1) を実行すると、メッセージキューができていることがわかる。 -q オプションをつけるとメッセージキューに関する情報だけ表示できる。

$ ipcs -q

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages    
0x00000100 0          vagrant    644        0            0           

キューに対してオブジェクトを送ってみよう。

>>> q.send("Hello, World!")

ここで、別のプロセスから Python のインタプリタを起動する。

$ python3

そして、先ほどと同じキー 0x100 を指定してメッセージキューを参照する。

>>> import sysv_ipc
>>> q = sysv_ipc.MessageQueue(key=0x100, mode=0o644)

キューのオブジェクトに対して receive() メソッドを実行すると、先ほど送ったメッセージが取得できる。

>>> q.receive()
(b'Hello, World!', 1)

結果はタプルになっていて、2 番目の要素はメッセージを送るときにつけた type を表している。 デフォルトでは 1 になっており、これはキューの中をさらに細分化して扱うための仕組みのようだ。

unshare(1) を使って IPC Namespace を操作する

さて、SystemV IPC の基本的な説明が終わったので、ここから本題の IPC Namespace を扱っていく。 まずは unshare(1) を使って IPC Namespace を操作してみよう。

現在のプロセスが所属する IPC Namespace は /proc/self/ns/ipc で確認できる。 以下であれば 4026531839 という識別子に所属している。

$ file /proc/self/ns/ipc 
/proc/self/ns/ipc: symbolic link to ipc:[4026531839]

ここで unshare(1) を --ipc オプションをつけて実行してみよう。 同時に bash(1) を起動する。

$ sudo unshare --ipc bash

すると、所属する IPC Namespace が 4026532177 へと変化したことがわかる。

# file /proc/self/ns/ipc 
/proc/self/ns/ipc: symbolic link to ipc:[4026532177]

ipcs(1) を実行すると、先ほどまで見えていたメッセージキューも表示されなくなっている。 これが正に IPC Namespace の機能であり、IPC に関するリソースを隔離できている。

# ipcs

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages    

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status      

------ Semaphore Arrays --------
key        semid      owner      perms      nsems     

試しに 0x1000 というキーでメッセージキューを作ると、ちゃんと作成できる。

# python3 -c "import sysv_ipc; sysv_ipc.MessageQueue(key=0x1000, flags=sysv_ipc.IPC_CREAT, mode=0o644)"
# ipcs

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages    
0x00001000 0          root       644        0            0           

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status      

------ Semaphore Arrays --------
key        semid      owner      perms      nsems     

元々のターミナル、つまりシステムにおいて ipcs(1) を実行するとキーが 0x100 のメッセージキューが見える。

$ ipcs

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages    
0x00000100 0          vagrant    644        0            0           

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status      

------ Semaphore Arrays --------
key        semid      owner      perms      nsems     

ちゃんとメッセージキューが Namespace ごとに隔離されている様子が確認できた。

unshare(1) で起動した bash(1) は一旦終了しておく。

# exit

unshare(2) を使って IPC Namespace を操作する

続いては unshare(2) のシステムコールを使って IPC Namespace を操作してみよう。

下記のサンプルコードでは unshare(2) の引数に CLONE_NEWIPC を指定することで新しく IPC Namespace を作成している。 その上で execvp(3) を使ってシェルを起動している。

#define _GNU_SOURCE
  
#include <sched.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    // unshare(2) で IPC Namespace を作成する
    if (unshare(CLONE_NEWIPC) != 0) {
        fprintf(stderr, "Failed to create a new IPC namespace: %s\n", strerror(errno));
        exit(EXIT_FAILURE);
    }

    // execvp(3) でシェルを起動する
    char* const args[] = {"bash", NULL};
    if (execvp(args[0], args) != 0) {
        fprintf(stderr, "Failed to exec \"%s\": %s\n", args[0], strerror(errno));
        exit(EXIT_FAILURE);
    }

    return EXIT_SUCCESS;
}

上記をビルドする。

$ gcc -Wall example.c

ビルドしたら実行しよう。

$ sudo ./a.out

実行して起動されるシェルからは、先ほどと同じようにシステムのメッセージキューが表示されない。

# ipcs

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages    

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status      

------ Semaphore Arrays --------
key        semid      owner      perms      nsems     

/proc/self/ns/ipc の識別子についても、システムとは異なっている。

# file /proc/self/ns/ipc
/proc/self/ns/ipc: symbolic link to ipc:[4026532177]

今回も、試しにメッセージキューを作ってみよう。

# python3 -c "import sysv_ipc; sysv_ipc.MessageQueue(key=0x1000, flags=sysv_ipc.IPC_CREAT, mode=0o644)"
# ipcs

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages    
0x00001000 0          root       644        0            0           

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status      

------ Semaphore Arrays --------
key        semid      owner      perms      nsems     

やはり、システムで実行する ipcs(1) とは SystemV IPC のリソースが隔離されていることが分かる。

$ ipcs

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages    
0x00000100 0          vagrant    644        0            0           

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status      

------ Semaphore Arrays --------
key        semid      owner      perms      nsems     

まとめ

今回は IPC Namespace を使って、SystemV IPC に関連するリソースが隔離される様子を観察してみた。

nvidia-smi(1) で GPU にパワーリミットを設定して消費電力や発熱を減らす

自宅にあるオンプレマシンでグラフィックカードを GPGPU の用途に使用していると、消費電力や発熱は切実な問題になりうる。 特に昨今は電気代の値上がりも著しいし、発熱は製品寿命の短縮や夏だと室温の上昇につながる。 そこで、今回は Linux の環境で nvidia-smi(1) を使って NVIDIA の GPU にパワーリミットを設定することで消費電力や発熱の低減を目指してみる。

使った環境は次のとおり。 Ubuntu 20.04 LTS のマシンに、Docker と nvidia-container-toolkit がインストールしてある。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.4 LTS
Release:    20.04
Codename:   focal
$ uname -srm
Linux 5.13.0-51-generic x86_64
$ docker version
Client: Docker Engine - Community
 Version:           20.10.17
 API version:       1.41
 Go version:        go1.17.11
 Git commit:        100c701
 Built:             Mon Jun  6 23:02:57 2022
 OS/Arch:           linux/amd64
 Context:           default
 Experimental:      true

Server: Docker Engine - Community
 Engine:
  Version:          20.10.17
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.17.11
  Git commit:       a89b842
  Built:            Mon Jun  6 23:01:03 2022
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.6.6
  GitCommit:        10c12954828e7c7c9b6e0ea9b0c02b01407d3ae1
 runc:
  Version:          1.1.2
  GitCommit:        v1.1.2-0-ga916309
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0
$ nvidia-smi
Fri Jun 24 18:59:39 2022       
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 515.48.07    Driver Version: 515.48.07    CUDA Version: 11.7     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|                               |                      |               MIG M. |
|===============================+======================+======================|
|   0  NVIDIA GeForce ...  On   | 00000000:01:00.0 Off |                  N/A |
|  0%   40C    P8     7W / 170W |     14MiB / 12288MiB |      0%      Default |
|                               |                      |                  N/A |
+-------------------------------+----------------------+----------------------+
                                                                               
+-----------------------------------------------------------------------------+
| Processes:                                                                  |
|  GPU   GI   CI        PID   Type   Process name                  GPU Memory |
|        ID   ID                                                   Usage      |
|=============================================================================|
|    0   N/A  N/A      1087      G   /usr/lib/xorg/Xorg                  9MiB |
|    0   N/A  N/A      1232      G   /usr/bin/gnome-shell                3MiB |
+-----------------------------------------------------------------------------+

もくじ

事前準備

ベンチマーク用に PyTorch を使いたいので、あらかじめ公式の Docker イメージをプルしておく。 そして、コンテナから GPU が見えることを確認する。

$ docker pull pytorch/pytorch
$ docker run --gpus all --rm -it pytorch/pytorch nvidia-smi
Fri Jun 24 19:03:09 2022       
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 515.48.07    Driver Version: 515.48.07    CUDA Version: 11.7     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|                               |                      |               MIG M. |
|===============================+======================+======================|
|   0  NVIDIA GeForce ...  On   | 00000000:01:00.0 Off |                  N/A |
|  0%   39C    P8     8W / 170W |     14MiB / 12288MiB |      0%      Default |
|                               |                      |                  N/A |
+-------------------------------+----------------------+----------------------+
                                                                               
+-----------------------------------------------------------------------------+
| Processes:                                                                  |
|  GPU   GI   CI        PID   Type   Process name                  GPU Memory |
|        ID   ID                                                   Usage      |
|=============================================================================|
+-----------------------------------------------------------------------------+

GPU で使用できるパワーリミットを確認する

まずは使っている GPU に設定できる最低・最大のパワーリミットを確認する。 今回使った製品であれば、最低が 100W で最大が 170W だと分かる。 デフォルトではパワーリミットの値は最大に設定されているはず。

$ nvidia-smi -q -d POWER

==============NVSMI LOG==============

Timestamp                                 : Fri Jun 24 19:05:09 2022
Driver Version                            : 515.48.07
CUDA Version                              : 11.7

Attached GPUs                             : 1
GPU 00000000:01:00.0
    Power Readings
        Power Management                  : Supported
        Power Draw                        : 7.75 W
        Power Limit                       : 170.00 W
        Default Power Limit               : 170.00 W
        Enforced Power Limit              : 170.00 W
        Min Power Limit                   : 100.00 W
        Max Power Limit                   : 170.00 W
    Power Samples
        Duration                          : 105.50 sec
        Number of Samples                 : 119
        Max                               : 8.42 W
        Min                               : 7.46 W
        Avg                               : 7.79 W

GPU にパワーリミットを設定する

GPU にパワーリミットを設定するには nvidia-smi(1) の -pl オプションを使う。 このオプションに、先ほど得られた設定できるパワーリミットの範囲でワット数を指定すれば良い。

たとえば、今回の環境における下限の 100W に設定するには次のようにする。

$ sudo nvidia-smi -pl 100
Power limit for GPU 00000000:01:00.0 was set to 100.00 W from 170.00 W.
All done.

もう一度 nvidia-smi(1) をオプションなしで実行してみると、ワット数の表示が 100W になっていることが確認できる。

$ nvidia-smi
Fri Jun 24 19:06:28 2022       
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 515.48.07    Driver Version: 515.48.07    CUDA Version: 11.7     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|                               |                      |               MIG M. |
|===============================+======================+======================|
|   0  NVIDIA GeForce ...  On   | 00000000:01:00.0 Off |                  N/A |
|  0%   39C    P8     9W / 100W |     14MiB / 12288MiB |      0%      Default |
|                               |                      |                  N/A |
+-------------------------------+----------------------+----------------------+
                                                                               
+-----------------------------------------------------------------------------+
| Processes:                                                                  |
|  GPU   GI   CI        PID   Type   Process name                  GPU Memory |
|        ID   ID                                                   Usage      |
|=============================================================================|
|    0   N/A  N/A      1087      G   /usr/lib/xorg/Xorg                  9MiB |
|    0   N/A  N/A      1232      G   /usr/bin/gnome-shell                3MiB |
+-----------------------------------------------------------------------------+

パワーリミットを設定してベンチマークを測ってみる

さて、パワーリミットを設定すれば、各瞬間の電力 (W) が減るのは分かる。 一方で、時間を通じた総和として消費電力量 (Wh) が減るのかは分からない。 そこで、パワーリミットを最小と最大に設定した状態でのパフォーマンスを調べる。

PyTorch のコンテナを起動する。

$ docker run --gpus all --rm -it pytorch/pytorch

動かすベンチマークについては、下記の PyTorch の公式チュートリアルを参照にした。

pytorch.org

下記のサンプルコードでは、内積を計算するシンプルなコードを使ってベンチマークする。

$ cat << 'EOF' > benchmark.py
import torch
import torch.utils.benchmark as benchmark


def batched_dot_bmm(a, b):
    '''Computes batched dot by reducing to bmm'''
    a = a.reshape(-1, 1, a.shape[-1])
    b = b.reshape(-1, b.shape[-1], 1)
    return torch.bmm(a, b).flatten(-3)


# Input for benchmarking
x = torch.randn(100000, 10000, device='cuda')


t = benchmark.Timer(
    stmt='batched_dot_bmm(x, x)',
    setup='from __main__ import batched_dot_bmm',
    globals={'x': x},
)

print(t.timeit(1000))
EOF

まずはパワーリミットを下限の 100W に設定した状態で実行する。 同時に、nvidia-smi(1) を実行して、消費電力が上限の 100W に張り付くことを確認しておこう。

# python benchmark.py 
<torch.utils.benchmark.utils.common.Measurement object at 0x7fd2ba7fa460>
batched_dot_bmm(x, x)
setup: from __main__ import batched_dot_bmm
  16.23 ms
  1 measurement, 1000 runs , 1 thread

上記から、1 回の演算に平均で 16.23ms かかることがわかった。

続いてはパワーリミットを上限の 170W に戻す。

$ sudo nvidia-smi -pl 170
Power limit for GPU 00000000:01:00.0 was set to 170.00 W from 100.00 W.
All done.

そして、プログラムをもう一度実行する。 今度も、nvidia-smi(1) を実行して、消費電力が上限の 170W に張り付くことを確認しておこう。

# python benchmark.py 
<torch.utils.benchmark.utils.common.Measurement object at 0x7ff3be37b460>
batched_dot_bmm(x, x)
setup: from __main__ import batched_dot_bmm
  13.76 ms
  1 measurement, 1000 runs , 1 thread

上記から、今度は 1 回の演算に平均で 13.76ms かかることがわかった。

演算にかかる電力は 170W と 100W なので約 40 % 低下している。 そして、演算にかかる時間は 13.76ms と 16.23ms なので約 18 % 増加した。 ここから、トータルの電力量は 100W において 0.6 * 1.18 = 0.708 になる。 あくまで今回の条件下ではという但し書きはつくものの、計算に使用する電力量は約 71% まで減らせたようだ。

一般に半導体のワットパフォーマンスはリニアな関係ではなく、入力する電力が大きくなるほどパフォーマンス向上の効率が悪くなると言われる。 その点からも、今回の結果には納得がいく。 また、電力量が減れば発熱も小さくなるため、暖房器具としての性能も低下するし製品寿命の延長が望める可能性もある。

まとめ

今回は nvidia-smi(1) を使って NVIDIA の GPU にパワーリミットを設定して消費電力と発熱の低減を試みた。 オンプレマシンの消費電力や発熱に悩んでいる場合には、パフォーマンスとのトレードオフはあるものの、一考の余地はあるかもしれない。

いじょう。

qrencode と viu を使ってターミナルで QR コードを作って表示する

情報共有などのために、ささっと QR コードを作って読み込ませたいときがある。 そんなときは qrencode と viu を使うとターミナル上で完結して楽そうだ。

使った環境は次のとおり。

$ sw_vers   
ProductName:    macOS
ProductVersion: 12.4
BuildVersion:   21F79
$ qrencode -V                         
qrencode version 4.1.1
Copyright (C) 2006-2017 Kentaro Fukuchi
$ viu -V
viu 1.4.0

もくじ

下準備

まずは qrencode と viu をインストールしておく。

$ brew install qrencode viu 

ターミナルで QR コードを作る

qrencode を使うと、次のように引数で指定した文字列を埋め込んだ QR コードが作れる。 -o オプションで保存先を指定する。

$ qrencode -o greet.png "Hello, World"

できあがる QR コードはこんな感じ。

ターミナルで QR コードを表示する

ターミナルで QR コードを作ったなら、表示までターミナルで完結したい気持ちもある。 そんなときは viu で開くと良い。

$ viu greet.png

こんな感じで表示できる。

viu を使って QR コードの画像を開く

目視ではかなりざらついてるように見えるけど、デバイスに読み込ませてみるとちゃんと読める。 QR コードの堅牢性おそるべし。 もし読めないときは viu コマンドの -w オプションとかを使って大き目に表示させると良い。

ワンライナーで QR コードを作って表示する

ちなみに qrencode も viu も標準入出力を読み書きできる。 そのため、両者を次のように組み合わせることも可能だ。

$ qrencode -o - "Hello, World" | viu -

これで、ファイルを介すことなく QR コードをターミナル上に表示できる。

めでたしめでたし。

子育てにかかる費用を公的データで調べる

一般的に、子どもを一人育てるのには 2,000 万円かかるとか言われている。 しかし、何も子どもが生まれた瞬間に 2,000 万円が必要になるわけではない。 もちろん、2,000 万円を均等に割った額が毎年かかるわけでもない。 そもそも、こういった数字はあくまで代表的な家庭における目安に過ぎないはず。 たとえば、子どもがどういった就学をするのかだったり、世帯収入にも依存していると考えられる。 そこで、今回は子育てにかかる費用に対する自分の解像度を上げるために、公的な統計資料を当たってみることにした。

もくじ

インターネットによる子育て費用に関する調査

まず、第1子が誕生してから中学校を卒業するまでの費用に関しては、平成21年度に内閣府が実施した調査が参考になる。 この資料は教育費に限らず、子育てにかかる全般的な費用について扱っている。

www8.cao.go.jp

上記資料の「第3章 調査結果」に掲載されているグラフを以下に引用する。 このグラフからは、それぞれの就学区分ごとに第1子の子育てをするのに年間でいくらかかっているか分かる。

出典: 平成21年度インターネットによる子育て費用に関する調査 全体版 第3章 調査結果 42 ページ 図表 1-1.第1子一人当たりの年間子育て費用額(対象者全体平均)【第1子の就学区分別】

未就園児は 84 万円、保育園・幼稚園児は 121 万円、小学生は 115 万円、中学生は 155 万円が平均で必要となるようだ。 なお、この資料では就学先が私立か国公立かを区別していない。 つまり、調査対象とした全体の平均である点に留意が必要となる。

また、上記のグラフが掲載されている次のページには、各年齢ごとに分解したグラフが掲載されている。 グラフからは、年齢が上がる毎に学校外教育費 (塾や習い事) がじわじわと増加していく様子や、学校教育費が中学校から一気に増加する様子が伺える。

世帯年収ごとの子育て費用額の比較も興味深い。 以下は「保育所・幼稚園児」について、世帯年収ごとの年間子育て費用額を比較したグラフになっている。 このグラフからは、子どもが小さいうちは世帯年収ごとの子育て費用額の差は「子どものための預貯金・保険」や「レジャー・旅行費」に大きく出ることが伺える。

出典: 平成21年度インターネットによる子育て費用に関する調査 全体版 【参考資料】 37ページ 図表 4-3.世帯年収別にみた「保育所・幼稚園児」第1子一人当たりの年間子育て費用額(対象者全体平均)

一方で、以下は「中学生」について、世帯年収ごとの年間子育て費用額を比較したグラフになっている。 こちらのグラフでは、特に高い世帯年収において、子育て費用額の差が「学校教育費」や「学校外活動費」に大きく出ている。 つまり、子どもの年齢によって資金の余裕が振り分けられる先が少し変わるようだ。 これらも、自分たちの世帯年収に合わせて考える上で参考になるだろう。

出典: 平成21年度インターネットによる子育て費用に関する調査 全体版 【参考資料】 38ページ 図表 4-5.世帯年収別にみた「中学生」第1子一人当たりの年間子育て費用額(対象者全体平均)

ちなみに、「【参考資料】」に掲載されているグラフには子育てにかかる費用の構造が図示されており、こちらも面白い。

出典: 平成21年度インターネットによる子育て費用に関する調査 全体版 【参考資料】110 ページ掲載図表

子供の学習費調査

高校までの学習費に焦点を絞った資料としては、文部科学省が平成30年度に実施した調査が参考になる。 この資料では幼稚園から高校までの学習費を、国公立と私立で区別して調査している。

www.mext.go.jp

上記資料の「調査の結果 > 結果の概要 > 平成30年度 > 2.調査結果の概要」に掲載されているグラフを以下に引用する。

出典: 平成30年度 子供の学習費調査 2.調査結果の概要 図1−2 学校種別にみた学習費総額

上記から、国公立と私立で学習費に大きな差が生じることが確認できる。 私立中学の場合は公立の約 3 倍、私立高校でも公立の約 2 倍かかる。 場合によっては私立中学に進学を希望することもあるだろうし、私立高校であればより現実的な可能性として捉えておく必要がありそうだ。

教育費に関する調査結果

先ほどの文科省の調査は高校までが範囲だった。 高専・専修学校や大学など、高校以降の教育費については日本政策金融公庫の調査が詳しい。 この調査では「教育費負担の実態調査結果」を毎年公表している。

www.jfc.go.jp

上記で令和 3 年度の調査結果として掲載されているグラフを以下に引用する。 このグラフには、進学先別の年間の在学費用が載っている。

令和3年度 教育費負担の実態調査結果 6ページ 図−3 在学先別にみた1年間の在学費用(子供1人当たりの費用)

上記から、高専・専修学校や大学などへ進学すると、最低でも 100 万円程度は年間でかかることがわかる。 なお、上記は大学の費用を国公立と私立で区別していない。

大学に関して、国公立と私立の文理で区別したグラフが以下になる。

出典: 令和3年度 教育費負担の実態調査結果 6ページ 図−4 国公立・私立別にみた大学の在学費用(子供1人当たりの費用)

大学の中でも、国公立と私立の文理で費用の差が激しい。 国公立は平均で 103 万円だが、私立の文系は 152 万円、理系では 183 万円となっている。 なお、最近だと理系は大学院への進学が一般的になってきている点も心に留めておく必要がありそうだ。

また、上記の資料には自宅外通学に関する資料も載っている。 私自身は関東圏に居住していることもあってさほど可能性は高くないものの、それ以外の場合には参考になりそうだ。 とはいえ、関東圏に住んでいる場合であっても、海外へ留学する可能性は考慮する必要があるのだろうか・・・?

国民生活白書

最初に登場した「インターネットによる子育て費用に関する調査」は、子育てにかかる費用を広範に扱った調査だったが、対象は第1子に限定されていた。 では、第2子以降にかかる費用はどうなるのだろうか。 この点に関しては、内閣府が平成17年度に実施した国民生活白書の調査が参考になりそうだった。

warp.da.ndl.go.jp

平成17年版「子育て世代の意識と生活」の「第3章 子育てにかかる費用と時間 > 二人目・三人目の子どもにかける費用は逓減」に記述がある。

warp.da.ndl.go.jp

ここまで、一人の子どもを育てるための費用を見てきたが、更に子どもを育てた場合に子育て費用はどれくらい増加するのだろうか。一人の場合と同様に、「基本的経費」、「教育費」、「住宅関係費」に分けて、子どもを二人持つ世帯の「子どもを育てる費用」から子どもを一人持つ世帯の「子どもを育てる費用」を差し引いて、22年間分を足したものを「二人目の子ども9を育てる」費用として推計した。ここでいう「子どもを育てる費用」は、前項と同じく、付注3−1−1に掲げた費目における子どもを育てるための追加的な費用である。 その結果、一人の子どもを育てる費用の1,302万円に対して、二人目の子どもを育てる費用10は1,052万円と、20%程度節約されていることが分かった(第3−1−14図)。内訳ごとに節約の程度を見ると、二人目の基本的経費は一人目の80.0%、教育費は83.6%、住宅関係費は63.0%となっている。同様に三人目の子どもにかかる費用を推計すると、22年間で769万円となり、二人目と比べて更に27%程度節約されており、子どもの増加にともない、子どもにかかる費用は逓減していくことがうかがわれる。

上記より、第2子の子育てにかかる費用は第1子の約 8 割まで減少 (節約) し、第3子に至っては第1子の約 6 割まで減少することがわかる。 もし、すでに第1子がいる場合には、その実績を元に第2子以降でどれくらいかかりそうかを計算する上で目安にできそうだ。 ただし、この結果は単純に「減っている」のではなく、諸々の事情から「減らしている」ことも考慮する必要があるだろう。

いじょう。

まとめ

子育てにかかる費用が分からなかったので、公的データを調べてわかったことについて自分用にまとめた。

NVMe ストレージのデータを nvme-cli(1) で完全に消去する

ストレージ機器を破棄または譲渡するときには、漏えいを防ぐためにあらかじめデータを消去しておく必要がある。 このとき、データの消去は後から読み取りが難しいように実施しなければいけない。 後から読み取りが難しい形でデータを消去することは Secure Erase と呼んだりするようだ。 今回は NVMe ストレージを Secure Erase する方法について扱う。

まず、最近のマザーボードの UEFI には、ストレージのデータを Secure Erase するためのツールが付属していることがある。 もし、ツールが付属している場合には、それを利用するのが手っ取り早い。 では、付属していない場合にどうするか、というのが今回の話になる。 Unix 系の OS で NVMe ストレージを使っている場合には、nvme-cli(1) を使うのが良さそうだ。

今回使った環境は次のとおり。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.4 LTS
Release:    20.04
Codename:   focal
$ uname -srm
Linux 5.13.0-44-generic x86_64
$ nvme version
nvme version 1.9

下準備

Ubuntu の場合には apt を使ってインストールできる。

$ sudo apt-get -y install nvme-cli

データを消去する

まず、nvme list コマンドで消去したいデバイスを確認する。

$ sudo nvme list
Node             SN                   Model                                    Namespace Usage                      Format           FW Rev  
---------------- -------------------- ---------------------------------------- --------- -------------------------- ---------------- --------
/dev/nvme0n1     XXXXXXXXXXXX         XXXXXXXXXXXXX                            1           1.00  TB /   1.00  TB    512   B +  0 B   XXXXXXX 

あとは、nvme format コマンドに確認したデバイスを指定して実行するだけ。 このとき、オプションとして -s 2 を指定するのが望ましい。

$ nvme format /dev/nvme0n1 -s 2

先ほどの形式だとデバイスを名前空間ブロックデバイス (/dev/nvmeXnY みたいな形式) として指定していた。 それ以外にもキャラクタデバイス (/device/nvmeX みたいな形式) も受け付けられる。 先ほどと同じように namespace を指定するときは -n オプションを指定する。

$ nvme format /dev/nvme0 -s 2 -n 1

実行すると、ストレージのデータが消去される。 なお、デバイスがシステムの起動ディスクになっていても問題ない。

-s オプションについて

先ほど指定した -s オプションについて補足する。 このオプションは、データの消去をどのように実施するかを表している。 詳しくは man ページに記載されている。

$ man 1 nvme-format

manpages.ubuntu.com

オプションに指定する値の説明を、上記から引用する。 基本的には 2 を指定することで Secure Erase できる。

           │Value │ Definition                       │
           ├──────┼──────────────────────────────────┤
           │0     │ No secure erase operation        │
           │      │ requested                        │
           ├──────┼──────────────────────────────────┤
           │1     │ User Data Erase: All user data   │
           │      │ shall be erased, contents of the │
           │      │ user data after the erase is     │
           │      │ indeterminate (e.g., the user    │
           │      │ data may be zero filled, one     │
           │      │ filled, etc). The controller may │
           │      │ perform a cryptographic erase    │
           │      │ when a User Data Erase is        │
           │      │ requested if all user data is    │
           │      │ encrypted.                       │
           ├──────┼──────────────────────────────────┤
           │2     │ Cryptographic Erase: All user    │
           │      │ data shall be erased             │
           │      │ cryptographically. This is       │
           │      │ accomplished by deleting the     │
           │      │ encryption key.                  │
           ├──────┼──────────────────────────────────┤
           │3–7   │ Reserved                         │
           └──────┴──────────────────────────────────┘

なお、ソースコードを読む限り1、これらはいずれも NVMe デバイスに発行するコマンドの引数として表現されているようだ。 言いかえると、製品のファームウェアのレベルで Secure Erase の機能が実装されていることを前提としている。 そのため、製品によってはサポートされていない可能性もある。

いじょう。


  1. 内部的には libnvme というライブラリが使われていて、ユーザ空間からは ioctl(2) でデバイスを操作する