ローファイ日記

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

CRIUをラップしてより簡単にプロセスのチェックポイント/リストアをするツールを作った話

Linux Advent Calendar 2019 12日目の記事です。

qiita.com

今日はCRIUに関わる自作ツールの話をします。

CRIU って?

Checkpoint and Restore In Userspace の略で、プロセス(や特殊なプロセスであるところのコンテナ)の動いているメモリ状態などをイメージにダンプし、またそのイメージからのリストア起動を実現するLinux向けのツールです。

詳細は、 TenForward さんの記事が詳しいです。

gihyo.jp

任意の状態のプロセスのセーブポイントや、 kvm などでいうライブマイグレーションのような用途での利用を想定しているようです。

で、この技術、やれることはとても魅力的なのですが、使い方がかなり難しいことでも有名です。

雑にプロセスを立ち上げてそのPIDをダンプしようとしてもエラーが出たり、

$ sudo criu dump -t 25010             
Warn  (compel/arch/x86/src/lib/infect.c:280): Will restore 25011 with interrupted system call
Warn  (compel/arch/x86/src/lib/infect.c:280): Will restore 25018 with interrupted system call                              
Error (criu/tty.c:1859): tty: Found dangling tty with sid 5228 pgid 25010 (pts) on peer fd 0.
Task attached to shell terminal. Consider using --shell-job option. More details on http://criu.org/Simple_loop
Error (criu/cr-dump.c:1743): Dumping FAILED.

言われた通り --shell-job をつけてもリストアでまたエラー。

$ sudo criu dump -t 25010 --shell-job
Warn  (compel/arch/x86/src/lib/infect.c:280): Will restore 25011 with interrupted system call
Warn  (compel/arch/x86/src/lib/infect.c:280): Will restore 25018 with interrupted system call
$ sudo criu restore
 25058: Error (criu/tty.c:412): tty: Found slave peer index 1 without correspond master peer
Error (criu/cr-restore.c:1457): 25058 killed by signal 9: Killed
Error (criu/cr-restore.c:2333): Restoring FAILED.
$ sudo criu restore --shell-job
pie: 25058: Error (criu/pie/restorer.c:1811): Unable to create a thread: 25060
pie: 25058: Error (criu/pie/restorer.c:1951): Restorer fail 25058
Error (criu/cr-restore.c:1454): 25058 exited, status=1
Error (criu/cr-restore.c:2333): Restoring FAILED.

エラーも見慣れないもので何もわからん...。ちなみに、ホストのプロセスをダンプする場合、 PID含めて リストアしようとするため、そのPIDがすでに別のプロセスに取られていた場合そもそもリストアができない(回避方法はないようです...)などの問題もあります。

また、 images-dir などの必要な設定など、いろいろな規約を自分で決めたり、ちょっと触りたいだけではなかなか面倒な印象を持つかもしれません。

やりたいことは普通にプロセスの状態のスナップショットをとって、戻したりしたいだけなのに...。

LXD のような場合は実は標準の checkpoint コマンドを経由することもできますが、「普通にVMで立ち上げてるアプリやプロセス」の場合は周辺の準備が面倒そうですよね。CRIUは基本的に非常にレイヤの低いオプションやAPIを提供するので、現実の用途のためには色々とラップする必要が出てきます。

ということで、普通のプロセス向けに、こういうツールを書いています。

github.com

Grenadine とは?

Grenadine(グレナデン)は「普通にVMであげてるアプリやプロセス」を普通に動かしつつ、任意の時点で状態をダンプし、任意のダンプイメージからプロセスを起動させられるようにするツールです。筆者が確認しているのが Ubuntu Bionic のみなので、以下はその環境での手順です。

インストールの前に、まずある程度バージョンの高い criu パッケージが必須です。以下の PPA はディストロのcriuパッケージメンテナが公開しているバージョン高めのもののリポジトリなのでこれを使うと良さそうです。

launchpad.net

sudo add-apt-repository ppa:criu/ppa
sudo apt update
sudo apt install criu

criu のサービスを先に立ち上げておきます。

sudo criu service -vv --address /var/run/criu_service.socket &

最後に、私が公開しているGrenadineのパッケージをダウンロードしてインストールしてください。

wget --content-disposition \
  https://packagecloud.io/udzura/grenadine/packages/ubuntu/bionic/grenadine_0.1.7-1_amd64.deb/download.deb
sudo apt install ./grenadine_0.1.7-1_amd64.deb

Grenadine の使い方

ひとまず、 memcached でも立ち上げてみましょう。以下のコマンドで立ち上がります。

$ sudo grenadine daemon --uid vagrant:vagrant -- /usr/bin/memcached                                 
Spawned: 25296

telnetなどで動作を確認できます。一つ値をセットしましょう。

$ telnet localhost 11211
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
get foo
END
set foo 0 0 10
grenadine!
STORED
get foo
VALUE foo 0 10
grenadine!
END
quit
Connection closed by foreign host.

ここで、おもむろにmemcachedプロセスをダンプします。

