ここ数日、 Grenadine (グレナデン)と名付けたOSSをやっていっていました。
Grenadine は、CRIUを用いて、いわゆるコンテナ化をしていないような、VMにデプロイしているようなサーバ型アプリケーション(Webなら、Rails, Django, node.js ...)でもチェックポイント/リストアの恩恵を受けられるようにするためのツールです。
今日はこのできたばかりのGrenadineを軽く紹介します。チェックポイント/リストア、と言われてもピンと来ないとは思うので、簡単なデモの手順を示しつつ。
インストール・セットアップ
Grenadine、まだパッケージ等は作っていません(お待ちを...)ので、一旦は自分でビルドすることになります。mruby に必要なビルド環境 と CRIUのビルドに必要なパッケージ類 が揃っていればビルドできると思います。めっちゃ多いですが。
パッケージが揃った後はコマンドは少なめです。
$ git clone https://github.com/udzura/grenadine.git; cd grenadine $ rake $ sudo mv ./mruby/bin/grenadine /usr/local/bin
また、 criu のコマンドをインストールしつつそれをサービスで立ち上げておく必要があります。criu自体はUbuntu BionicであればパッケージのものでOKです(3系を推奨するので、 Bionic 以降でお願いします)が、多くのLinuxディストリビューションではパッケージすらない可能性があります。その場合は公式の手順でビルドする必要があります。
全体に一言で言うと「頑張って!」と言う感じ...すいませんね...。
すべてが入った後、サービスの立ち上げは一旦こういう感じでOKです。
$ sudo criu service --address /var/run/criu_service.socket &
簡単なサンプル(SinatraなWebアプリケーションで)
Grenadine が入ったので、簡単なWebアプリケーションを作ってデプロイしておきます。
$ sudo mkdir /u/app; sudo chown vagrant: -R /u $ cd /u/app $ mkdir revision-1 $ vim revision-1/app.rb require "sinatra" get "/" do "Hello, Grenadine! version is 1\n" end $ vim revision-1/Gemfile source "https://rubygems.org" gem "sinatra" $ ( cd revision-1; bundle install --path vendor/bundle ) $ ln -s revision-1 current
これをgrenadineコマンド経由でデーモン化します。Dockerのようなツールではないので、ホストのネットワークのlocalhostをリスンしています。
$ sudo grenadine daemon \ --uid vagrant:vagrant \ -C /u/app/current \ -- \ /usr/bin/bundle exec ruby app.rb $ curl localhost:4567 Hello, Grenadine! version is 1
grenadine dump
というコマンドで、このアプリケーションのチェックポイントを作成できます。
$ sudo grenadine dump Dumped into: /var/lib/grenadine/images/3aac34584b38b2162821960f9fc2807f $ sudo grenadine list # 確認 IDX IMAGE_ID CTIME COMM MEM_SIZE 0 3aac34584b38b2162821960f9fc2807f Thu Mar 07 10:22:25 2019 ruby 15.61MiB
このチェックポイントから grenadine restore
でリストアします。リストアしたアプリは再度ダンプできます。で、 grenadine list
に履歴が残っていく。
$ sudo grenadine restore 0 Restored 3aac34584b38b2162821960f9fc2807f: 11265 $ curl localhost:4567 Hello, Grenadine! version is 1 $ sudo grenadine dump Dumped into: /var/lib/grenadine/images/a92b5bde9652528d09919b7ee9030f61 $ sudo grenadine list IDX IMAGE_ID CTIME COMM MEM_SIZE 0 a92b5bde9652528d09919b7ee9030f61 Thu Mar 07 10:23:19 2019 ruby 15.62MiB 1 3aac34584b38b2162821960f9fc2807f Thu Mar 07 10:22:25 2019 ruby 15.61MiB
ここで、アプリの新しいバージョンをデプロイして見ましょう。
$ cd /u/app $ mkdir revision-2; rsync -a revision-1/ revision-2/ $ vim revision-2/app.rb require "sinatra" require "term/ansicolor" get "/" do c = Term::ANSIColor c.on_red(c.green("Hello, Grenadine! version is 2")) + "\n" end $ vim revision-2/Gemfile source "https://rubygems.org" gem "sinatra" gem "term-ansicolor" $ ( cd revision-2; bundle install ) $ rm current $ ln -s revision-2 current
これをまた grenadine daemon
で立ち上げると、version 2なアプリが立ち上がります。curlすると色がついている。
$ sudo grenadine daemon \ --uid vagrant:vagrant \ -C /u/app/current \ -- \ /usr/bin/bundle exec ruby app.rb
これも一旦ダンプしちゃいます。
$ sudo grenadine dump Dumped into: /var/lib/grenadine/images/d9fe43fa857fec743b918ef1dddfdde6 $ sudo grenadine list IDX IMAGE_ID CTIME COMM MEM_SIZE 0 d9fe43fa857fec743b918ef1dddfdde6 Thu Mar 07 10:28:09 2019 ruby 17.03MiB 1 a92b5bde9652528d09919b7ee9030f61 Thu Mar 07 10:23:19 2019 ruby 15.62MiB 2 3aac34584b38b2162821960f9fc2807f Thu Mar 07 10:22:25 2019 ruby 15.61MiB
そしてこの状態で、一つ前のチェックポイントを指定してリストアすると、 /u/app/current
などを張り替えているにも関わらず、前のバージョンのアプリが立ち上がることがわかります。アプリケーションは起動時にRubyスクリプトをメモリに読み込んで、起動しているので、メモリの状態(その他)をリストアすることで切り戻しています。
こういう感じで、Grenadineを使うことで チェックポイントから一瞬で、以前のバージョンに戻す ことができるというわけですね。
$ sudo grenadine restore --from a92b5bde9652528d09919b7ee9030f61 Restored a92b5bde9652528d09919b7ee9030f61: 11558 $ curl localhost:4567 Hello, Grenadine! version is 1
もちろん、最新の d9fe43fa857fec743b918ef1dddfdde6
からリストアすると最新に戻ります。
モチベーションとしては、一般に、特にスクリプト言語のアプリケーションは起動、何よりデプロイに時間がかかることが多いです。したがってとあるデプロイでアプリケーションが壊れてしまった場合、GitのようなVCSで管理しているとはいえ、切り戻しには手間がかかりがちとなります。
チェックポイント/リストアの技術を用いて、その切り替えのリードタイムを大きく短縮できないかという興味から、このようなツールを書いてみました。
工夫したところ
Grenadine が対象とするのはコンテナ化されたアプリケーションではなく、既存のいわゆるVMなどにデプロイされたアプリケーションです。
そのような普通のプロセスをCRIUでダンプすると、PIDの情報を含めて保存してしまいます、したがって、PIDを再生しようとして、状況により既に存在するプロセスと重複して立ち上がらない場合があります。
そのようなことが起きないように、 Grenadine は PID Namespace と、 ( /proc
をワークさせるために)Mount Namespace だけを unshareしたコンテナを作成し、その上にアプリケーションを起動します。こうすると、毎回独立したPID Namespaceで綺麗にPID=1から始めることができ、重複の問題がなくなります。ただし、もちろん、ホストから見ると普通のプロセスのようなPIDが振られています。
また、DockerやLXCのようなフル機能のコンテナと違い、rootfsをホストのrootと別途用意する必要がないように、bind mountで自動的にホストのrootとほぼ同じものを作成して立ち上げています。なので、基本的には難しいことをしていなければ、いまVMにデプロイしているアプリケーションをそのまま動かせると考えています(が、正直いろんなケースがあるので潰していきたい)。
要するに、 CRIUで扱いやすく、ホストの環境とかけ離れない最低限の分離だけを行なったコンテナ を作成するという戦略を取っています。ある意味でコンテナランタイムというわけです。それにより、自由自在にチェックポイント/リストアができるというカラクリです。
将来は、Dockerの docker checkpoint
やKubernetes(や、その上のクラウドネイティブミドル)の機能で同じようなことができるようになるとは思います。今回の実装はPoC的な面がありますが、ここから多少現実的な運用の問題を潰せば、既存のアプリケーションをGrenadineに乗せることができるかもしれません。
そんな感じで mruby で書いたシステムツール、 Grenadine の詳しい話は、 RubyKaigi 2019 で行おうと思っています。Rails アプリケーションなどで Grenadine を試します!