ローファイ日記

出てくるコード片、ぼくが書いたものは断りがない場合 MIT License としています http://udzura.mit-license.org/

Time namespace を試す

Time namespace と聞くと、Guitar Freaks & Drum Mania の名曲「Timepiece Phase II」を思い出してしまうんですが、僕だけでしょうか?僕だけですね...。

今日はLinux 5.6 でマージされたらしい、 Time namespace を触ってみることにしました。

git.kernel.org

この辺りでマージされた機能ですね。

Linux namespace って?

Linux のコンテナは、ホストから様々なリソースを独立させて切り離された「名前空間」に所属します。リソースの種類ごとに名前空間があり、例えばネットワークのNamespace、マウントポイント情報のNamespace、ホスト名などUTSのNamespaceなどなど、5.5までで7つのドラゴンボールこと名前空間が実装されていました。詳細は コンテナの基礎 tenforward コンテナ技術入門 hayajo などのキーワードで検索可能で、他のコンテナ技術要素とともにわかりやすくまとまっています。

お二方、雑な紹介で申し訳ありません。

そんな7つで確定されるかと思われた名前空間に新たなるメンバーが爆誕しました。それが... Time Namespace! Linux kernelの久しぶりの名前空間の追加に全コンテナフリーク(??)が沸いていることと思います。

Time Namespace って何者?

Time Namespace は文字通り時間に関する名前空間です。つまり、ホストとは別の時間を持ったコンテナを作成できるということです。聞くからに便利そう!

ですが、実は我々が一番目にするクロックである CLOCK_REALTIME はこのNamespaceとは関係ないのです(パッチのテストケースでも追加されていません)。以下のように、Time Namespaceをunshareしたコンテナの中で date -s コマンドで時刻を修正すると、ホスト側も変わったままになります。

vagrant@ubuntu-focal:~/util-linux$ sudo ./unshare -T --fork
root@ubuntu-focal:/home/vagrant/util-linux# date
Thu Apr  2 18:38:02 UTC 2020
root@ubuntu-focal:/home/vagrant/util-linux# date -s '2010-01-01 00:00:00'
Fri Jan  1 00:00:00 UTC 2010
root@ubuntu-focal:/home/vagrant/util-linux# exit
logout
vagrant@ubuntu-focal:~/util-linux$ date
Fri Jan  1 00:00:33 UTC 2010

straceなどでわかりますが date -s は内部で clock_settime(CLOCK_REALTIME) を呼ぶため、ホスト側の時間にも影響を及ぼしてしまいます。

では Time Namespace の登場で嬉しい場面はなんでしょう? ひとつは、コンテナのマイグレーションに使いたいという要望があるようです(パッチのコメントにもある通りなのですが)。

CRIUという技術があり、プロセスやコンテナのチェックポイントを作成し、そこからタスクを復旧することができます。ただし、この技術は基本的にプロセスの属性やメモリダンプから「できる限り同じように」コンテナを復元する技術です。

このCRIUで、たとえばあるホストでチェックポイントをとったコンテナを、別のホストにマイグレーションして復旧しようとした時に、あるホストと別のホストではカーネルの起動開始からの時間(MONOTONIC and BOOTTIME)が異なっているので、コンテナの方もそれに影響されて認識される CLOCK_MONOTONIC/BOOTTIME の値がマイグレーションの前後で異なってしまうという状況が起こります。時間に関する重要な操作をしているプログラムであると、そういう場合に問題が発生するかもしれません。

(CLOCK_MONOTONICなどは常に増加することが保証されている、「巻き戻ってはいけない」クロックなのですが、プロセスマイグレーションでは結果的に巻き戻ったような状態が起こり得るということです。そのような状態に対応をしていないプログラムがあるのは仕方がないと言えます)

この問題は、CRIUの側で Time Namespace を用いて「その時点でのクロック情報」を保存・再生できれば解決できます。VMマイグレーションでは当然というか問題にならない内容ですが、コンテナならではの問題が起こり得たわけですね。

なお、 CLOCK_REALTIME 相当をユーザランドでスタブしたいような要件に対しては、プログラミング言語側で当該機能をオーバーライドしたり(例: Rubytimecop )、LD_PRELOADなどで特定関数を上書きしたり(例: libfaketime )することで実現ができます。

カーネル 5.6 から使ってみる

ということでまずはTime namespacedな環境を用意します。一旦は自分でのカーネルビルドすら惜しんで、 Ubuntu 20.04 Focal 環境にKernel PPAから5.6.0のビルドを持ってくることにします。

wget https://kernel.ubuntu.com/~kernel-ppa/mainline/v5.6/linux-headers-5.6.0-050600_5.6.0-050600.202003292333_all.deb \
  https://kernel.ubuntu.com/~kernel-ppa/mainline/v5.6/linux-headers-5.6.0-050600-generic_5.6.0-050600.202003292333_amd64.deb \
  https://kernel.ubuntu.com/~kernel-ppa/mainline/v5.6/linux-image-unsigned-5.6.0-050600-generic_5.6.0-050600.202003292333_amd64.deb \
  https://kernel.ubuntu.com/~kernel-ppa/mainline/v5.6/linux-modules-5.6.0-050600-generic_5.6.0-050600.202003292333_amd64.deb