$ sudo grenadine dump
Dumped into: /var/lib/grenadine/images/3d2e6724225b0c3e364a68cbaaa8af5a

繋がらなくなりますね。

$ telnet localhost 11211
Trying 127.0.0.1...
telnet: Unable to connect to remote host: Connection refused

イメージはlistコマンドで確認できます。

$ sudo grenadine list
IDX     IMAGE_ID                                CTIME                           COMM            MEM_SIZE
  0     3d2e6724225b0c3e364a68cbaaa8af5a        2019-12-12 13:12:02 +0000       memcached       1.79MiB 

このイメージからリストアしてみましょう。

$ sudo grenadine restore 0
Restored 3d2e6724225b0c3e364a68cbaaa8af5a: 25386

この時、普通に再起動したらmemcachedはもちろん全てのデータを飛ばします。ですが、リストアなので、データを 保持したまま 立ち上がっていることがわかります。

$ telnet localhost 11211
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
get foo
VALUE foo 0 10
grenadine!
END

dump/restore を繰り返すと、チェックポイントが増えていくのを確認できます。たとえばアプリケーションサーバを任意のデプロイの時点のものに瞬時に差し替える、と言ったことも可能です。いろいろなデーモンで試してみてください。

内部の話

Linux アドベントカレンダーなので少しだけ内部の話を詳しくすると、要するに「CRIUが扱いやすい」最低限の隔離をしたコンテナを作って、ダンプリストアをしやすくしています。

具体的には以下をしています。

  • setsid(2) を呼び出し、ttyから切り離すなどCRIUのダンプの邪魔にならないデーモン設定をしている
  • PID NamespaceとMount Namespaceをunshareする
  • bind mountしているところを限定し、CRIUのダンプ/リストア両方に渡している

(この辺は、snapcraftなんかも同じような最低限のコンテナを作っていたりしますね)

setsid(2)、fdの処理など

CRIUはちゃんとセッションリーダーからプロセスツリーが始まっていないと文句を言います。また、標準入出力がttyや、後述するpivot_root先の外のファイルに存在したりすると文句を言ってきます。

この辺をちゃんとした形でデーモンを作るのは結構面倒なので、 grenadine daemon コマンドを経由してその辺の処理をするようにしています。

PID NamespaceとMount Namespaceをunshare

先ほど少し言及しましたが、ホストの PID Namespace にあるプロセスをリストアすると、PID被りの問題が出ます。なので、PID Namespaceを隔離して、必ず PID=1 から始まるようにし、なおかつ新しい名前空間にはプロセスがいない状態を保持すると安定してリストアが可能になります。

この時、ホストの /proc がコンテナからは使い物にならなくなる(そうすると結構なアプリが困ってしまう)ので、Mount Namespaceも隔離して専用の /proc をマウントしています。

新しいMount Namespaceで作っている専用のrootfsを作る戦略は以下のような感じです(わかりづらいかも...)。

Host              Grenadine
/              -> ${newroot}/ に bind mount
                  ${newroot}/proc を自分でマウント

* --make-rprivate / しているので以下も自分で bind mount
/dev           -> ${newroot}/dev           
/dev/pts       -> ${newroot}/dev/pts       
/dev/shm       -> ${newroot}/dev/shm       
/dev/mqueue    -> ${newroot}/dev/mqueue    
/tmp           -> ${newroot}/tmp           
/sys           -> ${newroot}/sys           
/sys/fs/cgroup -> ${newroot}/sys/fs/cgroup 
/sys/fs/cgroup/cpu,cpuacct ...

* 最後に、 ${newroot} に pivot_root

上二つのポイントについて、実装は以下のあたりです(mrubyのコードなんですが、Rubyに慣れていない方はまあ擬似コードのように読んでください...)。

github.com

bind mount のCRIUでの扱いの話

上記のようにホストの /dev, /dev/pts, /sys... などを自分で bind mount すれば、ほぼホストと変わらないファイルシステムがコンテナの中にもできるので、Grenadineで管理しているデーモンはホストとほぼ同じ情報にアクセスできることになります。

一方、複雑なbind mountをしている場合、 CRIU でダンプリストアするためにどこを bind mount しているのか すべて オプション経由で教えてあげないといけなくなります。

criu.org

この点は、 Grenadine でCRIUのオプションを生成する際に全部ラップするようにしています。実装は下のあたりです。


...というように、実際はコンテナランタイムなのですが、ホストで動かしているのと同じように動かすために色々と工夫をしているつもりです。

コンテナ技術が様々なLinux機能の組み合わせであるおかげで、こういう「コンテナ」を作ることもできるという話でもあります。

GrenadineはWebサーバの他に、意外とLinuxデスクトップなどとも相性がいいかもしれません。お試しいただいて、使ってみて変な点があれば教えてください!

関連記事

3 月に一度紹介記事を書いています(もともとRubyKaigiのために作ったものでした)が、当時はパッケージもなかったので利用が面倒だったと思います。今はUbuntu Bionic向けだけですがパッケージを用意していますのでお試しください。

udzura.hatenablog.jp

packagecloud.io

明日はmei_9961 さんの記事です!