CUBE SUGAR CONTAINER

技術系のこと書きます。

Multipass の仮想マシンで X Window System のアプリケーションを使う

今回は Multipass で作った仮想マシン上で X Window System のアプリケーションを使う方法について。 やり方としては、Multipass の仮想マシンに ssh(1) を使ってログインできるようにした上で X11 Forwarding すれば良い。

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

$ sw_vers   
ProductName:    macOS
ProductVersion: 12.4
BuildVersion:   21F79
$ uname -srm
Darwin 21.5.0 x86_64
$ multipass version                                                                        
multipass   1.10.0+mac
multipassd  1.10.0+mac

もくじ

下準備

まずはローカルのマシンに X Server (XQuartz) と Multipass をインストールする。 今回は Homebrew を使って入れる場合の例になる。

$ brew install --cask xquartz multipass

XQuartz を初めてインストールする場合には、おそらくユーザの再ログインが必要になる。

そして、インストールした Multipass を使って仮想マシンを作る。 ここでは Ubuntu 20.04 LTS のホストを focal という名前で作成している。

$ multipass launch \
    --name focal \
    --cpus 2 \
    --mem 2048M \
    20.04

次のように仮想マシンが起動すれば良い。

$ multipass list       
Name                    State             IPv4             Image
focal                   Running           192.168.64.2     Ubuntu 20.04 LTS

仮想マシンに X Window System のアプリケーションをインストールする。 今回は例として x11-apps の xeyes(1) を使う。

$ multipass exec focal -- bash -c "sudo apt-get install x11-apps"

上記はワンライナーにしてあるけど、ログインした上で実行するのであれば次のようにする。 なお、ローカルのマシンと仮想マシンを区別するために、以降では仮想マシン上での操作はターミナルの先頭に (focal) とつけることにする。

$ multipass shell focal
(focal) $ sudo apt-get install x11-apps

公開鍵を仮想マシンに登録する

続いて、ローカルのマシンで生成した公開鍵を Multipass の仮想マシンに登録する。

もし、ローカルのマシンに公開鍵が無いときは、ssh-keygen(1) で作る。 オプションについてはお好みで。

$ ssh-keygen -t rsa -b 2048 -f ~/.ssh/id_rsa -P ''
$ ls ~/.ssh | grep id_rsa
id_rsa
id_rsa.pub

上記で作成した公開鍵を仮想マシンに登録する。 次のようなワンライナーで実行できる。 $(cat ~/.ssh/id_rsa.pub) の部分がローカルのマシンで生成した公開鍵にインラインで置換される。 <vm-name> の部分には仮想マシンの名前を入れる。

$ multipass exec <vm-name> -- bash -c "echo $(cat ~/.ssh/id_rsa.pub) >> ~/.ssh/authorized_keys"

今回であれば仮想マシンの名前は focal なので、次のとおり。

$ multipass exec focal -- bash -c "echo $(cat ~/.ssh/id_rsa.pub) >> ~/.ssh/authorized_keys"

ssh(1) で仮想マシンにログインする

公開鍵の登録ができたので、次は X11 Forwarding を有効にして ssh(1) で仮想マシンにログインする。

仮想マシンの IP アドレスは multipass info サブコマンドで取得できる。 ワンライナーで IP アドレスだけを抽出するには、次のようにすれば良い。

$ multipass info focal | grep IPv4 | awk '{print $2}'
192.168.64.2

上記の IP アドレスに ssh(1) でログインする。 このとき -X オプションを指定して X11 Forwarding する。

$ ssh -X \
    -o StrictHostKeyChecking=no \
    -o UserKnownHostsFile=/dev/null \
    ubuntu@$(multipass info focal | grep IPv4 | awk '{print $2}')

上記は仮想マシンのフィンガープリントをローカルのマシンに登録しない場合の例。 これは、Multipass を使っていると仮想マシンを作り直すことも多いため。

X Window System のアプリケーションを実行する