sudo apt install ./*.deb
sudo update-grub

再起動すると5.6です。

vagrant@ubuntu-focal:~$ uname -a
Linux ubuntu-focal 5.6.0-050600-generic #202003292333 SMP Sun Mar 29 23:35:58 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux

unshare(1) を試す

まず unshare コマンドで軽く試して... と言いたいところですが、Focal標準のunshareは当然7つのNamespaceしかサポートしていないので、

vagrant@ubuntu-focal:~$ unshare -h
...
Options:
 -m, --mount[=<file>]      unshare mounts namespace
 -u, --uts[=<file>]        unshare UTS namespace (hostname etc)
 -i, --ipc[=<file>]        unshare System V IPC namespace
 -n, --net[=<file>]        unshare network namespace
 -p, --pid[=<file>]        unshare pid namespace
 -U, --user[=<file>]       unshare user namespace
 -C, --cgroup[=<file>]     unshare cgroup namespace

最新のものを持ってきてビルドします。

git clone https://github.com/karelzak/util-linux.git; cd util-linux
sudo apt install bison libtool automake gettext autopoint pkg-config
./autogen.sh
./configure
make unshare
vagrant@ubuntu-focal:~/util-linux$ ./unshare -h 
   
Usage:
 unshare [options] [<program> [<argument>...]] 
 
Run a program with some namespaces unshared from the parent.  
 
Options:  
 -m, --mount[=<file>]      unshare mounts namespace 
 -u, --uts[=<file>]        unshare UTS namespace (hostname etc) 
 -i, --ipc[=<file>]        unshare System V IPC namespace
 -n, --net[=<file>]        unshare network namespace 
 -p, --pid[=<file>]        unshare pid namespace 
 -U, --user[=<file>]       unshare user namespace
 -C, --cgroup[=<file>]     unshare cgroup namespace
 -T, --time[=<file>]       unshare time namespace 
...
 --monotonic <offset>      set clock monotonic offset (seconds) in time namespaces
 --boottime <offset>       set clock boottime offset (seconds) in time namespaces

-T--monotonic/--boottime オプションがあります。

CLOCK_BOOTTIME なら /proc/uptime は影響を受けそうですね。試してみます。 unshare のオプションを工夫すればコンテナ内部独自の /proc ファイルシステムをマウントできるのでそれを利用しましょう。

vagrant@ubuntu-focal:~/util-linux$ uptime
 19:32:40 up 1 day, 14:17,  2 users,  load average: 0.07, 0.05, 0.01
vagrant@ubuntu-focal:~/util-linux$ sudo ./unshare --mount-proc -T --boottime=86400 --root=/var/run/myroot
root@ubuntu-focal:/# uptime
 19:32:47 up 2 days, 14:17,  0 users,  load average: 0.06, 0.05, 0.01
root@ubuntu-focal:/# exit
logout
vagrant@ubuntu-focal:~/util-linux$ sudo ./unshare --mount-proc -T --boottime=-86400 --root=/var/run/myroot
root@ubuntu-focal:/# uptime
 19:32:59 up 14:17,  0 users,  load average: 0.05, 0.05, 0.00
root@ubuntu-focal:/# exit
logout

見た目上、「コンテナ内部の起動時間」を変更できることがわかりますし、過去にも未来にも行くことができます。

あれ、でもunshareしたあとどうやって時間を「ずらして」いるんだろう...?

ここで不思議なことが一つあって、 unshare した直後は(ほかのNamespaceと同様の挙動として)ホストとコンテナのboottimeなどのオフセットは変わらないでしょう。ではどこで「ずらして」いるのでしょうか?

というのも、 clock_settime(2) などの関数は CLOCK_MONOTONIC/CLOCK_BOOTTIME を設定することができません。 ref: clock_getres(2) ja 別の方法で操作しているのでしょう。

ヒントは不正な値をあえて入れてみるとわかります。

vagrant@ubuntu-focal:~/util-linux$ sudo ./unshare --mount-proc -T --boottime=-186400 --mount-proc --root=/var/run/myroot
unshare: failed to write to /proc/self/timens_offsets: Numerical result out of range

/proc/$PID/timens_offsets というファイルが鍵のようです。

説明は以下の time_namespaces(7) のマニュアルページのRFCにあります(すみません、カーネルへのマージコミットを見つけられず...):

www.spinics.net

それによると、 /proc/PID/timens_offsets に以下のフォーマットで書き込み操作をすることで、monotonic/boottimeのオフセットを操作できるそうです。書き込み操作はそのNamespaceに自分以外のメンバーとなるプロセスがいない時点でのみ可能です。

<clock-id> <offset-secs> <offset-nanosecs>

ここで、 <clock-id>1 (= CLOCK_MONOTONIC )と 7 (= CLOCK_BOOTTIME )のみサポートされます。

また、読み出し操作で現在のオフセットもわかります。

vagrant@ubuntu-focal:~/util-linux$ sudo ./unshare --mount-proc -T --boottime=86400 --root=/var/run/myroot
root@ubuntu-focal:/# cat /proc/self/timens_offsets 
1 0 0
7 86400 0

unshare コマンドでのTime Namespace操作の実装例が非常に参考になります。

github.com


基本的にはパッチやmanのコメントにある通り、コンテナのマイグレーションに使う目的で追加されたNamespaceなのですが、たとえばコンテナ生成時にオフセット設定して 0 に戻し、uptimeコマンドの結果をそれぞれのコンテナ起動時からの時間に見せかける、等の用途にも使えるかもしれません。知っておけば、何かの役にたつかも...。

参考

関連するパッチや実装への参照を tenforward さんの方でまとめていらっしゃいます。

tenforward.hatenablog.com