ローファイ日記

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

Grenadine: 「普通のアプリケーション」がチェックポイント/リストアの恩恵を享受する

ここ数日、 Grenadine (グレナデン)と名付けたOSSをやっていっていました。

github.com

Grenadine は、CRIUを用いて、いわゆるコンテナ化をしていないような、VMにデプロイしているようなサーバ型アプリケーション(Webなら、Rails, Django, node.js ...)でもチェックポイント/リストアの恩恵を受けられるようにするためのツールです。

criu.org

今日はこのできたばかりの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ディストリビューションではパッケージすらない可能性があります。その場合は公式の手順でビルドする必要があります。

全体に一言で言うと「頑張って!」と言う感じ...すいませんね...。

criu.org

すべてが入った後、サービスの立ち上げは一旦こういう感じで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

f:id:udzura:20190307200209p:plain

これも一旦ダンプしちゃいます。

$ 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 checkpointKubernetes(や、その上のクラウドネイティブミドル)の機能で同じようなことができるようになるとは思います。今回の実装はPoC的な面がありますが、ここから多少現実的な運用の問題を潰せば、既存のアプリケーションをGrenadineに乗せることができるかもしれません。


そんな感じで mruby で書いたシステムツール、 Grenadine の詳しい話は、 RubyKaigi 2019 で行おうと思っています。Rails アプリケーションなどで Grenadine を試します!

rubykaigi.org

Join us!!