ログインできたら仮想マシン上で X Window System のアプリケーションを実行するだけ。

(focal) $ xeyes

上手くいけば次のようにウィンドウが表示されるはず。

xeyes(1) のウィンドウ

いじょう。

Vagrant の仮想マシンで X Window System のアプリケーションを使う

今回は Vagrant の仮想マシンで X Window System のアプリケーションを使う方法について。 これには、ローカルのマシンに X Server をインストールした上で、X11 Forwarding する必要がある。

使った環境は次のとおり。 Vagrant は ISA が x86 のマシンでしか動作しないので、ローカルが Intel Mac という状況は今後減っていくだろうけど。 ローカル環境ごとの違いは X Server のインストール方法くらいなので、特に支障はないはず。

$ sw_vers               
ProductName:    macOS
ProductVersion: 12.4
BuildVersion:   21F79
$ uname -srm
Darwin 21.5.0 x86_64
$ vagrant version | head -n 1
Installed Version: 2.2.19

もくじ

下準備

ローカルのマシンには X Server をインストールする。 macOS であれば XQuartz を入れれば良い。 下記は Homebrew を使って入れる場合のやり方。

$ brew install --cask xquartz

仮想マシンをセットアップする

適当なディレクトリに Vagrant の設定ファイルを作成する。 マシンイメージは、公式の Ubuntu 22.04 LTS を使った。

$ vagrant init ubuntu/jammy64

仮想マシンを起動する前に、設定ファイルに必要なオプションを指定する。 具体的には下記を Vagrantfile に追加する。 この設定がデフォルトの Vagrantfile にあればコメントアウトするだけで楽なんだけど、残念ながら無い。

config.ssh.forward_x11 = true

あとは仮想マシンを起動してログインする。

$ vagrant up && vagrant ssh

X Window System のアプリケーションを使う

仮想マシンにログインしたら、X Window System が必要になるので入れる。

$ sudo apt-get update
$ sudo apt-get -y install xserver-xorg

あとは X Window System のアプリケーションを使うだけ。 ここでは例として xeyes(1) を使う。

$ sudo apt-get -y install x11-apps
$ xeyes

うまくいけばアプリケーションのウィンドウが表示されるはず。

xeyes(1)

いじょう。

Ubuntu 22.04 LTS に後から GUI (X Window System) を追加する

サーバ版のインストールイメージを使ってセットアップした場合など、デフォルトで X Window System やデスクトップ環境が入っていないことがある。 しかし、後から必要になることも多い。 そこで、今回は Ubuntu 22.04 LTS にデスクトップ環境や X Window System を後から追加する場合のやり方を確認しておく。 なお、この確認は LTS 版のリリースが出る度に実施しているけど、やり方は Ubuntu 20.04 LTS と変わらなかった。

使った環境は次のとおり。 なお、下記は Vagrant で作成した仮想マシンを使っている。

$ 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-41-generic x86_64

もくじ

下準備

あらかじめリポジトリの情報を更新しておく。

$ sudo apt-get update

デスクトップ環境が必要な場合

デスクトップ環境が必要な場合は ubuntu-desktop パッケージをインストールする。

$ sudo apt-get -y install ubuntu-desktop

入るパッケージの数が多いので、相応に時間がかかる。

インストールが終わったらマシンを再起動する。

$ sudo shutdown -r now

再起動が終わると、グラフィカルモードでマシンが起動してくるはず。

Ubuntu 22.04 LTS のデスクトップ環境

X Window System だけで良い場合

デスクトップ環境は必要なくて、単に X Window System を使ったアプリケーションが使いたいだけという場合もある。 その場合は xserver-xorg パッケージをインストールすれば X Window System 関連の一式が入る。

$ sudo apt-get -y install xserver-xorg

後は X Window System を使ったアプリケーションを動かすだけ。 今回は試しに xeyes(1) を使ってみる。

$ sudo apt-get -y install x11-apps
$ xeyes

上手くいけば次のようにアプリケーションのウィンドウが表示されるはず。

xeyes(1)

補足

Vagrant を使って X Window System の動作確認をするには、ローカルのマシンに X Server が必要になる。

たとえば macOS を使っている場合には XQuartz をあらかじめインストールしておく。 以下は Homebrew を使った場合のインストール方法になる。

$ brew install --cask xquartz

また、Vagrant で X11 Forwarding をするには、Vagrantfile に下記の設定を入れておく必要がある。

config.ssh.forward_x11 = true

いじょう。

RPM 形式のパッケージからファイルを取り出す

主に RHEL 系の GNU/Linux ディストリビューションで採用されている RPM 形式のパッケージファイルからファイルを取り出す方法について。 必要になってたまに調べることになるのでメモとして残しておく。

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

$ cat /etc/redhat-release 
CentOS Linux release 7.9.2009 (Core)
$ uname -srm
Linux 3.10.0-1160.71.1.el7.x86_64 x86_64
$ rpm -q rpm
rpm-4.11.3-48.el7_9.x86_64
$ rpm -q cpio
cpio-2.11-28.el7.x86_64

もくじ

下準備

あらかじめ必要なパッケージとして rpm と cpio をインストールしておく。 一般的な環境であれば最初から入っているはず。

$ sudo yum install rpm cpio

また、動作確認のための RPM ファイルとして epel-release をダウンロードしておく。

$ sudo yum install --downloadonly --downloaddir=. epel-release
$ file epel-release-7-11.noarch.rpm 
epel-release-7-11.noarch.rpm: RPM v3.0 bin noarch epel-release-7-11

ファイルを取り出す

RPM 形式のパッケージからファイルを取り出すには、一旦 cpio 形式のアーカイブファイルに変換する。 そして、変換した cpio 形式のアーカイブからファイルを取り出せば良い。

ワンライナーで書くなら次のような感じ。 rpm2cpio(8) で形式を変換して、cpio(1) でファイルを展開する。

$ rpm2cpio epel-release-7-11.noarch.rpm | cpio -idv
./etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-7
./etc/yum.repos.d/epel-testing.repo
./etc/yum.repos.d/epel.repo
./usr/lib/systemd/system-preset/90-epel.preset
./usr/share/doc/epel-release-7
./usr/share/doc/epel-release-7/GPL
51 blocks

上記で使用している cpio(1) のオプションは、それぞれ次のような意味になっている。

  • -i
    • 抽出 (extract) モード
  • -d
    • 必要に応じてディレクトリを作成する
  • -v
    • 詳細モード

行を分けるなら次のとおり。 標準入力からデータを受け取らない場合、cpio(1) は -F オプションで入力ファイルを選択できる。

$ rpm2cpio epel-release-7-11.noarch.rpm > epel-release-7-11.noarch.rpm.cpio
$ file epel-release-7-11.noarch.rpm.cpio 
epel-release-7-11.noarch.rpm.cpio: ASCII cpio archive (SVR4 with no CRC)
$ cpio -idv -F epel-release-7-11.noarch.rpm.cpio 
./etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-7
./etc/yum.repos.d/epel-testing.repo
./etc/yum.repos.d/epel.repo
./usr/lib/systemd/system-preset/90-epel.preset
./usr/share/doc/epel-release-7
./usr/share/doc/epel-release-7/GPL
51 blocks

cpio(1) は展開先のディレクトリはオプションで指定できない。 そのため、必要に応じてカレントワーキングディレクトリを変更して実行する必要がある。

$ mkdir out
$ cd out
$ rpm2cpio ../epel-release-7-11.noarch.rpm | cpio -idv
./etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-7
./etc/yum.repos.d/epel-testing.repo
./etc/yum.repos.d/epel.repo
./usr/lib/systemd/system-preset/90-epel.preset
./usr/share/doc/epel-release-7
./usr/share/doc/epel-release-7/GPL
51 blocks

いじょう。

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 に関連するリソースが隔離される様子を観察してみた。